import torch

from ..utils import *
from .pifgsm import PIFGSM

class EMIFGSM(PIFGSM):
    """
    EMI-FGSM Attack
    'Boosting Adversarial Transferability through Enhanced Momentum (BMVC 2021)'(https://arxiv.org/abs/2103.10609)
    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.
        num_sample (int): the number of samples to enhance the momentum.
        radius (float): the relative radius for sampling.
        sample_method (str): the sampling method (linear/uniform/gaussian).
        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
        
    Official arguments:
        epsilon=16/255, alpha=epsilon/epoch=1.6/255, epoch=10, decay=1., num_sample=11, radius=7, sample_method='linear'
    """
    def __init__(self, model, epsilon=16/255, alpha=1.6/255, epoch=10, decay=1., num_sample=11, radius=7, sample_method='linear', 
                targeted=False, random_start=False, norm='linfty', loss='crossentropy', device=None, attack='EMI-FGSM', **kwargs):
        super().__init__(model, epsilon, alpha, epoch, decay, targeted, random_start, norm, loss, device, attack)
        self.num_sample = num_sample
        self.radius = radius
        self.sample_method = sample_method.lower()

    def get_factors(self):
        """
        Generate the sampling factors
        """
        if self.sample_method == 'linear':
            return np.linspace(-self.radius, self.radius, num=self.num_sample)
        elif self.sample_method == 'uniform':
            return np.random.uniform(-self.radius, self.radius, size=self.num_sample)
        elif self.sample_method == 'gaussian':
            return np.clip(np.random.normal(size=self.num_sample)/3, -1, 1)*self.radius
        else:
            raise Exception('Unsupported sampling method {}!'.format(self.sample_method)) 

    def transform(self, x, momentum, **kwargs):
        """
        Admix the input for Admix Attack
        """
        factors = np.linspace(-self.radius, self.radius, num=self.num_sample)
        return torch.concat([x+factor*self.alpha*momentum for factor in factors])

    def get_loss(self, logits, label):
        """
        Calculate the loss
        """
        return self.loss(logits, label.repeat(self.num_sample))