import torch
import torch.nn as nn
import itertools
import math

class CharbonnierLoss(nn.Module):
    """Charbonnier Loss (L1)"""

    def __init__(self, eps=1e-6):
        super(CharbonnierLoss, self).__init__()
        self.eps = eps

    def forward(self, x, y):
        diff = x - y
        loss = torch.sum(torch.sqrt(diff * diff + self.eps))
        return loss


class SynergyLoss(nn.Module):
    def __init__(self, eps=1e-6, use_L1 =True):
        super(SynergyLoss, self).__init__()
        self.eps = eps
        self.loss = nn.L1Loss() if use_L1 else nn.MSELoss()

    def forward(self, x, y, weight=0.1):
        x.append(y)
        n = len(x)
        loss = 0
        for i, j in itertools.combinations(range(n),2):
            l = (j)/(n-1)*math.pow(weight,j-i-1)*self.loss(x[i],x[j])
            loss += l
        return loss

#Hard example mining loss
class HEMLoss(nn.Module):
    def __init__(self, portion = 0.3, weight = 0.1, use_L1 = True):
        super(HEMLoss,self).__init__()
        self.portion = portion
        self.weight = weight
        self.unweighted_loss = nn.L1Loss(reduction='none') if use_L1 else nn.MSELoss(reduction='none')

    def forward(self, input, target):
        b, c, h, w = input.shape
        loss = self.unweighted_loss(input,target).view(b,c,h*w)
        if c != 1:
            loss = torch.mean(loss,dim=1,keepdim=True)
        loss_s,_ = torch.sort(loss)
        loss_s[..., :-int(self.portion*w*h)]=0
        return torch.mean(loss + self.weight*loss_s)

# Define GAN loss: [vanilla | lsgan | wgan-gp]
class GANLoss(nn.Module):
    def __init__(self, gan_type, real_label_val=1.0, fake_label_val=0.0):
        super(GANLoss, self).__init__()
        self.gan_type = gan_type.lower()
        self.real_label_val = real_label_val
        self.fake_label_val = fake_label_val

        if self.gan_type == 'gan' or self.gan_type == 'ragan':
            self.loss = nn.BCEWithLogitsLoss()
        elif self.gan_type == 'lsgan':
            self.loss = nn.MSELoss()
        elif self.gan_type == 'wgan-gp':

            def wgan_loss(input, target):
                # target is boolean
                return -1 * input.mean() if target else input.mean()

            self.loss = wgan_loss
        else:
            raise NotImplementedError('GAN type [{:s}] is not found'.format(self.gan_type))

    def get_target_label(self, input, target_is_real):
        if self.gan_type == 'wgan-gp':
            return target_is_real
        if target_is_real:
            return torch.empty_like(input).fill_(self.real_label_val)
        else:
            return torch.empty_like(input).fill_(self.fake_label_val)

    def forward(self, input, target_is_real):
        target_label = self.get_target_label(input, target_is_real)
        loss = self.loss(input, target_label)
        return loss


class GradientPenaltyLoss(nn.Module):
    def __init__(self, device=torch.device('cpu')):
        super(GradientPenaltyLoss, self).__init__()
        self.register_buffer('grad_outputs', torch.Tensor())
        self.grad_outputs = self.grad_outputs.to(device)

    def get_grad_outputs(self, input):
        if self.grad_outputs.size() != input.size():
            self.grad_outputs.resize_(input.size()).fill_(1.0)
        return self.grad_outputs

    def forward(self, interp, interp_crit):
        grad_outputs = self.get_grad_outputs(interp_crit)
        grad_interp = torch.autograd.grad(outputs=interp_crit, inputs=interp,
                                          grad_outputs=grad_outputs, create_graph=True,
                                          retain_graph=True, only_inputs=True)[0]
        grad_interp = grad_interp.view(grad_interp.size(0), -1)
        grad_interp_norm = grad_interp.norm(2, dim=1)

        loss = ((grad_interp_norm - 1)**2).mean()
        return loss

if __name__ == "__main__":
    a = torch.randn(1,1,3,3)
    b = torch.randn(1,1,3,3)
    l = CharbonnierLoss()
    l1 = torch.nn.L1Loss()
    print(l1(a,b))
    print(l(a,b))