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


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`.
    """

    def __init__(
        self,
        # TODO: Set the 4 types we have
        resource,
        render_context: 'RenderContext',
        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._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.
        """

        if metadata is None:
            self._reset_metadata()
            if do_autoextract_metadata:
                self._autodetect_metadata()
        else:
            metadata = _FrameMetadata.from_dict(
                frame = self,
                metadata = metadata
            )

        self.metadata: _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: Union[dict, None] = None
    ) -> '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 _reset_metadata(
        self
    ) -> 'Frame':
        """
        *For internal use only*

        Reset the metadata to hold nothing specific.
        
        This method will remove any additional dynamic
        metadata added before.
        """
        self.metadata = _FrameMetadata(
            frame = self
        )

        return self
    
    def _autodetect_metadata(
        self
    ) -> 'Frame':
        """
        *For internal use only*

        Autodetect the metadata from the resource.

        This method will remove any additional dynamic
        metadata added before.
        """
        self.metadata = _FrameMetadataExtractor.extract(self.resource.frame)

        return self

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

        type = FrameResourceType.to_enum(type)

        if type not in [FrameResourceType.CPU, FrameResourceType.GPU, FrameResourceType.CPU_AV_VIDEOFRAME]:
            raise Exception(f'The "type" {type.value} is not supported for the transformation.')

        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
            )
        )

        """
        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 = None
        )
        frame.metadata = self.metadata.copy(frame)

        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_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.CPU`
        - `FrameResourceType.CPU_AV_VIDEOFRAME`
        """
        from yta_editor_frame.frame.resource.converter import FrameResourceConverter

        type = FrameResourceType.to_enum(type)

        if type not in [FrameResourceType.CPU, FrameResourceType.GPU, FrameResourceType.CPU_AV_VIDEOFRAME]:
            raise Exception(f'The "type" {type.value} is not supported for the transformation.')
        
        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
            )
        )

        self.resource = new_resource

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

        # TODO: What about the metadata? It should be
        # updated somehow, no (?)
        self.metadata = (
            self.metadata.copy()
            if self.metadata is not None else
            None
        )

        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_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 could be sent back to the pool
        if no one else is using it.

        If no one is owning this `Frame` instance, it
        will be destroyed, and if the instance is a
        `RenderTarget` instance, it will be sent back to
        the pool.
        """
        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
        `RenderTarget` instance.
        """
        self.resource.release(self._render_context)
        self.resource = None

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