"""Linux-specific window capture using X11 via python-xlib.

This module captures windows through the X11 protocol, which works for:
- Native X11 applications
- XWayland applications (X11 apps running on Wayland)

Most retro game emulators run under XWayland, so this covers the primary use case.
"""

import struct
import threading
import time
from typing import Optional

from PIL import Image
from Xlib import X, display, Xatom
from Xlib.error import BadWindow, BadDrawable
from Xlib.ext import randr

from .. import log
from .base import FPSTrackerMixin

logger = log.get_logger()


# Module-level display connection (reused for efficiency)
_display: Optional[display.Display] = None


def _get_display() -> display.Display:
    """Get or create a shared X11 display connection."""
    global _display
    if _display is None:
        _display = display.Display()
    return _display


def _get_window_title(disp: display.Display, window) -> str:
    """Get the title of an X11 window."""
    try:
        # Try _NET_WM_NAME first (UTF-8, modern standard)
        net_wm_name = disp.intern_atom("_NET_WM_NAME")
        utf8_string = disp.intern_atom("UTF8_STRING")

        prop = window.get_full_property(net_wm_name, utf8_string)
        if prop and prop.value:
            return prop.value.decode("utf-8", errors="replace")

        # Fall back to WM_NAME (legacy)
        prop = window.get_full_property(Xatom.WM_NAME, Xatom.STRING)
        if prop and prop.value:
            if isinstance(prop.value, bytes):
                return prop.value.decode("latin-1", errors="replace")
            return str(prop.value)
    except (BadWindow, UnicodeDecodeError):
        pass

    return ""


def _get_window_geometry(window) -> Optional[dict]:
    """Get window geometry in absolute screen coordinates."""
    try:
        disp = _get_display()
        geom = window.get_geometry()
        # Translate to root window coordinates for absolute screen position
        root = disp.screen().root
        coords = root.translate_coords(window, 0, 0)
        return {
            "x": coords.x,
            "y": coords.y,
            "width": geom.width,
            "height": geom.height,
        }
    except (BadWindow, BadDrawable):
        return None


def _is_normal_window(disp: display.Display, window) -> bool:
    """Check if a window is a normal application window.

    Uses _NET_WM_WINDOW_TYPE to filter out dialogs, tooltips, etc.
    Windows without this property are assumed to be normal windows.
    """
    try:
        window_type_atom = disp.intern_atom("_NET_WM_WINDOW_TYPE")
        normal_atom = disp.intern_atom("_NET_WM_WINDOW_TYPE_NORMAL")

        prop = window.get_full_property(window_type_atom, Xatom.ATOM)
        if prop and prop.value:
            # Window has explicit type - check if it's NORMAL
            return normal_atom in prop.value

        # No window type property - assume it's a normal window
        return True
    except (BadWindow, BadDrawable):
        return False


def _enumerate_windows(disp: display.Display) -> list[dict]:
    """Enumerate top-level application windows.

    Only returns direct children of the root window that are normal
    application windows (not dialogs, tooltips, etc.).
    """
    root = disp.screen().root
    windows = []

    try:
        children = root.query_tree().children
    except BadWindow:
        return windows

    for child in children:
        try:
            # Get window attributes
            attrs = child.get_attributes()
            if attrs.map_state != X.IsViewable:
                continue

            # Only include normal application windows
            if not _is_normal_window(disp, child):
                continue

            title = _get_window_title(disp, child)
            geom = _get_window_geometry(child)

            # Only include windows with a title and reasonable size
            if title and geom and geom["width"] > 1 and geom["height"] > 1:
                windows.append({
                    "id": child.id,
                    "title": title,
                    "bounds": geom,
                })

        except BadWindow:
            continue

    return windows


def get_window_list() -> list[dict]:
    """Get list of all windows with their properties.

    Returns:
        List of window dictionaries with keys: id, title, bounds
    """
    disp = _get_display()
    windows = _enumerate_windows(disp)
    # Sort by window ID for consistent ordering
    windows.sort(key=lambda w: w["id"])
    return windows


def find_window_by_title(title_substring: str) -> Optional[dict]:
    """Find a window by partial title match.

    Args:
        title_substring: Substring to search for in window titles.

    Returns:
        Window dictionary if found, None otherwise.
    """
    title_lower = title_substring.lower()
    windows = get_window_list()

    for window in windows:
        if title_lower in window["title"].lower():
            return window

    return None


def _get_window_bounds(window_id: int) -> Optional[dict]:
    """Get the current bounds of a window by its ID in absolute screen coordinates.

    Args:
        window_id: The X11 window ID (XID).

    Returns:
        Bounds dictionary with x, y, width, height in root coordinates, or None if not found.
    """
    disp = _get_display()
    try:
        window = disp.create_resource_object("window", window_id)
        geom = window.get_geometry()

        # Translate to root window coordinates for absolute screen position
        # This is critical for multi-monitor setups
        root = disp.screen().root
        coords = root.translate_coords(window, 0, 0)

        return {
            "x": coords.x,
            "y": coords.y,
            "width": geom.width,
            "height": geom.height,
        }
    except (BadWindow, BadDrawable):
        return None


def _get_title_bar_height(window_id: int) -> int:
    """Detect the title bar height for CSD (client-side decoration) windows.

    Only returns a non-zero value for GTK/CSD windows where the title bar
    is part of the client window content. For SSD (server-side decoration)
    windows, the WM draws the title bar separately and X11 capture doesn't
    include it, so we return 0.

    Args:
        window_id: The X11 window ID (XID).

    Returns:
        Title bar height in pixels for CSD windows, 0 for SSD windows.
    """
    disp = _get_display()
    try:
        window = disp.create_resource_object("window", window_id)

        # Only use _GTK_FRAME_EXTENTS for CSD detection
        # This property is only set by GTK apps that draw their own title bar
        # For SSD apps (like RetroArch), the WM draws the title bar separately
        # and X11 get_image() doesn't include it
        gtk_frame_extents = disp.intern_atom('_GTK_FRAME_EXTENTS')
        prop = window.get_full_property(gtk_frame_extents, Xatom.CARDINAL)
        if prop and len(prop.value) >= 4:
            # Format: left, right, top, bottom
            top = prop.value[2]
            logger.debug("gtk frame extents (CSD)", left=prop.value[0], right=prop.value[1],
                        top=prop.value[2], bottom=prop.value[3])
            if top > 0:
                return top

        # No _GTK_FRAME_EXTENTS means SSD - WM draws title bar separately
        # X11 capture doesn't include it, so no cropping needed

    except (BadWindow, BadDrawable):
        pass

    return 0


def get_content_offset(window_id: int) -> tuple[int, int]:
    """Get the offset of the content area within a window.

    This finds the content child window (if any) and returns its position
    relative to the parent window. Used by overlay to align with captured content.

    When no content child window is found, returns the title bar height offset
    (since capture will crop the title bar in that case).

    Args:
        window_id: The X11 window ID (XID).

    Returns:
        Tuple of (x_offset, y_offset) in pixels.
    """
    disp = _get_display()
    try:
        window = disp.create_resource_object("window", window_id)
        content_info = _find_content_window(window)
        if content_info:
            return (content_info[1], content_info[2])

        # No content child found - capture will crop title bar
        # unless window is fullscreen
        if not _is_fullscreen(window_id):
            title_bar = _get_title_bar_height(window_id)
            return (0, title_bar)
    except (BadWindow, BadDrawable):
        pass
    return (0, 0)


def _is_fullscreen(window_id: int) -> bool:
    """Check if a window is in fullscreen mode.

    Detects fullscreen by comparing window bounds to screen bounds.

    Args:
        window_id: The X11 window ID.

    Returns:
        True if window appears to be fullscreen, False otherwise.
    """
    disp = _get_display()
    bounds = _get_window_bounds(window_id)
    if bounds is None:
        return False

    # Get screen dimensions
    screen = disp.screen()
    screen_width = screen.width_in_pixels
    screen_height = screen.height_in_pixels

    # Check if window fills the screen (with small tolerance)
    tolerance = 50
    is_fullscreen = (
        bounds["x"] <= tolerance and
        bounds["y"] <= tolerance and
        bounds["width"] >= screen_width - tolerance and
        bounds["height"] >= screen_height - tolerance
    )

    return is_fullscreen


# Cache for monitor list (RANDR queries can cause deadlocks, cache for entire session)
_monitors_cache: list[dict] = []


def _get_monitors() -> list[dict]:
    """Get list of monitors, cached for the entire session."""
    global _monitors_cache

    if _monitors_cache:
        return _monitors_cache

    disp = _get_display()
    monitors = []

    try:
        root = disp.screen().root
        resources = randr.get_screen_resources(root)

        for output in resources.outputs:
            output_info = randr.get_output_info(root, output, resources.config_timestamp)
            if output_info.crtc:
                crtc_info = randr.get_crtc_info(root, output_info.crtc, resources.config_timestamp)
                monitor = {
                    "x": crtc_info.x,
                    "y": crtc_info.y,
                    "width": crtc_info.width,
                    "height": crtc_info.height,
                }
                monitors.append(monitor)
    except Exception:
        pass

    if monitors:
        _monitors_cache = monitors

    return _monitors_cache


def _find_content_window(window) -> Optional[tuple]:
    """Find the largest child window (likely the content area).

    For CSD (client-side decoration) windows, the actual content is often
    in a child window, while the parent includes title bar and shadows.

    Args:
        window: The X11 window object.

    Returns:
        Tuple of (child_window, x_offset, y_offset) or None if no suitable child.
    """
    try:
        children = window.query_tree().children
        if not children:
            return None

        # Find the largest child window (by area)
        best_child = None
        best_area = 0
        best_offset = (0, 0)

        for child in children:
            try:
                geom = child.get_geometry()
                area = geom.width * geom.height
                # Must be reasonably sized (> 100x100) and larger than current best
                if geom.width > 100 and geom.height > 100 and area > best_area:
                    best_child = child
                    best_area = area
                    best_offset = (geom.x, geom.y)
            except (BadWindow, BadDrawable):
                continue

        return (best_child, best_offset[0], best_offset[1]) if best_child else None

    except (BadWindow, BadDrawable):
        return None


def capture_window(window_id: int) -> Optional[Image.Image]:
    """Capture a screenshot of a specific window.

    For CSD windows (with client-side decorations), this will attempt to
    capture the content child window directly, avoiding title bars and shadows.

    Args:
        window_id: The X11 window ID (XID) of the window to capture.

    Returns:
        PIL Image of the window content, or None if capture failed.
    """
    title_bar_height = _get_title_bar_height(window_id)
    disp = _get_display()

    try:
        window = disp.create_resource_object("window", window_id)

        # Check if window is viewable (ready for capture)
        attrs = window.get_attributes()
        if attrs.map_state != X.IsViewable:
            return None

        # Try to find content child window (for CSD windows)
        content_info = _find_content_window(window)
        if content_info:
            target_window = content_info[0]
            # Content window found - capture it directly (no cropping needed)
            crop_title_bar = False
        else:
            target_window = window
            crop_title_bar = True

        # Get window geometry
        geom = target_window.get_geometry()
        width = geom.width
        height = geom.height
        depth = geom.depth

        if width <= 0 or height <= 0:
            return None

        # Capture the window contents
        # ZPixmap format gives us packed pixel data
        raw = target_window.get_image(
            0, 0,           # x, y offset
            width, height,  # dimensions
            X.ZPixmap,      # format (packed pixels)
            0xFFFFFFFF      # plane mask (all planes)
        )

        # Get the raw data
        data = raw.data

        if depth == 24 or depth == 32:
            # 32-bit BGRX or BGRA format (X11 typically uses this)
            expected_size = width * height * 4
            if len(data) < expected_size:
                return None

            # Load as BGRA, then convert to RGB
            image = Image.frombytes(
                "RGBA", (width, height),
                bytes(data[:expected_size]),
                "raw", "BGRA"
            )
            image = image.convert("RGB")

        elif depth == 16:
            # 16-bit RGB (5-6-5 format)
            expected_size = width * height * 2
            if len(data) < expected_size:
                return None

            # Convert 5-6-5 to RGB24
            pixels = []
            for i in range(0, expected_size, 2):
                pixel = struct.unpack("<H", data[i:i+2])[0]
                r = ((pixel >> 11) & 0x1F) << 3
                g = ((pixel >> 5) & 0x3F) << 2
                b = (pixel & 0x1F) << 3
                pixels.extend([r, g, b])

            image = Image.frombytes("RGB", (width, height), bytes(pixels))

        else:
            # Unsupported depth
            return None

        # Only crop title bar if we couldn't find a content window
        if crop_title_bar:
            is_fullscreen = _is_fullscreen(window_id)
            if not is_fullscreen and title_bar_height > 0 and height > title_bar_height:
                image = image.crop((0, title_bar_height, width, height))

        return image

    except BadWindow:
        return None
    except BadDrawable:
        return None
    except Exception:
        return None


class LinuxCaptureStream(FPSTrackerMixin):
    """Continuous background window capture using X11.

    Captures frames in a background thread at a fixed interval.
    The main thread can get the latest frame without blocking.

    Provides the same interface as MacOSCaptureStream and WindowsCaptureStream
    for platform-agnostic capture code.
    """

    # Thresholds for capture time warnings
    CAPTURE_TIME_WARNING_MS = 100  # Warn if capture takes longer than this
    CAPTURE_TIME_SAMPLE_COUNT = 5  # Number of frames to sample for average

    def __init__(self, window_id: int, capture_interval: float = 0.25):
        """Initialize the capture stream.

        Args:
            window_id: The X11 window ID (XID) of the window to capture.
            capture_interval: Minimum seconds between captures.
        """
        self._window_id = window_id
        self._capture_interval = capture_interval
        self._latest_frame: Optional[Image.Image] = None
        self._frame_lock = threading.Lock()
        self._running = False
        self._thread: Optional[threading.Thread] = None
        self._capture_display: Optional[display.Display] = None
        self._window_invalid = False
        self._capture_times: list[float] = []  # Recent capture times in ms
        self._warning_shown = False

        # FPS tracking (from mixin)
        self._init_fps_tracking()

    def start(self):
        """Start the capture stream in background."""
        self._running = True
        self._reset_fps_tracking()
        self._thread = threading.Thread(target=self._capture_loop, daemon=True)
        self._thread.start()
        logger.debug("capture started (continuous)", window_id=self._window_id,
                    interval_ms=int(self._capture_interval * 1000))

    @property
    def window_invalid(self) -> bool:
        """Check if the window ID is invalid (window was destroyed)."""
        return self._window_invalid

    def _capture_loop(self):
        """Background thread that captures frames continuously."""
        # Create dedicated display connection for thread-safety
        self._capture_display = display.Display()

        while self._running and not self._window_invalid:
            # Measure capture time
            start_time = time.perf_counter()
            frame = self._capture_frame()
            capture_time_ms = (time.perf_counter() - start_time) * 1000

            if frame:
                with self._frame_lock:
                    self._latest_frame = frame
                    self._update_fps()

                # Track capture times and check for slow capture
                self._check_capture_performance(capture_time_ms, frame.size)

            time.sleep(self._capture_interval)

        # Cleanup
        if self._capture_display:
            try:
                self._capture_display.close()
            except Exception:
                pass
            self._capture_display = None

    def _check_capture_performance(self, capture_time_ms: float, frame_size: tuple[int, int]):
        """Check capture performance and warn if too slow.

        Args:
            capture_time_ms: Time taken for the last capture in milliseconds.
            frame_size: Tuple of (width, height) of the captured frame.
        """
        # Log on first successful capture
        if len(self._capture_times) == 0:
            width, height = frame_size
            logger.info("capture started", window_id=hex(self._window_id), resolution=f"{width}x{height}")

        # Only sample the first few frames
        if len(self._capture_times) < self.CAPTURE_TIME_SAMPLE_COUNT:
            self._capture_times.append(capture_time_ms)

        # Check after we have enough samples
        if len(self._capture_times) == self.CAPTURE_TIME_SAMPLE_COUNT and not self._warning_shown:
            avg_time = sum(self._capture_times) / len(self._capture_times)

            if avg_time > self.CAPTURE_TIME_WARNING_MS:
                self._warning_shown = True
                width, height = frame_size
                pixels = width * height
                megapixels = pixels / 1_000_000

                logger.warning(
                    "slow capture detected",
                    avg_capture_ms=int(avg_time),
                    resolution=f"{width}x{height}",
                    megapixels=f"{megapixels:.1f}MP",
                    hint="High resolution capture causes game stuttering. "
                         "Consider: (1) Run game in windowed mode at lower resolution, "
                         "(2) Lower display resolution, or (3) Increase capture interval in config."
                )

    def _capture_frame(self) -> Optional[Image.Image]:
        """Capture a single frame using the thread's display connection."""
        if self._capture_display is None:
            return None

        disp = self._capture_display

        try:
            window = disp.create_resource_object("window", self._window_id)

            attrs = window.get_attributes()
            if attrs.map_state != X.IsViewable:
                return None

            # Try to find content child window (for CSD windows)
            content_info = _find_content_window(window)
            if content_info:
                target_window = content_info[0]
                crop_title_bar = False
            else:
                target_window = window
                crop_title_bar = True

            geom = target_window.get_geometry()
            width = geom.width
            height = geom.height
            depth = geom.depth

            if width <= 0 or height <= 0:
                return None

            raw = target_window.get_image(0, 0, width, height, X.ZPixmap, 0xFFFFFFFF)
            data = raw.data

            if depth == 24 or depth == 32:
                expected_size = width * height * 4
                if len(data) < expected_size:
                    return None
                image = Image.frombytes("RGBA", (width, height),
                                       bytes(data[:expected_size]), "raw", "BGRA")
                image = image.convert("RGB")
            elif depth == 16:
                expected_size = width * height * 2
                if len(data) < expected_size:
                    return None
                pixels = []
                for i in range(0, expected_size, 2):
                    pixel = struct.unpack("<H", data[i:i+2])[0]
                    r = ((pixel >> 11) & 0x1F) << 3
                    g = ((pixel >> 5) & 0x3F) << 2
                    b = (pixel & 0x1F) << 3
                    pixels.extend([r, g, b])
                image = Image.frombytes("RGB", (width, height), bytes(pixels))
            else:
                return None

            # Crop title bar if needed
            if crop_title_bar and not _is_fullscreen(self._window_id):
                title_bar = _get_title_bar_height(self._window_id)
                if title_bar > 0 and height > title_bar:
                    image = image.crop((0, title_bar, width, height))

            return image

        except (BadDrawable, BadWindow):
            self._window_invalid = True
            with self._frame_lock:
                self._latest_frame = None
            return None
        except Exception:
            return None

    def get_frame(self) -> Optional[Image.Image]:
        """Get the latest captured frame (non-blocking).

        Returns:
            PIL Image of the captured frame, or None if no frame available.
        """
        with self._frame_lock:
            return self._latest_frame

    # fps property is inherited from FPSTrackerMixin

    def stop(self):
        """Stop the capture stream."""
        self._running = False
        if self._thread:
            self._thread.join(timeout=2.0)
        self._thread = None
