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

    @property
    def to_dict(
        self
    ) -> dict:
        """
        Get a copy of the metadata directly as a dict.
        """
        return self._metadata.copy()

    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._metadata.copy()
        )
    
    def __setattr__(
        self,
        name: str,
        value: any
    ):
        if self._is_internal_attribute(name):
            object.__setattr__(self, name, value)
        else:
            self._metadata[name] = value

    def __getattr__(
        self,
        name: str
    ):
        try:
            return self._metadata[name]
        except KeyError:
            raise AttributeError(f'{name} not found')

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


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

#     Class to wrap the metadata about a frame. Any value
#     that is `None` means that we don't have any data
#     about it yet.

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

#     @property
#     def width(
#         self
#     ) -> int:
#         """
#         The width of the frame, obtained from the resource.
#         """
#         return self._frame.resource.width
    
#     @property
#     def height(
#         self
#     ) -> int:
#         """
#         The height of the frame, obtained from the resource.
#         """
#         return self._frame.resource.height
    
#     @property
#     def format(
#         self
#     ) -> str:
#         """
#         The format of the frame, obtained from the resource.
#         """
#         return self._frame.resource.format

#     def __init__(
#         self,
#         frame: 'Frame',
#         has_alpha_pixels: Union[bool, None] = None,
#         is_fully_opaque: Union[bool, None] = None,
#         is_fully_transparent: Union[bool, None] = None,
#         **kwargs
#     ):
#         ParameterValidator.validate_mandatory_instance_of('frame', frame, 'Frame')

#         self._frame: 'Frame' = frame
#         """
#         *For internal use only*

#         The `Frame` instance this metadata belongs to.
#         """
#         self.has_alpha_pixels: Union[bool, None] = has_alpha_pixels
#         """
#         Boolean flag to indicate if there is at least one
#         pixel with some kind of transparency or not.
#         """
#         self.is_fully_opaque: Union[bool, None] = is_fully_opaque
#         """
#         Boolean flag to indicate if the frame is completely
#         opaque and there are no transparencies on it. This
#         is very useful when we need to combine frames or
#         we need to apply an effect based on the transparency,
#         so we can process it faster as we know no frame after
#         this one will be seen.
#         """
#         self.is_fully_transparent: Union[bool, None] = is_fully_transparent
#         """
#         Boolean flag to indicate if the frame is completely
#         transparent. This is very useful when we need to
#         combine frames because we can skip it when combining.
#         """

#         # Set the dynamic values
#         for key, value in kwargs.items():
#             setattr(self, key, value)

#     def copy(
#         self,
#         frame: 'Frame'
#     ) -> '_FrameMetadata':
#         """
#         Get a copy of this `_FrameMetadata` instance but
#         attached to the `frame` provided.
#         """
#         return _FrameMetadata.from_dict(
#             frame = frame,
#             metadata = {
#                 'has_alpha_pixels': self.has_alpha_pixels,
#                 'is_fully_opaque': self.is_fully_opaque,
#                 'is_fully_transparent': self.is_fully_transparent
#                 # TODO: What about the other possible fields (?)
#             }
#         )

#     @classmethod
#     def from_dict(
#         cls,
#         frame: 'Frame',
#         metadata: dict
#     ) -> '_FrameMetadata':
#         """
#         Get a `_FrameMetadata` instance associated to the
#         `frame` provided with the data included in the
#         `metadata` dict given.

#         This method is useful to build and attach metadata
#         when using the extractor.
#         """
#         # Dynamically add mandatory keys to metadata
#         # if missing
#         keys = {
#             'has_alpha_pixels',
#             'is_fully_opaque',
#             'is_fully_transparent'
#         }

#         for key in keys:
#             if key not in metadata:
#                 metadata[key] = None

#         return _FrameMetadata(
#             frame = frame,
#             **metadata
#             # has_alpha_pixels = has_alpha_pixels,
#             # is_fully_opaque = is_fully_opaque,
#             # is_fully_transparent = is_fully_transparent
#         )
        

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
        )