"""
wivexx_screenui.py — number the clickable things on screen, then "click N" (v1.17)
═══════════════════════════════════════════════════════════════════════
"Number the screen" → WIVEXX walks the foreground window's UI-Automation tree,
finds the clickable controls (buttons, links, menu items, tabs, checkboxes…),
and hands back their on-screen rectangles so the GUI can draw numbered badges.
"Click 3" then invokes element #3 — mouse-free navigation.

Windows-only, via the light `uiautomation` package (comtypes-based).  Optional:
`available()` is False without it and the feature simply reports that.

Public API (used by wivexx_api.py / wivexx_gui.py):
    available() -> bool
    enumerate_clickables(max_items=50) -> list[{n, name, kind, rect, ref}]
    click(item) -> bool
    virtual_screen() -> (x, y, w, h)
"""

import ctypes
from typing import Optional

_logger = None

# UIA ControlTypeName values that are clickable by their very nature.
_CLICKABLE = {
    "ButtonControl", "HyperlinkControl", "MenuItemControl", "ListItemControl",
    "TabItemControl", "CheckBoxControl", "RadioButtonControl", "SplitButtonControl",
    "TreeItemControl", "ComboBoxControl", "DataItemControl", "HeaderItemControl",
}
# Ambiguous containers — the work-horses of web/Electron UIs (Discord, Lightroom,
# the control room itself). A <div role=button> shows up here as a Group/Custom/
# Pane, so we treat them as clickable ONLY when they expose an action pattern.
_MAYBE = {
    "GroupControl", "PaneControl", "TextControl", "ImageControl", "CustomControl",
}


def set_logger(logger) -> None:
    global _logger
    _logger = logger


def available() -> bool:
    try:
        import uiautomation  # noqa: F401
        return True
    except Exception:
        return False


def virtual_screen():
    """(x, y, w, h) of the whole virtual desktop (all monitors)."""
    try:
        u = ctypes.windll.user32
        SM_XVIRTUAL, SM_YVIRTUAL, SM_CXVIRTUAL, SM_CYVIRTUAL = 76, 77, 78, 79
        return (u.GetSystemMetrics(SM_XVIRTUAL), u.GetSystemMetrics(SM_YVIRTUAL),
                u.GetSystemMetrics(SM_CXVIRTUAL), u.GetSystemMetrics(SM_CYVIRTUAL))
    except Exception:
        return (0, 0, 1920, 1080)


def enumerate_clickables(max_items: int = 120) -> list:
    """Clickable controls in the foreground window, numbered from 1.
    Each item: {n, name, kind, rect:(l,t,r,b), ref:<uia control>}.

    Detection is two-tier: (1) control types that are inherently clickable, and
    (2) ambiguous containers (Group/Pane/Custom/…) that expose an action pattern
    (Invoke / Toggle / SelectionItem / ExpandCollapse). Tier 2 is what catches
    the custom buttons in web/Electron apps that the type list alone misses."""
    if not available():
        return []
    try:
        import uiautomation as auto
    except Exception:
        return []
    P = auto.PropertyId
    avail_props = (P.IsInvokePatternAvailableProperty,
                   P.IsTogglePatternAvailableProperty,
                   P.IsSelectionItemPatternAvailableProperty,
                   P.IsExpandCollapsePatternAvailableProperty)
    out = []
    seen = set()                                       # de-dupe nested clickables
    try:
        hwnd = ctypes.windll.user32.GetForegroundWindow()
        win = auto.ControlFromHandle(hwnd) if hwnd else None
        if win is None:
            return []
        n = 0
        # WalkControl yields (control, depth); cap depth/breadth so a heavy app
        # doesn't take seconds.
        for ctrl, _depth in auto.WalkControl(win, includeTop=False, maxDepth=16):
            if n >= max_items:
                break
            try:
                ctn = ctrl.ControlTypeName
                clickable = ctn in _CLICKABLE
                if not clickable and ctn in _MAYBE:
                    try:
                        clickable = any(bool(ctrl.GetPropertyValue(pid))
                                        for pid in avail_props)
                    except Exception:
                        clickable = False
                if not clickable:
                    continue
                if ctrl.IsOffscreen:
                    continue
                r = ctrl.BoundingRectangle
                if r is None:
                    continue
                w, h = r.right - r.left, r.bottom - r.top
                if w <= 2 or h <= 2 or w > 4000 or h > 3000:
                    continue
                # skip a clickable that sits right on top of one already numbered
                # (a Group wrapping its own Button → one badge, not two)
                key = ((r.left + r.right) // 2 // 8, (r.top + r.bottom) // 2 // 8)
                if key in seen:
                    continue
                seen.add(key)
                name = (ctrl.Name or "").strip()
                kind = ctn.replace("Control", "")
                n += 1
                out.append({"n": n, "name": name[:40] or kind, "kind": kind,
                            "rect": (r.left, r.top, r.right, r.bottom),
                            "ref": ctrl})
            except Exception:
                continue
    except Exception as e:
        if _logger:
            _logger.warning("screenui enumerate failed: %s", e)
    return out


def click(item) -> bool:
    """Invoke the control (preferred) or click its centre as a fallback."""
    if not item:
        return False
    ctrl = item.get("ref")
    try:
        if ctrl is not None:
            try:
                pat = ctrl.GetInvokePattern()
                if pat is not None:
                    pat.Invoke()
                    return True
            except Exception:
                pass
            try:
                ctrl.Click(simulateMove=False)     # real click at control centre
                return True
            except Exception:
                pass
        # last resort: synthesize a click at the rect centre
        l, t, r, b = item["rect"]
        x, y = (l + r) // 2, (t + b) // 2
        u = ctypes.windll.user32
        u.SetCursorPos(int(x), int(y))
        u.mouse_event(0x0002, 0, 0, 0, 0)          # LEFTDOWN
        u.mouse_event(0x0004, 0, 0, 0, 0)          # LEFTUP
        return True
    except Exception as e:
        if _logger:
            _logger.warning("screenui click failed: %s", e)
        return False
