"""
Our awesome editor module in which we have
all the classes that interact with it and
make it possible.

This is the nodes module, in which we have
all the classes that make the concept work.
"""
from yta_editor_nodes.timeline.serial import SerialTimelineNode
from yta_editor_nodes.timeline.parallel import ParallelTimelineNode
from yta_editor_frame.buffer import FrameBuffer
from yta_editor_frame.decorators import force_framebuffer
from yta_editor_time.evaluation_context import EvaluationContext
from yta_validation import PythonValidator
from typing import Union


class TimelineGraph:
    """
    Graph of nodes (serie and parallel) to process
    the input, handle it and generate an output.

    The graph includes a starting (root) node that
    will begin the process, and maybe other nodes
    connected to it as output nodes.

    The nodes will be interconnected, having a
    `t_start` and `t_end`. The sequence of nodes
    determines the order, and the time range if
    they should modify the input or not.
    """

    def __init__(
        self,
        # TODO: Know 'timeline' associated to know fps (?)
        root_node: Union[SerialTimelineNode, ParallelTimelineNode]
    ):
        self.root_node: Union[SerialTimelineNode, ParallelTimelineNode] = root_node
        """
        The root node in which everything starts.
        """
        self._last_node: Union[SerialTimelineNode, ParallelTimelineNode] = root_node
        """
        *For internal use only*

        The last node we have and the one that will return
        the output.
        """
        # TODO: This is meant to be cached and not re-resolve
        # the specifications if it is not needed, but idk why
        # it is not working
        # self._last_context_signature: Union[tuple, None] = None
        # """
        # *For internal use only*

        # A tuple including the `fps` and `total_frames` of
        # the las `evaluation_context` that was used to 
        # resolve the specifications of the nodes.

        # This will force to re-resolve the specifications
        # if it is `None` or some of the parameters in it
        # are different than this one.
        # """

    def _connect_node_to_last(
        self,
        node: Union[SerialTimelineNode, ParallelTimelineNode]
    ) -> 'TimelineGraph':
        """
        *For internal use only*

        Connect the `node` provided to the last node of this
        timeline, and set this new node as the last one.
        """
        self._last_node.connect_to(node)
        self._last_node = node

        return self

    def add_node(
        self,
        node: Union[SerialTimelineNode, ParallelTimelineNode]
    ) -> 'TimelineGraph':
        """
        Add the `node` provided to the nodes list, connecting
        it (in serie mode) to the last one.
        """
        # TODO: Validate node (?)
        return self._connect_node_to_last(node)

    def add_parallel_node(
        self,
        nodes: list[SerialTimelineNode]
    ):
        """
        Create a `ParallelTimelineNode` with the `nodes` provided
        as parameter and connect it (in serie mode) to the last
        one.
        """
        parallel_node = ParallelTimelineNode(
            name = 'invented',
            # TODO: Validate nodes (?)
            nodes = nodes
        )
        
        return self._connect_node_to_last(parallel_node)
    
    def _resolve_specifications(
        self,
        evaluation_context: EvaluationContext
    ) -> 'TimelineGraph':
        """
        *For internal use only*

        Resolve all the specifications of the nodes that are
        attached to this `TimelineGraph` instance, to keep
        the specifications resolved in each of those nodes to
        be able to evaluate and render when active.

        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, by checking the internal variable
        `self._last_evaluation_context_used_to_resolve`.
        """
        node = self.root_node

        while node is not None:
            node._resolve_specification(evaluation_context)
            node = node.output_node

        return self

    # TODO: Maybe rename to 'process' (?)
    @force_framebuffer('inputs')
    def render(
        self,
        inputs: dict[str, Union['FrameBuffer', None]],
        evaluation_context: EvaluationContext,
        output_size: tuple[int, int],
        do_use_gpu: bool = True
    ) -> 'FrameBuffer':
        """
        Render the provided `inputs` with the given
        `evaluation_context` to generate an output according
        to the changes that the nodes that were active made.

        The different nodes of this instance will be applied
        in order and only if they are enabled and active for
        that `evaluation_context`.
        """
        # TODO: I don't know exactly why this is not working
        # current_signature = (
        #     evaluation_context.fps,
        #     evaluation_context.total_frames
        # )

        # do_resolve_specifications = False
        # if self._last_context_signature != current_signature:
        #     do_resolve_specifications = True
        #     self._last_context_signature = current_signature

        do_resolve_specifications = True

        # TODO: Maybe a copy of inputs to not affect by
        # reference (?)
        # We obtain the first available input
        output = next((input for input in inputs.values() if input is not None), None)
        node = self.root_node

        if node is None:
            return output

        while node is not None:
            # TODO: Check if we need to do this always or not
            if do_resolve_specifications:
                node._resolve_specification(evaluation_context)

            if node.is_active_at(
                evaluation_context = evaluation_context
            ):
                output = node.process(
                    inputs = inputs,
                    evaluation_context = evaluation_context,
                    output_size = output_size,
                    do_use_gpu = do_use_gpu
                )

            node = node.output_node

        # We return it as a 'FrameBuffer'
        return FrameBuffer.from_frame(output)
    
__all__ = [
    'TimelineGraph',
    'SerialTimelineNode',
    'ParallelTimelineNode'
]
    

"""
Note for the developer:

A guide to the different types of nodes we will have
when imitating DaVinci Resolve.

TimelineNode (abstract)
├── ProcessorNode (process one single flow)
│   ├── EffectNode (filtros, LUTs, shaders…)
│   └── TransitionNode (mix 2 inputs)
│
├── CompositeNode (combine multiple flows)
│   ├── SerialTimelineNode (one after another)
│   ├── ParallelTimelineNode (different branches simultaneously)
│   └── LayerNode (layers with blending or masks)
│
└── GroupNode (has a subnetwork of nodes)

"""
