from loguru import logger

import torch
import torch.nn as nn


class ASpanLoss(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.config = config  # config under the global namespace
        self.loss_config = config["aspan"]["loss"]
        self.match_type = self.config["aspan"]["match_coarse"]["match_type"]
        self.sparse_spvs = self.config["aspan"]["match_coarse"]["sparse_spvs"]
        self.flow_weight = self.config["aspan"]["loss"]["flow_weight"]

        # coarse-level
        self.correct_thr = self.loss_config["fine_correct_thr"]
        self.c_pos_w = self.loss_config["pos_weight"]
        self.c_neg_w = self.loss_config["neg_weight"]
        # fine-level
        self.fine_type = self.loss_config["fine_type"]

    def compute_flow_loss(self, coarse_corr_gt, flow_list, h0, w0, h1, w1):
        # coarse_corr_gt:[[batch_indices],[left_indices],[right_indices]]
        # flow_list: [L,B,H,W,4]
        loss1 = self.flow_loss_worker(flow_list[0], coarse_corr_gt[0], coarse_corr_gt[1], coarse_corr_gt[2], w1)
        loss2 = self.flow_loss_worker(flow_list[1], coarse_corr_gt[0], coarse_corr_gt[2], coarse_corr_gt[1], w0)
        total_loss = (loss1 + loss2) / 2
        return total_loss

    def flow_loss_worker(self, flow, batch_indicies, self_indicies, cross_indicies, w):
        bs, layer_num = flow.shape[1], flow.shape[0]
        flow = flow.view(layer_num, bs, -1, 4)
        gt_flow = torch.stack([cross_indicies % w, cross_indicies // w], dim=1)

        total_loss_list = []
        for layer_index in range(layer_num):
            cur_flow_list = flow[layer_index]
            spv_flow = cur_flow_list[batch_indicies, self_indicies][:, :2]
            spv_conf = cur_flow_list[batch_indicies, self_indicies][:, 2:]  # [#coarse,2]
            l2_flow_dis = (gt_flow - spv_flow) ** 2  # [#coarse,2]
            total_loss = spv_conf + torch.exp(-spv_conf) * l2_flow_dis  # [#coarse,2]
            total_loss_list.append(total_loss.mean())
        total_loss = torch.stack(total_loss_list, dim=-1) * self.flow_weight
        return total_loss

    def compute_coarse_loss(self, conf, conf_gt, weight=None):
        """Point-wise CE / Focal Loss with 0 / 1 confidence as gt.
        Args:
            conf (torch.Tensor): (N, HW0, HW1) / (N, HW0+1, HW1+1)
            conf_gt (torch.Tensor): (N, HW0, HW1)
            weight (torch.Tensor): (N, HW0, HW1)
        """
        pos_mask, neg_mask = conf_gt == 1, conf_gt == 0
        c_pos_w, c_neg_w = self.c_pos_w, self.c_neg_w
        # corner case: no gt coarse-level match at all
        if not pos_mask.any():  # assign a wrong gt
            pos_mask[0, 0, 0] = True
            if weight is not None:
                weight[0, 0, 0] = 0.0
            c_pos_w = 0.0
        if not neg_mask.any():
            neg_mask[0, 0, 0] = True
            if weight is not None:
                weight[0, 0, 0] = 0.0
            c_neg_w = 0.0

        if self.loss_config["coarse_type"] == "cross_entropy":
            assert not self.sparse_spvs, "Sparse Supervision for cross-entropy not implemented!"
            conf = torch.clamp(conf, 1e-6, 1 - 1e-6)
            loss_pos = -torch.log(conf[pos_mask])
            loss_neg = -torch.log(1 - conf[neg_mask])
            if weight is not None:
                loss_pos = loss_pos * weight[pos_mask]
                loss_neg = loss_neg * weight[neg_mask]
            return c_pos_w * loss_pos.mean() + c_neg_w * loss_neg.mean()
        elif self.loss_config["coarse_type"] == "focal":
            conf = torch.clamp(conf, 1e-6, 1 - 1e-6)
            alpha = self.loss_config["focal_alpha"]
            gamma = self.loss_config["focal_gamma"]

            if self.sparse_spvs:
                pos_conf = conf[:, :-1, :-1][pos_mask] if self.match_type == "sinkhorn" else conf[pos_mask]
                loss_pos = -alpha * torch.pow(1 - pos_conf, gamma) * pos_conf.log()
                # calculate losses for negative samples
                if self.match_type == "sinkhorn":
                    neg0, neg1 = conf_gt.sum(-1) == 0, conf_gt.sum(1) == 0
                    neg_conf = torch.cat([conf[:, :-1, -1][neg0], conf[:, -1, :-1][neg1]], 0)
                    loss_neg = -alpha * torch.pow(1 - neg_conf, gamma) * neg_conf.log()
                else:
                    # These is no dustbin for dual_softmax, so we left unmatchable patches without supervision.
                    # we could also add 'pseudo negtive-samples'
                    pass
                # handle loss weights
                if weight is not None:
                    # Different from dense-spvs, the loss w.r.t. padded regions aren't directly zeroed out,
                    # but only through manually setting corresponding regions in sim_matrix to '-inf'.
                    loss_pos = loss_pos * weight[pos_mask]
                    if self.match_type == "sinkhorn":
                        neg_w0 = (weight.sum(-1) != 0)[neg0]
                        neg_w1 = (weight.sum(1) != 0)[neg1]
                        neg_mask = torch.cat([neg_w0, neg_w1], 0)
                        loss_neg = loss_neg[neg_mask]

                loss = (
                    c_pos_w * loss_pos.mean() + c_neg_w * loss_neg.mean()
                    if self.match_type == "sinkhorn"
                    else c_pos_w * loss_pos.mean()
                )
                return loss
                # positive and negative elements occupy similar propotions. => more balanced loss weights needed
            else:  # dense supervision (in the case of match_type=='sinkhorn', the dustbin is not supervised.)
                loss_pos = -alpha * torch.pow(1 - conf[pos_mask], gamma) * (conf[pos_mask]).log()
                loss_neg = -alpha * torch.pow(conf[neg_mask], gamma) * (1 - conf[neg_mask]).log()
                if weight is not None:
                    loss_pos = loss_pos * weight[pos_mask]
                    loss_neg = loss_neg * weight[neg_mask]
                return c_pos_w * loss_pos.mean() + c_neg_w * loss_neg.mean()
                # each negative element occupy a smaller propotion than positive elements. => higher negative loss weight needed
        else:
            raise ValueError("Unknown coarse loss: {type}".format(type=self.loss_config["coarse_type"]))

    def compute_fine_loss(self, expec_f, expec_f_gt):
        if self.fine_type == "l2_with_std":
            return self._compute_fine_loss_l2_std(expec_f, expec_f_gt)
        elif self.fine_type == "l2":
            return self._compute_fine_loss_l2(expec_f, expec_f_gt)
        else:
            raise NotImplementedError()

    def _compute_fine_loss_l2(self, expec_f, expec_f_gt):
        """
        Args:
            expec_f (torch.Tensor): [M, 2] <x, y>
            expec_f_gt (torch.Tensor): [M, 2] <x, y>
        """
        correct_mask = torch.linalg.norm(expec_f_gt, ord=float("inf"), dim=1) < self.correct_thr
        if correct_mask.sum() == 0:
            if self.training:  # this seldomly happen when training, since we pad prediction with gt
                logger.warning("assign a false supervision to avoid ddp deadlock")
                correct_mask[0] = True
            else:
                return None
        flow_l2 = ((expec_f_gt[correct_mask] - expec_f[correct_mask]) ** 2).sum(-1)
        return flow_l2.mean()

    def _compute_fine_loss_l2_std(self, expec_f, expec_f_gt):
        """
        Args:
            expec_f (torch.Tensor): [M, 3] <x, y, std>
            expec_f_gt (torch.Tensor): [M, 2] <x, y>
        """
        # correct_mask tells you which pair to compute fine-loss
        correct_mask = torch.linalg.norm(expec_f_gt, ord=float("inf"), dim=1) < self.correct_thr

        # use std as weight that measures uncertainty
        std = expec_f[:, 2]
        inverse_std = 1.0 / torch.clamp(std, min=1e-10)
        weight = (inverse_std / torch.mean(inverse_std)).detach()  # avoid minizing loss through increase std

        # corner case: no correct coarse match found
        if not correct_mask.any():
            if self.training:  # this seldomly happen during training, since we pad prediction with gt
                # sometimes there is not coarse-level gt at all.
                logger.warning("assign a false supervision to avoid ddp deadlock")
                correct_mask[0] = True
                weight[0] = 0.0
            else:
                return None

        # l2 loss with std
        flow_l2 = ((expec_f_gt[correct_mask] - expec_f[correct_mask, :2]) ** 2).sum(-1)
        loss = (flow_l2 * weight[correct_mask]).mean()

        return loss

    @torch.no_grad()
    def compute_c_weight(self, data):
        """compute element-wise weights for computing coarse-level loss."""
        if "mask0" in data:
            c_weight = (data["mask0"].flatten(-2)[..., None] * data["mask1"].flatten(-2)[:, None]).float()
        else:
            c_weight = None
        return c_weight

    def forward(self, data):
        """
        Update:
            data (dict): update{
                'loss': [1] the reduced loss across a batch,
                'loss_scalars' (dict): loss scalars for tensorboard_record
            }
        """
        loss_scalars = {}
        # 0. compute element-wise loss weight
        c_weight = self.compute_c_weight(data)

        # 1. coarse-level loss
        loss_c = self.compute_coarse_loss(
            data["conf_matrix_with_bin"] if self.sparse_spvs and self.match_type == "sinkhorn" else data["conf_matrix"],
            data["conf_matrix_gt"],
            weight=c_weight,
        )
        loss = loss_c * self.loss_config["coarse_weight"]
        loss_scalars.update({"loss_c": loss_c.clone().detach().cpu()})

        # 2. fine-level loss
        loss_f = self.compute_fine_loss(data["expec_f"], data["expec_f_gt"])
        if loss_f is not None:
            loss += loss_f * self.loss_config["fine_weight"]
            loss_scalars.update({"loss_f": loss_f.clone().detach().cpu()})
        else:
            assert self.training is False
            loss_scalars.update({"loss_f": torch.tensor(1.0)})  # 1 is the upper bound

        # 3. flow loss
        coarse_corr = [data["spv_b_ids"], data["spv_i_ids"], data["spv_j_ids"]]
        loss_flow = self.compute_flow_loss(
            coarse_corr, data["predict_flow"], data["hw0_c"][0], data["hw0_c"][1], data["hw1_c"][0], data["hw1_c"][1]
        )
        loss_flow = loss_flow * self.flow_weight
        for index, loss_off in enumerate(loss_flow):
            loss_scalars.update({"loss_flow_" + str(index): loss_off.clone().detach().cpu()})  # 1 is the upper bound
            conf = data["predict_flow"][0][:, :, :, :, 2:]
            layer_num = conf.shape[0]
            for layer_index in range(layer_num):
                loss_scalars.update(
                    {"conf_" + str(layer_index): conf[layer_index].mean().clone().detach().cpu()}
                )  # 1 is the upper bound

        loss += loss_flow.sum()
        # print((loss_c * self.loss_config['coarse_weight']).data,loss_flow.data)
        loss_scalars.update({"loss": loss.clone().detach().cpu()})
        data.update({"loss": loss, "loss_scalars": loss_scalars})
