#!/usr/bin/env python3
"""Compare the two VM routes that bind object+0xa8 to 0x004576d8.

The selector byte writer is still unresolved, but the base binding itself is
now grounded through two VM families:

* active-object VM dispatcher 0x00402321, table 0x00440538, opcode 0x43;
* text/status 0x40-prefixed interpreter 0x0041b66d, table 0x0047f1d8,
  sub-opcode 0x1b with mode 1.

This report records that bridge explicitly so future analysis can target the
remaining value producer rather than re-proving the base.
"""
from __future__ import annotations

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


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

if str(TOOLS) not in sys.path:
    sys.path.insert(0, str(TOOLS))

from probe_exe_scene_tables import offset_to_va, read_sections, va_to_offset  # noqa: E402


ACTIVE_DISPATCHER_VA = 0x00402321
ACTIVE_HANDLER_TABLE_VA = 0x00440538
ACTIVE_BASE_BIND_HANDLER_VA = 0x004064D5
TEXT_STATUS_INTERPRETER_VA = 0x0041B66D
TEXT_STATUS_HANDLER_TABLE_VA = 0x0047F1D8
TEXT_STATUS_BASE_BIND_HANDLER_VA = 0x0041D89D
BACKING_BASE_VA = 0x004576D8


def hx(value: int | None, width: int = 8) -> str:
    if value is None:
        return ""
    return f"0x{value:0{width}x}"


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


def load_json(name: str) -> dict[str, Any]:
    try:
        data = json.loads((OUT / name).read_text(encoding="utf-8"))
    except FileNotFoundError:
        return {}
    return data if isinstance(data, dict) else {}


def read_dword(exe: bytes, sections: list[dict[str, Any]], va: int) -> int:
    off = va_to_offset(sections, va)
    if off is None:
        raise RuntimeError(f"unmapped VA {hx(va)}")
    return struct.unpack_from("<I", exe, off)[0]


def table_index_for_handler(exe: bytes, sections: list[dict[str, Any]], table_va: int, handler_va: int, max_entries: int) -> int | None:
    for index in range(max_entries):
        try:
            value = read_dword(exe, sections, table_va + index * 4)
        except RuntimeError:
            return None
        if value == handler_va:
            return index
    return None


def exact_dword_refs(exe: bytes, sections: list[dict[str, Any]], value: int) -> list[dict[str, Any]]:
    needle = struct.pack("<I", value)
    rows: list[dict[str, Any]] = []
    pos = exe.find(needle)
    while pos != -1:
        va = offset_to_va(sections, pos)
        section_name = ""
        for section in sections:
            if int(section["raw"]) <= pos < int(section["raw"]) + int(section["raw_size"]):
                section_name = str(section["name"])
                break
        rows.append(
            {
                "fileOffsetHex": hx(pos),
                "vaHex": hx(va),
                "section": section_name,
                "contextHex": exe[max(0, pos - 16) : min(len(exe), pos + 24)].hex(" "),
            }
        )
        pos = exe.find(needle, pos + 1)
    return rows


def find_pattern_rows(exe: bytes, sections: list[dict[str, Any]], pattern: bytes) -> list[dict[str, Any]]:
    rows: list[dict[str, Any]] = []
    pos = exe.find(pattern)
    while pos != -1:
        va = offset_to_va(sections, pos)
        section_name = ""
        for section in sections:
            if int(section["raw"]) <= pos < int(section["raw"]) + int(section["raw_size"]):
                section_name = str(section["name"])
                break
        if section_name == ".data":
            rows.append(
                {
                    "vaHex": hx(va),
                    "fileOffsetHex": hx(pos),
                    "patternHex": pattern.hex(" "),
                    "classification": classify_401b_payload(va),
                    "contextHex": exe[max(0, pos - 16) : min(len(exe), pos + 56)].hex(" "),
                }
            )
        pos = exe.find(pattern, pos + 1)
    return rows


def classify_401b_payload(va: int | None) -> str:
    if va is None:
        return "unknown"
    if va == 0x004E85FC:
        return "status-comment-group-selector-base-bind-before-40-14"
    if va == 0x004E8B02:
        return "chapter-number-title-selector-base-bind"
    if va == 0x004E80A0:
        return "status-menu-region-payload-selector-base-bind"
    if va == 0x004E8DFA:
        return "status-menu-secondary-payload-selector-base-bind"
    return "other-40-1b-mode1"


def build() -> dict[str, Any]:
    exe = EXE.read_bytes()
    sections = read_sections(exe)
    active_index = table_index_for_handler(exe, sections, ACTIVE_HANDLER_TABLE_VA, ACTIVE_BASE_BIND_HANDLER_VA, 256)
    text_index = table_index_for_handler(exe, sections, TEXT_STATUS_HANDLER_TABLE_VA, TEXT_STATUS_BASE_BIND_HANDLER_VA, 64)
    active_refs = exact_dword_refs(exe, sections, ACTIVE_BASE_BIND_HANDLER_VA)
    text_refs = exact_dword_refs(exe, sections, TEXT_STATUS_BASE_BIND_HANDLER_VA)
    pattern_rows = find_pattern_rows(exe, sections, b"\x40\x1b\x01\x00")

    handler_rows = [
        {
            "vmFamily": "active-object",
            "dispatcherVaHex": hx(ACTIVE_DISPATCHER_VA),
            "handlerTableVaHex": hx(ACTIVE_HANDLER_TABLE_VA),
            "opcodeHex": hx(active_index, 2) if active_index is not None else "",
            "handlerVaHex": hx(ACTIVE_BASE_BIND_HANDLER_VA),
            "handlerTableRefCount": len(active_refs),
            "commandShape": "43 00 00 00 (4-byte active-object command shape; stream use may be context-dependent)",
            "action": "object+0xa8 = 0x004576d8; object+0x40 += 4",
            "baseBindProven": active_index == 0x43,
        },
        {
            "vmFamily": "text/status 0x40-prefixed",
            "dispatcherVaHex": hx(TEXT_STATUS_INTERPRETER_VA),
            "handlerTableVaHex": hx(TEXT_STATUS_HANDLER_TABLE_VA),
            "opcodeHex": "0x40 0x1b",
            "handlerVaHex": hx(TEXT_STATUS_BASE_BIND_HANDLER_VA),
            "handlerTableRefCount": len(text_refs),
            "commandShape": "40 1b mode slot; mode 1 selects 0x004576d8",
            "action": "mode 0=0x0059e310, mode 1=0x004576d8, mode 2=0x00457750+slot*0xd8, mode 3=0x0059db30[object+0xf2]",
            "baseBindProven": text_index == 0x1B,
        },
    ]

    backing_base = load_json("status_comment_backing_base_ref_review.json")
    meaning = load_json("status_comment_selector_meaning_review.json")
    summary = {
        "activeObjectOpcodeHex": hx(active_index, 2) if active_index is not None else "",
        "activeObjectBaseBindHandlerVaHex": hx(ACTIVE_BASE_BIND_HANDLER_VA),
        "textStatusOpcodeHex": "0x40 0x1b",
        "textStatusSubOpcodeIndexHex": hx(text_index, 2) if text_index is not None else "",
        "textStatusBaseBindHandlerVaHex": hx(TEXT_STATUS_BASE_BIND_HANDLER_VA),
        "mode1PayloadUseCount": len(pattern_rows),
        "statusCommentPayloadMode1UseFound": any(row["classification"].startswith("status-comment") for row in pattern_rows),
        "chapterPayloadMode1UseFound": any(row["classification"].startswith("chapter") for row in pattern_rows),
        "objectA8BackingBaseBindCount": backing_base.get("summary", {}).get("objectA8BackingBaseBindCount", 0),
        "selectorMeaningCandidate": meaning.get("summary", {}).get("meaningCandidate", ""),
        "selectorValueWriterProven": False,
        "decision": (
            "The base bridge is grounded in two VM families: active-object opcode 0x43 and text/status command 40 1b mode 1. "
            "The status-comment and chapter payloads use the text/status route.  This still proves only the selector base, not the writer of backing byte +0."
        ),
    }
    return {
        "version": 1,
        "kind": "hwanse-status-comment-base-bind-route-review",
        "sourceArtifacts": [
            "out/status_comment_backing_base_ref_review.json",
            "out/status_comment_selector_meaning_review.json",
            "out/status_menu_vm_table_review.json",
        ],
        "summary": summary,
        "handlerRows": handler_rows,
        "mode1PayloadRows": pattern_rows,
        "handlerPointerRefs": {
            "activeObjectHandlerRefs": active_refs,
            "textStatusHandlerRefs": text_refs,
        },
    }


def render_html(data: dict[str, Any]) -> str:
    cards = "".join(
        f"<div class='card'><b>{h(k)}</b><span>{h(v)}</span></div>"
        for k, v in data["summary"].items()
    )
    handler_rows = "".join(
        "<tr>"
        f"<td>{h(row['vmFamily'])}</td>"
        f"<td><code>{h(row['dispatcherVaHex'])}</code></td>"
        f"<td><code>{h(row['handlerTableVaHex'])}</code></td>"
        f"<td><code>{h(row['opcodeHex'])}</code></td>"
        f"<td><code>{h(row['handlerVaHex'])}</code></td>"
        f"<td>{h(row['commandShape'])}</td>"
        f"<td>{h(row['action'])}</td>"
        f"<td>{h(row['baseBindProven'])}</td>"
        "</tr>"
        for row in data["handlerRows"]
    )
    payload_rows = "".join(
        "<tr>"
        f"<td><code>{h(row['vaHex'])}</code></td>"
        f"<td>{h(row['classification'])}</td>"
        f"<td><code>{h(row['contextHex'])}</code></td>"
        "</tr>"
        for row in data["mode1PayloadRows"]
    )
    return f"""<!doctype html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Status Comment Base Bind Route Review</title>
<style>
  :root {{ color-scheme: light dark; }}
  body {{ margin:24px; font-family:system-ui,-apple-system,Segoe UI,sans-serif; line-height:1.45; }}
  h1 {{ margin:0 0 8px; font-size:24px; }}
  h2 {{ margin:24px 0 10px; font-size:18px; }}
  .muted {{ color:#667085; }}
  .cards {{ display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:10px; margin:16px 0; }}
  .card {{ border:1px solid #d0d5dd; border-radius:8px; padding:10px 12px; background:rgba(127,127,127,.05); }}
  .card b {{ display:block; color:#667085; font-size:12px; }}
  .card span {{ font-family:ui-monospace,SFMono-Regular,Menlo,monospace; overflow-wrap:anywhere; }}
  .scroll {{ overflow-x:auto; }}
  table {{ width:100%; border-collapse:collapse; }}
  th,td {{ border:1px solid #d0d5dd; padding:7px 8px; vertical-align:top; }}
  th {{ text-align:left; background:rgba(127,127,127,.08); }}
  code {{ font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:12px; }}
</style>
<h1>Status Comment Base Bind Route Review</h1>
<p class="muted">상태창 문구 selector의 base bind 경로를 active-object VM과 text/status VM으로 분리합니다.</p>
<div class="cards">{cards}</div>
<h2>Handler Routes</h2>
<div class="scroll"><table>
<thead><tr><th>VM</th><th>dispatcher</th><th>table</th><th>opcode</th><th>handler</th><th>command shape</th><th>action</th><th>proven</th></tr></thead>
<tbody>{handler_rows}</tbody>
</table></div>
<h2>40 1b mode 1 Payload Uses</h2>
<div class="scroll"><table>
<thead><tr><th>VA</th><th>classification</th><th>context</th></tr></thead>
<tbody>{payload_rows}</tbody>
</table></div>
"""


def main() -> None:
    data = build()
    OUT.mkdir(exist_ok=True)
    WEB.mkdir(exist_ok=True)
    (OUT / "status_comment_base_bind_route_review.json").write_text(
        json.dumps(data, ensure_ascii=False, indent=2),
        encoding="utf-8",
    )
    (WEB / "status_comment_base_bind_route_review.html").write_text(
        render_html(data),
        encoding="utf-8",
    )


if __name__ == "__main__":
    main()
