from collections import defaultdict
from typing import List, Union

from scipy.optimize import linear_sum_assignment
import PIL.Image as Image
import numpy as np
import torch
import torch.nn.functional as F


__all__ = ['compute_iou', 'MeanEvaluator', 'load_mask']


def compute_iou(a: torch.Tensor, b: torch.Tensor) -> float:
    if a.shape[0] != b.shape[0]:
        a = F.interpolate(a.unsqueeze(0).unsqueeze(0).float(), size=b.shape, mode='bicubic').squeeze()
        a[a < 1] = 0
        a[a >= 1] = 1

    intersection = (a * b).sum()
    union = a.sum() + b.sum() - intersection

    return (intersection / (union + 1e-8)).item()


def load_mask(path: str) -> torch.Tensor:
    mask = np.array(Image.open(path))
    mask = torch.from_numpy(mask).float()[:, :, 3]  # use alpha channel
    mask = (mask > 0).float()

    return mask


class UnsupervisedEvaluator:
    def __init__(self, name: str = 'UnsupervisedEvaluator'):
        self.name = name
        self.ious = defaultdict(list)
        self.num_samples = 0

    def log_iou(self, preds: Union[torch.Tensor, List[torch.Tensor]], truth: torch.Tensor, gt_idx: int = 0, pred_idx: int = 0):
        if not isinstance(preds, list):
            preds = [preds]

        iou = max(compute_iou(pred, truth) for pred in preds)
        self.ious[gt_idx].append((pred_idx, iou))

    @property
    def mean_iou(self) -> float:
        n = max(max(self.ious), max([y[0] for x in self.ious.values() for y in x])) + 1
        iou_matrix = np.zeros((n, n))
        count_matrix = np.zeros((n, n))

        for gt_idx, ious in self.ious.items():
            for pred_idx, iou in ious:
                iou_matrix[gt_idx, pred_idx] += iou
                count_matrix[gt_idx, pred_idx] += 1

        row_ind, col_ind = linear_sum_assignment(iou_matrix, maximize=True)
        return iou_matrix[row_ind, col_ind].sum() / count_matrix[row_ind, col_ind].sum()

    def increment(self):
        self.num_samples += 1

    def __len__(self) -> int:
        return self.num_samples

    def __str__(self):
        return f'{self.name}<{self.mean_iou:.4f} (mIoU) {len(self)} samples>'


class MeanEvaluator:
    def __init__(self, name: str = 'MeanEvaluator'):
        self.ious: List[float] = []
        self.intensities: List[float] = []
        self.name = name

    def log_iou(self, preds: Union[torch.Tensor, List[torch.Tensor]], truth: torch.Tensor):
        if not isinstance(preds, list):
            preds = [preds]

        self.ious.append(max(compute_iou(pred, truth) for pred in preds))
        return self

    def log_intensity(self, pred: torch.Tensor):
        self.intensities.append(pred.mean().item())
        return self

    @property
    def mean_iou(self) -> float:
        return np.mean(self.ious)

    @property
    def mean_intensity(self) -> float:
        return np.mean(self.intensities)

    @property
    def ci95_miou(self) -> float:
        return 1.96 * np.std(self.ious) / np.sqrt(len(self.ious))

    def __len__(self) -> int:
        return max(len(self.ious), len(self.intensities))

    def __str__(self):
        return f'{self.name}<{self.mean_iou:.4f} (±{self.ci95_miou:.3f} mIoU) {self.mean_intensity:.4f} (mInt) {len(self)} samples>'


if __name__ == '__main__':
    mask = load_mask('truth/output/452/sink.gt.png')

    print(MeanEvaluator().log_iou(mask, mask))
