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 frames. This
        can be calculated only if the specification has been
        resolved.

        The formula:
        - `self._specification_resolved.end_frame - self._specification_resolved.start_frame`
        """
        return self._specification_resolved.end_frame - self._specification_resolved.start_frame

    def __init__(
        self,
        name: str
    ):
        ParameterValidator.validate_mandatory_string('name', name, do_accept_empty = False)

        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.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.
        """

    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
    
    @abstractmethod
    def _resolve_specification(
        self,
        evaluation_context: EvaluationContext
    ) -> 'TimelineNode':
        """
        *For internal use only*

        Resolve the specification of this node by using the
        `evaluation_context` provided, to be able to know
        when the node is active and when we should process
        the input.

        This method must be called each time we modify
        something that affects to the `duration` or other
        field that makes the specifications resolved be
        different.
        """
        pass
    
    @abstractmethod
    def is_active_at(
        self,
        evaluation_context: EvaluationContext
    ) -> bool:
        """
        Check if any internal node is active and needs
        to be applied for the `evaluation_context`
        provided.
        """
        pass

    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
    
    @abstractmethod
    def process(
        self,
        inputs: dict[str, Union['np.ndarray', 'moderngl.Texture']],
        evaluation_context: 'EvaluationContext',
        output_size: tuple[int, int],
        do_use_gpu: bool = True,
        # 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.
        """
        pass