# -*- coding: utf-8 -*-
# Masking function to generate tissue masks
#
# @ Fabian Hörst, fabian.hoerst@uk-essen.de
# Institute for Artifical Intelligence in Medicine,
# University Medicine Essen

import os
import warnings
from typing import List, Tuple, Union

import cv2
import numpy as np
import rasterio
import skimage.color as sk_color
import skimage.filters as sk_filters
import skimage.morphology as sk_morphology
#from histolab.filters.image_filters import BluePenFilter, GreenPenFilter, RedPenFilter
from PIL import Image
from rasterio.mask import mask as rasterio_mask
from shapely.affinity import scale
from shapely.geometry import Polygon

from preprocessing.patch_extraction import logger


def generate_tissue_mask(
    tissue_tile: np.ndarray,
    mask_otsu: bool = False,
    polygons: List[Polygon] = None,
    region_labels: List[str] = None,
    otsu_annotation: Union[List[str], str] = "object",
    downsample: int = 1,
    apply_prefilter: bool = False,
) -> np.ndarray:
    """Generate a tissue mask using otsu thresholding.

    Per Default, otsu-thresholding is performed. If mask_otsu is true, first a masked image is calculate
    using the annotation matching the otsu_annotation label.

    Args:
        tissue_tile (np.ndarray): Tissue tile as numpy array with shape (height, width, 3)
        mask_otsu (bool, optional): If masking is applied before thresholding. Defaults to False.
        polygons (List[Polygon], optional):  Annotations of this WSI as a list of polygons (referenced to highest level of WSI). Defaults to None.
        region_labels (List[str], optional): List of labels for the annotations provided as polygons parameter. Defaults to None.
        otsu_annotation (Union[List[str], str], optional):  List with annotation names or string with annotation name to use for a masked otsu thresholding.
            Defaults to "object".
        downsample (int, optional): Downsampling of the tissue tile compared to highest WSI level. Used for matching annotations with tissue-tile size.
            Defaults to 1.
        apply_prefilter (bool, optional): If a prefilter should be used to remove markers before applying otsu. Defaults to False.

    Returns:
        np.ndarray: Binary tissue mask with shape (height, width)
    """
    if polygons is not None:
        assert len(polygons) == len(
            region_labels
        ), "Polygon list and polygon labels are not having the same length"

    if mask_otsu:
        # filter
        otsu_polgyon = get_filtered_polygons(
            polygons=polygons,
            region_labels=region_labels,
            filter_labels=otsu_annotation,
            downsample=downsample,
        )
        if len(otsu_polgyon) != 0:
            logger.debug(
                "Mask tissue thumbnail with region before applying Otsu thresholding"
            )
            tissue_tile = mask_tile_with_region(tile=tissue_tile, polygons=otsu_polgyon)
        else:
            logger.error("ValueError:")
            logger.error(
                "Annotation with given label does not exist. Using unmasked thresholding"
            )
    # apply otsu thresholding

    if apply_prefilter:
        tissue_tile = remove_marker_filters(tile=tissue_tile)

    tissue_mask = apply_otsu_thresholding(tile=tissue_tile)
    assert len(np.unique(tissue_mask)) <= 2, "Mask is not binary"

    return tissue_mask


def convert_polygons_to_mask(
    polygons: Tuple[List[Polygon], Polygon],
    reference_size: tuple[int],
    downsample: int = 1,
) -> np.ndarray:
    """Convert a polygon to a mask

    The function is assuming that polygons have already been filtered (see get_filtered_polygon).

    Args:
        polygons (Tuple[List[Polygon], Polygon]): List of polygons converted to a mask. Can work with Polygons with holes inside
        reference_size (tuple[int]): Shape of resulting mask image. Shape should be (height, width, channels).
        downsample (int, optional): Set the factor by which the polygon should be scaled down. Defaults to 1.

    Returns:
        np.ndarray: Binary mask with shape (height, width)
    """

    if type(polygons) is not List:
        polygons = list(polygons)
    polygons_downsampled = [
        scale(
            poly,
            xfact=1 / downsample,
            yfact=1 / downsample,
            origin=(0, 0),
        )
        for poly in polygons
    ]
    src = 255 * np.ones(shape=reference_size, dtype=np.uint8)
    im = Image.fromarray(src)
    im.save("tmp.tif")
    with warnings.catch_warnings():
        warnings.filterwarnings("ignore")
        with rasterio.open("tmp.tif") as src:
            out_image, _ = rasterio_mask(src, polygons_downsampled, crop=False)
            mask = out_image.transpose(1, 2, 0)
            mask = np.invert(mask)
    os.remove("tmp.tif")
    mask = (mask / 255).astype(np.uint8)

    assert len(np.unique(mask)) <= 2, "Mask is not binary"

    return mask[:, :, 0]


def get_filtered_polygons(
    polygons: List[Polygon],
    region_labels: List[str],
    filter_labels: List[str],
    downsample: int = 1,
) -> List[Polygon]:
    """Filter Polygons by a list of filter labels

    Returns a list with filtered polygons containing just the polygons with
    the label specified in filter_labels

    Args:
        polygons (List[Polygon]): Annotations as a list of polygons.
        region_labels (List[str]): List of labels
        filter_labels (List[str]): List of labels to filter
        downsample (int, optional): Scaling factor to downscale polygon. Defaults to 1.

    Returns:
        List[Polygon]: List with filtered polygons
    """
    logger.debug(
        f"Filter polygons for label: {filter_labels} and downsample results to {downsample}"
    )
    filtered_polygons = []
    for poly, region_label in zip(polygons, region_labels):
        if region_label in filter_labels:
            filtered_polygons.append(
                scale(poly, xfact=1 / downsample, yfact=1 / downsample, origin=(0, 0))
            )
    if len(filtered_polygons) == 0:
        logger.debug(
            "ValueError: Annotation with given label does not exist or Annotation has a non-valid Type."
        )

    return filtered_polygons


def mask_tile_with_region(
    tile: np.ndarray, polygons: Union[List[Polygon], Polygon]
) -> np.ndarray:
    """Mask a tile with a region and return the masked tile

    Args:
        tile (np.ndarray): Tile which should be masked
        polygons (Union[List[Polygon], Polygon]): List of mask polygons or a polygon to mask

    Returns:
        np.ndarray: Masked tile
    """
    if type(polygons) is not List:
        polygons = list(polygons)

    # create temp file for rasterio
    src = 255 * np.ones(shape=(tile.shape[0:2]), dtype=np.uint8)
    im = Image.fromarray(src)
    im.save("tmp.tif")
    # get mask out of polygon
    with warnings.catch_warnings():
        warnings.filterwarnings("ignore")
        with rasterio.open("tmp.tif") as src:
            out_image, out_transform = rasterio_mask(src, polygons, crop=False)
            mask = out_image.transpose(1, 2, 0)
    # remove temp file
    os.remove("tmp.tif")
    # create masked figure
    fg = cv2.bitwise_or(tile, tile, mask=mask)
    inverse_mask = cv2.bitwise_not(mask)
    background = np.full(tile.shape, 255, dtype=np.uint8)
    bk = cv2.bitwise_or(background, background, mask=inverse_mask)

    return cv2.bitwise_or(fg, bk)


def apply_otsu_thresholding(tile: np.ndarray) -> np.ndarray:
    """Generate a binary tissue mask by using Otsu thresholding

    Args:
        tile (np.ndarray): Tile with tissue with shape (height, width, 3)

    Returns:
        np.ndarray: Binary mask with shape (height, width)
    """
    hsv_img = cv2.cvtColor(tile.astype(np.uint8), cv2.COLOR_RGB2HSV)
    gray_mask = cv2.inRange(hsv_img, (0, 0, 70), (180, 10, 255))
    black_mask = cv2.inRange(hsv_img, (0, 0, 0), (180, 255, 85))
    # Set all grey/black pixels to white
    full_tile_bg = np.copy(tile)
    full_tile_bg[np.where(gray_mask | black_mask)] = 255

    # apply otsu mask first time for removing larger artifacts
    masked_image_gray = 255 * sk_color.rgb2gray(full_tile_bg)
    thresh = sk_filters.threshold_otsu(masked_image_gray)
    otsu_masking = masked_image_gray < thresh
    # improving mask
    otsu_masking = sk_morphology.remove_small_objects(otsu_masking, 60)
    otsu_masking = sk_morphology.dilation(otsu_masking, sk_morphology.square(12))
    otsu_masking = sk_morphology.closing(otsu_masking, sk_morphology.square(5))
    otsu_masking = sk_morphology.remove_small_holes(otsu_masking, 250)
    tile = mask_rgb(tile, otsu_masking).astype(np.uint8)

    # apply otsu mask second time for removing small artifacts
    masked_image_gray = 255 * sk_color.rgb2gray(tile)
    thresh = sk_filters.threshold_otsu(masked_image_gray)
    otsu_masking = masked_image_gray < thresh
    otsu_masking = sk_morphology.remove_small_holes(otsu_masking, 5000)
    otsu_thr = ~otsu_masking
    otsu_thr = otsu_thr.astype(np.uint8)

    return otsu_thr


def mask_rgb(rgb: np.ndarray, mask: np.ndarray) -> np.ndarray:
    """Mask an RGB image

    Args:
        rgb (np.ndarray): RGB image to mask with shape (height, width, 3)
        mask (np.ndarray): Binary mask with shape (height, width)

    Returns:
        np.ndarray: Masked image
    """
    assert (
        rgb.shape[:-1] == mask.shape
    ), "Mask and RGB shape are different. Cannot mask when source and mask have different dimension."
    mask_positive = np.dstack([mask, mask, mask])
    mask_negative = np.dstack([~mask, ~mask, ~mask])
    positive = rgb * mask_positive
    negative = rgb * mask_negative
    negative = 255 * (negative > 0.0001).astype(int)

    masked_image = positive + negative

    return np.clip(masked_image, a_min=0, a_max=255)


def remove_marker_filters(tile: np.ndarray) -> np.ndarray:
    """Generate a binary tissue mask by using Otsu thresholding

    Args:
        tile (np.ndarray): Tile with tissue with shape (height, width, 3)

    Returns:
        np.ndarray: Binary mask with shape (height, width)
    """
    red_pen_filter = RedPenFilter()
    green_pen_filter = GreenPenFilter()
    blue_pen_filter = BluePenFilter()

    tile = Image.fromarray(tile.astype(np.uint8))

    tile = blue_pen_filter(tile)
    tile = green_pen_filter(tile)
    tile = red_pen_filter(tile)

    image_rgb_np = np.array(tile)
    black_pixels = (
        (image_rgb_np[:, :, 0] == 0)
        & (image_rgb_np[:, :, 1] == 0)
        & (image_rgb_np[:, :, 2] == 0)
    )
    image_rgb_np[black_pixels] = [255, 255, 255]

    return image_rgb_np
