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

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

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
    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
        window_list = Quartz.CGWindowListCopyWindowInfo(
            Quartz.kCGWindowListOptionOnScreenOnly | Quartz.kCGWindowListExcludeDesktopElements,
            Quartz.kCGNullWindowID,
        )
        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))
    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.
    """
    if Image is None:
        return None
    try:
        import Quartz

        # 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 the CGImage into a bitmap context and extract raw bytes
        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,
        )
        if context is None:
            return None

        Quartz.CGContextDrawImage(
            context, Quartz.CGRectMake(0, 0, width, height), cg_image
        )

        # Create a new CGImage from the context and get its data provider
        result_image = Quartz.CGBitmapContextCreateImage(context)
        if result_image is None:
            return None

        provider = Quartz.CGImageGetDataProvider(result_image)
        if provider is None:
            return None

        cf_data = Quartz.CGDataProviderCopyData(provider)
        if cf_data is None:
            return None

        # cf_data is a CFData object; bytes() extracts the raw buffer
        raw = bytes(cf_data)
        img = Image.frombuffer(
            "RGBA", (width, height), raw, "raw", "BGRA", bytes_per_row, 1
        )
        return img.convert("RGB")
    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
