"""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 CoreFoundation
        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
            try:
                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))
            finally:
                # window_list is owned (Copy rule) — release explicitly
                try:
                    CoreFoundation.CFRelease(window_list)
                except Exception:
                    pass
    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) -> Optional["Image.Image"]:
    """Capture a specific window. Windows uses PrintWindow, macOS uses Quartz."""
    if IS_WINDOWS:
        return _capture_window_win32(hwnd)
    elif IS_MAC:
        return _capture_window_mac(hwnd)
    return None


def _capture_window_mac(window_id: int) -> Optional["Image.Image"]:
    """Capture a specific window on macOS via Quartz CGWindowListCreateImage.

    Uses the window ID (kCGWindowNumber) to capture just that window,
    regardless of what's in front of or behind it.

    All CoreFoundation/CoreGraphics objects from Create/Copy calls are released
    explicitly via CoreFoundation.CFRelease() to prevent memory leaks.  pyobjc's
    normal ref-counting via ``del`` does not trigger CFRelease fast enough,
    causing ~100 MB/s growth at 15 FPS.
    """
    if Image is None:
        return None
    try:
        import CoreFoundation
        import Quartz
        import objc

        def _cf_release(obj):
            """Safely release a CoreFoundation object.  No-op on None."""
            if obj is not None:
                try:
                    CoreFoundation.CFRelease(obj)
                except Exception:
                    pass

        # Track all owned CG objects so we can release them even on error
        cg_image = None
        color_space = None
        context = None
        result_image = None
        cf_data = None

        try:
            with objc.autorelease_pool():
                # CGRectNull captures the window at its natural bounds
                cg_image = Quartz.CGWindowListCreateImage(
                    Quartz.CGRectNull,
                    Quartz.kCGWindowListOptionIncludingWindow,
                    window_id,
                    Quartz.kCGWindowImageBoundsIgnoreFraming
                    | Quartz.kCGWindowImageNominalResolution,
                )
                if cg_image is None:
                    return None

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

                # Render into a bitmap context so we get predictable BGRA layout
                bytes_per_row = width * 4
                color_space = Quartz.CGColorSpaceCreateDeviceRGB()
                bitmap_info = (Quartz.kCGImageAlphaPremultipliedFirst
                               | Quartz.kCGBitmapByteOrder32Little)
                context = Quartz.CGBitmapContextCreate(
                    None, width, height, 8, bytes_per_row, color_space, bitmap_info,
                )
                # color_space is owned (Create rule) — release now
                _cf_release(color_space); color_space = None

                if context is None:
                    return None

                Quartz.CGContextDrawImage(
                    context, Quartz.CGRectMake(0, 0, width, height), cg_image,
                )
                # cg_image is owned (Create rule) — release now
                _cf_release(cg_image); cg_image = None

                # Extract bytes via a new CGImage from the context
                result_image = Quartz.CGBitmapContextCreateImage(context)
                # context is owned (Create rule) — release now
                _cf_release(context); context = None

                if result_image is None:
                    return None

                # CGImageGetDataProvider returns a borrowed reference (Get rule)
                # — do NOT release it
                provider = Quartz.CGImageGetDataProvider(result_image)
                if provider is None:
                    return None

                # CGDataProviderCopyData returns an owned CFData (Copy rule)
                cf_data = Quartz.CGDataProviderCopyData(provider)
                # result_image is owned (Create rule) — release now
                _cf_release(result_image); result_image = None

                if cf_data is None:
                    return None

                # Copy bytes into a Python bytes object
                raw = bytes(cf_data)
                # cf_data is owned (Copy rule) — release now
                _cf_release(cf_data); cf_data = None

                img = Image.frombytes(
                    "RGBA", (width, height), raw, "raw", "BGRA", bytes_per_row, 1,
                )
                return img.convert("RGB")
        finally:
            # Safety net: release anything that didn't get released above
            _cf_release(cg_image)
            _cf_release(color_space)
            _cf_release(context)
            _cf_release(result_image)
            _cf_release(cf_data)

    except ImportError:
        return None
    except Exception:
        return None


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
