#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Real Win32 input + DPI handling — the actual hands.

This is the piece that was a `NotImplementedError` stub in the old code. Here it
is a working SendInput implementation: absolute mouse move+click, unicode typing,
named-key presses, and a mouse wheel scroll. Plus DPI awareness so a screenshot
pixel and a click coordinate are the SAME point (the classic legacy-RPA bug).

Everything is imported lazily and only ever runs on Windows. On any non-Windows
host `is_windows()` is False and the action functions raise a clear error, so the
module imports cleanly for inspection on Linux.

Coordinate space: we drive the PRIMARY monitor and normalize against
SM_CXSCREEN/SM_CYSCREEN, so click coords line up 1:1 with a primary-monitor
ImageGrab screenshot (after set_dpi_aware()). Run the target on the primary
display at 100% scale for the cleanest mapping; set_dpi_aware() makes 125%/150%
report true pixels too, but 100% removes all doubt.
"""

from __future__ import annotations

import sys
import time

IS_WINDOWS = sys.platform.startswith("win")


def is_windows():
    return IS_WINDOWS


def _require_windows():
    if not IS_WINDOWS:
        raise RuntimeError(
            "win_input action called on non-Windows host (sys.platform=%r). "
            "The implementation is real; it only runs on Windows." % sys.platform)


# --------------------------------------------------------------------------
# DPI awareness
# --------------------------------------------------------------------------
def set_dpi_aware():
    """Make this process DPI-aware so screen capture and coordinates match.

    Tries Per-Monitor-V2 (Win10), then PROCESS_PER_MONITOR_DPI_AWARE (Win8.1),
    then the legacy SetProcessDPIAware. Safe to call once at startup; returns the
    method that succeeded, or 'none'.
    """
    if not IS_WINDOWS:
        return "skipped-non-windows"
    import ctypes
    # Win10 1703+: SetProcessDpiAwarenessContext(-4) = PER_MONITOR_AWARE_V2
    try:
        ctypes.windll.user32.SetProcessDpiAwarenessContext(
            ctypes.c_void_p(-4))
        return "per-monitor-v2"
    except Exception:  # noqa: BLE001
        pass
    # Win8.1+: shcore.SetProcessDpiAwareness(2) = PROCESS_PER_MONITOR_DPI_AWARE
    try:
        ctypes.windll.shcore.SetProcessDpiAwareness(2)
        return "per-monitor"
    except Exception:  # noqa: BLE001
        pass
    try:
        ctypes.windll.user32.SetProcessDPIAware()
        return "system"
    except Exception:  # noqa: BLE001
        return "none"


def screen_size():
    """Primary-monitor size in physical pixels (after set_dpi_aware)."""
    _require_windows()
    import ctypes
    u = ctypes.windll.user32
    return int(u.GetSystemMetrics(0)), int(u.GetSystemMetrics(1))  # SM_CXSCREEN/CY


# --------------------------------------------------------------------------
# SendInput structures
# --------------------------------------------------------------------------
def _structs():
    import ctypes
    from ctypes import wintypes
    ULONG_PTR = ctypes.POINTER(ctypes.c_ulong)

    class MOUSEINPUT(ctypes.Structure):
        _fields_ = [("dx", wintypes.LONG), ("dy", wintypes.LONG),
                    ("mouseData", wintypes.DWORD), ("dwFlags", wintypes.DWORD),
                    ("time", wintypes.DWORD), ("dwExtraInfo", ULONG_PTR)]

    class KEYBDINPUT(ctypes.Structure):
        _fields_ = [("wVk", wintypes.WORD), ("wScan", wintypes.WORD),
                    ("dwFlags", wintypes.DWORD), ("time", wintypes.DWORD),
                    ("dwExtraInfo", ULONG_PTR)]

    class _INPUTunion(ctypes.Union):
        _fields_ = [("mi", MOUSEINPUT), ("ki", KEYBDINPUT)]

    class INPUT(ctypes.Structure):
        _fields_ = [("type", wintypes.DWORD), ("u", _INPUTunion)]

    return ctypes, INPUT, MOUSEINPUT, KEYBDINPUT


# event flags
_INPUT_MOUSE = 0
_INPUT_KEYBOARD = 1
_M_MOVE = 0x0001
_M_ABSOLUTE = 0x8000
_M_LEFTDOWN = 0x0002
_M_LEFTUP = 0x0004
_M_WHEEL = 0x0800
_K_UNICODE = 0x0004
_K_KEYUP = 0x0002
_K_EXTENDED = 0x0001

# named virtual-key codes (extended where relevant)
_VK = {
    "enter": 0x0D, "return": 0x0D, "tab": 0x09, "escape": 0x1B, "esc": 0x1B,
    "space": 0x20, "backspace": 0x08, "delete": 0x2E, "del": 0x2E,
    "home": 0x24, "end": 0x23, "pageup": 0x21, "pagedown": 0x22,
    "up": 0x26, "down": 0x28, "left": 0x25, "right": 0x27,
    "f1": 0x70, "f2": 0x71, "f3": 0x72, "f4": 0x73, "f5": 0x74, "f6": 0x75,
    "f7": 0x76, "f8": 0x77, "f9": 0x78, "f10": 0x79, "f11": 0x7A, "f12": 0x7B,
}
_VK_EXTENDED = {0x2E, 0x24, 0x23, 0x21, 0x22, 0x26, 0x28, 0x25, 0x27}


def _send(inputs):
    ctypes, INPUT, _MI, _KI = _structs()
    n = len(inputs)
    arr = (INPUT * n)(*inputs)
    sent = ctypes.windll.user32.SendInput(n, arr, ctypes.sizeof(INPUT))
    if sent != n:
        raise RuntimeError("SendInput sent %d/%d events (input blocked? UAC-"
                           "elevated target needs elevated python)" % (sent, n))
    return sent


def _abs_xy(x, y):
    sw, sh = screen_size()
    # normalize to 0..65535 over the primary screen
    nx = int(round(x * 65535.0 / max(1, sw - 1)))
    ny = int(round(y * 65535.0 / max(1, sh - 1)))
    return max(0, min(65535, nx)), max(0, min(65535, ny))


# --------------------------------------------------------------------------
# Public actions
# --------------------------------------------------------------------------
def move_click(x, y, settle=0.12):
    """Absolute move to (x,y) then a left click."""
    _require_windows()
    ctypes, INPUT, MOUSEINPUT, _KI = _structs()
    nx, ny = _abs_xy(x, y)

    def mouse(flags):
        mi = MOUSEINPUT(nx, ny, 0, flags | _M_ABSOLUTE, 0, None)
        inp = INPUT(); inp.type = _INPUT_MOUSE; inp.u.mi = mi
        return inp

    _send([mouse(_M_MOVE)])
    time.sleep(0.03)
    _send([mouse(_M_LEFTDOWN), mouse(_M_LEFTUP)])
    time.sleep(settle)


def type_unicode(text):
    """Type a unicode string into the focused control via KEYEVENTF_UNICODE."""
    _require_windows()
    ctypes, INPUT, _MI, KEYBDINPUT = _structs()
    inputs = []
    for ch in str(text):
        code = ord(ch)
        down = KEYBDINPUT(0, code, _K_UNICODE, 0, None)
        up = KEYBDINPUT(0, code, _K_UNICODE | _K_KEYUP, 0, None)
        for ki in (down, up):
            inp = INPUT(); inp.type = _INPUT_KEYBOARD; inp.u.ki = ki
            inputs.append(inp)
    if inputs:
        _send(inputs)
    time.sleep(0.05)


def press_key(name):
    """Press a named key (enter/tab/escape/arrows/f-keys/...)."""
    _require_windows()
    ctypes, INPUT, _MI, KEYBDINPUT = _structs()
    vk = _VK.get(str(name).lower())
    if vk is None:
        # single printable char -> route through unicode typing
        if len(str(name)) == 1:
            return type_unicode(name)
        raise RuntimeError("unknown key name %r" % name)
    flags_down = _K_EXTENDED if vk in _VK_EXTENDED else 0
    down = KEYBDINPUT(vk, 0, flags_down, 0, None)
    up = KEYBDINPUT(vk, 0, flags_down | _K_KEYUP, 0, None)
    seq = []
    for ki in (down, up):
        inp = INPUT(); inp.type = _INPUT_KEYBOARD; inp.u.ki = ki
        seq.append(inp)
    _send(seq)
    time.sleep(0.05)


def scroll(amount_notches, x=None, y=None):
    """Mouse wheel scroll. Positive = up, negative = down. Optionally move to
    (x,y) first so the scroll lands on the intended pane."""
    _require_windows()
    ctypes, INPUT, MOUSEINPUT, _KI = _structs()
    if x is not None and y is not None:
        nx, ny = _abs_xy(x, y)
        mi = MOUSEINPUT(nx, ny, 0, _M_MOVE | _M_ABSOLUTE, 0, None)
        inp = INPUT(); inp.type = _INPUT_MOUSE; inp.u.mi = mi
        _send([inp]); time.sleep(0.03)
    delta = int(amount_notches) * 120  # WHEEL_DELTA
    mi = MOUSEINPUT(0, 0, delta & 0xFFFFFFFF, _M_WHEEL, 0, None)
    inp = INPUT(); inp.type = _INPUT_MOUSE; inp.u.mi = mi
    _send([inp])
    time.sleep(0.1)
