from abc import abstractmethod
from enum import Enum

import torch
import torch.distributed as dist
from torch import Tensor

from colossalai.amp.naive_amp.grad_scaler import DynamicGradScaler
from colossalai.utils import get_current_device

from .base import MixedPrecisionMixin


class OptimState(Enum):
    SCALED = 0
    UNSCALED = 1


class FP16MixedPrecisionMixin(MixedPrecisionMixin):
    dtype = torch.float16

    def __init__(self,
                 initial_scale: float = 2**16,
                 min_scale: float = 1,
                 growth_factor: float = 2,
                 backoff_factor: float = 0.5,
                 growth_interval: int = 1000,
                 hysteresis: int = 2,
                 max_scale: float = 2**32) -> None:
        super().__init__()
        self.grad_scaler = DynamicGradScaler(initial_scale=initial_scale,
                                             min_scale=min_scale,
                                             growth_factor=growth_factor,
                                             backoff_factor=backoff_factor,
                                             growth_interval=growth_interval,
                                             hysteresis=hysteresis,
                                             max_scale=max_scale)
        self.optim_state = OptimState.UNSCALED
        self.found_overflow = torch.zeros(1, dtype=torch.float, device=get_current_device())

    @property
    def loss_scale(self) -> float:
        return self.grad_scaler.scale.item()

    @abstractmethod
    def check_local_overflow(self) -> bool:
        """Check whether there is overflow in the local process. This method should be implemented by subclasses.

        Returns:
            bool: Whether there is overflow in the local process.
        """
        pass

    def check_overflow(self) -> bool:
        # clear previous overflow record
        self.found_overflow.fill_(0.0)
        if self.check_local_overflow():
            self.found_overflow.fill_(1.0)
        dist.all_reduce(self.found_overflow, op=dist.ReduceOp.MAX)
        return self.found_overflow.item() > 0

    def pre_backward(self, loss: Tensor) -> Tensor:
        loss = self.loss_scale * loss
        self.optim_state = OptimState.SCALED
        return loss

    def pre_backward_by_grad(self, tensor: Tensor, grad: Tensor) -> Tensor:
        self.optim_state = OptimState.SCALED
        return grad

    def should_skip_step(self) -> bool:
        found_inf = self.check_overflow()
        self.grad_scaler.update(found_inf)
        if found_inf:
            self.optim_state = OptimState.UNSCALED
        return found_inf

    def pre_zero_grad(self) -> None:
        pass

    def get_grad_div_scale(self) -> float:
        assert self.optim_state == OptimState.SCALED, 'grads should be scaled before clipping'
        self.optim_state = OptimState.UNSCALED
        return self.loss_scale
