#!/usr/bin/env python3
"""Document the redraw consumer side of layer1 bit 0x40 map animation.

Earlier reports prove that layer1 high bit 0x40 marks animated/redraw cells and
that the generic VM has commands capable of writing the live map buffers.  This
report covers the other half of the pipeline: the viewport redraw consumer.

The key distinction is important for later work:

* this path proves how 0x40 cells are invalidated and redrawn; but
* it does not prove which alternate tile/palette frame should be drawn.

So this report should prevent us from repeatedly re-promoting redraw coverage as
visible motion execution.
"""
from __future__ import annotations

import html
import json
import sys
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 read_sections, va_to_offset  # noqa: E402


LANDMARKS = [
    {
        "name": "viewport dirty scan",
        "va": 0x00425403,
        "endVa": 0x00425620,
        "role": "Scans the 37x21 visible viewport and marks dirty cells.",
    },
    {
        "name": "full surface dirty redraw",
        "va": 0x00425067,
        "endVa": 0x004250E4,
        "role": "Sets draw surface/mask, renders dirty rows, snapshots camera.",
    },
    {
        "name": "dirty row renderer",
        "va": 0x00425163,
        "endVa": 0x0042530F,
        "role": "Reads live layer0 tile ids and draws dirty visible rows.",
    },
    {
        "name": "active object projection",
        "va": 0x00424A2A,
        "endVa": 0x00424CD6,
        "role": "Projects object tile positions into viewport/screen positions.",
    },
    {
        "name": "occlusion dirty projection",
        "va": 0x00424CD6,
        "endVa": 0x00425024,
        "role": "Projects object/occlusion coverage into the dirty work grid.",
    },
]

GLOBALS = {
    "liveLayer0Grid": 0x00595AF0,
    "liveLayer1FlagGrid": 0x0058D7D0,
    "dirtyWorkGrid": 0x005957D0,
    "mapWidth": 0x00595ADA,
    "mapHeight": 0x00595ADC,
    "cameraCurrentX": 0x004576DC,
    "cameraCurrentY": 0x004576DE,
    "cameraPreviousX": 0x00595ADE,
    "cameraPreviousY": 0x00595AE0,
    "drawSurfaceSlot": 0x0055B580,
    "drawDirtyMask": 0x0055B588,
}


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


def byte_snippet(exe: bytes, sections: list[dict[str, Any]], va: int, size: int = 64) -> str:
    offset = va_to_offset(sections, va)
    if offset is None:
        return ""
    return exe[offset : offset + size].hex(" ")


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

    landmark_rows = []
    for row in LANDMARKS:
        landmark_rows.append(
            {
                **row,
                "vaHex": hx(row["va"]),
                "endVaHex": hx(row["endVa"]),
                "byteSnippet": byte_snippet(exe, sections, int(row["va"])),
            }
        )

    steps = [
        {
            "step": "clear dirty bits before viewport scan",
            "vaHex": "0x00425413",
            "evidence": "The 37x21 dirty work grid at 0x005957d0 is masked before the new viewport pass.",
            "meaning": "The routine starts from an invalidation pass, not from a frame-selection table.",
        },
        {
            "step": "scan visible layer1 flags",
            "vaHex": "0x00425494",
            "evidence": "Reads WORD PTR [index*2 + 0x0058d7d0] and tests low byte with 0x40.",
            "meaning": "layer1 bit 0x40 is consumed as redraw/animation coverage over the current viewport.",
        },
        {
            "step": "mark dirty work grid",
            "vaHex": "0x004254a5",
            "evidence": "When 0x40 is present, ORs the matching byte in 0x005957d0 with 0x01.",
            "meaning": "The consumer schedules redraw work.  It does not choose a replacement tile id here.",
        },
        {
            "step": "camera movement invalidation",
            "vaHex": "0x004254e0",
            "evidence": "Compares current camera 0x004576dc/de with previous 0x00595ade/e0, then compares visible layer0 cells.",
            "meaning": "Camera scrolling also marks dirty cells when the visible layer0 tile differs.",
        },
        {
            "step": "render dirty rows from live layer0",
            "vaHex": "0x00425163",
            "evidence": "Row renderer reads live layer0 grid 0x00595af0 and combines it with the active surface slot.",
            "meaning": "The renderer draws whatever tile id is currently in the live layer0 grid.",
        },
        {
            "step": "snapshot camera after redraw",
            "vaHex": "0x004250c4",
            "evidence": "After dirty row rendering, current camera is copied into 0x00595ade/e0.",
            "meaning": "The pass is a viewport redraw/cache maintenance step.",
        },
        {
            "step": "object/occlusion dirty projection",
            "vaHex": "0x00424a2a",
            "evidence": "Active object positions are projected relative to the camera and coverage is written into dirty/occlusion buffers.",
            "meaning": "This explains foreground/actor redraw interaction, not map animation frame generation.",
        },
    ]

    surfaces = [
        {
            "surface": "layer1 0x40 viewport consumer",
            "promotion": "confirmed-redraw-consumer",
            "finding": "0x00425403 scans 37x21 visible layer1 cells and marks 0x005957d0 dirty bit 0x01 when layer1&0x40 is set.",
            "gap": "No alternate tile id, palette frame, or shift offset is selected in this path.",
        },
        {
            "surface": "live layer0 row draw",
            "promotion": "confirmed-render-consumer",
            "finding": "0x00425163 draws dirty rows by reading live layer0 0x00595af0.",
            "gap": "If visible motion uses changing tile ids, the producer must mutate live layer0 before this renderer.",
        },
        {
            "surface": "camera scroll invalidation",
            "promotion": "confirmed-redraw-cache",
            "finding": "0x00425403 also compares current/previous camera and marks dirty cells when visible layer0 differs.",
            "gap": "This is camera cache invalidation, not animation timing.",
        },
        {
            "surface": "object/foreground dirty projection",
            "promotion": "confirmed-occlusion-redraw",
            "finding": "0x00424a2a..0x00425024 projects active-object coverage into redraw buffers.",
            "gap": "This handles actor/foreground redraw and does not bind animated map cells to frame loops.",
        },
    ]

    return {
        "kind": "hwanse-map-animation-redraw-consumer-review",
        "status": "redraw-consumer-grounded-visible-motion-producer-unbound",
        "source": [
            "Hwanse2.exe",
            "tools/build_map_animation_redraw_consumer_review.py",
            "out/map_animation_tile_review.json",
            "out/map_animation_live_buffer_writer_review.json",
        ],
        "globals": {key: hx(value) for key, value in GLOBALS.items()},
        "summary": {
            "viewportTileWidth": 37,
            "viewportTileHeight": 21,
            "layer1AnimatedFlagBit": "0x40",
            "dirtyGridBitFor0x40": "0x01",
            "redrawPathGrounded": True,
            "layer1AnimatedFlagConsumerGrounded": True,
            "liveLayer0RendererGrounded": True,
            "alternateTileSelectionFound": False,
            "visibleMotionProducerFound": False,
            "decision": (
                "0x40 cells are definitely consumed by the viewport redraw path, but this consumer only invalidates/redraws. "
                "Visible motion still needs either a live layer0 mutation producer or a draw-time tile transform that is not proven here."
            ),
        },
        "landmarks": landmark_rows,
        "steps": steps,
        "surfaces": surfaces,
        "nextFrontier": [
            "Inspect draw-time tile conversion under/after 0x00425163 and 0x004175d3 for frame-counter-based tile transforms.",
            "Search for a timer/tick path that mutates 0x00595af0 before the redraw consumer runs.",
            "If static route remains blocked, runtime-watch 0x00595af0 on map1_01a/map1_02b during one visible animation loop.",
        ],
    }


def render_html(report: dict[str, Any]) -> str:
    summary = report["summary"]
    cards = [
        ("status", report["status"]),
        ("viewport", f"{summary['viewportTileWidth']}x{summary['viewportTileHeight']} tiles"),
        ("0x40 consumer", "grounded" if summary["layer1AnimatedFlagConsumerGrounded"] else "missing"),
        ("layer0 renderer", "grounded" if summary["liveLayer0RendererGrounded"] else "missing"),
        ("alternate tile", "found" if summary["alternateTileSelectionFound"] else "not found"),
        ("producer", "found" if summary["visibleMotionProducerFound"] else "unbound"),
    ]
    card_html = "".join(f"<div class='card'><b>{h(k)}</b><span>{h(v)}</span></div>" for k, v in cards)
    global_rows = "".join(
        f"<tr><td>{h(k)}</td><td><code>{h(v)}</code></td></tr>"
        for k, v in report["globals"].items()
    )
    step_rows = "".join(
        "<tr>"
        f"<td>{h(row['step'])}<br><code>{h(row['vaHex'])}</code></td>"
        f"<td>{h(row['evidence'])}</td>"
        f"<td>{h(row['meaning'])}</td>"
        "</tr>"
        for row in report["steps"]
    )
    surface_rows = "".join(
        "<tr>"
        f"<td>{h(row['surface'])}</td>"
        f"<td>{h(row['promotion'])}</td>"
        f"<td>{h(row['finding'])}</td>"
        f"<td>{h(row['gap'])}</td>"
        "</tr>"
        for row in report["surfaces"]
    )
    landmark_rows = "".join(
        "<tr>"
        f"<td><b>{h(row['name'])}</b><br><code>{h(row['vaHex'])}..{h(row['endVaHex'])}</code></td>"
        f"<td>{h(row['role'])}</td>"
        f"<td><code>{h(row['byteSnippet'])}</code></td>"
        "</tr>"
        for row in report["landmarks"]
    )
    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 Redraw Consumer Review</title>
  <style>
    body {{ margin:0; background:#101318; color:#edf1f7; font-family:system-ui,sans-serif; }}
    main {{ max-width:1240px; 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(170px,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:920px; 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; }}
    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">live writers</a>
    <a class="chip" href="map_animation_execution_boundary_review.html">object-relative writes</a>
  </div>
  <h1>Map Animation Redraw Consumer Review</h1>
  <p>{h(summary["decision"])}</p>
  <div class="summary">{card_html}</div>
  <section>
    <h2>Globals</h2>
    <table><thead><tr><th>name</th><th>VA</th></tr></thead><tbody>{global_rows}</tbody></table>
  </section>
  <section>
    <h2>Execution Steps</h2>
    <table><thead><tr><th>step</th><th>evidence</th><th>meaning</th></tr></thead><tbody>{step_rows}</tbody></table>
  </section>
  <section>
    <h2>Surfaces</h2>
    <table><thead><tr><th>surface</th><th>promotion</th><th>finding</th><th>gap</th></tr></thead><tbody>{surface_rows}</tbody></table>
  </section>
  <section>
    <h2>Landmarks</h2>
    <table><thead><tr><th>range</th><th>role</th><th>byte snippet</th></tr></thead><tbody>{landmark_rows}</tbody></table>
  </section>
  <section>
    <h2>Next Frontier</h2>
    <ul>{frontier}</ul>
  </section>
  <section>
    <h2>Raw JSON</h2>
    <pre id="json"></pre>
  </section>
</main>
<script>
window.HWANSE_MAP_ANIMATION_REDRAW_CONSUMER_REVIEW = {payload};
document.getElementById('json').textContent = JSON.stringify(window.HWANSE_MAP_ANIMATION_REDRAW_CONSUMER_REVIEW, null, 2);
</script>
</body>
</html>
"""


def main() -> int:
    OUT.mkdir(exist_ok=True)
    WEB.mkdir(exist_ok=True)
    report = build_report()
    write_json(OUT / "map_animation_redraw_consumer_review.json", report)
    html_text = render_html(report)
    print("map_animation_redraw_consumer_review ok")
    print(json.dumps(report["summary"], ensure_ascii=False, indent=2))
    return 0


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