from yta_editor_frame.frame.metadata import _FrameMetadata, _FrameMetadataExtractor
from yta_editor_frame.frame.resource.identifier import FrameResourceIdentifier
from yta_editor_frame.frame.resource.converter import FrameResourceConverter
from yta_editor_frame.frame.resource.types import FrameResourceType
from yta_validation.parameter import ParameterValidator
from yta_validation import PythonValidator
from typing import Union


# TODO: Maybe rename to 'VideoFrame' or similar, but
# careful with the 'pyav.VideoFrame' then...
class Frame:
    """
    A frame, the main class that is shared in between
    the nodes and all the rendering process.

    If the `metadata` is provided, it will be set. If
    it is not provided, it will be extracted if the
    `do_autoextract_metadata` is `True`, or set as
    unknown if `False`.
    """

    @property
    def size(
        self
    ) -> tuple[int, int]:
        """
        The size of the resource expressed as
        `(width, height)`, in pixels.
        """
        self._assert_alive()
        return (self.resource.width, self.resource.height)
    
    @property
    def render_context(
        self
    ) -> 'RenderContext':
        """
        The `RenderContext` instance that allows us to use
        the `RenderTargetProvider` and more resource 
        handlers.
        """
        return self.resource.render_context

    def __init__(
        self,
        # TODO: Set the 4 types we have
        resource,
        metadata: Union[dict, None] = None,
        do_autoextract_metadata: bool = False
    ):
        ParameterValidator.validate_dict('metadata', metadata)

        # TODO: Set the 4 types we have
        self.resource = resource
        self._ref_count: int = 1
        """
        *For internal use only*

        Internal counter to determine when no one is using
        this frame and we can destroy it and free the
        resources.
        """

        metadata = (
            {}
            if metadata == None else
            metadata
        )

        self.metadata = (
            self._get_metadata_autodetected()
            if do_autoextract_metadata else
            _FrameMetadata(**metadata)
        )
        """
        Metadata we want to include about the frame.
        """

    @classmethod
    def from_frame(
        cls,
        frame: Union['Frame', 'av.VideoFrame', 'np.ndarray', 'moderngl.Texture', 'RenderTarget'],
        render_context: 'RenderContext',
        metadata: dict = {}
    ) -> 'Frame':
        """
        Get a `Frame` instance by creating the resource from
        the `frame` parameter provided, that must be one of
        the following:
        - `av.VideoFrame`
        - `np.ndarray`
        - `moderngl.Texture`
        - `RenderTarget`
        """
        # TODO: Refactor if possible (?)
        from yta_editor_frame.frame.resource.av_video_frame_resource import AvVideoFrameResource
        from yta_editor_frame.frame.resource.cpu_frame_resource import CPUNumpyArrayFrameResource
        from yta_editor_frame.frame.resource.gpu_frame_resource import GPUTextureFrameResource
        from yta_editor_frame.frame.resource.render_target_frame_resource import RenderTargetFrameResource

        ParameterValidator.validate_mandatory_instance_of('frame', frame, ['Frame', 'VideoFrame', 'ndarray', 'Texture', 'RenderTarget'])
        ParameterValidator.validate_mandatory_instance_of('render_context', render_context, 'RenderContext')
        ParameterValidator.validate_mandatory_dict('metadata', metadata)

        if PythonValidator.is_instance_of(frame, 'Frame'):
            # TODO: Maybe we need to clone the resource...
            # return frame.retain()
            raise Exception('Using "from_frame" on a "Frame"... Maybe parse, mate (?)')
        
        resource = (
            AvVideoFrameResource(
                frame = frame,
                render_context = render_context
            )
            if PythonValidator.is_instance_of(frame, 'VideoFrame') else
            CPUNumpyArrayFrameResource(
                frame = frame,
                render_context = render_context
            )
            if PythonValidator.is_instance_of(frame, 'ndarray') else
            GPUTextureFrameResource(
                frame = frame,
                render_context = render_context
            )
            if PythonValidator.is_instance_of(frame, 'Texture') else
            RenderTargetFrameResource(
                frame = frame,
                render_context = render_context
            )
            # if PythonValidator.is_instance_of('RenderTarget') else
        )

        frame = Frame(
            resource = resource,
            metadata = metadata
            # TODO: Maybe send 'do_autoextract_metadata' as True (?)
        )

        return frame

    def _get_metadata_autodetected(
        self
    ) -> '_FrameMetadata':
        """
        *For internal use only*

        Autodetect the metadata from the resource.
        """
        return _FrameMetadataExtractor.extract_from_video(self.resource.frame)
    
    def _to_resource(
        self,
        type: FrameResourceType
    ) -> 'Frame':
        """
        *For internal use only*

        Get the `FrameResource` of the given `type`
        that must be associated to a new `Frame`
        instance.

        This method is meant to be used for the `_to()`
        and the `_as()` methods.
        """
        type = FrameResourceType.to_enum(type)

        if type not in FrameResourceType.get_all():
            raise Exception(f'The "type" {type.value} is not supported for the transformation.')
        
        if self.resource.type == type:
            return self.resource.copy()
        
        frame_resource_converter = FrameResourceConverter(self.render_context)

        return (
            frame_resource_converter.to_numpy(
                resource = self.resource
            )
            if type == FrameResourceType.CPU else
            frame_resource_converter.to_texture(
                resource = self.resource
            )
            if type == FrameResourceType.GPU else
            frame_resource_converter.to_videoframe(
                resource = self.resource
            )
            if type == FrameResourceType.CPU_AV_VIDEOFRAME else
            frame_resource_converter.to_render_target(
                resource = self.resource
            )
        )

    def _to(
        self,
        type: FrameResourceType
    ) -> 'Frame':
        """
        *For internal use only*

        Get a new instance with the same metadata but
        of the `type` provided, that must be one of
        the following:
        - `FrameResourceType.GPU`
        - `FrameResourceType.GPU_RENDER_TARGET`
        - `FrameResourceType.CPU`
        - `FrameResourceType.CPU_AV_VIDEOFRAME`

        This method will NOT release the resources of
        the previous instance.
        """
        type = FrameResourceType.to_enum(type)

        if type not in FrameResourceType.get_all():
            raise Exception(f'The "type" {type.value} is not supported for the transformation.')
        
        if self.resource.type == type:
            # TODO: Is this ok? Or should I do nothing (?)
            return self.retain()
        
        new_resource = self._to_resource(type)
        
        """
        By now we are returning a new instance to
        avoid errors if this `Frame` instance is
        shared and being used by other instances.
        """
        frame = Frame(
            resource = new_resource,
            metadata = self.metadata.to_dict()
        )
        
        return frame
    
    def to_texture(
        self
    ) -> 'Frame':
        """
        Get a new instance with the same metadata but
        of the `FrameResourceType.GPU`, that is the
        one that works with `moderngl.Texture`.

        This method will NOT release the resources of
        the previous instance.
        """
        self._assert_alive()

        return self._to(type = FrameResourceType.GPU)
    
    def to_render_target(
        self
    ) -> 'Frame':
        """
        Get a new instance with the same metadata but
        of the `FrameResourceType.GPU_RENDER_TARGET`,
        that is the one that works with `RenderTarget`.

        This method will NOT release the resources of
        the previous instance.
        """
        self._assert_alive()

        return self._to(type = FrameResourceType.GPU_RENDER_TARGET)
    
    def to_numpy(
        self
    ) -> 'Frame':
        """
        Get a new instance with the same metadata but
        of the `FrameResourceType.CPU`, that is the
        one that works with `np.ndarray`.

        This method will NOT release the resources of
        the previous instance.
        """
        self._assert_alive()

        return self._to(type = FrameResourceType.CPU)
    
    def to_videoframe(
        self
    ) -> 'Frame':
        """
        Get a new instance with the same metadata but
        of the `FrameResourceType.CPU_AV_VIDEOFRAME`,
        that is the one that works with `av.VideoFrame`.

        This method will NOT release the resources of
        the previous instance.
        """
        self._assert_alive()

        return self._to(type = FrameResourceType.CPU_AV_VIDEOFRAME)

    def _as(
        self,
        type: FrameResourceType
    ) -> 'Frame':
        """
        Mutate this Frame by converting its resource type.

        Only allowed if this Frame has exclusive ownership
        of its resource (ref_count == 1).
        """
        type = FrameResourceType.to_enum(type)

        if type not in FrameResourceType.get_all():
            raise Exception(f'The "type" {type.value} is not supported.')

        # No-op
        if self.resource.type == type:
            return self

        if self._ref_count != 1:
            raise Exception(
                f'Cannot mutate Frame with multiple references (references: {str(self._ref_count)}). '
                'Use `.to()` instead.'
            )

        old_resource = self.resource

        new_resource = self._to_resource(type)

        # Swap resource
        self.resource = new_resource

        # Release old AFTER swap
        # TODO: This is failing, I don't know why
        # old_resource.release()

        return self

    def as_numpy(
        self
    ) -> 'Frame':
        """
        Modify the current instance to transform it into
        a `FrameResourceType.CPU`, that is the one that
        works with the `np.ndarray`.
        
        This method will release the resources of the
        previous instance.
        """
        return self._as(FrameResourceType.CPU)
    
    def as_texture(
        self
    ) -> 'Frame':
        """
        Modify the current instance to transform it into
        a `FrameResourceType.GPU`, that is the one that
        works with the `moderngl.Texture`.

        This method will release the resources of the
        previous instance.
        """
        return self._as(FrameResourceType.GPU)
    
    def as_render_target(
        self
    ) -> 'Frame':
        """
        Modify the current instance to transform it into
        a `FrameResourceType.GPU_RENDER_TARGET`, that is
        the one that works with the `RenderTarget`.

        This method will release the resources of the
        previous instance.
        """
        return self._as(FrameResourceType.GPU_RENDER_TARGET)
    
    def as_videoframe(
        self
    ) -> 'Frame':
        """
        Modify the current instance to transform it into
        a `FrameResourceType.CPU_AV_VIDEOFRAME`, that is
        the one that works with the `av.VideoFrame`.

        This method will release the resources of the
        previous instance.
        """
        return self._as(FrameResourceType.CPU_AV_VIDEOFRAME)
    
    # TODO: Recommended by ChatGPT but still don't know...
    # def convert(
    #     self,
    #     type: FrameResourceType
    # ) -> 'Frame':
    #     """
    #     TODO: Add doc
    #     """
    #     if self._ref_count == 1:
    #         return self._as(type)
    #     else:
    #         return self._to(type)

    def retain(
        self
    ) -> 'Frame':
        """
        Method to indicate that the frame is being used by
        someone else so it must be retained and not sent
        back to the pool.
        """
        if self._ref_count <= 0:
            raise Exception('Cannot retain destroyed frame.')

        self._ref_count += 1

        return self

    def release(
        self
    ):
        """
        Method to indicate that the frame has been freed
        by one owner and must be completely released.

        If no one is owning this `Frame` instance, it
        will be destroyed, and if the instance needs to
        free the resource, it will be done.
        """
        self._ref_count -= 1

        if self._ref_count < 0:
            raise Exception('Frame released too many times.')

        if self._ref_count == 0:
            self._destroy()

    def _destroy(
        self
    ):
        """
        Method to destroy the frame instance, that will
        send the resource to the pool if it is a
        resource that must be released.
        """
        if self.resource:
            self.resource.release()

        self.resource = None

    def _assert_alive(
        self
    ):
        if self.resource is None:
            raise Exception('Frame already destroyed')

    def __repr__(
        self
    ):
        return f'<Frame rc={self._ref_count} res={self.resource}>'
    
# TODO: Implement
class AudioFrame:
    pass