from yta_editor_frame.frame.metadata import _FrameMetadata, _FrameMetadataExtractor
from yta_editor_frame.frame.resource.identifier import FrameResourceIdentifier
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.
        """
        return (self.resource.width, self.resource.height)

    def __init__(
        self,
        # TODO: Set the 4 types we have
        resource,
        render_context: 'RenderContext',
        metadata: dict = {},
        do_autoextract_metadata: bool = False
    ):
        ParameterValidator.validate_dict('metadata', metadata)

        # TODO: Set the 4 types we have
        self.resource = resource
        self._render_context: 'RenderContext' = render_context
        """
        *For internal use only*

        The `RenderContext` instance that allows us to use
        the `RenderTargetProvider` and more resource 
        handlers.
        """
        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.
        """

        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_dict('metadata', metadata)

        if PythonValidator.is_instance_of(frame, 'Frame'):
            return frame

        resource = (
            AvVideoFrameResource(frame)
            if PythonValidator.is_instance_of(frame, 'VideoFrame') else
            CPUNumpyArrayFrameResource(frame)
            if PythonValidator.is_instance_of(frame, 'ndarray') else
            GPUTextureFrameResource(frame)
            if PythonValidator.is_instance_of(frame, 'Texture') else
            RenderTargetFrameResource(frame)
            # if PythonValidator.is_instance_of('RenderTarget') else
        )

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

    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(
        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`
        """
        from yta_editor_frame.frame.resource.converter import FrameResourceConverter

        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 Frame(
                # TODO: Does this 'copy' exist (?)
                resource = self.resource.copy(),
                render_context = self._render_context,
                metadata = self.metadata.copy()
            )

        new_resource = (
            FrameResourceConverter.to_numpy(
                resource = self.resource
            )
            if type == FrameResourceType.CPU else
            FrameResourceConverter.to_texture(
                resource = self.resource,
                moderngl_context = self._render_context.opengl_context
            )
            if type == FrameResourceType.GPU else
            FrameResourceConverter.to_videoframe(
                resource = self.resource
            )
            if type == FrameResourceType.CPU_AV_VIDEOFRAME else
            FrameResourceConverter.to_render_target(
                resource = self.resource,
                render_target_provider = self._render_context.render_resources._render_target_provider
            )
        )

        """
        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,
            render_context = self._render_context,
            metadata = self.metadata.copy()
        )

        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`.
        """
        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`.
        """
        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`.
        """
        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`.
        """
        return self._to(type = FrameResourceType.CPU_AV_VIDEOFRAME)

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

        Modify the current instance to transform it
        into one with the `type` provided, that must
        be one of the following:
        - `FrameResourceType.GPU`
        - `FrameResourceType.GPU_RENDER_TARGET`
        - `FrameResourceType.CPU`
        - `FrameResourceType.CPU_AV_VIDEOFRAME`
        """
        from yta_editor_frame.frame.resource.converter import FrameResourceConverter

        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.')
        
        # Same type doesn't need transformation
        if self.resource.type == type:
            return self
        
        if self._ref_count > 1:
            raise Exception('This "Frame" instance is being used by more than one instance.')
        
        old_resource = self.resource

        new_resource = (
            FrameResourceConverter.to_numpy(
                resource = self.resource
            )
            if type == FrameResourceType.CPU else
            FrameResourceConverter.to_texture(
                resource = self.resource,
                moderngl_context = self._render_context.render_resources.opengl_context
            )
            if type == FrameResourceType.GPU else
            FrameResourceConverter.to_videoframe(
                resource = self.resource
            )
            if type == FrameResourceType.CPU_AV_VIDEOFRAME else
            FrameResourceConverter.to_render_target(
                resource = self.resource,
                # TODO: Maybe use accessor instead (?)
                render_target_provider = self._render_context.render_resources._render_target_provider
            )
            # if type == FrameResourceType.GPU_RENDER_TARGET
        )

        self.resource = new_resource

        # Release the old resource
        old_resource.release(self._render_context)

        # TODO: Metadata should be the same no? The frame
        # doesn't change, its just the format...
        # self.metadata = self.metadata.copy()

        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`.
        """
        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`.
        """
        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`.
        """
        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`.
        """
        return self._as(FrameResourceType.CPU_AV_VIDEOFRAME)
    
    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 (
            FrameResourceIdentifier.is_gpu(self.resource) or
            FrameResourceIdentifier.is_gpu_render_target(self.resource)
        ):
            self.resource.release(self._render_context)

        self.resource = None

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