"""macOS CoreGraphics backend for MSS.

Uses the CoreGraphics APIs to capture windows and enumerates up to
``max_displays`` active displays.
"""

from __future__ import annotations

import ctypes
import ctypes.util
import sys
import warnings
from ctypes import (
    POINTER,
    Structure,
    c_double,
    c_float,
    c_int32,
    c_long,
    c_size_t,
    c_ubyte,
    c_uint32,
    c_void_p,
)
from platform import mac_ver
from typing import TYPE_CHECKING

from mss.base import MSS as _MSS
from mss.base import MSSImplementation
from mss.exception import ScreenShotError
from mss.screenshot import Size

if TYPE_CHECKING:
    from typing import Any

    from mss.models import CFunctions, Monitor, Monitors

__all__ = ("IMAGE_OPTIONS", "MSS")

BACKENDS = ["default"]

MAC_VERSION_CATALINA = 10.16

kCGWindowImageBoundsIgnoreFraming = 1 << 0  # noqa: N816
kCGWindowImageNominalResolution = 1 << 4  # noqa: N816
kCGWindowImageShouldBeOpaque = 1 << 1  # noqa: N816
#: For advanced users: as a note, you can set ``IMAGE_OPTIONS = 0`` to turn on scaling; see issue #257 for more
#: information.
IMAGE_OPTIONS: int = kCGWindowImageBoundsIgnoreFraming | kCGWindowImageShouldBeOpaque | kCGWindowImageNominalResolution


class MSS(_MSS):
    """Deprecated macOS compatibility constructor.

    Use :class:`mss.MSS` instead.
    """

    def __init__(self, /, **kwargs: Any) -> None:
        # TODO(jholveck): #493 Remove compatibility constructor after 10.x transition period.
        warnings.warn(
            "mss.darwin.MSS is deprecated and will be removed in 11.0; use mss.MSS instead",
            DeprecationWarning,
            stacklevel=2,
        )
        super().__init__(**kwargs)


def cgfloat() -> type[c_double | c_float]:
    """Get the appropriate value for a float."""
    return c_double if sys.maxsize > 2**32 else c_float


class CGPoint(Structure):
    """Structure that contains coordinates of a rectangle."""

    _fields_ = (("x", cgfloat()), ("y", cgfloat()))

    def __repr__(self) -> str:
        return f"{type(self).__name__}(left={self.x} top={self.y})"


class CGSize(Structure):
    """Structure that contains dimensions of an rectangle."""

    _fields_ = (("width", cgfloat()), ("height", cgfloat()))

    def __repr__(self) -> str:
        return f"{type(self).__name__}(width={self.width} height={self.height})"


class CGRect(Structure):
    """Structure that contains information about a rectangle."""

    _fields_ = (("origin", CGPoint), ("size", CGSize))

    def __repr__(self) -> str:
        return f"{type(self).__name__}<{self.origin} {self.size}>"


# C functions that will be initialised later.
#
# Available attr: core.
#
# Note: keep it sorted by cfunction.
CFUNCTIONS: CFunctions = {
    # Syntax: cfunction: (attr, argtypes, restype)
    "CGDataProviderCopyData": ("core", [c_void_p], c_void_p),
    "CGDisplayBounds": ("core", [c_uint32], CGRect),
    "CGDisplayRotation": ("core", [c_uint32], c_double),
    "CFDataGetBytePtr": ("core", [c_void_p], POINTER(c_ubyte)),
    "CFDataGetLength": ("core", [c_void_p], c_long),
    "CFRelease": ("core", [c_void_p], None),
    "CGGetActiveDisplayList": ("core", [c_uint32, POINTER(c_uint32), POINTER(c_uint32)], c_int32),
    "CGImageGetBitsPerPixel": ("core", [c_void_p], c_size_t),
    "CGImageGetBytesPerRow": ("core", [c_void_p], c_size_t),
    "CGImageGetDataProvider": ("core", [c_void_p], c_void_p),
    "CGImageGetHeight": ("core", [c_void_p], c_size_t),
    "CGImageGetWidth": ("core", [c_void_p], c_size_t),
    "CGRectStandardize": ("core", [CGRect], CGRect),
    "CGRectUnion": ("core", [CGRect, CGRect], CGRect),
    "CGWindowListCreateImage": ("core", [CGRect, c_uint32, c_uint32, c_uint32], c_void_p),
}


class MSSImplDarwin(MSSImplementation):
    """Multiple ScreenShots implementation for macOS.
    It uses intensively the CoreGraphics library.

    :param max_displays: maximum number of displays to handle (default: 32).
    :type max_displays: int

    .. seealso::

        :py:class:`mss.MSS`
            Lists other parameters.
    """

    __slots__ = {"core", "max_displays"}

    def __init__(self, *, backend: str = "default", max_displays: int = 32) -> None:
        super().__init__()

        if backend != "default":
            msg = 'The only valid backend on this platform is "default".'
            raise ScreenShotError(msg)

        # max_displays is only used by monitors(), while the lock is held.
        #: Maximum number of displays to handle.
        self.max_displays = max_displays

        self._init_library()
        self._set_cfunctions()

    def _init_library(self) -> None:
        """Load the CoreGraphics library."""
        version = float(".".join(mac_ver()[0].split(".")[:2]))
        if version < MAC_VERSION_CATALINA:
            coregraphics = ctypes.util.find_library("CoreGraphics")
        else:
            # macOS Big Sur and newer
            coregraphics = "/System/Library/Frameworks/CoreGraphics.framework/Versions/Current/CoreGraphics"

        if not coregraphics:
            msg = "No CoreGraphics library found."
            raise ScreenShotError(msg)
        # :meta:private:
        self.core = ctypes.cdll.LoadLibrary(coregraphics)

    def _set_cfunctions(self) -> None:
        """Set all ctypes functions and attach them to attributes."""
        cfactory = self._cfactory
        attrs = {"core": self.core}
        for func, (attr, argtypes, restype) in CFUNCTIONS.items():
            cfactory(attrs[attr], func, argtypes, restype)

    def monitors(self) -> Monitors:
        """Get positions of monitors."""
        int_ = int
        core = self.core

        monitors: Monitors = []

        # All monitors
        # We need to update the value with every single monitor found
        # using CGRectUnion.  Else we will end with infinite values.
        all_monitors = CGRect()
        monitors.append({})

        # Each monitor
        display_count = c_uint32(0)
        active_displays = (c_uint32 * self.max_displays)()
        core.CGGetActiveDisplayList(self.max_displays, active_displays, ctypes.byref(display_count))
        for idx in range(display_count.value):
            display = active_displays[idx]
            rect = core.CGDisplayBounds(display)
            rect = core.CGRectStandardize(rect)
            width, height = rect.size.width, rect.size.height

            # 0.0: normal
            # 90.0: right
            # -90.0: left
            if core.CGDisplayRotation(display) in {90.0, -90.0}:
                width, height = height, width

            monitors.append(
                {
                    "left": int_(rect.origin.x),
                    "top": int_(rect.origin.y),
                    "width": int_(width),
                    "height": int_(height),
                },
            )

            # Update AiO monitor's values
            all_monitors = core.CGRectUnion(all_monitors, rect)

        # Set the AiO monitor's values
        monitors[0] = {
            "left": int_(all_monitors.origin.x),
            "top": int_(all_monitors.origin.y),
            "width": int_(all_monitors.size.width),
            "height": int_(all_monitors.size.height),
        }

        return monitors

    def grab(self, monitor: Monitor, /) -> tuple[bytearray, Size]:
        """Retrieve all pixels from a monitor. Pixels have to be RGB."""
        core = self.core
        rect = CGRect((monitor["left"], monitor["top"]), (monitor["width"], monitor["height"]))

        image_ref = core.CGWindowListCreateImage(rect, 1, 0, IMAGE_OPTIONS)
        if not image_ref:
            msg = "CoreGraphics.CGWindowListCreateImage() failed."
            raise ScreenShotError(msg)

        width = core.CGImageGetWidth(image_ref)
        height = core.CGImageGetHeight(image_ref)
        copy_data = None
        try:
            prov = core.CGImageGetDataProvider(image_ref)
            copy_data = core.CGDataProviderCopyData(prov)
            data_ref = core.CFDataGetBytePtr(copy_data)
            buf_len = core.CFDataGetLength(copy_data)
            raw = ctypes.cast(data_ref, POINTER(c_ubyte * buf_len))
            data = bytearray(raw.contents)

            # Remove padding per row
            bytes_per_row = core.CGImageGetBytesPerRow(image_ref)
            bytes_per_pixel = core.CGImageGetBitsPerPixel(image_ref)
            bytes_per_pixel = (bytes_per_pixel + 7) // 8

            if bytes_per_pixel * width != bytes_per_row:
                cropped = bytearray()
                for row in range(height):
                    start = row * bytes_per_row
                    end = start + width * bytes_per_pixel
                    cropped.extend(data[start:end])
                data = cropped
        finally:
            if copy_data:
                core.CFRelease(copy_data)
            core.CFRelease(image_ref)

        return data, Size(width, height)

    def cursor(self) -> None:
        """Retrieve all cursor data. Pixels have to be RGB."""
        return
