import numpy as np
import warnings


class SemSegMetric(object):
    """Metrics for semantic segmentation.

    Accumulate confusion matrix over training loop and
    computes accuracy and mean IoU.
    """

    def __init__(self):
        super(SemSegMetric, self).__init__()
        self.confusion_matrix = None
        self.num_classes = None

    def update(self, scores, labels):
        conf = self.get_confusion_matrix(scores, labels)
        if self.confusion_matrix is None:
            self.confusion_matrix = conf.copy()
            self.num_classes = conf.shape[0]
        else:
            assert self.confusion_matrix.shape == conf.shape
            self.confusion_matrix += conf

    def acc(self):
        """Compute the per-class accuracies and the overall accuracy.

        Args:
            scores (tf.FloatTensor, shape (B?, C, N):
                raw scores for each class.
            labels (tf.LongTensor, shape (B?, N)):
                ground truth labels.

        Returns:
            A list of floats of length num_classes+1.
            Consists of per class accuracy. Last item is Overall Accuracy.
        """
        if self.confusion_matrix is None:
            return None

        accs = []
        for label in range(self.num_classes):
            tp = np.longlong(self.confusion_matrix[label, label])
            fn = np.longlong(self.confusion_matrix[label, :].sum()) - tp

            if tp + fn == 0:
                acc = float('nan')
            else:
                acc = tp / (tp + fn)

            accs.append(acc)

        accs.append(np.nanmean(accs))

        return accs

    def iou(self):
        """Compute the per-class IoU and the mean IoU.

        Args:
            scores (tf.FloatTensor, shape (B?, C, N):
                raw scores for each class.
            labels (tf.LongTensor, shape (B?, N)):
                ground truth labels.

        Returns:
            A list of floats of length num_classes+1.
            Consists of per class IoU. Last item is mIoU.
        """
        if self.confusion_matrix is None:
            return None

        ious = []
        for label in range(self.num_classes):
            tp = np.longlong(self.confusion_matrix[label, label])
            fn = np.longlong(self.confusion_matrix[label, :].sum()) - tp
            fp = np.longlong(self.confusion_matrix[:, label].sum()) - tp

            if tp + fp + fn == 0:
                iou = float('nan')
            else:
                iou = (tp) / (tp + fp + fn)

            ious.append(iou)

        ious.append(np.nanmean(ious))

        return ious

    def reset(self):
        self.confusion_matrix = None

    @staticmethod
    def get_confusion_matrix(scores, labels):
        """Computes the confusion matrix of one batch

        Args:
            scores (tf.FloatTensor, shape (B?, N, C):
                raw scores for each class.
            labels (tf.LongTensor, shape (B?, N)):
                ground truth labels.

        Returns:
            Confusion matrix for current batch.
        """
        C = scores.shape[-1]
        y_pred = scores.cpu().numpy().reshape(-1, C)  # (N, C)
        y_pred = np.argmax(y_pred, axis=1)  # (N,)

        y_true = labels.cpu().numpy().reshape(-1,)

        y = np.bincount(C * y_true + y_pred, minlength=C * C)

        if len(y) < C * C:
            y = np.concatenate([y, np.zeros((C * C - len(y)), dtype=np.long)])
        else:
            if len(y) > C * C:
                warnings.warn(
                    "Prediction has fewer classes than ground truth. This may affect accuracy."
                )
            y = y[-(C * C):]  # last c*c elements.

        y = y.reshape(C, C)

        return y
