import torch

from ..utils import *
from .nifgsm import NIFGSM
import torch.nn.functional as F


def exp(val):
    return 2**val
def ln(val):
    # return np.log(val)
    # print('change ln')
    return np.log(val)
def power(val, factor=2):
    return val ** factor

def linear(val, factor=1):
    return val * factor

def normalize(fn, len):
    val = np.array([fn(len-i) for i in range(len)])
    summation = np.sum(val)
    return 16/255*(val / summation).astype(np.float32)[::-1]

def pvalue(val,epsion=8/255, order=0.6):
    return epsion/(val+1)**order

def identity(val):
    return 1.


class PIFGSM(NIFGSM):
    """
    PI-FGSM Attack
    'Patch-wise Attack for Fooling Deep Neural Network (ECCV 2020)'(https://arxiv.org/abs/2007.06765)
    Arguments:
        model (torch.nn.Module): the surrogate model for attack.
        epsilon (float): the perturbation budget.
        alpha (float): the step size.
        epoch (int): the number of iterations.
        decay (float): the decay factor for momentum calculation.
        targeted (bool): targeted/untargeted attack.
        random_start (bool): whether using random initialization for delta.
        norm (str): the norm of perturbation, l2/linfty.
        loss (str): the loss function.
        device (torch.device): the device for data. If it is None, the device would be same as model
        
    """
    
    def __init__(self, model, epsilon=16/255, alpha=1.6/255, epoch=10, decay=1., targeted=False, random_start=False, 
                norm='linfty', loss='crossentropy', device=None, attack='PI-FGSM', **kwargs):
        super().__init__(model, epsilon, alpha, epoch, decay, targeted, random_start, norm, loss, device, attack, **kwargs)
        self.kern_size = 3
        self.alpha_beta = self.alpha * epoch
        self.gamma = self.alpha_beta
        P_kern = self.project_kern(self.kern_size)
        self.device = "cuda:0"# next(model.parameters()).device if device is None else device
        self.w = torch.from_numpy(P_kern).to(self.device) # project kernel
        self.ai = normalize(linear, self.epoch)

    
    def project_kern(self, kern_size):
        kern = np.ones((kern_size, kern_size), dtype=np.float32) / (kern_size ** 2 - 1)
        kern[kern_size // 2, kern_size // 2] = 0.0
        kern = kern.astype(np.float32)
        stack_kern = np.stack([kern, kern, kern])
        stack_kern = np.expand_dims(stack_kern, 1)
        return stack_kern
    
    def project_noise(self, x, stack_kern, padding_size):
        # x = tf.pad(x, [[0,0],[kern_size,kern_size],[kern_size,kern_size],[0,0]], "CONSTANT")
        x = F.conv2d(x, stack_kern, padding=(padding_size, padding_size), groups=3)
        return x
    
    
    def update_delta(self, delta, data, grad,  **kwargs):
        if self.norm == 'linfty':
            delta = torch.clamp(delta +  grad, -self.epsilon, self.epsilon)
        else:
            grad_norm = torch.norm(grad.view(grad.size(0), -1), dim=1).view(-1, 1, 1, 1)
            scaled_grad = grad / (grad_norm + 1e-20)
            delta = (delta + scaled_grad ).view(delta.size(0), -1).renorm(p=2, dim=0, maxnorm=self.epsilon).view_as(delta)
        delta = clamp(delta, img_min-data, img_max-data)
        return delta

    def forward(self, data, label, **kwargs):
        """
        Overriden for PI-FGSM
        Arguments:
            data: (N, C, H, W) tensor for input images
            labels: (N,) tensor for ground-truth labels if untargetd, otherwise targeted labels
        """
        data = data.clone().detach().to(self.device)
        label = label.clone().detach().to(self.device)

        # Initialize adversarial perturbation
        delta = self.init_delta(data)
        delta.requires_grad = True
        grad = 0.

        amplification = 0.0 
        momentum = 0.0

        for _ in range(self.epoch):
            # Obtain the output
            logits = self.get_logits(self.transform(data+delta, momentum=momentum))
            self.alpha_beta = self.ai[_] * self.epoch
            # Calculate the loss
            loss = self.get_loss(logits, label)
            penalty = self.penalty(delta)
            loss = loss + self.penalty_factor * penalty

            # Calculate the gradients
            grad = self.get_grad(loss, delta)
            momentum = self.get_momentum(grad, momentum)
            # Calculate the cut noise
            amplification += self.alpha_beta*self.epoch * momentum.sign()
            cut_noise = torch.clamp(abs(amplification) - self.epsilon, 0., 10000.0) * torch.sign(amplification)
            projection = self.alpha_beta*self.epoch * torch.sign(self.project_noise(cut_noise, self.w, self.kern_size//2))
            amplification += projection
            # sum_grad = torch.sum(grad)
            # second_grad = torch.autograd.grad(sum_grad, delta, create_graph=True)[0]
            delta = self.update_delta(delta, data, self.alpha_beta*self.epoch * momentum.sign() + projection)
            # print('iter: ', _,'mean: ', torch.mean(delta),'std: ', torch.std(delta),'grad l2 norm: ', torch.norm(grad,p=2),'2-th order grad: mean:', torch.mean(second_grad),'std: ', torch.std(second_grad))
        # exit()

        return delta.detach()
    
