"""Platform abstraction layer for screen capture, window enumeration, and webcam backends.

Provides a consistent API across Windows, macOS, and Linux.
"""

import ctypes
import platform
from dataclasses import dataclass
from typing import List, Optional, Tuple

# ─── Platform detection ──────────────────────────────────────────────────────

PLATFORM = platform.system()
IS_WINDOWS = PLATFORM == "Windows"
IS_MAC = PLATFORM == "Darwin"
IS_LINUX = PLATFORM == "Linux"

# ─── Conditional Windows imports ─────────────────────────────────────────────

if IS_WINDOWS:
    import ctypes.wintypes as wintypes

    user32 = ctypes.windll.user32
    gdi32 = ctypes.windll.gdi32
    EnumWindowsProc = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, ctypes.c_void_p)

    DIB_RGB_COLORS = 0
    BI_RGB = 0
    PW_RENDERFULLCONTENT = 0x00000002

    class BITMAPINFOHEADER(ctypes.Structure):
        _fields_ = [
            ("biSize", wintypes.DWORD),
            ("biWidth", wintypes.LONG),
            ("biHeight", wintypes.LONG),
            ("biPlanes", wintypes.WORD),
            ("biBitCount", wintypes.WORD),
            ("biCompression", wintypes.DWORD),
            ("biSizeImage", wintypes.DWORD),
            ("biXPelsPerMeter", wintypes.LONG),
            ("biYPelsPerMeter", wintypes.LONG),
            ("biClrUsed", wintypes.DWORD),
            ("biClrImportant", wintypes.DWORD),
        ]

    class BITMAPINFO(ctypes.Structure):
        _fields_ = [
            ("bmiHeader", BITMAPINFOHEADER),
            ("bmiColors", wintypes.DWORD * 3),
        ]

# ─── Lazy PIL import (may not be available at import time) ───────────────────

try:
    from PIL import Image, ImageGrab
except ImportError:
    Image = None
    ImageGrab = None


# ═══════════════════════════════════════════════════════════════════════════════
# DATA
# ═══════════════════════════════════════════════════════════════════════════════

@dataclass
class WindowInfo:
    """Information about a visible window on any platform."""
    hwnd: int       # window handle (Windows) or window id (Mac/Linux)
    title: str
    left: int
    top: int
    width: int
    height: int


# ═══════════════════════════════════════════════════════════════════════════════
# WINDOW ENUMERATION
# ═══════════════════════════════════════════════════════════════════════════════

def list_visible_windows() -> List[WindowInfo]:
    """List visible windows. Dispatches to platform-specific implementation."""
    if IS_WINDOWS:
        return _list_windows_win32()
    elif IS_MAC:
        return _list_windows_mac()
    elif IS_LINUX:
        return _list_windows_linux()
    return []


def _list_windows_win32() -> List[WindowInfo]:
    windows: List[WindowInfo] = []

    @EnumWindowsProc
    def enum_proc(hwnd, lParam):
        if not user32.IsWindowVisible(hwnd):
            return True
        if user32.IsIconic(hwnd):
            return True
        length = user32.GetWindowTextLengthW(hwnd)
        if length == 0:
            return True
        buf = ctypes.create_unicode_buffer(length + 1)
        user32.GetWindowTextW(hwnd, buf, length + 1)
        title = buf.value.strip()
        if not title:
            return True
        rect = _get_window_rect_win32(hwnd)
        if rect is None:
            return True
        left, top, right, bottom = rect
        width = right - left
        height = bottom - top
        if width < 100 or height < 60:
            return True
        blacklist = {"Program Manager", "Windows Input Experience", "Microsoft Text Input Application"}
        if title in blacklist:
            return True
        windows.append(WindowInfo(hwnd=hwnd, title=title, left=left, top=top, width=width, height=height))
        return True

    user32.EnumWindows(enum_proc, 0)
    windows.sort(key=lambda w: w.title.lower())
    return windows


def _list_windows_mac() -> List[WindowInfo]:
    """List visible windows on macOS via Quartz CGWindowListCopyWindowInfo."""
    windows: List[WindowInfo] = []
    try:
        import Quartz
        import objc

        with objc.autorelease_pool():
            window_list = Quartz.CGWindowListCopyWindowInfo(
                Quartz.kCGWindowListOptionOnScreenOnly | Quartz.kCGWindowListExcludeDesktopElements,
                Quartz.kCGNullWindowID,
            )
            if window_list is None:
                return windows
            for win in window_list:
                title = win.get("kCGWindowName", "")
                owner = win.get("kCGWindowOwnerName", "")
                if not title and not owner:
                    continue
                bounds = win.get("kCGWindowBounds", {})
                x = int(bounds.get("X", 0))
                y = int(bounds.get("Y", 0))
                w = int(bounds.get("Width", 0))
                h = int(bounds.get("Height", 0))
                if w < 100 or h < 60:
                    continue
                display_title = title if title else owner
                wid = int(win.get("kCGWindowNumber", 0))
                windows.append(WindowInfo(hwnd=wid, title=display_title, left=x, top=y, width=w, height=h))
            del window_list  # let pyobjc handle CFRelease via GC
    except ImportError:
        pass
    except Exception:
        pass
    windows.sort(key=lambda w: w.title.lower())
    return windows


def _list_windows_linux() -> List[WindowInfo]:
    """List visible windows on Linux via wmctrl."""
    windows: List[WindowInfo] = []
    try:
        import subprocess
        result = subprocess.run(["wmctrl", "-lG"], capture_output=True, text=True, timeout=5)
        if result.returncode == 0:
            for line in result.stdout.strip().split("\n"):
                parts = line.split(None, 7)
                if len(parts) < 8:
                    continue
                wid = int(parts[0], 16)
                x, y, w, h = int(parts[2]), int(parts[3]), int(parts[4]), int(parts[5])
                title = parts[7]
                if w < 100 or h < 60:
                    continue
                windows.append(WindowInfo(hwnd=wid, title=title, left=x, top=y, width=w, height=h))
    except Exception:
        pass
    windows.sort(key=lambda w: w.title.lower())
    return windows


# ═══════════════════════════════════════════════════════════════════════════════
# WINDOW RECT / CAPTURE
# ═══════════════════════════════════════════════════════════════════════════════

def get_window_rect(hwnd: int) -> Optional[Tuple[int, int, int, int]]:
    """Get window rectangle (left, top, right, bottom). Windows only."""
    if IS_WINDOWS:
        return _get_window_rect_win32(hwnd)
    return None


def _get_window_rect_win32(hwnd: int) -> Optional[Tuple[int, int, int, int]]:
    rect = wintypes.RECT()
    if not user32.GetWindowRect(hwnd, ctypes.byref(rect)):
        return None
    return rect.left, rect.top, rect.right, rect.bottom


def grab_monitor_rect(rect: dict) -> Optional["Image.Image"]:
    """Capture a region of the screen via Pillow ImageGrab."""
    if ImageGrab is None:
        return None
    try:
        return ImageGrab.grab(
            bbox=(rect["left"], rect["top"], rect["left"] + rect["width"], rect["top"] + rect["height"]),
            all_screens=True,
        ).convert("RGB")
    except Exception:
        return None


def capture_window(hwnd: int, expected_size: Optional[Tuple[int, int]] = None) -> Optional["Image.Image"]:
    """Capture a specific window. Windows uses PrintWindow, macOS uses Quartz.

    expected_size: (width, height) in logical points.  On macOS Retina displays
    the captured image may be 2x the logical size; if expected_size is given the
    image is resized to match.
    """
    if IS_WINDOWS:
        return _capture_window_win32(hwnd)
    elif IS_MAC:
        return _capture_window_mac(hwnd, expected_size)
    return None


def _capture_window_mac(window_id: int, expected_size: Optional[Tuple[int, int]] = None) -> Optional["Image.Image"]:
    """Capture a specific window on macOS via CoreGraphics C API (ctypes).

    Uses ctypes to call CoreGraphics directly, bypassing pyobjc entirely.
    This gives us raw C pointers so we can call CFRelease at exactly the
    right time — no double-free from pyobjc GC, no leaked objects from
    deferred release.

    Previous attempts using pyobjc wrappers leaked ~100 MB/s because
    pyobjc's ref-counting didn't trigger CFRelease fast enough.  Calling
    CFRelease explicitly on pyobjc wrappers caused double-free segfaults.
    """
    if Image is None:
        return None
    try:
        img = _capture_window_mac_ctypes(window_id)
        if img is None:
            return None
        # On Retina displays the capture may be at 2x pixel resolution.
        # Resize to logical point dimensions so regions map correctly.
        if expected_size is not None:
            ew, eh = expected_size
            if ew > 0 and eh > 0 and (img.width != ew or img.height != eh):
                img = img.resize((ew, eh), Image.LANCZOS)
        return img
    except Exception:
        return None


# ─── ctypes setup for CoreGraphics (loaded once, used by _capture_window_mac) ─
if IS_MAC:
    try:
        import ctypes.util as _cu

        _cg_path = _cu.find_library("CoreGraphics")
        _cf_path = _cu.find_library("CoreFoundation")
        _CG = ctypes.cdll.LoadLibrary(_cg_path) if _cg_path else None
        _CF = ctypes.cdll.LoadLibrary(_cf_path) if _cf_path else None

        if _CG and _CF:
            # ── CGRect struct ────────────────────────────────────────────
            class _CGPoint(ctypes.Structure):
                _fields_ = [("x", ctypes.c_double), ("y", ctypes.c_double)]

            class _CGSize(ctypes.Structure):
                _fields_ = [("width", ctypes.c_double), ("height", ctypes.c_double)]

            class _CGRect(ctypes.Structure):
                _fields_ = [("origin", _CGPoint), ("size", _CGSize)]

            _CGRectNull = _CGRect(_CGPoint(ctypes.c_double(float('inf')),
                                           ctypes.c_double(float('inf'))),
                                  _CGSize(ctypes.c_double(0), ctypes.c_double(0)))

            # ── Function signatures ──────────────────────────────────────
            _CG.CGWindowListCreateImage.restype = ctypes.c_void_p
            _CG.CGWindowListCreateImage.argtypes = [
                _CGRect, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32,
            ]
            _CG.CGImageGetWidth.restype = ctypes.c_size_t
            _CG.CGImageGetWidth.argtypes = [ctypes.c_void_p]
            _CG.CGImageGetHeight.restype = ctypes.c_size_t
            _CG.CGImageGetHeight.argtypes = [ctypes.c_void_p]
            _CG.CGColorSpaceCreateDeviceRGB.restype = ctypes.c_void_p
            _CG.CGColorSpaceCreateDeviceRGB.argtypes = []
            _CG.CGBitmapContextCreate.restype = ctypes.c_void_p
            _CG.CGBitmapContextCreate.argtypes = [
                ctypes.c_void_p, ctypes.c_size_t, ctypes.c_size_t,
                ctypes.c_size_t, ctypes.c_size_t, ctypes.c_void_p, ctypes.c_uint32,
            ]
            _CG.CGContextDrawImage.restype = None
            _CG.CGContextDrawImage.argtypes = [ctypes.c_void_p, _CGRect, ctypes.c_void_p]
            _CG.CGBitmapContextGetData.restype = ctypes.c_void_p
            _CG.CGBitmapContextGetData.argtypes = [ctypes.c_void_p]
            _CG.CGImageRelease.restype = None
            _CG.CGImageRelease.argtypes = [ctypes.c_void_p]
            _CG.CGContextRelease.restype = None
            _CG.CGContextRelease.argtypes = [ctypes.c_void_p]
            _CG.CGColorSpaceRelease.restype = None
            _CG.CGColorSpaceRelease.argtypes = [ctypes.c_void_p]

            # Constants
            _kCGWindowListOptionIncludingWindow = 1 << 3  # 8
            _kCGWindowImageBoundsIgnoreFraming = 1 << 0   # 1
            _kCGWindowImageNominalResolution = 1 << 9     # 512
            _kCGImageAlphaPremultipliedFirst = 2
            _kCGBitmapByteOrder32Little = 2 << 12  # 8192

            _MAC_CTYPES_OK = True
        else:
            _MAC_CTYPES_OK = False
    except Exception:
        _MAC_CTYPES_OK = False
else:
    _MAC_CTYPES_OK = False


def _capture_window_mac_ctypes(window_id: int) -> Optional["Image.Image"]:
    """CoreGraphics window capture via ctypes — no pyobjc, no leaks."""
    if not _MAC_CTYPES_OK:
        return None

    cg_image = None
    color_space = None
    context = None

    try:
        cg_image = _CG.CGWindowListCreateImage(
            _CGRectNull,
            _kCGWindowListOptionIncludingWindow,
            window_id,
            _kCGWindowImageBoundsIgnoreFraming | _kCGWindowImageNominalResolution,
        )
        if not cg_image:
            return None

        width = _CG.CGImageGetWidth(cg_image)
        height = _CG.CGImageGetHeight(cg_image)
        if width < 1 or height < 1:
            return None

        bytes_per_row = width * 4
        color_space = _CG.CGColorSpaceCreateDeviceRGB()
        if not color_space:
            return None

        bitmap_info = _kCGImageAlphaPremultipliedFirst | _kCGBitmapByteOrder32Little
        context = _CG.CGBitmapContextCreate(
            None, width, height, 8, bytes_per_row, color_space, bitmap_info,
        )
        # Done with color_space
        _CG.CGColorSpaceRelease(color_space); color_space = None

        if not context:
            return None

        rect = _CGRect(_CGPoint(0, 0), _CGSize(width, height))
        _CG.CGContextDrawImage(context, rect, cg_image)
        # Done with cg_image
        _CG.CGImageRelease(cg_image); cg_image = None

        # Read pixels directly from the bitmap context buffer — no extra
        # CGImage/CFData objects needed.  The buffer is owned by the context
        # and valid until we release it.
        data_ptr = _CG.CGBitmapContextGetData(context)
        if not data_ptr:
            return None

        buf_size = bytes_per_row * height
        raw = ctypes.string_at(data_ptr, buf_size)

        # Done with context (raw already copied the bytes)
        _CG.CGContextRelease(context); context = None

        img = Image.frombytes(
            "RGBA", (width, height), raw, "raw", "BGRA", bytes_per_row, 1,
        )
        return img.convert("RGB")

    finally:
        # Safety net — release anything not yet released
        if cg_image:
            _CG.CGImageRelease(cg_image)
        if color_space:
            _CG.CGColorSpaceRelease(color_space)
        if context:
            _CG.CGContextRelease(context)


def _capture_window_win32(hwnd: int) -> Optional["Image.Image"]:
    if Image is None:
        return None
    rect = _get_window_rect_win32(hwnd)
    if rect is None:
        return None
    left, top, right, bottom = rect
    width = max(1, right - left)
    height = max(1, bottom - top)

    hwnd_dc = user32.GetWindowDC(hwnd)
    if not hwnd_dc:
        return None

    mem_dc = gdi32.CreateCompatibleDC(hwnd_dc)
    bitmap = gdi32.CreateCompatibleBitmap(hwnd_dc, width, height)
    old_obj = gdi32.SelectObject(mem_dc, bitmap)

    try:
        result = user32.PrintWindow(hwnd, mem_dc, PW_RENDERFULLCONTENT)
        if result != 1:
            result = user32.PrintWindow(hwnd, mem_dc, 0)
        if result != 1:
            return None

        bmi = BITMAPINFO()
        bmi.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER)
        bmi.bmiHeader.biWidth = width
        bmi.bmiHeader.biHeight = -height
        bmi.bmiHeader.biPlanes = 1
        bmi.bmiHeader.biBitCount = 32
        bmi.bmiHeader.biCompression = BI_RGB

        buffer_len = width * height * 4
        buffer = ctypes.create_string_buffer(buffer_len)
        lines = gdi32.GetDIBits(mem_dc, bitmap, 0, height, buffer, ctypes.byref(bmi), DIB_RGB_COLORS)
        if lines == 0:
            return None

        img = Image.frombuffer("RGBA", (width, height), buffer, "raw", "BGRA", 0, 1)
        return img.convert("RGB")
    finally:
        gdi32.SelectObject(mem_dc, old_obj)
        gdi32.DeleteObject(bitmap)
        gdi32.DeleteDC(mem_dc)
        user32.ReleaseDC(hwnd, hwnd_dc)


# ═══════════════════════════════════════════════════════════════════════════════
# WEBCAM BACKEND SELECTION
# ═══════════════════════════════════════════════════════════════════════════════

def get_webcam_backends() -> list:
    """Return preferred OpenCV webcam backends for the current platform."""
    try:
        import cv2
    except ImportError:
        return []

    backends = []
    if IS_WINDOWS:
        if hasattr(cv2, "CAP_MSMF"):
            backends.append(cv2.CAP_MSMF)
        if hasattr(cv2, "CAP_DSHOW"):
            backends.append(cv2.CAP_DSHOW)
    elif IS_MAC:
        if hasattr(cv2, "CAP_AVFOUNDATION"):
            backends.append(cv2.CAP_AVFOUNDATION)
    elif IS_LINUX:
        if hasattr(cv2, "CAP_V4L2"):
            backends.append(cv2.CAP_V4L2)

    if not backends:
        backends.append(0)  # default backend
    return backends


# ═══════════════════════════════════════════════════════════════════════════════
# macOS SCREEN CAPTURE PERMISSIONS
# ═══════════════════════════════════════════════════════════════════════════════

def check_macos_screen_permission() -> Optional[bool]:
    """Check if screen capture permission is granted on macOS.

    Returns:
        True if granted, False if denied, None if not on macOS or check unavailable.
    """
    if not IS_MAC:
        return None
    try:
        import Quartz
        # CGPreflightScreenCaptureAccess available since macOS 10.15
        if hasattr(Quartz, "CGPreflightScreenCaptureAccess"):
            return bool(Quartz.CGPreflightScreenCaptureAccess())
    except ImportError:
        pass
    return None


def request_macos_screen_permission() -> Optional[bool]:
    """Request screen capture permission on macOS (shows system dialog).

    Returns:
        True if granted, False if denied, None if not on macOS or unavailable.
    """
    if not IS_MAC:
        return None
    try:
        import Quartz
        if hasattr(Quartz, "CGRequestScreenCaptureAccess"):
            return bool(Quartz.CGRequestScreenCaptureAccess())
    except ImportError:
        pass
    return None
