from yta_validation.parameter import ParameterValidator
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 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(
        frame: Union['np.ndarray', 'moderngl.Texture', 'av.VideoFrame', 'RenderTarget']
    ) -> _FrameMetadata:
        """
        Extract the metadata from the given `frame`
        that is easy to get and available.
        
        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 = {
            'has_alpha_pixels': None,
            'is_fully_opaque': None,
            'is_fully_transparent': 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['has_alpha_pixels'] = a_min < 255
                metadata_dict['is_fully_opaque'] = a_min == 255
                metadata_dict['is_fully_transparent'] = a_max == 0
            else:
                metadata_dict['has_alpha_pixels'] = False
                metadata_dict['is_fully_opaque'] = True
                metadata_dict['is_fully_transparent'] = False
        elif PythonValidator.is_instance_of(frame, 'VideoFrame'):
            # [2] `av.VideoFrame`
            metadata_dict['has_alpha_pixels'] = 'a' in frame.format.name
            metadata_dict['is_fully_opaque'] = (
                True
                if not metadata_dict['has_alpha_pixels'] else
                # We don't know yet
                None
            )
            metadata_dict['is_fully_transparent'] = (
                False
                if not metadata_dict['has_alpha_pixels'] 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.from_dict(
            frame = frame,
            metadata = metadata_dict
        )