from yta_multimedia_core.parser import VideoParser
from yta_multimedia_core.video.frame.t_handler import VideoFrameTHandler
from yta_multimedia_core.video.frame.extractor import VideoFrameExtractor
from yta_multimedia_core.video.frame.mask import FrameMaskingMethod
from yta_multimedia_core.video.frame import VideoFrame
from yta_multimedia_core.video.mp_video import calculate_real_video_duration
from yta_validation.parameter import ParameterValidator
from moviepy.Clip import Clip
from moviepy import VideoClip
from typing import Union

import numpy as np


class VideoWrapped:
    """
    A moviepy video but wrapped to simplify the way
    we interact with its properties and try to solve
    some issues that can exist in that library.
    """

    @property
    def duration(
        self
    ) -> float:
        """
        Real duration of the video, that has been
        checked according to the available frames
        if the 'do_calculate_real_duration'
        parameter was set as True when instantiated.
        """
        return self.video.duration
    
    @property
    def fps(
        self
    ) -> float:
        """
        Frames per second of the original video.
        """
        return self.video.fps

    @property
    def audio_fps(
        self
    ) -> Union[float, None]:
        """
        Frames per second of the audio clip attached
        to this video clip, that can be None if no
        audio attached.
        """
        return (
            None
            if not self.has_audio else
            self.audio.fps
        )
    
    @property
    def original_size(
        self
    ):
        """
        The size of the original video clip.
        """
        return self.video.size

    @property
    def original_width(
        self
    ):
        """
        The width of the original video clip.
        """
        return self.original_size[0]
    
    @property
    def original_height(
        self
    ):
        """
        The height of the original video clip.
        """
        return self.original_size[1]

    @property
    def frame_duration(
        self
    ) -> float:
        """
        Frame duration, based on video frames per second.
        """
        return 1 / self.fps

    @property
    def audio_frame_duration(
        self
    ) -> Union[float, None]:
        """
        Frame duration of the audio clip attached to this
        video clip, if attached, or None if not.
        """
        return (
            None
            if not self.has_audio else
            1 / self.audio_fps
        )
    
    @property
    def audio(
        self
    ) -> Union['AudioClip', None]:
        """
        Get the audio of the video, if an AudioClip is
        attached, or None if not.
        """
        return self.video.audio
    
    @property
    def has_audio(
        self
    ) -> bool:
        """
        Indicate if the video has an audio clip attached
        or not.
        """
        return self.audio is not None
    
    @property
    def without_audio(
        self
    ) -> 'Clip':
        """
        Get the video clip but without any audio clip 
        attached to it.
        
        This method doesn't affect to the original
        video, it is just a copy.
        """
        return self.video.without_audio()
    
    # TODO: Should I keep this here? I think this is
    # an additional functionality that is not about
    # wrapping the class but adding functionality, and
    # that must be done in another library related
    # to video editing
    # @property
    # def with_audio(
    #     self
    # ) -> 'Clip':
    #     """
    #     Get the video clip with an audio clip attached
    #     to it. If there is not an audio clip attached,
    #     a new silent audio clip will be created and
    #     attached to it.

    #     This method doesn't affect to the original
    #     video, it is just a copy.
    #     """
    #     # TODO: Create a silent audio clip with the
    #     # same duration and return a copy with it
    #     # attached to
    #     return self.video.copy()

    @property
    def mask(
        self
    ) -> Union['Clip', None]:
        """
        Get the mask video clip attached to this video,
        if existing, or None if not.
        """
        return self.video.mask
    
    @property
    def is_mask(
        self
    ) -> bool:
        """
        Indicate if the video is set as a mask video. A
        video that is a mask cannot has another video
        attached as a mask.
        """
        return self.video.is_mask
    
    @property
    def has_mask(
        self
    ) -> bool:
        """
        Indicate if the video has another video attached
        as a mask.
        """
        return self.mask is not None
    
    @property
    def has_transparent_mask(
        self
    ) -> bool:
        """
        Check if the first frame of the mask of this
        video has, at least, one transparent pixel,
        which means that the mask is actually a
        transparent mask.
        """
        # TODO: I would like to check all the frames not
        # only the first one, but that takes time...
        # TODO: While 1 meaks that the mask is fully
        # transparent, it could have partial transparency
        # which is a value greater than 0.0, and we are
        # not considering that here...
        return (
            self.has_mask and
            #np.any(self.mask.get_frame(t = 0) == 1) # Full transparent pixel only
            np.any(self.mask.get_frame(t = 0) > 0) # Partial transparent pixel
        )
    
    @property
    def frames(
        self
    ):
        """
        All the frames of this video clip.
        """
        return VideoFrameExtractor.get_all_frames(self.video)

    @property
    def audio_frames(
        self
    ) -> Union[list[any], None]:
        """
        All the frames of the audio clip that is
        attached to this video, if attached, or None
        if not.
        """
        return (
            None
            if not self.has_audio else
            VideoFrameExtractor.get_all_frames(self.audio)
        )
    
    @property
    def number_of_frames(
        self
    ) -> int:
        """
        Get the number of video frames that exist in
        the video, using the duration of this instance
        that can be the one checked if it was requested
        in the '__init__' method.
        """
        return VideoFrameTHandler.get_number_of_frames(self.duration, self.fps)
    
    @property
    def number_of_audio_frames(
        self
    ) -> Union[int, None]:
        """
        Get the number of audio frames that exist in
        the audio clip that is attached to the video,
        if attached, or None if not audio.
        """
        return (
            None
            if not self.has_audio else
            VideoFrameTHandler.get_number_of_frames(self.duration, self.audio_fps)
        )

    @property
    def frame_indexes(
        self
        # TODO: Please, type
    ) -> list[int]:
        """
        The indexes of all the video frames.
        """
        return VideoFrameTHandler.get_video_frames_indexes_from_duration_and_fps(self.duration, self.fps)
    
    @property
    def audio_frame_indexes(
        self
        # TODO: Please, type
    ) -> Union[list[int], None]:
        """
        The indexes of all the audio frames if there is
        an audio clip attached to this video clip, or 
        None if not.
        """
        return (
            None
            if not self.has_audio else
            VideoFrameTHandler.get_video_frames_indexes_from_duration_and_fps(self.duration, self.audio_fps)
        )
    
    @property
    def frame_time_moments(
        self
    ) -> list[float]:
        """
        All the frame time moments of this video.
        """
        return VideoFrameTHandler.get_video_frames_ts_from_duration_and_fps(self.duration, self.fps)
    
    @property
    def audio_frame_time_moments(
        self
    ) -> Union[list[float], None]:
        """
        All the frame time moments of the audio clip
        that is attached to this video clip, if
        attached, or None if not.
        """
        return (
            None
            if not self.has_audio else
            VideoFrameTHandler.get_video_frames_ts_from_duration_and_fps(self.duration, self.audio_fps)
        )

    @property
    def inverted(
        self
    ):
        """
        The video but inverted, a process in which the numpy
        array values of each frame are inverted by
        substracting the highest value. If the frame is an
        RGB frame with values between 0 and 255, it will be
        inverted by doing 255 - X on each frame pixel value.
        If it is normalized and values are between 0 and 1
        (it is a mask clip frame), by doing 1 - X on each
        mask frame pixel value.
        """
        mask_frames = [
            VideoFrame(frame).inverted
            for frame in self.video.iter_frames()
        ]

        # TODO: Which calculation is better, t * fps or t // frame_duration (?)
        # TODO: What if we have fps like 29,97 (?) I proposed forcing
        # the videos to be 60fps always so we avoid this problem
        return VideoClip(
            make_frame = lambda t: mask_frames[int(t * self.video.fps)],
            is_mask = self.video.is_mask
        ).with_fps(self.video.fps)

    def __init__(
        self,
        video: Union[str, Clip],
        is_mask: Union[bool, None] = None,
        do_include_mask: Union[bool, None] = None,
        do_calculate_real_duration: bool = False,
    ):
        ParameterValidator.validate_mandatory_instance_of('video', video, Clip)
        ParameterValidator.validate_bool('is_mask', is_mask)
        ParameterValidator.validate_bool('do_include_mask', do_include_mask)
        ParameterValidator.validate_mandatory_bool('do_calculate_real_duration', do_calculate_real_duration)

        # If 'is_mask' or 'do_include_mask' is None is 
        # because we don't know and/or it must be handled
        # automatically
        is_mask = (
            is_mask
            if is_mask is not None else
            # TODO: Maybe auto-check? How (?)
            False
        )
        do_include_mask = (
            do_include_mask
            if do_include_mask is not None else
            False
        )

        video = VideoParser.to_moviepy(
            video,
            do_include_mask = do_include_mask,
            do_calculate_real_duration =  do_calculate_real_duration
        )

        if do_calculate_real_duration:
            # 1. Ensure real duration and force to 60fps
            video = video.with_subclip(0, calculate_real_video_duration(video))#.with_fps(60)

        self.video = video
        """
        The original moviepy video.
        """