from yta_editor_frame.frame.metadata.fields import AudioFrameMetadataField, VideoFrameMetadataField
from yta_validation import PythonValidator
from dataclasses import dataclass
from typing import Union


@dataclass
class _FrameMetadata:
    """
    *For internal use only*

    Class to wrap the metadata about an audio or video
    frame. Any value that is `None` means that the data
    is unknown.

    The metadata is useful to know if the frame has any
    kind of transparency, if it is fully transparent or
    fully opaque, if the audio frame is silent, etc.

    This class uses an internal dict to manage the
    information and works directly with it. Only the
    fields that are registered in the following classes
    are accepted:
    - `AudioFrameMetadataField`
    - `VideoFrameMetadataField`
    """

    def __init__(
        self,
        **kwargs
    ):
        object.__setattr__(self, '_metadata', {})
        """
        *For internal use only*

        The actual metadata dict.
        """

        for key, value in kwargs.items():
            self.__setattr__(key, value)

    def copy(
        self
    ):
        """
        Create a copy of this metadata by shallow copying
        the internal dictionary.
        """
        return _FrameMetadata(
            metadata = self.to_dict()
        )
    
    def to_dict(
        self
    ) -> dict:
        """
        Get a copy of the metadata directly as a dict.
        """
        return self._metadata.copy()
    
    def __setattr__(
        self,
        name: str,
        value: any
    ):
        if (
            not VideoFrameMetadataField.is_valid_value(name) and
            not AudioFrameMetadataField.is_valid_value(name)
        ):
            raise Exception(f'The attribute "{name}" is not accepted in our metadata.')
        
        if self._is_internal_attribute(name):
            object.__setattr__(self, name, value)
        else:
            self._metadata[name] = value

    def __getattr__(
        self,
        name: str
    ) -> Union[any, None]:
        return self._metadata.get(name, None)

    def __delattr__(
        self,
        name: str
    ):
        if self._is_internal_attribute(name):
            object.__delattr__(self, name)
        else:
            try:
                del self._metadata[name]
            except KeyError:
                raise AttributeError(f'{name} not found')
            
    def _is_internal_attribute(
        self,
        name: str
    ) -> bool:
        """
        Decide whether an attribute belongs to Python/runtime
        or should be treated as metadata.
        """
        return (
            name.startswith('_') or
            hasattr(type(self), name)
        )
    
    def copy(
        self
    ):
        """
        Get a new `_FrameMetadata` instance with a copy
        of the metadata inside.
        """
        frame_metadata = _FrameMetadata()
        object.__setattr__(frame_metadata, '_metadata', self._metadata.copy())

        return frame_metadata

    def update(
        self,
        dict: dict
    ):
        """
        Update the internal metadata of this instance.
        """
        self._metadata.update(dict)

    def __repr__(
        self
    ):
        return f'FrameMetadata({self._metadata})'        

class _FrameMetadataExtractor:
    """
    Class to extract the information of a frame
    to a `_FrameMetadata` instance.
    """

    @staticmethod
    def extract_from_video(
        frame: Union['np.ndarray', 'moderngl.Texture', 'av.VideoFrame', 'RenderTarget']
    ) -> _FrameMetadata:
        """
        Extract the metadata from the given video
        `frame` that is easy to get and available.
        
        The `frame` must be one of the following types:
        - `np.ndarray`
        - `moderngl.Texture`
        - `av.VideoFrame`
        - `RenderTarget`
        
        There is metadata that has to be manually
        written when the frame is modified by a node
        and we know exactly some metadata of the
        output we will generate and return, but this
        is just an easy and automated extractor.
        """
        metadata_dict = {
            VideoFrameMetadataField.HAS_ALPHA_PIXELS.value: None,
            VideoFrameMetadataField.IS_FULLY_OPAQUE.value: None,
            VideoFrameMetadataField.IS_FULLY_TRANSPARENT.value: None
        }

        # TODO: Extract it
        if PythonValidator.is_numpy_array(frame):
            # [1] `np.ndarray`
            if (
                frame.ndim == 3 and
                frame.shape[2] == 4
            ):
                alpha = frame[..., 3]
                a_min = alpha.min()
                a_max = alpha.max()
                metadata_dict[VideoFrameMetadataField.HAS_ALPHA_PIXELS.value] = a_min < 255
                metadata_dict[VideoFrameMetadataField.IS_FULLY_OPAQUE.value] = a_min == 255
                metadata_dict[VideoFrameMetadataField.IS_FULLY_TRANSPARENT.value] = a_max == 0
            else:
                metadata_dict[VideoFrameMetadataField.HAS_ALPHA_PIXELS.value] = False
                metadata_dict[VideoFrameMetadataField.IS_FULLY_OPAQUE.value] = True
                metadata_dict[VideoFrameMetadataField.IS_FULLY_TRANSPARENT.value] = False
        elif PythonValidator.is_instance_of(frame, 'VideoFrame'):
            # [2] `av.VideoFrame`
            metadata_dict[VideoFrameMetadataField.HAS_ALPHA_PIXELS.value] = 'a' in frame.format.name
            metadata_dict[VideoFrameMetadataField.IS_FULLY_OPAQUE.value] = (
                True
                if not metadata_dict[VideoFrameMetadataField.HAS_ALPHA_PIXELS.value] else
                # We don't know yet
                None
            )
            metadata_dict[VideoFrameMetadataField.IS_FULLY_TRANSPARENT.value] = (
                False
                if not metadata_dict[VideoFrameMetadataField.HAS_ALPHA_PIXELS.value] else
                # We don't know yet
                None
            )

        """
        # [3] `moderngl.Texture`
        # [4] `RenderTarget`
        
        The `moderngl.Texture` (or `RenderTarget`, that has
        a texture inside) are not easy to access, so we
        don't. If we are using a specific node that we know
        that will generate no transparency, then we can set
        that metadata manually, but this is to extract the
        information automatically.
        """

        return _FrameMetadata(
            **metadata_dict
        )
    
    def extract_from_audio(
        frame: 'av.AudioFrame'
    ) -> _FrameMetadata:
        """
        Extract the metadata from the given audio
        `frame` that is easy to get and available.
        
        The `frame` must be one of the following types:
        - `av.AudioFrame`
        
        There is metadata that has to be manually
        written when the frame is modified by a node
        and we know exactly some metadata of the
        output we will generate and return, but this
        is just an easy and automated extractor.
        """
        metadata_dict = {
            AudioFrameMetadataField.IS_SILENT.value: None
        }

        # TODO: Handle whatever I can

        return _FrameMetadata(
            **metadata_dict
        )