from yta_video_opengl.nodes.video import BreathingFrameVideoNodeProcessor, WavingFramesVideoNodeProcessor
from yta_video_opengl.context import OpenGLContext
from yta_video_opengl.nodes.audio import ChorusAudioNodeProcessor, VolumeAudioNodeProcessor
from yta_video_opengl.nodes.timed import TimedVideoNode, TimedAudioNode
from yta_video_opengl.utils import texture_to_frame
from yta_validation.parameter import ParameterValidator
from yta_validation import PythonValidator
from yta_programming.singleton import SingletonMeta
from typing import Union
from abc import abstractmethod

import moderngl


class _AudioEffects:
    """
    *For internal use only*

    The audio effects that will be available
    throught our internal _Effects class to
    wrap and make available all the audio
    effects we want to be available.
    """

    def __init__(
        self,
        effects: 'Effects'
    ):
        self._effects: Effects = effects
        """
        The parent instance that includes this
        class instance as a property.
        """

    """
    Here below we expose all the effects
    we want the users to have available to
    be used.
    """
    def chorus(
        self,
        sample_rate: int,
        depth: int = 0,
        frequency: float = 0.25,
        start: Union[int, float, 'Fraction'] = 0,
        end: Union[int, float, 'Fraction', None] = None
    ) -> TimedAudioNode:
        return _create_timed_node(
            ChorusAudioNodeProcessor(
                sample_rate = sample_rate,
                depth = depth,
                frequency = frequency
            ),
            start = start,
            end = end
        )

    def volume(
        self,
        factor: callable,
        start: Union[int, float, 'Fraction'] = 0,
        end: Union[int, float, 'Fraction', None] = None
    ) -> TimedAudioNode:
        return _create_timed_node(
            VolumeAudioNodeProcessor(
                factor_fn = factor
            ),
            start = start,
            end = end
        )
    
    # TODO: Include definitive and tested audio
    # effects here below

class _VideoEffects:
    """
    *For internal use only*

    The video effects that will be available
    throught our internal _Effects class to
    wrap and make available all the video
    effects we want to be available.
    """

    def __init__(
        self,
        effects: 'Effects'
    ):
        self._effects: Effects = effects
        """
        The parent instance that includes this
        class instance as a property.
        """

    """
    Here below we expose all the effects
    we want the users to have available to
    be used.
    """
    def waving_node(
        self,
        # TODO: Maybe 'frame_size' (?)
        size: tuple[int, int],
        amplitude: float = 0.05,
        frequency: float = 10.0,
        speed: float = 2.0,
        start: Union[int, float, 'Fraction'] = 0.0,
        end: Union[int, float, 'Fraction', None] = None
    ) -> TimedVideoNode:
        """
        TODO: Explain this effect better.

        The 'start' and 'end' time moments are the
        limits of the time range in which the effect
        has to be applied to the frames inside that
        time range. Providing start=0 and end=None
        will make the effect to be applied to any
        frame.
        """
        return _create_timed_node(
            WavingFramesVideoNodeProcessor(
                # TODO: Maybe make this a parameter (?)
                do_use_gpu = True,
                amplitude = amplitude,
                frequency = frequency,
                speed = speed
            ),
            start = start,
            end = end
        )
    
    def breathing_node(
        self,
        # TODO: Handle this
        output_size: tuple[int, int],
        zoom: float = 0.05,
        start: Union[int, float, 'Fraction'] = 0.0,
        end: Union[int, float, 'Fraction', None] = None
    ) -> TimedVideoNode:
        """
        TODO: Explain this effect better.

        The 'start' and 'end' time moments are the
        limits of the time range in which the effect
        has to be applied to the frames inside that
        time range. Providing start=0 and end=None
        will make the effect to be applied to any
        frame.
        """
        return _create_timed_node(
            BreathingFrameVideoNodeProcessor(
                # TODO: Maybe make this a parameter (?)
                do_use_gpu = True,
                zoom = zoom
            ),
            start = start,
            end = end
        )
    
    # TODO: Include definitive and tested video
    # effects here below

class Effects(metaclass = SingletonMeta):
    """
    *Singleton class*

    It is a singleton instance to have a
    unique context for all the instances
    that need it and instantiate this
    class to obtain it. Here we group all
    the nodes we have available for the
    user.

    This class is to simplify the access to
    the effect nodes and also to have the
    single context always available.

    Even though we can have more effects,
    this class is also the way we expose only
    the ones we actually want to expose to 
    the user.

    The GPU will make the calculations in
    parallel by itself, so we can handle a
    single context to make the nodes share
    textures and buffers.
    """

    def __init__(
        self,
        opengl_context: Union[moderngl.Context, None] = None
    ):
        """
        If you provide an OpenGL context, it will be
        used, or a new one will be created if the
        'opengl_context' parameter is None.
        """
        ParameterValidator.validate_instance_of('opengl_context', opengl_context, moderngl.Context)

        self.opengl_context = (
            OpenGLContext().context
            if opengl_context is None else
            opengl_context
        )
        """
        The opengl context that will be shared
        by all the opengl nodes.
        """
        self.audio: _AudioEffects = _AudioEffects(self)
        """
        Shortcut to the audio effects that are
        available.
        """
        self.video: _VideoEffects = _VideoEffects(self)
        """
        Shortcut to the video effects that are
        available.
        """

def _create_timed_node(
    node: Union['_AudioNodeProcessor', '_VideoNodeProcessor'],
    start: Union[int, float, 'Fraction'],
    end: Union[int, float, 'Fraction', None]
) -> Union[TimedVideoNode, TimedAudioNode]:
    """
    *For internal use only*

    Create a TimedNode with the provided 'node' and
    the 'start' and 'end' time moments.
    """
    # We could be other classes in the middle,
    # because an OpenglNode inherits from
    # other class
    ParameterValidator.validate_mandatory_subclass_of('node', node, ['_AudioNodeProcessor', '_VideoNodeProcessor', '_OpenGLBase'])

    timed_node_class = (
        TimedAudioNode
        if PythonValidator.is_subclass_of(node, '_AudioNodeProcessor') else
        TimedVideoNode
    )

    # We have to create a node wrapper with the
    # time range in which it has to be applied
    # to all the frames
    return timed_node_class(
        node = node,
        start = start,
        end = end
    )

class _EffectStacked:
    """
    *For internal use only*

    Class to wrap an effect that will be
    stacked with an specific priority.

    Priority is higher when lower value,
    and lower when higher value.
    """

    @property
    def is_audio_effect(
        self
    ) -> bool:
        """
        Flag to indicate if it is an audio effect
        or not.
        """
        return self.effect.is_audio_node

    @property
    def is_video_effect(
        self
    ) -> bool:
        """
        Flag to indicate if it is a video effect
        or not.
        """
        return self.effect.is_video_node

    def __init__(
        self,
        effect: Union[TimedVideoNode, TimedAudioNode],
        priority: int
    ):
        self.effect: Union[TimedVideoNode, TimedAudioNode] = effect
        """
        The effect to be applied.
        """
        self.priority: int = priority
        """
        The priority this stacked frame has versus
        the other stacked effects.
        """

class _VideoEffectStacked(_EffectStacked):
    """
    *For internal use only*

    Class to wrap a video effect that will
    be stacked with a specific priority.

    Priority is higher when lower value,
    and lower when higher value.
    """

    def __init__(
        self,
        effect: TimedVideoNode,
        priority: int
    ):
        ParameterValidator.validate_mandatory_instance_of('effect', effect, TimedVideoNode)

        super().__init__(
            effect = effect,
            priority = priority
        )

class _AudioEffectStacked(_EffectStacked):
    """
    *For internal use only*

    Class to wrap an audio effect that will
    be stacked with a specific priority.

    Priority is higher when lower value,
    and lower when higher value.
    """

    def __init__(
        self,
        effect: TimedAudioNode,
        priority: int
    ):
        ParameterValidator.validate_mandatory_instance_of('effect', effect, TimedAudioNode)
        
        super().__init__(
            effect = effect,
            priority = priority
        )

# TODO: Move to another py file (?)
class _EffectsStack:
    """
    *For internal use only*

    *This class is to be inherited by the
    specific video and audio classes.

    Class to include a collection of effects
    we want to apply in some entity, that 
    will make easier applying them.

    You can use this stack to keep the effects
    you want to apply on a Media or on the
    Timeline of your video editor.
    """

    @property
    def copy(
        self
    ) -> '_EffectsStack':
        """
        Get a copy of this instance.
        """
        effects_stack = self.__class__()

        for effect_sttacked in self._effects:
            effects_stack.add_effect(
                effect = effect_sttacked.effect.copy,
                priority = effect_sttacked.priority
            )

        return effects_stack
    
    @property
    def number_of_effects(
        self
    ) -> int:
        """
        The number of effects that are stacked.
        """
        return len(self._effects)

    @property
    def effects(
        self
    ) -> list[_EffectStacked]:
        """
        The effects but ordered by 'priority' and
        'start' time moment.
        """
        return sorted(
            [
                effect
                for effect in self._effects
            ],
            key = lambda effect: (effect.priority, effect.effect.start)
        )
    
    @property
    def lowest_priority(
        self
    ) -> int:
        """
        The priority of the effect with the lowest
        one, or 0 if no video effects.
        """
        return min(
            (
                effect.priority
                for effect in self.effects
            ),
            default = 0
        )
    
    def __init__(
        self
    ):
        self._effects: list[Union[_AudioEffectStacked, _VideoEffectStacked]] = []
        """
        A list containing all the effects that
        have been added to this stack, unordered.
        """

    def get_effects_at(
        self,
        t: Union[int, float, 'Fraction']
    ) -> list[Union[TimedVideoNode, TimedAudioNode]]:
        """
        Get the effects, ordered by priority and the
        'start' field, that must be applied within the
        't' time moment provided because they are in
        the [start, end) time range.
        """
        return [
            effect.effect
            for effect in self.effects
            if effect.effect.is_within_time(t)
        ]

    def add_effect(
        self,
        effect: Union[TimedVideoNode, TimedAudioNode],
        priority: Union[int, None] = None
    ) -> 'EffectsStack':
        """
        Add the provided 'effect' to the stack with
        the also given 'priority'.
        """
        ParameterValidator.validate_mandatory_instance_of('effect', effect, [TimedVideoNode, TimedAudioNode])
        ParameterValidator.validate_positive_int('priority', priority, do_include_zero = True)

        # TODO: What about the same effect added
        # twice during the same time range? Can we
        # allow it? It will be applied twice for
        # specific 't' time moments but with 
        # different attributes. is it ok (?)

        # TODO: What if priority is already taken?
        # Should we let some effects have the same
        # priority (?)
        priority = (
            self.lowest_priority + 1
            if priority is None else
            priority
        )

        self._effects.append(_EffectStacked(
            effect = effect,
            priority = priority
        ))

        return self
    
    # TODO: Create 'remove_effect'
    
    @abstractmethod
    def apply_effects_at(
        self,
        frame: Union['np.ndarray', moderngl.Texture],
        t: Union[int, float, 'Fraction']
    ) -> Union['np.ndarray', moderngl.Texture]:
        """
        Apply all the effects that must be applied
        for the given `t` time moment to the also
        provided `frame` (that must be the video or
        audio frame of that time moment).
        """
        pass
    
class VideoEffectsStack(_EffectsStack):
    """
    Class to include a collection of video effects
    we want to apply in some entity, that will
    make easier applying and organizing them.

    You can use this stack to keep the video effects
    you want to apply on a Media or on the Timeline
    of your editor.
    """

    def add_effect(
        self,
        effect: TimedVideoNode,
        priority: Union[int, None] = None
    ) -> 'VideoEffectsStack':
        """
        Add the provided video 'effect' to the stack
        with the also given 'priority'.
        """
        ParameterValidator.validate_mandatory_instance_of('effect', effect, TimedVideoNode)

        return super().add_effect(
            effect = effect,
            priority = priority
        )
    
    def apply_effects_at(
        self,
        frame: Union['np.ndarray', moderngl.Texture],
        t: Union[int, float, 'Fraction']
    ) -> Union['np.ndarray', moderngl.Texture]:
        """
        Apply all the effects that must be applied
        for the given `t` time moment to the also
        provided `frame` (that must be the video
        frame of that time moment).
        """
        for effect in self.get_effects_at(t):
            frame = effect.process(frame, t)

        # TODO: Check when the frame comes as a
        # Texture and when as a numpy array. I
        # think when we apply an opengl node it
        # is a texture, but we need to return it
        # as a numpy, always
        return (
            texture_to_frame(frame)
            if PythonValidator.is_instance_of(frame, moderngl.Texture) else
            frame
        )
    
class AudioEffectsStack(_EffectsStack):
    """
    Class to include a collection of audio effects
    we want to apply in some entity, that will
    make easier applying and organizing them.

    You can use this stack to keep the audio effects
    you want to apply on a Media or on the Timeline
    of your editor.
    """

    def add_effect(
        self,
        effect: TimedAudioNode,
        priority: Union[int, None] = None
    ) -> 'VideoEffectsStack':
        """
        Add the provided audio 'effect' to the stack
        with the also given 'priority'.
        """
        ParameterValidator.validate_mandatory_instance_of('effect', effect, TimedAudioNode)

        return super().add_effect(
            effect = effect,
            priority = priority
        )

    def apply_effects_at(
        self,
        frame: Union['np.ndarray', moderngl.Texture],
        t: Union[int, float, 'Fraction']
    ) -> Union['np.ndarray', moderngl.Texture]:
        """
        Apply all the effects that must be applied
        for the given `t` time moment to the also
        provided `frame` (that must be the audio
        frame of that time moment).
        """
        for effect in self.get_effects_at(t):
            frame = effect.process(frame, t)

        # Frame can only by a numpy array here
        return frame