#!/usr/bin/env python3
"""Review EXE writers/readers of the live field-map tile buffers.

Map animation has three nearby-but-different pieces:

* layer1 bit 0x40 marks tiles that need redraw/animation attention;
* the generic VM has commands that can write live map tile buffers; and
* the visible fire/waterfall frame loop still needs a producer/tick binding.

This report narrows the second piece.  It scans exact references to the live
layer0/layer1 buffers and classifies whether those references are the initial
map loader, dirty redraw/foreground preparation, movement/collision logic, or
generic VM live-tile write handlers.
"""
from __future__ import annotations

import html
import json
import struct
import sys
from collections import Counter, defaultdict
from pathlib import Path
from typing import Any


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

sys.path.insert(0, str(ROOT / "tools"))
from probe_exe_scene_tables import offset_to_va, read_sections  # noqa: E402


LIVE_TARGETS = {
    "layer0-grid": 0x00595AF0,
    "layer1-flag-grid": 0x0058D7D0,
    "layer1-edge-helper": 0x0058D7CE,
    "map-width": 0x00595ADA,
    "map-height": 0x00595ADC,
}

RANGES = [
    ("resource-map-load-copy", 0x0042449C, 0x00424600),
    ("draw-foreground-redraw", 0x00424A2A, 0x00425620),
    ("actor-movement-collision", 0x0043022D, 0x00432020),
    ("vm-live-tile-write-candidate", 0x00407000, 0x0040B000),
]

KNOWN_HANDLERS = {
    0x00407686: {"opcode": "0x58", "name": "absolute live tile write"},
    0x00408D10: {"opcode": "0x6b", "name": "active-object-relative live tile write"},
    0x00409F23: {"opcode": "0x76", "name": "active-object-list live tile write"},
}


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


def hx(value: int | None) -> str | None:
    return None if value is None else f"0x{value:08x}"


def read_json(name: str, fallback: Any) -> Any:
    try:
        return json.loads((OUT / name).read_text(encoding="utf-8"))
    except FileNotFoundError:
        return fallback


def write_json(path: Path, payload: Any) -> None:
    path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")


def section_name_for_va(sections: list[dict[str, Any]], va: int) -> str | None:
    for section in sections:
        start = int(section["va"])
        end = start + int(section["raw_size"])
        if start <= va < end:
            return str(section["name"])
    return None


def classify_va(va: int) -> str:
    for label, start, end in RANGES:
        if start <= va < end:
            return label
    return "other"


def handler_for_va(va: int) -> dict[str, str] | None:
    for start, info in KNOWN_HANDLERS.items():
        # Handler extents are deliberately broad here; the focus is provenance,
        # not instruction-level control flow reconstruction.
        if start <= va < start + 0x220:
            return {"handlerVaHex": hx(start) or "", **info}
    return None


def exact_refs(exe: bytes, sections: list[dict[str, Any]], target: int) -> list[dict[str, Any]]:
    needle = struct.pack("<I", target)
    rows: list[dict[str, Any]] = []
    for section in sections:
        raw = int(section["raw"])
        raw_size = int(section["raw_size"])
        blob = exe[raw : raw + raw_size]
        start = 0
        while True:
            hit = blob.find(needle, start)
            if hit < 0:
                break
            file_offset = raw + hit
            va = offset_to_va(sections, file_offset)
            if va is not None:
                rows.append(
                    {
                        "fileOffset": file_offset,
                        "fileOffsetHex": hx(file_offset),
                        "va": va,
                        "vaHex": hx(va),
                        "section": section["name"],
                    }
                )
            start = hit + 1
    return rows


def context_bytes(exe: bytes, file_offset: int, before: int = 12, after: int = 16) -> bytes:
    return exe[max(0, file_offset - before) : min(len(exe), file_offset + 4 + after)]


def classify_instruction(ctx: bytes, needle_offset: int = 12) -> dict[str, Any]:
    before4 = ctx[max(0, needle_offset - 4) : needle_offset]
    before8 = ctx[max(0, needle_offset - 8) : needle_offset]
    after8 = ctx[needle_offset + 4 : needle_offset + 12]
    hint = "unknown"
    write_like = False
    read_like = False
    flag_test_0x40 = b"\xf6\xc1\x40" in ctx or b"\x80\xe1\x40" in ctx

    if before4 == b"\x66\x89\x04\x55":
        hint = "indexed-word-write"
        write_like = True
    elif before4 == b"\x66\x8b\x04\x55":
        hint = "indexed-word-read"
        read_like = True
    elif b"\x66\x89" in before8:
        hint = "near-word-write"
        write_like = True
    elif b"\x66\x8b" in before8:
        hint = "near-word-read"
        read_like = True
    elif b"\xa1" in before4 or b"\x8b" in before8:
        hint = "near-read-or-load"
        read_like = True

    return {
        "instructionHint": hint,
        "writeLike": write_like,
        "readLike": read_like,
        "flagTest0x40Nearby": flag_test_0x40,
        "before4": before4.hex(" "),
        "after8": after8.hex(" "),
    }


def roots_containing_va(resource_probe: dict[str, Any], va: int) -> list[dict[str, Any]]:
    matches: list[dict[str, Any]] = []
    for row in resource_probe.get("rows", []):
        for root in row.get("exactResourceRoots", []) or []:
            start_hex = root.get("rootVaHex")
            end_hex = root.get("rangeEndVaHex")
            if not isinstance(start_hex, str) or not isinstance(end_hex, str):
                continue
            start = int(start_hex, 16)
            end = int(end_hex, 16)
            if start <= va < end:
                matches.append(
                    {
                        "map": row.get("map"),
                        "rootVaHex": start_hex,
                        "rangeEndVaHex": end_hex,
                        "selectorKeys": root.get("selectorKeys") or [],
                    }
                )
    return matches


def build_report() -> dict[str, Any]:
    exe = EXE.read_bytes()
    sections = read_sections(exe)
    resource_probe = read_json("map_animation_resource_binding_probe.json", {})
    generic_focus = read_json("generic_script_handler_focus.json", {})

    handler_rows = []
    for row in generic_focus.get("focusRows", []):
        if row.get("handlerVaHex") in {"0x00407686", "0x00408d10", "0x00409f23"}:
            handler_rows.append(
                {
                    "opcodeHex": row.get("opcodeHex"),
                    "handlerVaHex": row.get("handlerVaHex"),
                    "entryVaHex": row.get("entryVaHex"),
                    "categories": row.get("categories") or [],
                    "refs": row.get("refs") or [],
                    "calls": [
                        call for call in row.get("calls", [])
                        if call.get("targetVaHex") == "0x004255eb"
                    ],
                }
            )

    target_rows: list[dict[str, Any]] = []
    all_refs: list[dict[str, Any]] = []
    for label, target in LIVE_TARGETS.items():
        refs = exact_refs(exe, sections, target)
        class_counts: Counter[str] = Counter()
        instruction_counts: Counter[str] = Counter()
        target_ref_rows = []
        for ref in refs:
            file_offset = int(ref["fileOffset"])
            va = int(ref["va"])
            ctx = context_bytes(exe, file_offset)
            instr = classify_instruction(ctx)
            class_label = classify_va(va)
            class_counts[class_label] += 1
            instruction_counts[instr["instructionHint"]] += 1
            handler = handler_for_va(va)
            roots = roots_containing_va(resource_probe, va)
            row = {
                "target": label,
                "targetVaHex": hx(target),
                "refVaHex": ref["vaHex"],
                "section": ref["section"],
                "class": class_label,
                "handler": handler,
                "contextHex": ctx.hex(" "),
                "animatedResourceRootsContainingRef": roots,
                **instr,
            }
            target_ref_rows.append(row)
            all_refs.append(row)
        target_rows.append(
            {
                "target": label,
                "targetVaHex": hx(target),
                "refCount": len(refs),
                "classCounts": dict(sorted(class_counts.items())),
                "instructionHintCounts": dict(sorted(instruction_counts.items())),
                "refs": target_ref_rows,
            }
        )

    class_counts = Counter(row["class"] for row in all_refs)
    write_like_rows = [row for row in all_refs if row.get("writeLike")]
    read_like_rows = [row for row in all_refs if row.get("readLike")]
    vm_write_like_rows = [
        row for row in write_like_rows
        if row["class"] == "vm-live-tile-write-candidate"
        and row["target"] in {"layer0-grid", "layer1-flag-grid"}
    ]
    loader_rows = [row for row in all_refs if row["class"] == "resource-map-load-copy"]
    draw_rows = [row for row in all_refs if row["class"] == "draw-foreground-redraw"]
    collision_rows = [row for row in all_refs if row["class"] == "actor-movement-collision"]
    roots_bound = [
        row for row in all_refs
        if row.get("animatedResourceRootsContainingRef")
    ]

    handler_summary = {
        row["opcodeHex"]: {
            "handlerVaHex": row["handlerVaHex"],
            "entryVaHex": row["entryVaHex"],
            "categories": row["categories"],
            "invalidateCalls": row["calls"],
        }
        for row in handler_rows
    }

    surfaces = [
        {
            "surface": "initial field-map load",
            "promotion": "confirmed-loader",
            "finding": f"{len(loader_rows)} live-buffer refs inside 0x0042449c map resource loader.",
            "gap": "초기 layer0/layer1 복사이며, per-frame animation frame 선택은 아니다.",
        },
        {
            "surface": "dirty redraw / foreground scan",
            "promotion": "confirmed-redraw-consumer",
            "finding": f"{len(draw_rows)} live-buffer refs inside draw/foreground preparation range.",
            "gap": "layer1 0x40을 dirty/foreground work buffer로 소비하지만 alternate tile id를 고르지 않는다.",
        },
        {
            "surface": "movement / collision",
            "promotion": "confirmed-collision-consumer",
            "finding": f"{len(collision_rows)} live-buffer refs inside actor movement/collision helpers.",
            "gap": "통행/충돌 판정 경로이며 visible animation source가 아니다.",
        },
        {
            "surface": "generic VM live tile writers",
            "promotion": "confirmed-handler / animation-root-unbound",
            "finding": (
                f"{len(vm_write_like_rows)} write-like refs in VM tile-write range; "
                f"handlers {', '.join(sorted(handler_summary)) or '-'}."
            ),
            "gap": "opcode handler semantics are grounded, but no animated map tick/root currently binds these handlers to 0x40 cells.",
        },
        {
            "surface": "animated resource root containment",
            "promotion": "negative-binding",
            "finding": f"{len(roots_bound)} live-buffer refs are inside animated map exact resource roots.",
            "gap": "animated map resource packages do not contain the live-buffer writer refs; tick/command producer remains elsewhere.",
        },
    ]

    summary = {
        "targetCount": len(LIVE_TARGETS),
        "totalRefCount": len(all_refs),
        "layer0RefCount": next((row["refCount"] for row in target_rows if row["target"] == "layer0-grid"), 0),
        "layer1RefCount": next((row["refCount"] for row in target_rows if row["target"] == "layer1-flag-grid"), 0),
        "edgeRefCount": next((row["refCount"] for row in target_rows if row["target"] == "layer1-edge-helper"), 0),
        "writeLikeRefCount": len(write_like_rows),
        "readLikeRefCount": len(read_like_rows),
        "classCounts": dict(sorted(class_counts.items())),
        "loaderRefCount": len(loader_rows),
        "drawForegroundRefCount": len(draw_rows),
        "collisionRefCount": len(collision_rows),
        "candidateVmTileWriteRefCount": len(vm_write_like_rows),
        "candidateVmTileWriteRefsInsideAnimatedResourceRoots": sum(
            1 for row in vm_write_like_rows if row.get("animatedResourceRootsContainingRef")
        ),
        "liveBufferRefsInsideAnimatedResourceRoots": len(roots_bound),
        "handlerOpcodeCount": len(handler_summary),
        "visibleMotionExecutionBindingProven": False,
        "decision": (
            "live layer0/layer1 buffer writer handlers are grounded, but their refs are not inside animated-map "
            "resource roots and no per-map/per-tick animation producer has been found. Keep visible map animation unpromoted."
        ),
    }

    return {
        "kind": "hwanse-map-animation-live-buffer-writer-review",
        "status": "live-buffer-writers-grounded-animation-producer-unbound",
        "source": [
            "Hwanse2.exe",
            "out/generic_script_handler_focus.json",
            "out/map_animation_resource_binding_probe.json",
            "tools/build_map_animation_live_buffer_writer_review.py",
        ],
        "liveTargets": {key: hx(value) for key, value in LIVE_TARGETS.items()},
        "classificationRanges": [
            {"class": label, "startVaHex": hx(start), "endVaHex": hx(end)}
            for label, start, end in RANGES
        ],
        "summary": summary,
        "handlerSummary": handler_summary,
        "surfaces": surfaces,
        "targets": target_rows,
        "interestingRefs": {
            "vmWriteLikeRefs": vm_write_like_rows,
            "flagTest0x40Refs": [row for row in all_refs if row.get("flagTest0x40Nearby")],
            "animatedResourceRootContainedRefs": roots_bound,
        },
        "nextFrontier": [
            "opcode 0x58/0x6b/0x76 command stream을 어느 tick/root가 공급하는지 찾는다.",
            "known animated maps의 0x40 cell 좌표와 VM tile-write command operands가 같은 root에서 만나는지 재검증한다.",
            "정적 route가 계속 막히면 map1_01a/map1_02b에서 live layer0 buffer watchpoint를 좁게 잡는다.",
        ],
    }


def render_html(report: dict[str, Any]) -> str:
    summary = report["summary"]
    cards = [
        ("status", report["status"]),
        ("refs", summary["totalRefCount"]),
        ("layer0/layer1", f"{summary['layer0RefCount']} / {summary['layer1RefCount']}"),
        ("VM write refs", summary["candidateVmTileWriteRefCount"]),
        ("root-bound refs", summary["liveBufferRefsInsideAnimatedResourceRoots"]),
        ("visible binding", "proven" if summary["visibleMotionExecutionBindingProven"] else "unproven"),
    ]
    card_html = "".join(f"<div class='card'><b>{h(k)}</b><span>{h(v)}</span></div>" for k, v in cards)

    surface_rows = []
    for row in report["surfaces"]:
        surface_rows.append(
            "<tr>"
            f"<td><b>{h(row['surface'])}</b></td>"
            f"<td>{h(row['promotion'])}</td>"
            f"<td>{h(row['finding'])}</td>"
            f"<td>{h(row['gap'])}</td>"
            "</tr>"
        )

    target_rows = []
    for row in report["targets"]:
        target_rows.append(
            "<tr>"
            f"<td><code>{h(row['target'])}</code><br><code>{h(row['targetVaHex'])}</code></td>"
            f"<td>{h(row['refCount'])}</td>"
            f"<td><code>{h(row['classCounts'])}</code></td>"
            f"<td><code>{h(row['instructionHintCounts'])}</code></td>"
            "</tr>"
        )

    ref_rows = []
    for row in report["interestingRefs"]["vmWriteLikeRefs"]:
        handler = row.get("handler") or {}
        ref_rows.append(
            "<tr>"
            f"<td><code>{h(row['refVaHex'])}</code></td>"
            f"<td>{h(row['target'])}</td>"
            f"<td>{h(row['instructionHint'])}</td>"
            f"<td>{h(handler.get('opcode', ''))}<br><code>{h(handler.get('handlerVaHex', ''))}</code></td>"
            f"<td>{h(len(row.get('animatedResourceRootsContainingRef') or []))}</td>"
            f"<td><code>{h(row['contextHex'])}</code></td>"
            "</tr>"
        )

    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>Map Animation Live Buffer Writer 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(160px,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:980px; 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; }}
    .warn {{ color:#fbbf24; }}
    pre {{ white-space:pre-wrap; background:#0b0f14; border:1px solid #253044; border-radius:8px; padding:12px; max-height:360px; overflow:auto; }}
  </style>
</head>
<body>
<main>
  <div class="nav">
    <a class="chip" href="index.html">index</a>
    <a class="chip" href="map_review.html">map review</a>
    <a class="chip" href="map_animation_execution_boundary_review.html">animation boundary</a>
    <a class="chip" href="map_animation_execution_boundary_review.html">EXE pattern</a>
  </div>
  <h1>Map Animation Live Buffer Writer Review</h1>
  <p class="warn">{h(summary['decision'])}</p>
  <div class="summary">{card_html}</div>
  <section>
    <h2>Surfaces</h2>
    <table><thead><tr><th>surface</th><th>promotion</th><th>finding</th><th>gap</th></tr></thead><tbody>{''.join(surface_rows)}</tbody></table>
  </section>
  <section>
    <h2>Targets</h2>
    <table><thead><tr><th>target</th><th>refs</th><th>classes</th><th>instruction hints</th></tr></thead><tbody>{''.join(target_rows)}</tbody></table>
  </section>
  <section>
    <h2>VM Write-Like Refs</h2>
    <table><thead><tr><th>ref</th><th>target</th><th>hint</th><th>handler</th><th>animated root hits</th><th>context</th></tr></thead><tbody>{''.join(ref_rows)}</tbody></table>
  </section>
  <section>
    <h2>Next Frontier</h2>
    <ul>{frontier}</ul>
  </section>
  <section>
    <h2>Raw JSON</h2>
    <pre id="raw"></pre>
  </section>
</main>
<script>
window.HWANSE_MAP_ANIMATION_LIVE_BUFFER_WRITER_REVIEW = {payload};
document.getElementById('raw').textContent = JSON.stringify(window.HWANSE_MAP_ANIMATION_LIVE_BUFFER_WRITER_REVIEW, null, 2);
</script>
</body>
</html>
"""


def main() -> None:
    report = build_report()
    write_json(OUT / "map_animation_live_buffer_writer_review.json", report)
    html_text = render_html(report)


if __name__ == "__main__":
    main()
