from collections.abc import Mapping
from enum import Enum

import cython
from cython.cimports.libc.stdint import int32_t, uintptr_t

from av.sidedata.encparams import VideoEncParams
from av.sidedata.motionvectors import MotionVectors

_cinit_bypass_sentinel = cython.declare(object, object())


class Type(Enum):
    """
    Enum class representing different types of frame data in audio/video processing.
    Values are mapped to corresponding AV_FRAME_DATA constants from FFmpeg.

    From: https://github.com/FFmpeg/FFmpeg/blob/master/libavutil/frame.h
    """

    PANSCAN = lib.AV_FRAME_DATA_PANSCAN
    A53_CC = lib.AV_FRAME_DATA_A53_CC
    STEREO3D = lib.AV_FRAME_DATA_STEREO3D
    MATRIXENCODING = lib.AV_FRAME_DATA_MATRIXENCODING
    DOWNMIX_INFO = lib.AV_FRAME_DATA_DOWNMIX_INFO
    REPLAYGAIN = lib.AV_FRAME_DATA_REPLAYGAIN
    DISPLAYMATRIX = lib.AV_FRAME_DATA_DISPLAYMATRIX
    AFD = lib.AV_FRAME_DATA_AFD
    MOTION_VECTORS = lib.AV_FRAME_DATA_MOTION_VECTORS
    SKIP_SAMPLES = lib.AV_FRAME_DATA_SKIP_SAMPLES
    AUDIO_SERVICE_TYPE = lib.AV_FRAME_DATA_AUDIO_SERVICE_TYPE
    MASTERING_DISPLAY_METADATA = lib.AV_FRAME_DATA_MASTERING_DISPLAY_METADATA
    GOP_TIMECODE = lib.AV_FRAME_DATA_GOP_TIMECODE
    SPHERICAL = lib.AV_FRAME_DATA_SPHERICAL
    CONTENT_LIGHT_LEVEL = lib.AV_FRAME_DATA_CONTENT_LIGHT_LEVEL
    ICC_PROFILE = lib.AV_FRAME_DATA_ICC_PROFILE
    S12M_TIMECODE = lib.AV_FRAME_DATA_S12M_TIMECODE
    DYNAMIC_HDR_PLUS = lib.AV_FRAME_DATA_DYNAMIC_HDR_PLUS
    REGIONS_OF_INTEREST = lib.AV_FRAME_DATA_REGIONS_OF_INTEREST
    VIDEO_ENC_PARAMS = lib.AV_FRAME_DATA_VIDEO_ENC_PARAMS
    SEI_UNREGISTERED = lib.AV_FRAME_DATA_SEI_UNREGISTERED
    FILM_GRAIN_PARAMS = lib.AV_FRAME_DATA_FILM_GRAIN_PARAMS
    DETECTION_BBOXES = lib.AV_FRAME_DATA_DETECTION_BBOXES
    DOVI_RPU_BUFFER = lib.AV_FRAME_DATA_DOVI_RPU_BUFFER
    DOVI_METADATA = lib.AV_FRAME_DATA_DOVI_METADATA
    DYNAMIC_HDR_VIVID = lib.AV_FRAME_DATA_DYNAMIC_HDR_VIVID
    AMBIENT_VIEWING_ENVIRONMENT = lib.AV_FRAME_DATA_AMBIENT_VIEWING_ENVIRONMENT
    VIDEO_HINT = lib.AV_FRAME_DATA_VIDEO_HINT
    LCEVC = lib.AV_FRAME_DATA_LCEVC
    VIEW_ID = lib.AV_FRAME_DATA_VIEW_ID
    THREE_D_REFERENCE_DISPLAYS = lib.AV_FRAME_DATA_3D_REFERENCE_DISPLAYS
    EXIF = lib.AV_FRAME_DATA_EXIF

    @classmethod
    def _missing_(cls, value):
        """Name types added by an FFmpeg newer than the one PyAV was written against.

        The members above only cover what the oldest supported FFmpeg defines,
        so a frame decoded by a newer one can carry a type that is not here.
        Give it an ``UNKNOWN_<value>`` member instead of raising ``ValueError``
        and taking :attr:`av.Frame.side_data` down with it.
        """
        if not isinstance(value, int):
            return None

        member = object.__new__(cls)
        member._name_ = f"UNKNOWN_{value}"
        member._value_ = value
        return cls._value2member_map_.setdefault(value, member)


@cython.cfunc
def wrap_side_data(frame: Frame, index: cython.int) -> SideData:
    if frame.ptr.side_data[index].type == lib.AV_FRAME_DATA_MOTION_VECTORS:
        return MotionVectors(_cinit_bypass_sentinel, frame, index)
    elif frame.ptr.side_data[index].type == lib.AV_FRAME_DATA_VIDEO_ENC_PARAMS:
        return VideoEncParams(_cinit_bypass_sentinel, frame, index)
    else:
        return SideData(_cinit_bypass_sentinel, frame, index)


@cython.cfunc
def get_display_rotation(frame: Frame) -> cython.int:
    for i in range(frame.ptr.nb_side_data):
        if frame.ptr.side_data[i].type == lib.AV_FRAME_DATA_DISPLAYMATRIX:
            return int(
                lib.av_display_rotation_get(
                    cython.cast(
                        cython.pointer[cython.const[int32_t]],
                        frame.ptr.side_data[i].data,
                    )
                )
            )
    return 0


@cython.cclass
class SideData(Buffer):
    def __init__(self, sentinel, frame: Frame, index: cython.int):
        if sentinel is not _cinit_bypass_sentinel:
            raise RuntimeError("cannot manually instatiate SideData")
        self.frame = frame
        self.ptr = frame.ptr.side_data[index]
        self.metadata = wrap_dictionary(self.ptr.metadata)

    @cython.cfunc
    def _buffer_size(self) -> cython.size_t:
        return self.ptr.size

    @cython.cfunc
    def _buffer_ptr(self) -> cython.p_void:
        return self.ptr.data

    @cython.cfunc
    def _buffer_writable(self) -> cython.bint:
        return False

    def __repr__(self):
        return f"<av.sidedata.{self.__class__.__name__} {self.ptr.size} bytes of {self.type} at 0x{cython.cast(uintptr_t, self.ptr.data):0x}>"

    @property
    def type(self):
        return Type(self.ptr.type)


@cython.cclass
class _SideDataContainer:
    def __init__(self, frame: Frame):
        self.frame = frame
        self._by_index: list[SideData] = []
        self._by_type: dict = {}

        i: cython.int
        data: SideData

        for i in range(self.frame.ptr.nb_side_data):
            data = wrap_side_data(frame, i)
            self._by_index.append(data)
            self._by_type[data.type] = data

    def __len__(self):
        return len(self._by_type)

    def __iter__(self):
        """Iterate the :class:`Type` keys, as a mapping must.

        The values are reachable positionally too, via an integer or a slice.
        """
        return iter(self._by_type)

    def __getitem__(self, key):
        if isinstance(key, slice):
            # Typed as list[SideData], so slice through an untyped alias.
            entries: object = self._by_index
            return entries[key]
        if isinstance(key, int):
            return self._by_index[key]
        if isinstance(key, str):
            return self._by_type[Type[key]]
        return self._by_type[key]


class SideDataContainer(_SideDataContainer, Mapping):
    pass
