from abc import ABC, abstractclassmethod, abstractproperty

from .stream import Stream

from math import tanh

from copy import deepcopy

from random import random


class AbstractTaskScaler(ABC):

    def __init__(self,
                 interval: int = 1,
                 min: int = 1,
                 max: int = 50):
        """
        The function is a constructor that initializes three variables with default values.
        
        Args:
          interval (int): The `interval` parameter is an integer that represents the step size between
        consecutive values in a range. It determines the spacing between the numbers generated within the
        specified range. Defaults to 1
          min (int): The `min` parameter represents the minimum value for the range of numbers. Defaults to
        1
          max (int): The `max` parameter represents the maximum value that can be generated by the code.
        Defaults to 50
        """
        self.interval = interval
        self.min = min
        self.max = max
    
    @abstractclassmethod
    def scale(self, *args) -> list:
        """
        The above function is an abstract class method that scales a list of values.
        """
        pass

    def copy(self):
        """
        The `copy` function returns a deep copy of the object it is called on.
        
        Returns:
          The `copy` method is returning a deep copy of the object `self`.
        """
        return deepcopy(self)

    def clip(self, proposal: int, final: int):
        """
        The `clip` function returns a value that is within the range defined by `self.min` and `self.max`,
        based on the given `proposal` and `final` values.
        
        Args:
          proposal (int): The proposed value that needs to be clipped within the range of `self.min` and
        `self.max`.
          final (int): The `final` parameter represents the final value that you want to clip.
        
        Returns:
          the clipped value of the proposal. If the final value is greater than the maximum value
        (self.max), it returns the difference between the maximum value and the final value plus the
        proposal value. If the final value is less than the minimum value (self.min), it returns the
        difference between the minimum value and the final value plus the proposal value. Otherwise, it
        returns the proposal value
        """
        if final > self.max:
            return self.max - final + proposal
        elif final < self.min:
            return self.min - final + proposal
        else:
            return proposal

    def sleep(self):
        """
        The function returns the value of the interval attribute.
        
        Returns:
          The method is returning the value of the variable `self.interval`.
        """
        return self.interval


class StaticTaskScaler(AbstractTaskScaler):

    def __init__(self,
                 val: int = 1,
                 **kwargs):
        """
        The above function is a constructor that initializes an object with a default value and additional
        keyword arguments.
        
        Args:
          val (int): The `val` parameter is an integer that has a default value of 1. It is used to
        initialize the `val` attribute of the class. Defaults to 1
        """
        super().__init__(**kwargs)

        self.val = val

        self.initialized = False

    def scale(self, runners: list, input: Stream):
        """
        The function "scale" returns the difference between the initial value "val" and the length of the
        "runners" list if the function has not been initialized, otherwise it returns 0.
        
        Args:
          runners (list): The `runners` parameter is a list that represents the current runners in a race.
          input (Stream): The "input" parameter is of type "Stream". It is not clear what the "Stream" type
        represents in this context. It could be a stream of data or events that the function needs to
        process. Without more information, it is difficult to provide a more specific explanation.
        
        Returns:
          If the `self.initialized` flag is `False`, then the function returns `self.val - len(runners)`.
        Otherwise, if the flag is `True`, the function returns `0`.
        """

        if not self.initialized:
            self.initialized = True
            return self.val - len(runners)
        else:
            return 0


class TanhTaskScaler(AbstractTaskScaler):

    def __init__(self,
                 max_step_size: int = 5,
                 sample_window: int = 10,
                 kappa: float = 0.02,
                 buffer: int = 1,
                 **kwargs):
        """
        The above function is a constructor that initializes various parameters and variables for a class.
        
        Args:
          max_step_size (int): The `max_step_size` parameter determines the maximum size of the step that
        can be taken during the optimization process. It limits the magnitude of the update to the model's
        parameters. Defaults to 5
          sample_window (int): The `sample_window` parameter determines the number of previous samples to
        consider when calculating the gradient. Defaults to 10
          kappa (float): The `kappa` parameter is a float value that determines the step size adjustment
        factor. It is used in the calculation of the step size for gradient descent. A higher value of
        `kappa` will result in a smaller step size, while a lower value will result in a larger step size.
          buffer (int): The `buffer` parameter is an integer that determines the number of previous samples
        to keep in memory. It is used to calculate the gradient of the samples. Defaults to 1
        """
        super().__init__(**kwargs)

        self.max_step_size = max_step_size
        self.sample_window = sample_window
        self.kappa = kappa
        self.buffer = buffer

        self.samples = []
        self.gradient = 0

    def sample(self, qsize: int):
        """
        The `sample` function appends a value to a list and removes the first element if the list exceeds a
        certain length.
        
        Args:
          qsize (int): The parameter `qsize` represents the size of a queue.
        """

        self.samples.append(qsize)
        if len(self.samples) > self.sample_window:
            self.samples.pop(0)

    def get_gradient(self):
        """
        The function calculates the average gradient of a list of samples, or returns 0 if there are not
        enough samples.
        
        Returns:
          the average gradient of the samples. If the number of samples is less than the sample window, it
        returns 0.
        """
        if len(self.samples) >= self.sample_window:
            gradients = [ s - self.samples[i] for i, s in enumerate(self.samples[1:])]
            return sum(gradients) / (len(self.samples) - 1)
        else:
            return 0

    def scale(self, runners: list, input: Stream):
        """
        The `scale` function calculates a proposal value based on the number of runners, the size of the
        input queue, and the gradient, and returns the clipped value.
        
        Args:
          runners (list): A list of runners. It is not clear what the runners represent in this context. It
        could be a list of objects or some other data structure.
          input (Stream): The `input` parameter is a `Stream` object, which is likely a queue or a stream of
        data that the `scale` method is processing.
        
        Returns:
          the value of `val`.
        """
        qsize = input.queue.qsize()
        self.sample(qsize)
        gradient = self.get_gradient()

        current = len(runners)
        proposal = 0
        if gradient != 0.0:
            proposal = int(self.max_step_size * tanh((qsize + gradient * self.sample_window) * self.kappa))
        elif qsize > 0:
            proposal = self.buffer

        val = self.clip(proposal, current+proposal)

        return val


DefaultTaskScaler = StaticTaskScaler