from yta_editor_nodes.timeline import TimelineNode
from yta_editor_parameters.values.abstract import VideoEditorParameterValue
from yta_editor_parameters.values.utils import parse_parameters
from yta_editor_frame.decorators import force_framebuffer
from yta_editor_nodes.timeline.utils import evaluate_parameters
from yta_editor_time.specifications import FrameIndexSpecification, ProgressSpecification, TimeSpecification
from yta_editor_time.specifications.resolved import FrameIndexSpecificationResolved
from yta_editor_time.specifications.resolver import SpecificationResolver
from yta_editor_time.evaluation_context import EvaluationContext
from typing import Union


class SerialTimelineNode(TimelineNode):
    """
    A node that is executed from a specific input and
    generates a single output that is sent to the next
    node.

    Check the `.process` method definition of the node
    you add to see the parameters you need to execute
    it and pass them through the `parameters`
    parameter.
    """

    @property
    def is_gpu_available(
        self
    ) -> bool:
        """
        Boolean flag to indicate if the GPU is available
        in the node or not.
        """
        return self.node.is_gpu_available

    def __init__(
        self,
        # TODO: Put the correct class
        # TODO: Maybe I need a shortcut to 'is_gpu_available'...
        node: 'NodeProcessor',
        name: str,
        specification: Union[FrameIndexSpecification, ProgressSpecification, TimeSpecification],
        parameters: dict[str, VideoEditorParameterValue] = {}
    ):
        # ParameterValidator.validate_mandatory_dict_of('parameters', parameters, VideoEditorParameterValue)
        # Validate the 'parameters' by accepting
        # basic and non iterable types. This will raise
        # exception if not valid
        parameters = parse_parameters(parameters)

        self.node: 'NodeProcessor' = node
        """
        The node to execute and to obtain the output from.
        """
        self.parameters: dict[str, VideoEditorParameterValue] = parameters
        """
        A dict including the `key` of the parameter and
        its value to be calculated.
        """
        self.specification: Union[FrameIndexSpecification, ProgressSpecification, TimeSpecification] = specification
        """
        The specification of when we should apply the settings,
        that can be based on frame index, time or normalized
        progress.
        """
        self._specification_resolved: Union[FrameIndexSpecificationResolved, None] = None
        """
        *For internal use only*

        The specification that has been resolved according to
        an specific evaluation context. This is the one that
        has to be used to check if the node is active and the
        input provided has to be processed.
        """

        super().__init__(
            name = name
        )

    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.
        """
        self._specification_resolved = SpecificationResolver.resolve(
            specification = self.specification,
            fps = evaluation_context.fps,
            duration = evaluation_context.duration,
            total_frames = evaluation_context.total_frames
        )

        return self
    
    def is_active_at(
        self,
        evaluation_context: EvaluationContext
    ) -> bool:
        """
        Flag to indicate if the `evaluation_context`
        provided is in the range of this TimedNode instance, 
        which means that it is in between the limits of this
        `TimelineNode` instance.

        The formula:
        - `start_frame <= evaluation_context.frame_index < end_frame`
        """
        return (
            self.is_enabled and
            self._specification_resolved.start_frame <= evaluation_context.frame_index < self._specification_resolved.end_frame
        )

    @force_framebuffer('inputs')
    def process(
        self,
        inputs: dict[str, 'FrameBuffer'],
        evaluation_context: 'EvaluationContext',
        output_size: tuple[int, int],
        do_use_gpu: bool = True,
        **kwargs
    ) -> Union['np.ndarray', 'moderngl.Texture']:
        """
        Process the `inputs` provided and obtain a result
        as the output by using the `evaluation_context`
        given.
        """
        # This method is only called if it is active
        self._resolve_specification(evaluation_context)

        # Create a new 'EvaluationContext' to propagate
        evaluation_context = EvaluationContext(
            fps = evaluation_context.fps,
            # TODO: Is this 'total_frames' ok? Does it  affect (?)
            total_frames = evaluation_context.total_frames,
            frame_index = evaluation_context.frame_index - self._specification_resolved.start_frame
        )

        # TODO: Do we evaluate with the new one or with the old?
        evaluated_parameters = evaluate_parameters(
            parameters = self.parameters,
            evaluation_context = evaluation_context
        )

        # Combine but giving priority to **kwargs
        evaluated_parameters = {**evaluated_parameters, **kwargs}

        return self.node.process(
            inputs = inputs,
            evaluation_context = evaluation_context,
            output_size = output_size,
            do_use_gpu = do_use_gpu,
            parameters = evaluated_parameters
        )