"""Ragdoll support probe.

Collects display-scaling and mouse-input information from a running
Maya, to help diagnose reports of the Ragdoll manipulator UI not
responding to the mouse.

How to run: with the Ragdoll manipulator showing in the viewport, drag
and drop this file into Maya (or run it from the Script Editor's Python
tab). It prints a report, then asks for a few test clicks -- follow the
numbered steps it prints. The LAST step, a click on a shelf button or
menu (anywhere away from the 3D viewport), ends the test and prints the
final report. Then copy the ENTIRE output from the Script Editor and
send it to Ragdoll support.

The script only reads information and changes nothing in your scene or
preferences. It collects: Maya/Ragdoll versions, display scaling
numbers, Qt-related environment variables, and the names of windows
under your cursor at the moment you click. It sends nothing anywhere;
you see everything it collected in the printed output.
"""

import os
import platform


# ---------------------------------------------------------------- helpers

def _wrap(ptr):
    """QWidget from a Maya pointer, across Maya's PySide2/PySide6 split."""
    for mod in ("shiboken6", "shiboken2"):
        try:
            shiboken = __import__(mod)
        except ImportError:
            continue
        qtmod = "PySide6" if mod == "shiboken6" else "PySide2"
        QtWidgets = __import__(qtmod + ".QtWidgets", fromlist=["QtWidgets"])
        return shiboken.wrapInstance(int(ptr), QtWidgets.QWidget)
    raise ImportError("neither shiboken6 nor shiboken2 is importable")


def _qt(submodule):
    for mod in ("PySide6", "PySide2"):
        try:
            return __import__(mod + "." + submodule, fromlist=[submodule])
        except ImportError:
            continue
    raise ImportError("neither PySide6 nor PySide2 is importable")


def _line(label, fn):
    try:
        print("  %-30s %s" % (label, fn()))
    except Exception as e:
        print("  %-30s <unavailable: %s: %s>" % (label, type(e).__name__, e))


# ---------------------------------------------------------------- sections

def section_host():
    import maya.OpenMayaUI as omui
    from maya import cmds

    print("\n[host]")
    _line("platform", lambda: "%s %s" % (platform.system(), platform.release()))
    _line("Maya version", lambda: cmds.about(installedVersion=True))
    _line("Ragdoll plug-in version", lambda: cmds.pluginInfo(
        "ragdoll", query=True, version=True))
    _line("MQtUtil.dpiScale(1.0)", lambda: omui.MQtUtil.dpiScale(1.0))
    _line(
        "ragdollUiScale optionVar",
        lambda: cmds.optionVar(query="ragdollUiScale")
        if cmds.optionVar(exists="ragdollUiScale")
        else "(unset)",
    )


def section_env():
    print("\n[env] Qt-related environment inside Maya")
    for key in ("QT_SCALE_FACTOR", "QT_ENABLE_HIGHDPI_SCALING",
                "QT_AUTO_SCREEN_SCALE_FACTOR", "QT_SCREEN_SCALE_FACTORS",
                "QT_QPA_PLATFORM", "QT_DEVICE_PIXEL_RATIO"):
        print("  %-30s %s" % (key, os.environ.get(key)))


def section_dpi():
    import maya.OpenMayaUI as omui

    view = omui.M3dView.active3dView()
    widget = _wrap(view.widget())

    print("\n[qt] active viewport widget")
    _line("devicePixelRatioF()", lambda: widget.devicePixelRatioF())
    _line("logical size (w x h)", lambda: "%d x %d" % (widget.width(), widget.height()))
    _line("screen name", lambda: widget.screen().name())
    _line("screen geometry", lambda: "%s" % (widget.screen().geometry().getRect(),))

    print("\n[maya] M3dView port size (render-target proxy)")
    _line("portWidth x portHeight", lambda: "%d x %d" % (view.portWidth(), view.portHeight()))

    print("\n[derived] the ratio Maya ACTUALLY renders at")
    try:
        lw, lh = widget.width(), widget.height()
        pw, ph = view.portWidth(), view.portHeight()
        rx = (pw / float(lw)) if lw else float("nan")
        ry = (ph / float(lh)) if lh else float("nan")
        print("  portWidth  / widget.width()  = %d / %d = %.4f" % (pw, lw, rx))
        print("  portHeight / widget.height() = %d / %d = %.4f" % (ph, lh, ry))
        print("  qt devicePixelRatioF()       = %.4f" % widget.devicePixelRatioF())
        agree = abs(rx - widget.devicePixelRatioF()) < 0.01
        print("  -> %s" % ("AGREE" if agree else "DISAGREE  <- suspicious, report this"))
    except Exception as e:
        print("  <unavailable: %s: %s>" % (type(e).__name__, e))

    print("\n[qt] every screen Qt can see  (* = Qt's primary)")
    try:
        gui = _qt("QtGui")
        primary_name = gui.QGuiApplication.primaryScreen().name()
        current_name = widget.screen().name()
        for s in gui.QGuiApplication.screens():
            tag = "*" if s.name() == primary_name else " "
            here = "  <- Maya's viewport is here" if s.name() == current_name else ""
            print("  %s %-24s dpr=%-6s geom=%s%s"
                  % (tag, s.name(), s.devicePixelRatio(), s.geometry().getRect(), here))
    except Exception as e:
        print("  <unavailable: %s: %s>" % (type(e).__name__, e))


# ---------------------------------------------- deferred cursor sample

def _cursor_sample(sx, sy, where):
    """What Qt AND Win32 see at point (sx, sy) -- catches click-through
    overlays (recorder, Magnifier, PowerToys, GPU overlays) that defeat
    QApplication.widgetAt, which Ragdoll's hover/click gates rely on."""

    QtCore = _qt("QtCore")
    QtGui = _qt("QtGui")
    QtWidgets = _qt("QtWidgets")

    print("=" * 72)
    print("WINDOW-UNDER-POINT SAMPLE  (sampled at: %s)" % where)

    print("\n[qt]  sample point             (%d, %d)" % (sx, sy))
    widget = QtWidgets.QApplication.widgetAt(QtCore.QPoint(sx, sy))
    print("[qt]  widgetAt -> %s" % (
        "%s (%s)" % (widget.metaObject().className(), widget.objectName())
        if widget else "None  <- Ragdoll's gates FAIL here"))

    if platform.system() != "Windows":
        print("\n[w32] skipped (not Windows)")
        print("=" * 72)
        return

    import ctypes
    from ctypes import wintypes

    user32 = ctypes.windll.user32
    kernel32 = ctypes.windll.kernel32

    GWL_EXSTYLE = -20
    styles = (
        (0x00000008, "TOPMOST"),
        (0x00080000, "LAYERED"),
        (0x00000020, "TRANSPARENT"),
        (0x08000000, "NOACTIVATE"),
    )
    PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
    WNDENUMPROC = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)

    def process_name(hwnd):
        pid = wintypes.DWORD(0)
        user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
        handle = kernel32.OpenProcess(
            PROCESS_QUERY_LIMITED_INFORMATION, False, pid.value)
        if not handle:
            return "pid=%d (no access)" % pid.value
        try:
            buf = ctypes.create_unicode_buffer(1024)
            size = wintypes.DWORD(len(buf))
            if kernel32.QueryFullProcessImageNameW(
                    handle, 0, buf, ctypes.byref(size)):
                return buf.value.rsplit("\\", 1)[-1]
            return "pid=%d (query failed)" % pid.value
        finally:
            kernel32.CloseHandle(handle)

    def describe(hwnd):
        cls = ctypes.create_unicode_buffer(256)
        user32.GetClassNameW(hwnd, cls, 256)
        title = ctypes.create_unicode_buffer(256)
        user32.GetWindowTextW(hwnd, title, 256)
        ex = user32.GetWindowLongW(hwnd, GWL_EXSTYLE)
        flags = " ".join(name for bit, name in styles if ex & bit) or "-"
        return "%-24s class=%-28s title=%-24s exstyle=[%s]" % (
            process_name(hwnd), cls.value, title.value[:24] or "-", flags)

    # Coordinate-space sanity: current cursor as Qt vs Win32 sees it.
    # A mismatch means the two live in different coordinate spaces.
    cur = wintypes.POINT()
    user32.GetCursorPos(ctypes.byref(cur))
    qcur = QtGui.QCursor.pos()
    print("\n[w32] GetCursorPos now         (%d, %d)   vs QCursor.pos (%d, %d)  %s"
          % (cur.x, cur.y, qcur.x(), qcur.y(),
             "MATCH" if (cur.x, cur.y) == (qcur.x(), qcur.y()) else "MISMATCH <- report this"))

    pt = wintypes.POINT(sx, sy)
    hwnd = user32.WindowFromPoint(pt)
    print("[w32] WindowFromPoint(sample) -> %s" % describe(hwnd))

    hits = []

    def _cb(hwnd, _lparam):
        if user32.IsWindowVisible(hwnd):
            rect = wintypes.RECT()
            user32.GetWindowRect(hwnd, ctypes.byref(rect))
            if rect.left <= pt.x < rect.right and rect.top <= pt.y < rect.bottom:
                hits.append(hwnd)
        return len(hits) < 8  # EnumWindows walks top of z-order first

    print("\n[w32] visible windows covering this point, top of z-order first:")
    print("      (healthy: maya.exe first; anything above it is the suspect)")
    user32.EnumWindows(WNDENUMPROC(_cb), 0)
    for i, h in enumerate(hits):
        print("  %d. %s" % (i + 1, describe(h)))

    print("\nDONE. Copy ALL output from the first banner down and send it back.")
    print("=" * 72)


# ------------------------------------------------- click-triggered sample

_probe_state = {
    "filter": None,
    "fired": False,
    "presses": [],          # (line, is_viewport, x, y)
    "task_index": 0,
    "last_advance": 0.0,    # time.monotonic of the last task advance
}


def _fire_sample(reason):
    if _probe_state["fired"]:
        return
    _probe_state["fired"] = True

    # Uninstall ourselves first: the sample prints plenty, and a stray
    # extra click must not re-enter.
    QtWidgets = _qt("QtWidgets")
    app = QtWidgets.QApplication.instance()
    if _probe_state["filter"] is not None:
        app.removeEventFilter(_probe_state["filter"])
        _probe_state["filter"].deleteLater()
        _probe_state["filter"] = None

    presses = _probe_state["presses"]

    print("\n(report triggered by: %s)" % reason)
    print("\n[qt]  all press-like input seen (mouse, tablet, touch), in order:")
    if presses:
        for i, (line, _is_vp, _x, _y) in enumerate(presses):
            print("  %d. %s" % (i + 1, line))
    else:
        print("  (none -- NO press ever reached the application event filter)")

    viewport_presses = [p for p in presses if p[1]]
    if presses and not viewport_presses:
        print("  NOTE: no press was over the 3D viewport. Either the viewport")
        print("  clicks never reached Qt's application filter (report this),")
        print("  or the viewport steps were skipped.")

    # Anchor the overlay check at the first viewport click if we have one;
    # otherwise fall back to wherever the cursor is now.
    try:
        if viewport_presses:
            _, _, sx, sy = viewport_presses[0]
            where = "first click over the 3D viewport"
        else:
            QtGui = _qt("QtGui")
            qpos = QtGui.QCursor.pos()
            sx, sy = qpos.x(), qpos.y()
            where = "current cursor position (no viewport click recorded)"
        _cursor_sample(sx, sy, where)
    except Exception as e:
        print("  <sample failed: %s: %s>" % (type(e).__name__, e))


# One instruction shown at a time; each physical click advances to the
# next. The expected-target label lets the report say whether the click
# landed on the right kind of widget.
_TASKS = [
    ("CLICK the Ragdoll button that ignores your mouse.", "3D viewport"),
    ("CLICK once on an EMPTY area of the 3D viewport.", "3D viewport"),
    ("CLICK once on a shelf button or a menu. This LAST click ends "
     "the test and prints the final report.", "other UI"),
]


def _print_task(index):
    print("\n[step %d/%d] %s" % (index + 1, len(_TASKS), _TASKS[index][0]))


def _install_click_trigger():
    import time

    QtCore = _qt("QtCore")
    QtGui = _qt("QtGui")
    QtWidgets = _qt("QtWidgets")

    def _etype(name):
        # QEvent enum values moved under QEvent.Type in strict-enum PySide6
        value = getattr(QtCore.QEvent, name, None)
        if value is None:
            value = getattr(QtCore.QEvent.Type, name, None)
        return value

    # A pen under WM_POINTER can arrive as a tablet or touch press, with
    # the mouse press synthesized afterwards -- or never. Record all
    # press-like kinds, so "no mouse press" and "no input at all" read
    # differently in the report.
    press_kinds = {}
    for name, label in (("MouseButtonPress", "MousePress"),
                        ("TabletPress", "TabletPress"),
                        ("TouchBegin", "TouchBegin")):
        value = _etype(name)
        if value is not None:
            press_kinds[value] = label

    class _ClickTrigger(QtCore.QObject):
        def eventFilter(self, obj, event):
            if event.type() in press_kinds and not _probe_state["fired"]:
                kind = press_kinds[event.type()]
                # One physical click is delivered to several receivers in
                # turn (window, then widget). Record each: which classes
                # appear here decides whether Ragdoll handles the press.
                try:
                    source = str(event.source()).rsplit(".", 1)[-1]
                except Exception:
                    source = "-"  # tablet/touch events carry no source()

                # Classify by what is UNDER the click, so the report shows
                # whether the right kind of widget was clicked.
                pos = QtGui.QCursor.pos()
                under = QtWidgets.QApplication.widgetAt(pos)
                under_cls = under.metaObject().className() if under else None
                is_viewport = under_cls == "QmayaGLWidget"
                target = (
                    "3D viewport" if is_viewport
                    else "NO QT WIDGET" if under_cls is None
                    else "other UI: %s" % under_cls)

                try:
                    line = "%-11s receiver=%s (%s)  source=%s  at=(%d, %d)  under=%s" % (
                        kind,
                        obj.metaObject().className(),
                        obj.objectName() or "-",
                        source,
                        pos.x(), pos.y(),
                        target)
                except Exception as e:
                    line = "<record failed: %s>" % e
                _probe_state["presses"].append(
                    (line, is_viewport, pos.x(), pos.y()))

                # Immediate feedback, so a click that is NOT seen here is
                # visible to the user as an absent line.
                print("  [press seen] %s" % line)

                # Advance one task per PHYSICAL click: the sibling
                # deliveries of one click arrive within a few ms of each
                # other, so a short window groups them.
                now = time.monotonic()
                if (now - _probe_state["last_advance"] > 0.2 and
                        _probe_state["task_index"] < len(_TASKS)):
                    _probe_state["last_advance"] = now

                    index = _probe_state["task_index"]
                    expected = _TASKS[index][1]
                    observed = "3D viewport" if is_viewport else "other UI"
                    if expected != observed:
                        print("  (note: expected a %s click here, this one "
                              "was over: %s)" % (expected, target))

                    _probe_state["task_index"] = index + 1
                    if _probe_state["task_index"] < len(_TASKS):
                        _print_task(_probe_state["task_index"])
                    else:
                        print("\nAll steps done -- compiling the report...")
                        # Short grace so this click's remaining sibling
                        # deliveries get recorded before printing.
                        QtCore.QTimer.singleShot(
                            500, lambda: _fire_sample("all steps completed"))
            return False  # never swallow anything

    app = QtWidgets.QApplication.instance()
    _probe_state["filter"] = _ClickTrigger(app)
    app.installEventFilter(_probe_state["filter"])

    # Janitor only, NOT a sampling window: the report is triggered by the
    # last step's click. This exists so the filter cannot linger forever
    # if the steps are abandoned mid-way.
    QtCore.QTimer.singleShot(
        180000, lambda: _fire_sample("3min timeout, steps never completed"))


# ---------------------------------------------------------------- main

def main():
    print("=" * 72)
    print("RAGDOLL SUPPORT PROBE (DPI + window-under-cursor)")
    print("=" * 72)

    for section in (section_host, section_env, section_dpi):
        try:
            section()
        except Exception as e:
            print("  <section failed: %s: %s>" % (type(e).__name__, e))

    print("\n" + "=" * 72)
    print("NOW follow the steps below, ONE CLICK PER STEP, and do not")
    print("click anything else in between. Each click you make should")
    print("print a '[press seen]' line -- note which clicks do NOT.")
    print("=" * 72)

    _install_click_trigger()
    _print_task(0)


main()
