from yta_editor_nodes import TimelineGraph, SerialTimelineNode
from yta_editor_nodes.compositor import DisplacementWithRotationNodeCompositor
from yta_editor_parameters.values.abstract import VideoEditorParameterValue
from yta_editor_parameters.values import ConstantVideoEditorParameterValue
from yta_editor_parameters.values.utils import parse_parameter, is_this_constant_value
from yta_editor_time.evaluation_context import EvaluationContext
from yta_editor_time.specifications import FrameIndexSpecification, ProgressSpecification, TimeSpecification
from yta_validation.parameter import ParameterValidator
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Union

import numpy as np


@dataclass
class ParameterValueUnset:
    """
    A dataclass just to indicate that a parameter value
    has not been set but is not None.
    """

    pass

VALUE_UNSET = ParameterValueUnset()
"""
A parameter value that has not been set yet and
is different than being `None`.
"""


@dataclass
class AudioTransformState:
    """
    *Dataclass*

    A dataclass to represent the information about the
    state of a transformation we should apply to 
    transform an audio, that has been calculated for a
    specific `t` time moment.
    """

    def __init__(
        self,
        volume: float = 0.0
    ):
        self.volume: float = volume
        """
        The volume to apply at the audio at that moment.
        """

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

    Abstract class to be inherited by the `Transform`
    implementation related to audio and video.
    """

    @abstractmethod
    def apply(
        self,
        # TODO: Set the type
        frame: any,
        evaluation_context: EvaluationContext
    # TODO: Set the type
    ) -> any:
        """
        Apply the transformation to the given `frame` at the also
        provided `t` (which will be used to obtain the transform
        state to apply).
        """
        pass

class VideoTransform(Transform):
    """
    The transformation conditions to define how we want
    to modify the different time moments of a video in
    the timeline.

    This is attached to a timeline video track item and
    will be applied on it. This is useful to apply the
    basic modifications.

    These values are time functions, and the values
    change according to the `t` time moment in which
    the values are requested: position, rotation,
    scale, opacity, etc.

    Giving not `width` and/nor `height` will make the
    system use the size of the media associated (if
    available and possible) to keep its original size.

    TODO: I think we should have 't_start' and 't_end'
    for another version that is applied on the track
    instead.
    """

    @property
    def copy(
        self
    ) -> 'VideoTransform':
        """
        Get a copy of it.
        """
        return VideoTransform(
            # TODO: Maybe we need a '.copy' of these below (?)
            specification = self.specification,
            output_size = self.output_size,
            x = self.x,
            y = self.y,
            width = self.width,
            height = self.height,
            rotation = self.rotation,
            opacity = self.opacity,
        )
    
    @property
    def timeline_graph(
        self
    ) -> TimelineGraph:
        """
        The `TimelineGraph` to make the changes, that is
        created when needed.
        """
        if self._timeline_graph is None:
            self._timeline_graph = self._build_timeline_graph()

        return self._timeline_graph
    
    @property
    def output_size(
        self
    ) -> tuple[int, int]:
        """
        The size that must be used to render the frames as the
        final output.
        """
        return self._output_size
    
    @output_size.setter
    def output_size(
        self,
        value
    ):
        self._output_size = value
        self._invalidate_graph()
    
    @property
    def x(
        self
    ) -> VideoEditorParameterValue:
        """
        The `x` position of the frame on the canvas, which is
        attached to the center of the item.
        """
        return self._x
    
    @x.setter
    def x(
        self,
        value
    ) -> None:
        self._x = value
        self._invalidate_graph()

    @property
    def y(
        self
    ) -> VideoEditorParameterValue:
        """
        The `y` position of the frame on the canvas, which is
        attached to the center of the item.
        """
        return self._y
    
    @y.setter
    def y(
        self,
        value
    ) -> None:
        self._y = value
        self._invalidate_graph()

    @property
    def width(
        self
    ) -> VideoEditorParameterValue:
        """
        The `width` of the item when displayed on the canvas.
        This value can be different than the width of the
        `output_size` and could be filled with transparent
        pixels to reach the `output_size`.
        """
        return self._width
    
    @width.setter
    def width(
        self,
        value
    ) -> None:
        self._width = value
        self._invalidate_graph()

    @property
    def height(
        self
    ) -> VideoEditorParameterValue:
        """
        The `height` of the item when displayed on the canvas.
        This value can be different than the height of the
        `output_size` and could be filled with transparent
        pixels to reach the `output_size`.
        """
        return self._height
    
    @height.setter
    def height(
        self,
        value
    ) -> None:
        self._height = value
        self._invalidate_graph()

    @property
    def rotation(
        self
    ) -> VideoEditorParameterValue:
        """
        The `rotation` of the item on the canvas.
        """
        return self._rotation
    
    @rotation.setter
    def rotation(
        self,
        value
    ) -> None:
        self._rotation = value
        self._invalidate_graph()

    @property
    def opacity(
        self
    ) -> VideoEditorParameterValue:
        """
        The output size we want to be able to reshape the frame
        after the transformation, that must fit the output of
        the track in which the item is placed.
        """
        return self._opacity
    
    @opacity.setter
    def opacity(
        self,
        value
    ) -> None:
        self._opacity = value
        self._invalidate_graph()
    
    def __init__(
        self,
        specification: Union[FrameIndexSpecification, ProgressSpecification, TimeSpecification],
        output_size: Union[tuple[int, int], ParameterValueUnset] = VALUE_UNSET,
        do_use_gpu: bool = True,
        opengl_context: Union['moderngl.Context', None] = None,
        x: Union[VideoEditorParameterValue, ParameterValueUnset] = ConstantVideoEditorParameterValue(0),
        y: Union[VideoEditorParameterValue, ParameterValueUnset] = ConstantVideoEditorParameterValue(0),
        width: Union[VideoEditorParameterValue, ParameterValueUnset] = VALUE_UNSET,
        height: Union[VideoEditorParameterValue, ParameterValueUnset] = VALUE_UNSET,
        rotation: Union[VideoEditorParameterValue, ParameterValueUnset] = ConstantVideoEditorParameterValue(0),
        opacity: Union[VideoEditorParameterValue, ParameterValueUnset] = ConstantVideoEditorParameterValue(1.0),
    ):
        # THe parameters are validated when parsed
        self.specification: Union[FrameIndexSpecification, ProgressSpecification, TimeSpecification] = specification
        """
        The specification of when the `VideoTransform` must be
        applied.
        """
        self._output_size: tuple[int, int] = output_size
        """
        The size that must be used to render the frames as the
        final output.
        """
        self.do_use_gpu: bool = do_use_gpu
        """
        Boolean flag to indicate if the nodes must use the GPU
        when rendering or if not.
        """
        self.opengl_context: Union['moderngl.Context', None] = opengl_context
        """
        The OpenGL context to be used if the transformation is
        based on the GPU.
        """
        self._x: VideoEditorParameterValue = x
        """
        The `x` position of the frame on the canvas, which is
        attached to the center of the item.
        """
        self._y: VideoEditorParameterValue = y
        """
        The `y` position of the frame on the canvas, which is
        attached to the center of the item.
        """
        self._width: VideoEditorParameterValue = width
        """
        The `width` of the item when displayed on the canvas.
        This value can be different than the width of the
        `output_size` and could be filled with transparent
        pixels to reach the `output_size`.
        """
        self._height: VideoEditorParameterValue = height
        """
        The `height` of the item when displayed on the canvas.
        This value can be different than the height of the
        `output_size` and could be filled with transparent
        pixels to reach the `output_size`.
        """
        self._rotation: VideoEditorParameterValue = rotation
        """
        The `rotation` of the item on the canvas.
        """
        self._opacity: VideoEditorParameterValue = opacity
        """
        The output size we want to be able to reshape the frame
        after the transformation, that must fit the output of
        the track in which the item is placed.
        """

        self._timeline_graph: Union[TimelineGraph, None] = None
        """
        *For internal use only*

        The internal timeline graph to be able to apply
        the transform changes.
        """

    def _invalidate_graph(
        self
    ) -> 'VideoTransform':
        """
        Set the internal `timeline_graph` variable to None
        to force it to be rebuilt with the parameters that
        are set in this instance.
        """
        self._timeline_graph = None

        return self

    def _build_timeline_graph(
        self
    ) -> 'VideoTransform':
        """
        *For internal use only*

        Build the `TimelineGraph` instance with the parameters
        provided.
        """
        # Parse parameters
        self.x = parse_parameter(self.x)
        self.y = parse_parameter(self.y)
        self.width = parse_parameter(self.width)
        self.height = parse_parameter(self.height)
        self.rotation = parse_parameter(self.rotation)
        self.opacity = parse_parameter(self.opacity)

        # TODO: We should validate 'output_size' as tuple
        if self.output_size is VALUE_UNSET:
            raise Exception('The "output_size" provided is UNSET')

        self._timeline_graph = TimelineGraph(
            root_node = SerialTimelineNode(
                node = DisplacementWithRotationNodeCompositor(
                    # TODO: Where do I have the context (?)
                    opengl_context = None
                ),
                name = 'video_transform_base',
                specification = self.specification,
                parameters = {
                    'x': self.x,
                    'y': self.y,
                    'width': self.width,
                    'height': self.height,
                    'rotation': self.rotation
                }
            )
        )

        return self

    def apply(
        self,
        frame: Union['np.ndarray', 'Texture'],
        evaluation_context: EvaluationContext
    ) -> Union['np.ndarray', 'Texture']:
        """
        Apply the transformation to the given `frame` at the also
        provided `t` (which will be used to obtain the transform
        state to apply).
        """
        if self._timeline_graph is None:
            self._build_timeline_graph()

        # We process only if there are changes to apply
        if (
            not is_this_constant_value(self.x, 0) or
            not is_this_constant_value(self.y, 0) or
            not is_this_constant_value(self.width, 1920) or
            not is_this_constant_value(self.height, 1080) or
            not is_this_constant_value(self.rotation, 0)
        ):
            frame_processed = self._timeline_graph.render(
                inputs = {
                    'base_input': None,
                    'overlay_input': frame
                },
                evaluation_context = evaluation_context,
                output_size = self.output_size,
                do_use_gpu = self.do_use_gpu
            )

            # # The frame here can be 'ndarray' or 'Texture'
            # # depending on if the node was CPU or GPU
            return frame_processed

        # TODO: By now I'm not handling the 'opacity' as it is
        # not very important
        # if transform_state.opacity != 1:
        #     # TODO: Apply the opacity transformation
        #     pass

        return frame
    
    @staticmethod
    def default(
    ) -> 'VideoTransform':
        """
        Get an instance with the default values. Useful to
        instantiate it when `None` provided.
        """
        return VideoTransform(
            specification = ProgressSpecification(
                start_progress = 0.0,
                end_progress = 1.0
            ),
            output_size = VALUE_UNSET,
            # TODO: Maybe 'output_size = None' (?)
            # output_size = (1920, 1080),
            do_use_gpu = True,
            x = ConstantVideoEditorParameterValue(int(1920 / 2)),
            y = ConstantVideoEditorParameterValue(int(1080 / 2)),
            width = ConstantVideoEditorParameterValue(1920),
            height = ConstantVideoEditorParameterValue(1080),
            rotation = ConstantVideoEditorParameterValue(0),
            opacity = ConstantVideoEditorParameterValue(1.0)
        )
    
class AudioTransform(Transform):
    """
    The transformation conditions to define how we want
    to modify the different time moments of an audio in
    the timeline.

    This is attached to a timeline audio track item and
    will be applied on it. This is useful to apply the
    basic modifications.

    These values are time functions, and the values
    change according to the `t` time moment in which
    the values are requested: volume, intensity,
    equalization, noise reduction, etc.

    TODO: I think we should have 't_start' and 't_end'
    for another version that is applied on the track
    instead.
    """

    @property
    def copy(
        self
    ) -> 'AudioTransform':
        """
        Get a copy of it.
        """
        return AudioTransform(
            # TODO: Maybe we need a '.copy' of these below (?)
            volume = self.volume
        )

    def __init__(
        self,
        t_start: float,
        t_end: float,
        # TODO: By now I'm only modifying the volume
        volume: VideoEditorParameterValue = ConstantVideoEditorParameterValue(0.0)
    ):
        ParameterValidator.validate_positive_float('t_start', t_start, do_include_zero = True)
        ParameterValidator.validate_positive_float('t_end', t_end, do_include_zero = True)

        volume = parse_parameter(volume)

        # TODO: Still being developed
        self.volume: VideoEditorParameterValue = volume

    # def _get_state_at(
    #     self,
    #     t: float
    # ) -> AudioTransformState:
    #     """
    #     Get the `TransformState` dataclass that will indicate
    #     the values we have to apply to transform the timeline
    #     for the `t` time moment provided.
    #     """
    #     return AudioTransformState(
    #         volume = self.volume.get_value_at(t),
    #     )
    
    def apply(
        self,
        # TODO: Set the type
        frame: any,
        t: float
    # TODO: Set the type
    ) -> any:
        """
        Apply the transformation to the given `frame` at the also
        provided `t` (which will be used to obtain the transform
        state to apply).
        """
        transform_state = self._get_state_at(t)

        # TODO: Use the node and transform it directly
        # imitating the VideoTransform

        # Apply the effects to the 'frame' if needed
        if transform_state.volume != 1:
            # TODO: Apply the audio modifications
            # TODO: We don't have any audio modification node
            pass

        return frame
    
    # TODO: This should be updated when the audio is
    # being modified by a node with a TimelineGraph also
    def update(
        self,
        t_start: float,
        t_end: float
    ) -> 'VideoTransform':
        """
        Update the `t_start` and `t_end`.

        This method must be called each time the video
        duration this `AudioTransform` instance belongs
        to is modified.
        """
        self.t_start = t_start
        self.t_end = t_end

        return self
    
    @staticmethod
    def default(
    ) -> 'AudioTransform':
        """
        Get an instance with the default values. Useful to
        instantiate it when `None` provided.
        """
        return AudioTransform(
            t_start = 0.0,
            # TODO: I don't like this value
            t_end = 999.99,
            volume = ConstantVideoEditorParameterValue(0.0)
        )