from yta_editor_parameters.abstract import VideoEditorParameter
from yta_editor_time.specifications import FrameIndexSpecification, ProgressSpecification, TimeSpecification
from yta_editor_time.evaluation_context import EvaluationContext
from yta_validation.parameter import ParameterValidator
from abc import ABC, abstractmethod
from typing import Union


class TimelineNode(ABC):
    """
    *Abstract class*

    *This class has to be inherited by any class that is
    able to handle some input to obtain an output as a
    result*

    The abstract class of a TimelineNode, which is the
    entity that is able to process some input to return
    an output that can be sent to the next node, and able
    to connect (by storing the references) to the other
    nodes.
    """

    @property
    def has_output_node(
        self
    ) -> bool:
        """
        Boolean flag to indicate if this node has an
        output node or not.
        """
        return self.output_node is not None
    
    @property
    def has_input_node(
        self
    ) -> bool:
        """
        Boolean flag to indicate if this node has an
        input node or not.
        """
        return self.input_node is not None
    
    @property
    def duration(
        self
    ) -> float:
        """
        The duration of the timeline node, in seconds.

        The formula:
        - `self.t_end - self.t_start`
        """
        return self.t_end - self.t_start

    def __init__(
        self,
        # TODO: We need a reference to the Timeline to know fps
        name: str,
        # TODO: By now I'm using 'TimeSpecification' because
        # of how it was built before, but I should accept any
        # of them
        specification: TimeSpecification,
        parameters: dict[str, VideoEditorParameter] = {}
    ):
        ParameterValidator.validate_mandatory_string('name', name, do_accept_empty = False)
        # TODO: Validate 'parameters' as dict of 
        # 'str': VideoEditorParameter

        self.name: str = name
        """
        Just a simple name for the node.
        """
        self.is_enabled: bool = True
        """
        Boolean flag to indicate if the node is enabled or not,
        which means that it will not be working if not enabled.
        Basically, if it is on or off.
        """
        self.specification: TimeSpecification = specification
        """
        The specification of when we should apply the settings,
        that can be based on frame index, time or normalized
        progress.
        """
        self.parameters: dict[str, VideoEditorParameter] = parameters
        """
        A dict including the `key` of the parameter and
        its value to be calculated.
        """

        self.input_node: Union['TimelineNode', None] = None
        """
        The node that is connected to this one as an
        input, so the output of that previous node will
        be passed as the input for this one.
        """
        self.output_node: Union['TimelineNode', None] = None
        """
        This node is connected as the input of another
        one that is after this. The output of this one
        will be sent as the input for that one.
        """
        self._cached_output: Union['np.ndarray', 'moderngl.Texture', None] = None
        """
        The output, but cached, so when it is generated
        it is stored here.
        """

    # TODO: Should we transform this into a progress (?)
    def _get_t(
        self,
        t: Union[int, float, 'Fraction']
    ) -> float:
        """
        Obtain the `t` time moment relative to the
        effect duration.

        Imagine `start=3` and `end=5`, and we receive 
        a `t=4`. It is inside the range, so we have
        to apply the effect, but as the effect
        lasts from second 3 to second 5 (`duration=2`),
        the `t=4` is actually a `t=1` for the effect
        because it is the time elapsed since the
        effect started being applied, that was on the 
        second 3.

        The formula:
        - `t - self.specification.start_t`
        """
        return float(t) - float(self.specification.start_t)
    
    def enable(
        self
    ) -> 'TimelineNode':
        """
        Enable the node by setting the internal flag to
        `True`, which means that will be working.
        """
        self.is_enabled = True

        return self
    
    def disable(
        self
    ) -> 'Timeline':
        """
        Disable the node by setting the internal flag
        to `False`, which means that will be working not.
        """
        self.is_enabled = False

        return self
    
    def is_active_at(
        self,
        t_timeline: float
    ) -> bool:
        """
        Flag to indicate if the `t_timeline` time moment
        provided is in the range of this TimedNode instance, 
        which means between the `t_start` and the `t_end`,
        and if it is activated or not (on or off).

        The formula:
        - `t_start <= t_timeline < t_end`
        """
        # TODO: We need to resolve with the new methods
        # that are in 'yta_editor_time'
        from yta_editor_time.specifications.resolver import SpecificationResolver

        # TODO: We need the real values...
        fps = 60.0
        duration = 2.0

        frame_index_specification_resolved = SpecificationResolver.resolve(
            specification = self.specification,
            fps = fps,
            duration = duration
        )

        # By now we transform it into 't' to work
        # as we were used to

        t_start = int(frame_index_specification_resolved.start_frame) / fps
        t_end = int(frame_index_specification_resolved.end_frame) / fps

        return (
            self.is_enabled and
            t_start <= float(t_timeline) < t_end
        )

        return (
            self.is_enabled and
            self.t_start <= t_timeline < self.t_end
        )

    def connect_to(
        self,
        node: 'TimelineNode'
    ) -> 'TimelineNode':
        """
        Connect the `node` provided to this one as an
        output, and also this one as an input of the
        `node` provided.

        TODO: The connection has to be done with another
        class that inherits from this 'TimelineNode'.
        """
        self.output_node = node
        node.input_node = self

        return self

    def clear_cache(
        self
    ) -> 'TimelineNode':
        """
        Clear the cache of this node, but also for
        the nodes that are outputs of this one, as
        the results would change.
        """
        # TODO: I think this cache is not valid because
        # we are using a `t` and different inputs...
        self._cached_output = None

        self.output_node.clear_cache()

        return self
    
    # TODO: Review this and with the context
    def _evaluate_parameters(
        self,
        evaluation_context: 'EvaluationContext'
    ) -> dict[str, any]:
        """
        *For internal use only*

        Evaluate all the parameters for the given `t` time
        moment and obtain the values for that moment.
        """
        parameters_values = {}

        # t_local_normalized = self._get_t(t_timeline) / self.duration

        # TODO: Create the context as we do in 'yta_editor'
        # from yta_editor_parameters.context.utils import get_evaluation_context

        # evaluation_context = get_evaluation_context(
        #     t_track = t_timeline,
        #     t_start_item = self.t_start,
        #     t_end_item = self.t_end,
        #     # TODO: Need reference to Timeline
        #     # fps = self._timeline.fps,
        #     # track_duration = self._timeline.duration
        # )

        for key, parameter in self.parameters.items():
            parameters_values[key] = parameter.evaluate(
                evaluation_context = evaluation_context
            )

        return parameters_values
    
    def process(
        self,
        inputs: dict[str, Union['np.ndarray', 'moderngl.Texture']],
        evaluation_context: 'EvaluationContext',
        # TODO: I think this has to be gone
        **kwargs
    ) -> Union['moderngl.Texture', 'np.ndarray']:
        """
        Process the `inputs` by using the `t_timeline` time
        moment provided, that could be necessary for the
        node if it is a dynamic one that depends on this
        value.
        """
        # TODO: Maybe a copy of inputs (?)

        """
        TODO: This condition is evalulated in the
        TimelineGraph this node belongs to, but we need
        the `t` for some dynamic NodeProcessors
        """
        # if not self.is_active_at(t_timeline):
        #     return result

        # TODO: With simple nodes we don't need the 't' and
        # this is failing if we don't put the **kwargs in
        # our CPU or GPU processors
        # This will be ignored if no needed as we use **kwargs
        parameters_values = self._evaluate_parameters(
            evaluation_context = evaluation_context
        )

        # This should be 'float32' or 'Texture'
        return self._process(
            inputs = inputs,
            evaluation_context = evaluation_context,
            **parameters_values
            # **kwargs
        )

    @abstractmethod
    def _process(
        self,
        inputs: dict[str, Union['np.ndarray', 'moderngl.Texture']],
        evaluation_context: 'EvaluationContext',
        **kwargs
    ) -> Union['np.ndarray', 'moderngl.Texture']:
        """
        Process the `inputs` and obtain a result as the
        output.

        This method must be overwritten for every special
        class implementation.
        """
        pass