"""
Module to include the functionality related
to progressions.
"""
from yta_math_easings.abstract import EasingFunction
from yta_math_easings.enums import EasingFunctionName
from yta_validation import PythonValidator
from yta_validation.parameter import ParameterValidator
from typing import Union

import numpy as np


# TODO: Maybe make 'easings' optional (?)
class Progression:
    """
    Class to represent a progression of `n` values from
    `start_value` to `end_value` (including both in that
    `n` number of values), with an associated rate
    function to calculate the values between those
    'start_value' and `end_value` limits.

    The progression can be ascending or descending.

    This class is useful to obtain each individual value
    that must be applied in different use cases.

    The values of the specific progression can be accessed
    by the `.values` property. These values will be cached
    and re-calculated only if some parameter has changed
    since the last time they were accessed.
    """

    @property
    def start_value(
        self
    ):
        """
        The first value of the progression, that acts as a
        limit.
        """
        return self._start_value
    
    @start_value.setter
    def start_value(
        self,
        value: float
    ):
        """
        Set the `start_value` parameter and flag the 
        progression to be recalculated if needed due to the
        change.
        """
        ParameterValidator.validate_mandatory_number('value', value)

        if (
            not hasattr(self, '_start_value') or
            self._start_value != value
        ):
            self._values = None
            self._start_value = value

    @property
    def end_value(
        self
    ):
        """
        The last value of the progression, that acts as a
        limit.
        """
        return self._end_value
    
    @end_value.setter
    def end_value(
        self,
        value: float
    ):
        """
        Set the `end_value` parameter and flag the 
        progression to be recalculated if needed due to the
        change.
        """
        ParameterValidator.validate_mandatory_number('value', value)

        if (
            not hasattr(self, '_end_value') or
            self._end_value != value
        ):
            self._values = None
            self._end_value = value

    @property
    def number_of_values(
        self
    ):
        """
        The amount of values that will be calculated for
        the progression, including the limits (`start_value`
        and `end_value`).
        """
        return self._end_value
    
    @number_of_values.setter
    def number_of_values(
        self,
        value: float
    ):
        """
        Set the `number_of_values` parameter and flag the 
        progression to be recalculated if needed due to the
        change.
        """
        ParameterValidator.validate_mandatory_positive_number('value', value)

        if (
            not hasattr(self, '_number_of_values') or
            self._number_of_values != value
        ):
            self._values = None
            self._number_of_values = value

    @property
    def easing_function(
        self
    ):
        """
        The easing function that will be applied to calculate
        the `n` values between `start` and `end`, including
        both limits.
        """
        return self._easing_function
    
    @easing_function.setter
    def easing_function(
        self,
        value: Union['EasingFunction', None]
    ):
        """
        Set the `easing_function` parameter and flag the 
        progression to be recalculated if needed due to the
        change.
        """
        ParameterValidator.validate_subclass_of('easing_function', value, 'EasingFunction')

        if self._easing_function != value:
            self._values = None
            self._easing_function = value

    @property
    def values(
        self
    ):
        """
        The list of `n` values (including `start_value` and
        `end_value`) between those limits and according to
        the provided `easing_function`.
        """
        if self._values is None:
            start_value, end_value = (
                (self.end_value, self.start)
                if self.start_value > self.end_value else
                (self.start_value, self.end_value)
            )

            from yta_math_normalization.value_normalizable import ValueNormalizable

            values = [
                float(ValueNormalizable.init_from_value_normalized(
                    value_normalized = normalized_value,
                    lower_limit = start_value,
                    upper_limit = end_value
                ))
                for normalized_value in Progression.get_n_eased_values(
                    n = self.number_of_values,
                    easing_function = self._easing_function
                )
            ]

            # If limits are switched, we need to recalculate it
            # TODO: Refactor this if possible
            values = (
                [
                    self.end_value - value + self.start_value
                    for value in values
                ]
                if self.start_value > self.end_value else
                values
            )

            self._values = values

        return self._values

    def __init__(
        self,
        start_value: float,
        end_value: float,
        number_of_values: int,
        easing_function_name: EasingFunctionName = EasingFunctionName.LINEAR
    ):
        easing_function_name: EasingFunctionName = EasingFunctionName.to_enum(easing_function_name)

        self.start_value: float = start_value
        """
        *For internal use only*

        The starting value of the progression, that acts as a
        limit and is the first value in the progression.
        """
        self.end_value: float = end_value
        """
        *For internal use only*

        The ending value of the progression, that acts as a
        limit and is the last value in the progression.
        """
        self.number_of_values: int = number_of_values
        """
        The number of values in the progression, including
        the `start_value` and `end_value`.
        """

        self._easing_function: EasingFunction = EasingFunction.get(easing_function_name)()
        """
        *For internal use only*

        The easing function to apply for the calculations.
        """

    # TODO: Migrate the 'get_n_equally_distributed_normalized_values'
    # and refactor if needed
    # TODO: Migrate the 'get_n_normalized_values' and refactor
    # if needed

    @staticmethod
    def get_n_equally_distributed_normalized_values(
        n: int
    ):
        """
        Get a list containing `n` equally distributed (and
        normalized) values between 0.0 and 1.0. The `n`
        value must be equal or greater than 2.

        This is returning a list of `d` values. Normalized
        values representing the progress in the easing
        function.

        If `n=6`:
        - `[0.0, 0.2, 0.4, 0.6, 0.8, 1.0]`
        """
        return Progression.get_n_eased_values(
            n = n,
            easing_function = EasingFunctionName.LINEAR
        )
        # ParameterValidator.validate_mandatory_positive_int('n', n, do_include_zero = False)

        # if n < 2:
        #     raise Exception('The provided "n" parameter is not greater or equal than 2.')

        # return np.linspace(0.0, 1.0, n).tolist()
    
    @staticmethod
    def get_n_eased_values(
        n: int,
        easing_function: Union[EasingFunction, EasingFunctionName] = EasingFunctionName.default()
    ):
        """
        Get a list including the `0.0` and `1.0` limits
        and `n - 2` values in between, according to the
        `easing_function` provided (that can be the name
        or the function itself).

        The values could surpass the limits due to 
        overshoot according to the easing function
        applied.

        This is returning the `n` normalized values.

        If `n = 6` and `rate_function = ease_in_quad`:
        - `[0.0, 0.04, 0.16, 0.36, 0.64, 1.0]`
        """
        ParameterValidator.validate_mandatory_positive_int('n', n, do_include_zero = False)

        if n < 2:
            raise Exception('The provided "n" parameter is not greater or equal than 2.')
        
        easing_function = (
            EasingFunction.get(EasingFunctionName.to_enum(easing_function))()
            if not PythonValidator.is_subclass_of(EasingFunction) else
            easing_function
        )

        return [
            easing_function.ease(normalized_value)
            for normalized_value in Progression.get_n_equally_distributed_normalized_values(n)
        ]