#!/usr/bin/env python3
"""Classify the HUD menu mode/state dispatcher layer.

The previous opener/frontier passes proved two negative facts:

* The exact #6/#3/#4 top-menu object construction sequence is grounded, but
  does not directly appear in an input consumer.
* Selected-root and global 0x62 descriptor-add scans do not promote a
  normal-field ESC/X opener.

This pass records the next layer down: functions that are clearly menu
state/cursor/selector machinery after the HUD menu object already exists.  The
goal is not to claim the opener; it is to prevent future analysis from treating
cursor movement, confirm/cancel handling, or selector-table production as the
normal-field opener.
"""
from __future__ import annotations

import html
import json
from pathlib import Path
from typing import Any

from build_hud_menu_opener_frontier_review import (
    ADD_DESCRIPTOR_ROUTINE_VA,
    EDGE_ACTION_MASK_VA,
    TOP_MENU_OBJECT_SEQUENCE_VA,
    call_sites,
    find_function,
    function_ranges,
    hx,
    refs_in_function,
    text_refs,
)
from probe_exe_scene_tables import read_sections


ROOT = Path(__file__).resolve().parents[1]
EXE = ROOT / "Hwanse2.exe"
OUT = ROOT / "out"
WEB = ROOT / "web"

CURRENT_ACTION_MASK_VA = 0x0059E310

MENU_STATE_GLOBALS = {
    "input.currentActionMask": CURRENT_ACTION_MASK_VA,
    "input.edgeActionMask": EDGE_ACTION_MASK_VA,
    "runtime.enabledTableA": 0x0059E360,
    "runtime.enabledTableB": 0x0059E370,
    "runtime.menuCategory": 0x0059E340,
    "runtime.menuCursor": 0x0059E34A,
    "runtime.currentDescriptorIndex": 0x0059E33E,
    "runtime.activeComparisonGroup": 0x0059E344,
    "menu.dispatchLatch": 0x0055B128,
    "menu.dispatchStopFlag": 0x0055B280,
    "menu.cursorMax": 0x0059DB58,
    "menu.cursorMode": 0x0059DB5A,
    "menu.cursorSourceOffset": 0x0059DB5B,
    "menu.cursorYBase": 0x0059DB5C,
    "menu.cursorY": 0x0059DB5E,
    "menu.soundHandle": 0x0059DD6C,
    "descriptor.activeOrder": 0x004576E9,
    "descriptor.rowTable": 0x00457750,
    "descriptor.persistFlag": 0x00457744,
    "descriptor.statusBase": 0x004576D8,
    "topMenu.objectSequence": TOP_MENU_OBJECT_SEQUENCE_VA,
}

CALL_TARGETS = {
    "call.menuCursorApply": 0x00410570,
    "call.menuCursorMotion": 0x004106BA,
    "call.soundCue": 0x0042AF73,
    "call.availabilityBuilder": 0x00410C90,
    "call.addDescriptor": ADD_DESCRIPTOR_ROUTINE_VA,
}

DISPATCHER_FUNCTIONS = {
    0x0041D61B: {
        "label": "menu-internal input dispatcher",
        "promotion": "confirmed-menu-internal-not-opener",
        "decision": (
            "edge mask를 읽고 이미 존재하는 menu object의 cursor/confirm/cancel 상태를 처리한다. "
            "0x55b128 latch, 0x59db58..0x59db5e cursor globals, 0x59dd6c sound handle, "
            "0x4106ba cursor motion helper, 0x42af73 sound cue를 함께 쓰지만 "
            "top-menu object sequence나 descriptor-add 호출은 없다."
        ),
    },
    0x0040B13B: {
        "label": "directional input dispatch to cursor helper",
        "promotion": "confirmed-menu-internal-not-opener",
        "decision": (
            "input edge 방향 bit를 해석해 0x410570 cursor helper로 넘기는 방향키 처리층이다. "
            "descriptor/materializer 근거가 없어 opener가 아니다."
        ),
    },
    0x00410570: {
        "label": "cursor apply/helper",
        "promotion": "confirmed-helper",
        "decision": "cursor movement helper로, 선택 buffer/object state를 갱신한다.",
    },
    0x004106BA: {
        "label": "cursor motion/sound helper",
        "promotion": "confirmed-helper",
        "decision": "0x59db5a/5b/5c/5e cursor globals를 소비하는 menu cursor motion 계층이다.",
    },
    0x0040B49E: {
        "label": "availability table builder",
        "promotion": "confirmed-selector-state-layer",
        "decision": "0x59e360/0x59e370 enabled table을 준비하는 selector availability producer다.",
    },
    0x0040B4E6: {
        "label": "availability branch consumer",
        "promotion": "confirmed-selector-state-layer",
        "decision": "object+0xa8 selector와 enabled table 값을 비교해 분기한다.",
    },
    0x0040B55F: {
        "label": "next enabled row producer",
        "promotion": "confirmed-selector-state-layer",
        "decision": "enabled table에서 다음 선택 가능 row를 찾고 object+0xa8 slot에 쓴다.",
    },
    0x0040B696: {
        "label": "active descriptor row selector producer",
        "promotion": "confirmed-selector-state-layer",
        "decision": "active descriptor row의 +0x48/+0x4a 값을 비교해 menu selector slot을 만든다.",
    },
    0x0040B752: {
        "label": "persistent menu category/cursor load-save",
        "promotion": "confirmed-selector-state-layer",
        "decision": (
            "active descriptor row와 0x59e340/0x59e34a 사이에 menu category/cursor 상태를 "
            "load/save하는 persistent state handler다."
        ),
    },
    0x0041D89D: {
        "label": "display VM selector base binder",
        "promotion": "confirmed-base-binder-not-opener",
        "decision": "display/object VM의 base pointer를 고르는 binder다. opener 증거로 쓰면 안 된다.",
    },
}


def h(value: Any) -> str:
    return html.escape("" if value is None else str(value), quote=True)


def refs_for_function(row: dict[str, Any], refs_by_name: dict[str, list[int]]) -> dict[str, list[str]]:
    return {
        name: [hx(ref) for ref in refs_in_function(row, refs)]
        for name, refs in refs_by_name.items()
        if refs_in_function(row, refs)
    }


def calls_for_function(row: dict[str, Any], calls_by_name: dict[str, list[int]]) -> dict[str, list[str]]:
    return {
        name: [hx(ref) for ref in refs_in_function(row, refs)]
        for name, refs in calls_by_name.items()
        if refs_in_function(row, refs)
    }


def classify_known_functions(
    functions: list[dict[str, Any]],
    refs_by_name: dict[str, list[int]],
    calls_by_name: dict[str, list[int]],
) -> list[dict[str, Any]]:
    rows: list[dict[str, Any]] = []
    for va, meta in sorted(DISPATCHER_FUNCTIONS.items()):
        function = find_function(functions, va)
        if not function:
            rows.append(
                {
                    "functionVa": va,
                    "functionVaHex": hx(va),
                    "label": meta["label"],
                    "promotion": "missing-function-range",
                    "decision": "function prologue range not found by static scanner",
                    "refHits": {},
                    "callHits": {},
                }
            )
            continue
        start_va = int(function["startVa"])
        rows.append(
            {
                "functionVa": start_va,
                "functionVaHex": hx(start_va),
                "requestedVaHex": hx(va),
                "endVaHex": hx(int(function["endVa"])),
                "size": int(function["size"]),
                "label": meta["label"],
                "promotion": meta["promotion"],
                "decision": meta["decision"],
                "refHits": refs_for_function(function, refs_by_name),
                "callHits": calls_for_function(function, calls_by_name),
            }
        )
    return rows


def discover_intersections(
    functions: list[dict[str, Any]],
    refs_by_name: dict[str, list[int]],
    calls_by_name: dict[str, list[int]],
) -> list[dict[str, Any]]:
    touched: dict[int, dict[str, Any]] = {}
    for refs in refs_by_name.values():
        for ref in refs:
            function = find_function(functions, ref)
            if function:
                touched[int(function["startVa"])] = function
    for calls in calls_by_name.values():
        for call in calls:
            function = find_function(functions, call)
            if function:
                touched[int(function["startVa"])] = function

    rows: list[dict[str, Any]] = []
    for function in sorted(touched.values(), key=lambda item: int(item["startVa"])):
        ref_hits = refs_for_function(function, refs_by_name)
        call_hits = calls_for_function(function, calls_by_name)
        input_hit = any(name.startswith("input.") for name in ref_hits)
        menu_state_hit = any(
            name.startswith(("runtime.", "menu.", "descriptor."))
            for name in ref_hits
        )
        top_hit = "topMenu.objectSequence" in ref_hits
        add_hit = "call.addDescriptor" in call_hits
        if input_hit and (menu_state_hit or top_hit or add_hit):
            rows.append(
                {
                    "functionVaHex": hx(int(function["startVa"])),
                    "endVaHex": hx(int(function["endVa"])),
                    "inputHit": input_hit,
                    "menuStateHit": menu_state_hit,
                    "topMenuHit": top_hit,
                    "descriptorAddCallHit": add_hit,
                    "refHits": ref_hits,
                    "callHits": call_hits,
                }
            )
    return rows


def build_report() -> dict[str, Any]:
    exe = EXE.read_bytes()
    sections = read_sections(exe)
    functions = function_ranges(exe, sections)

    refs_by_name = {
        name: text_refs(exe, sections, va)
        for name, va in MENU_STATE_GLOBALS.items()
    }
    calls_by_name = {
        name: call_sites(exe, sections, va)
        for name, va in CALL_TARGETS.items()
    }
    dispatcher_rows = classify_known_functions(functions, refs_by_name, calls_by_name)
    intersections = discover_intersections(functions, refs_by_name, calls_by_name)
    opener_like = [
        row for row in intersections
        if row.get("topMenuHit") or row.get("descriptorAddCallHit")
    ]
    confirmed_internal = [
        row for row in dispatcher_rows
        if str(row.get("promotion", "")).startswith("confirmed")
    ]

    summary = {
        "knownDispatcherRows": len(dispatcher_rows),
        "confirmedInternalRows": len(confirmed_internal),
        "inputMenuStateIntersectionCount": len(intersections),
        "inputTopMenuOrAddDescriptorIntersectionCount": len(opener_like),
        "menuInternalDispatcherProven": any(
            row["functionVaHex"] == hx(0x0041D61B)
            and row["promotion"] == "confirmed-menu-internal-not-opener"
            for row in dispatcher_rows
        ),
        "normalFieldEscOpenerProven": False,
        "decision": (
            "0x0041d61b 및 0x0040b49e..0x0040b752 계열은 메뉴가 이미 열린 뒤의 "
            "cursor/selector/mode-state dispatcher로 분류된다. 이 층은 입력, cursor globals, "
            "selector table을 강하게 참조하지만 top-menu object sequence 생성이나 descriptor-add "
            "호출과 연결되지 않으므로 normal-field ESC/X opener 증거가 아니다."
        ),
    }
    return {
        "kind": "hwanse-hud-menu-mode-state-dispatcher-review",
        "source": "tools/build_hud_menu_mode_state_dispatcher_review.py",
        "status": "menu-internal-dispatcher-grounded-opener-pending",
        "summary": summary,
        "dispatcherRows": dispatcher_rows,
        "inputMenuStateIntersections": intersections,
        "openerLikeIntersections": opener_like,
        "refsByName": {name: [hx(ref) for ref in refs] for name, refs in refs_by_name.items()},
        "callsByName": {name: [hx(call) for call in calls] for name, calls in calls_by_name.items()},
        "negativeEvidence": [
            "0x0041d61b reads input edge mask and menu cursor globals, but does not reference 0x004ddc6c and does not call 0x00431fe8.",
            "0x0040b49e..0x0040b752 selector handlers prepare/consume enabled tables and persistent cursor state, not menu object construction.",
            "The exact top-menu #6/#3/#4 construction sequence remains data-grounded at 0x004ddc84 with no input-consumer intersection.",
        ],
        "nextFrontier": [
            "Trace the higher-level field mode/state transition that creates or attaches the top-menu descriptor before 0x0041d61b starts consuming cursor input.",
            "Search for producers of descriptor stack materialization or selected-root switch, not for cursor/confirm/cancel handlers.",
            "Promote only if a candidate connects input/state transition to the exact 0x004ddc6c top-menu object sequence or its descriptor attachment path.",
        ],
    }


def render_html(report: dict[str, Any]) -> str:
    summary = report["summary"]
    cards = [
        ("status", report["status"]),
        ("dispatcher rows", summary["knownDispatcherRows"]),
        ("confirmed internal", summary["confirmedInternalRows"]),
        ("input/menu intersections", summary["inputMenuStateIntersectionCount"]),
        ("opener-like intersections", summary["inputTopMenuOrAddDescriptorIntersectionCount"]),
        ("opener proven", summary["normalFieldEscOpenerProven"]),
    ]
    card_html = "".join(f"<div class='card'><b>{h(k)}</b><span>{h(v)}</span></div>" for k, v in cards)
    rows = "".join(
        "<tr>"
        f"<td><code>{h(row['functionVaHex'])}</code><br>{h(row['label'])}</td>"
        f"<td>{h(row['promotion'])}<br><span class='note'>{h(row['decision'])}</span></td>"
        f"<td>{h(json.dumps(row.get('refHits', {}), ensure_ascii=False))}</td>"
        f"<td>{h(json.dumps(row.get('callHits', {}), ensure_ascii=False))}</td>"
        "</tr>"
        for row in report["dispatcherRows"]
    )
    inter_rows = "".join(
        "<tr>"
        f"<td><code>{h(row['functionVaHex'])}</code></td>"
        f"<td>{h(json.dumps(row['refHits'], ensure_ascii=False))}</td>"
        f"<td>{h(json.dumps(row['callHits'], ensure_ascii=False))}</td>"
        "</tr>"
        for row in report["inputMenuStateIntersections"]
    )
    negatives = "".join(f"<li>{h(item)}</li>" for item in report["negativeEvidence"])
    frontier = "".join(f"<li>{h(item)}</li>" for item in report["nextFrontier"])
    payload = json.dumps(report, ensure_ascii=False)
    return f"""<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>HUD Menu Mode/State Dispatcher Review</title>
  <style>
    body {{ margin:0; background:#101318; color:#edf1f7; font-family:system-ui,sans-serif; }}
    main {{ max-width:1280px; margin:0 auto; padding:24px; }}
    a {{ color:#8ecbff; }}
    .nav {{ display:flex; flex-wrap:wrap; gap:8px; margin-bottom:16px; }}
    .chip {{ border:1px solid #334155; border-radius:999px; padding:6px 10px; text-decoration:none; background:#161b22; }}
    .summary {{ display:grid; grid-template-columns:repeat(auto-fit,minmax(180px,1fr)); gap:12px; margin:16px 0; }}
    .card {{ border:1px solid #2b3544; border-radius:8px; padding:12px; background:#161b22; }}
    .card b {{ display:block; color:#9fb1c9; font-size:12px; text-transform:uppercase; }}
    .card span {{ display:block; margin-top:8px; font-size:18px; overflow-wrap:anywhere; }}
    section {{ border:1px solid #273244; border-radius:10px; padding:16px; margin:16px 0; background:#141922; overflow:auto; }}
    table {{ border-collapse:collapse; width:100%; min-width:1080px; font-size:13px; }}
    th,td {{ border-bottom:1px solid #283342; padding:8px; text-align:left; vertical-align:top; }}
    th {{ color:#b9c7dc; background:#111722; }}
    code {{ color:#dbeafe; overflow-wrap:anywhere; }}
    .note {{ display:block; margin-top:6px; color:#9fb1c9; line-height:1.35; }}
    pre {{ white-space:pre-wrap; background:#0b0f14; border:1px solid #253044; border-radius:8px; padding:12px; max-height:420px; overflow:auto; }}
  </style>
</head>
<body>
<main>
  <div class="nav">
    <a class="chip" href="index.html">index</a>
    <a class="chip" href="hud_menu_opener_frontier_review.html">opener frontier</a>
    <a class="chip" href="hud_menu_selected_root_frontier_review.html">selected-root frontier</a>
    <a class="chip" href="hud_menu_preview.html">HUD menu preview</a>
    <a class="chip" href="../out/hud_menu_mode_state_dispatcher_review.json">JSON</a>
  </div>
  <h1>HUD Menu Mode/State Dispatcher Review</h1>
  <p>{h(summary["decision"])}</p>
  <div class="summary">{card_html}</div>
  <section>
    <h2>Confirmed Dispatcher/Selector Layer</h2>
    <table>
      <thead><tr><th>function</th><th>classification</th><th>refs</th><th>calls</th></tr></thead>
      <tbody>{rows}</tbody>
    </table>
  </section>
  <section>
    <h2>Input + Menu-State Intersections</h2>
    <table>
      <thead><tr><th>function</th><th>refs</th><th>calls</th></tr></thead>
      <tbody>{inter_rows or "<tr><td colspan='3'>No input/menu-state intersections.</td></tr>"}</tbody>
    </table>
  </section>
  <section>
    <h2>Negative Evidence</h2>
    <ul>{negatives}</ul>
  </section>
  <section>
    <h2>Next Frontier</h2>
    <ul>{frontier}</ul>
  </section>
  <section>
    <h2>Raw JSON</h2>
    <pre id="json"></pre>
  </section>
</main>
<script>
window.HWANSE_HUD_MENU_MODE_STATE_DISPATCHER_REVIEW = {payload};
document.getElementById('json').textContent = JSON.stringify(window.HWANSE_HUD_MENU_MODE_STATE_DISPATCHER_REVIEW, null, 2);
</script>
</body>
</html>
"""


def main() -> int:
    report = build_report()
    OUT.mkdir(exist_ok=True)
    WEB.mkdir(exist_ok=True)
    (OUT / "hud_menu_mode_state_dispatcher_review.json").write_text(
        json.dumps(report, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    (WEB / "hud_menu_mode_state_dispatcher_review.html").write_text(render_html(report), encoding="utf-8")
    print("hud_menu_mode_state_dispatcher_review ok")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
