#!/usr/bin/env python3
"""Review draw-time tile conversion for field map rendering.

This follows the row renderer after it reads the live layer0 tile id.  The goal
is to check whether visible map animation could be implemented by changing the
tile id at draw time rather than by mutating the live layer0 buffer.

Current finding: the inspected draw path converts a tile id to a source rect by
ordinary grid math (mod/div by surface columns).  No frame counter, 0x40 flag, or
per-map animation sequence is consumed in this conversion path.
"""
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": "dirty row renderer",
        "va": 0x00425163,
        "endVa": 0x00425279,
        "role": "Reads live layer0 tile id and calls the generic draw wrapper.",
    },
    {
        "name": "generic draw wrapper",
        "va": 0x004175D3,
        "endVa": 0x00417660,
        "role": "Copies draw arguments into a temporary object and dispatches draw.",
    },
    {
        "name": "draw object dispatcher",
        "va": 0x0041747B,
        "endVa": 0x004175B6,
        "role": "Extracts surface slot from the packed tile descriptor and calls the surface draw method.",
    },
    {
        "name": "tile id to source rect converter",
        "va": 0x004199A0,
        "endVa": 0x00419B80,
        "role": "Converts a tile id to x/y/w/h source rect by columns and tile dimensions.",
    },
]


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 = 72) -> 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)
    landmarks = [
        {
            **row,
            "vaHex": hx(row["va"]),
            "endVaHex": hx(row["endVa"]),
            "byteSnippet": byte_snippet(exe, sections, int(row["va"])),
        }
        for row in LANDMARKS
    ]

    steps = [
        {
            "step": "pack tile id with surface slot",
            "vaHex": "0x004251e5",
            "evidence": "Row renderer reads WORD [index*2+0x00595af0], ORs it with WORD 0x0055b580 << 16, then passes that packed descriptor to 0x004175d3.",
            "meaning": "The live layer0 tile id is carried forward unchanged in the low word.",
        },
        {
            "step": "store draw descriptor into temporary object",
            "vaHex": "0x004175dc",
            "evidence": "0x004175d3 stores the packed descriptor at temporary object offset +0x28 and x/y at +0x1c/+0x20.",
            "meaning": "The wrapper prepares a generic draw object; it does not inspect layer1 0x40 or a frame counter.",
        },
        {
            "step": "select surface by high word",
            "vaHex": "0x004174bb",
            "evidence": "0x0041747b shifts object +0x28 by 16, masks to 0xff, and indexes surface table 0x0055abd8.",
            "meaning": "The high word selects the loaded CNS/surface slot.",
        },
        {
            "step": "convert tile id to source rect",
            "vaHex": "0x004199a9",
            "evidence": "0x004199a0 masks the descriptor to low 16 bits, divides by surface +0x32 columns, and multiplies by +0x2e/+0x30 tile width/height.",
            "meaning": "This is ordinary grid rect math: x=(tile%columns)*w, y=(tile/columns)*h.",
        },
        {
            "step": "no animation selector observed",
            "vaHex": "0x004199a0",
            "evidence": "The inspected converter uses tile id, columns, tile width/height, clipping/scaling flags, and draw object flags only.",
            "meaning": "No layer1 0x40, animation-map identity, frame counter, or alternate-tile sequence is consumed here.",
        },
    ]

    return {
        "kind": "hwanse-map-animation-draw-tile-transform-review",
        "status": "draw-time-tile-transform-negative-for-visible-animation",
        "source": [
            "Hwanse2.exe",
            "tools/build_map_animation_draw_tile_transform_review.py",
            "out/map_animation_redraw_consumer_review.json",
        ],
        "summary": {
            "rowRendererGrounded": True,
            "drawWrapperGrounded": True,
            "tileIdToSourceRectConverterGrounded": True,
            "tileGridFormula": "sourceX=(tileId % columns) * tileWidth; sourceY=(tileId / columns) * tileHeight",
            "usesLayer1AnimatedFlag0x40": False,
            "usesFrameCounter": False,
            "usesPerMapAnimationSequence": False,
            "drawTimeVisibleMotionTransformFound": False,
            "decision": (
                "The inspected draw path maps the current live layer0 tile id to a source rect by fixed grid math. "
                "It does not provide the missing fire/waterfall frame loop."
            ),
        },
        "landmarks": landmarks,
        "steps": steps,
        "nextFrontier": [
            "Search for a producer/tick path that mutates live layer0 0x00595af0 before redraw.",
            "Search for another specialized map-animation draw callback outside the generic tile converter.",
            "If static scans stay negative, runtime-watch live layer0 values during map1_01a/map1_02b animation.",
        ],
    }


def render_html(report: dict[str, Any]) -> str:
    summary = report["summary"]
    cards = [
        ("status", report["status"]),
        ("row renderer", summary["rowRendererGrounded"]),
        ("draw wrapper", summary["drawWrapperGrounded"]),
        ("rect converter", summary["tileIdToSourceRectConverterGrounded"]),
        ("uses 0x40", summary["usesLayer1AnimatedFlag0x40"]),
        ("uses frame counter", summary["usesFrameCounter"]),
        ("draw-time motion", summary["drawTimeVisibleMotionTransformFound"]),
    ]
    card_html = "".join(f"<div class='card'><b>{h(k)}</b><span>{h(v)}</span></div>" for k, v in cards)
    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"]
    )
    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 Draw Tile Transform 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_animation_execution_boundary_review.html">animation boundary</a>
    <a class="chip" href="map_animation_execution_boundary_review.html">redraw consumer</a>
    <a class="chip" href="map_animation_execution_boundary_review.html">live writers</a>
  </div>
  <h1>Map Animation Draw Tile Transform Review</h1>
  <p>{h(summary["decision"])}</p>
  <div class="summary">{card_html}</div>
  <section>
    <h2>Formula</h2>
    <p><code>{h(summary["tileGridFormula"])}</code></p>
  </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>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_DRAW_TILE_TRANSFORM_REVIEW = {payload};
document.getElementById('json').textContent = JSON.stringify(window.HWANSE_MAP_ANIMATION_DRAW_TILE_TRANSFORM_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_draw_tile_transform_review.json", report)
    html_text = render_html(report)
    print("map_animation_draw_tile_transform_review ok")
    print(json.dumps(report["summary"], ensure_ascii=False, indent=2))
    return 0


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