#!/usr/bin/env python3
"""Probe whether animated maps bind to palette/tile-write command roots.

This is intentionally narrower than the broader map animation reviews.  It
asks one concrete question:

    Do the selector/resource roots that load animated maps also contain the
    candidate palette or tile-write commands that would drive visible motion?

The current answer is no.  That means map resource loading is grounded, but the
per-frame animation loop is still somewhere else or encoded differently.
"""
from __future__ import annotations

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


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


def load_json(path: Path, fallback: Any) -> Any:
    if not path.exists():
        return fallback
    return json.loads(path.read_text(encoding="utf-8"))


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


def int_va(value: Any) -> int | None:
    if isinstance(value, int):
        return value
    if isinstance(value, str) and value.startswith("0x"):
        try:
            return int(value, 16)
        except ValueError:
            return None
    return None


def root_contains(root: dict[str, Any], va: int) -> bool:
    start = int_va(root.get("rootVa")) or int_va(root.get("rootVaHex"))
    end = int_va(root.get("rangeEndVa")) or int_va(root.get("rangeEndVaHex"))
    return start is not None and end is not None and start <= va < end


def root_label(root: dict[str, Any]) -> str:
    keys = ",".join(root.get("selectorKeys") or [])
    return f"{root.get('rootVaHex', '')} {keys}".strip()


def command_va(row: dict[str, Any]) -> int | None:
    return int_va(row.get("va") or row.get("vaHex"))


def palette_near_map(command: dict[str, Any], map_name: str) -> bool:
    binding = command.get("resourceBinding") or {}
    for record in binding.get("nearestAnimatedSceneRecords") or []:
        if record.get("map") == map_name:
            return True
    return False


def palette_near_status(command: dict[str, Any]) -> str:
    binding = command.get("resourceBinding") or {}
    return binding.get("status", "")


def build() -> dict[str, Any]:
    animation = load_json(OUT / "map_animation_tile_review.json", {})
    roots = load_json(OUT / "selector_root_structure_review.json", {}).get("roots") or []
    exe = load_json(OUT / "map_animation_exe_pattern_review.json", {})

    palette_commands = ((animation.get("paletteCommandScan") or {}).get("commands") or [])
    tile_write_scan = exe.get("tileWriteCommandScan") or {}
    tile_write_rows: list[dict[str, Any]] = []
    for key in [
        "directAnimatedTileWrites",
        "coordinateOnlyAlignedHits",
        "rawAnimatedTileFalsePositiveSamples",
        "asciiFalsePositiveSamples",
    ]:
        for row in tile_write_scan.get(key) or []:
            item = dict(row)
            item["_bucket"] = key
            tile_write_rows.append(item)

    rows: list[dict[str, Any]] = []
    for animated in animation.get("maps") or []:
        map_name = animated.get("map", "")
        tileset = animated.get("sourceTileset", "")
        exact_roots = []
        tileset_only_roots = []
        for root in roots:
            field_maps = root.get("fieldMaps") or []
            linked_cns = root.get("linkedCns") or []
            if map_name in field_maps or f"{map_name}.cns" in linked_cns:
                exact_roots.append(root)
            elif f"{tileset}.cns" in linked_cns:
                tileset_only_roots.append(root)

        palette_inside = []
        palette_near = []
        for command in palette_commands:
            va = command_va(command)
            if va is None:
                continue
            inside_roots = [root for root in exact_roots if root_contains(root, va)]
            if inside_roots:
                palette_inside.append(
                    {
                        "vaHex": command.get("va"),
                        "opcode": command.get("opcode"),
                        "kind": command.get("kind"),
                        "effectScope": (command.get("effectScope") or {}).get("class", ""),
                        "insideRoots": [root_label(root) for root in inside_roots],
                    }
                )
            if palette_near_map(command, map_name):
                palette_near.append(
                    {
                        "vaHex": command.get("va"),
                        "opcode": command.get("opcode"),
                        "kind": command.get("kind"),
                        "effectScope": (command.get("effectScope") or {}).get("class", ""),
                        "bindingStatus": palette_near_status(command),
                    }
                )

        tile_inside = []
        tile_near_nonanimated = []
        for command in tile_write_rows:
            va = command_va(command)
            if va is None:
                continue
            inside_roots = [root for root in exact_roots if root_contains(root, va)]
            if inside_roots:
                tile_inside.append(
                    {
                        "vaHex": command.get("va") or command.get("vaHex"),
                        "bucket": command.get("_bucket"),
                        "tile": command.get("tile"),
                        "x": command.get("x"),
                        "y": command.get("y"),
                        "insideRoots": [root_label(root) for root in inside_roots],
                    }
                )
            binding = command.get("resourceBinding") or {}
            if binding.get("status") == "resource-group-contained-nonanimated":
                groups = binding.get("containingGroups") or []
                for group in groups:
                    if map_name in (group.get("animatedMaps") or []):
                        tile_near_nonanimated.append(
                            {
                                "vaHex": command.get("va") or command.get("vaHex"),
                                "bucket": command.get("_bucket"),
                                "group": group.get("id"),
                                "rootVaHex": group.get("rootVaHex"),
                            }
                        )

        rows.append(
            {
                "map": map_name,
                "sourceTileset": tileset,
                "animatedCellCount": animated.get("animatedCellCount"),
                "componentCount": animated.get("componentCount"),
                "exactResourceRootCount": len(exact_roots),
                "exactResourceRoots": [
                    {
                        "rootVaHex": root.get("rootVaHex"),
                        "rangeEndVaHex": root.get("rangeEndVaHex"),
                        "selectorKeys": root.get("selectorKeys") or [],
                        "rootClass": root.get("rootClass"),
                        "fieldMaps": root.get("fieldMaps") or [],
                        "linkedCns": (root.get("linkedCns") or [])[:16],
                    }
                    for root in exact_roots
                ],
                "tilesetOnlyRootCount": len(tileset_only_roots),
                "tilesetOnlyRootSample": [
                    {
                        "rootVaHex": root.get("rootVaHex"),
                        "selectorKeys": root.get("selectorKeys") or [],
                        "fieldMaps": root.get("fieldMaps") or [],
                    }
                    for root in tileset_only_roots[:6]
                ],
                "paletteCommandsInsideExactResourceRoots": palette_inside,
                "paletteCommandsNearAnimatedSceneRecord": palette_near,
                "tileWriteCommandsInsideExactResourceRoots": tile_inside,
                "tileWriteNonanimatedContainmentHits": tile_near_nonanimated,
                "bindingAssessment": (
                    "resource-root-unbound"
                    if not palette_inside and not tile_inside
                    else "candidate-command-inside-resource-root"
                ),
            }
        )

    summary = {
        "animatedMapCount": len(rows),
        "mapsWithExactResourceRoot": sum(1 for row in rows if row["exactResourceRootCount"] > 0),
        "mapsWithoutExactResourceRoot": sum(1 for row in rows if row["exactResourceRootCount"] == 0),
        "paletteCommandInsideExactResourceRootCount": sum(
            len(row["paletteCommandsInsideExactResourceRoots"]) for row in rows
        ),
        "tileWriteCommandInsideExactResourceRootCount": sum(
            len(row["tileWriteCommandsInsideExactResourceRoots"]) for row in rows
        ),
        "paletteNearAnimatedSceneRecordCount": sum(
            len(row["paletteCommandsNearAnimatedSceneRecord"]) for row in rows
        ),
        "allExactResourceRootsUnbound": all(
            not row["paletteCommandsInsideExactResourceRoots"]
            and not row["tileWriteCommandsInsideExactResourceRoots"]
            for row in rows
        ),
        "decision": (
            "animated map resource roots were found for most animated maps, but none of those exact root ranges "
            "contain candidate palette/tile-write animation commands.  The visible animation loop is therefore "
            "not the same data root as the map resource package."
        ),
    }
    return {
        "kind": "hwanse-map-animation-resource-binding-probe",
        "status": "animated-resource-roots-found-animation-commands-unbound",
        "source": [
            "out/map_animation_tile_review.json",
            "out/map_animation_exe_pattern_review.json",
            "out/selector_root_structure_review.json",
            "tools/build_map_animation_resource_binding_probe.py",
        ],
        "summary": summary,
        "rows": rows,
        "nextFrontier": [
            "Do not treat map resource opcode 0x10 roots as animation-loop roots.",
            "Search for frame/tick consumers that touch the live map buffers after resource opcode 0x10 completes.",
            "If static search remains blocked, narrow runtime tracing to map1_01a/map1_02b after load and watch tile/palette writes only.",
        ],
    }


def render_html(report: dict[str, Any]) -> str:
    summary = report["summary"]
    cards = "".join(
        f"<div class='card'><b>{h(key)}</b><span>{h(value)}</span></div>"
        for key, value in summary.items()
        if key != "decision"
    )
    rows = []
    for row in report["rows"]:
        roots = "<br>".join(
            f"<code>{h(root['rootVaHex'])}</code> {h(','.join(root.get('selectorKeys') or []))}"
            for root in row["exactResourceRoots"]
        ) or "<span class='muted'>none</span>"
        near = "<br>".join(
            f"<code>{h(cmd['vaHex'])}</code> {h(cmd['opcode'])} {h(cmd['kind'])} / {h(cmd['bindingStatus'])}"
            for cmd in row["paletteCommandsNearAnimatedSceneRecord"]
        ) or "<span class='muted'>none</span>"
        inside = []
        for cmd in row["paletteCommandsInsideExactResourceRoots"]:
            inside.append(f"palette <code>{h(cmd['vaHex'])}</code>")
        for cmd in row["tileWriteCommandsInsideExactResourceRoots"]:
            inside.append(f"tile <code>{h(cmd['vaHex'])}</code>")
        inside_html = "<br>".join(inside) or "<span class='muted'>none</span>"
        rows.append(
            "<tr>"
            f"<td><a href='map_review.html?map={h(row['map'])}&visualFilter=animation'><code>{h(row['map'])}</code></a><br><span class='muted'>{h(row['sourceTileset'])}</span></td>"
            f"<td>{h(row['animatedCellCount'])}</td>"
            f"<td>{roots}</td>"
            f"<td>{inside_html}</td>"
            f"<td>{near}</td>"
            f"<td>{h(row['bindingAssessment'])}</td>"
            "</tr>"
        )
    raw = json.dumps(report, ensure_ascii=False)
    return f"""<!doctype html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Map Animation Resource Binding Probe</title>
<style>
  body {{ margin:0; background:#f5f6f8; color:#222832; font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; }}
  main {{ width:min(1280px, calc(100vw - 24px)); margin:0 auto; padding:22px 0 36px; }}
  h1 {{ margin:0 0 8px; font-size:28px; }}
  .muted {{ color:#667085; }}
  .nav {{ display:flex; flex-wrap:wrap; gap:8px; margin-bottom:14px; }}
  .chip {{ border:1px solid #d0d7e2; border-radius:7px; padding:6px 10px; background:#fff; color:#1d2939; text-decoration:none; }}
  .cards {{ display:grid; grid-template-columns:repeat(auto-fit,minmax(180px,1fr)); gap:10px; margin:16px 0; }}
  .card {{ border:1px solid #d0d7e2; border-radius:8px; background:#fff; padding:12px; }}
  .card b {{ display:block; color:#667085; font-size:12px; }}
  .card span {{ display:block; margin-top:6px; font-weight:700; overflow-wrap:anywhere; }}
  section {{ margin-top:16px; background:#fff; border:1px solid #d0d7e2; border-radius:8px; overflow:auto; }}
  table {{ border-collapse:collapse; width:100%; min-width:1060px; font-size:13px; }}
  th,td {{ border-bottom:1px solid #e4e7ec; padding:8px; text-align:left; vertical-align:top; }}
  th {{ background:#f0f3f8; }}
  code {{ color:#164a8b; }}
  pre {{ max-height:360px; overflow:auto; padding:12px; background:#101828; color:#e4e7ec; }}
</style>
<main>
  <div class="nav">
    <a class="chip" href="index.html">index</a>
    <a class="chip" href="map_animation_execution_boundary_review.html">execution boundary</a>
    <a class="chip" href="map_animation_execution_boundary_review.html">EXE pattern</a>
    <a class="chip" href="../out/map_animation_tile_review.html">tile review</a>
  </div>
  <h1>Map Animation Resource Binding Probe</h1>
  <p class="muted">{h(summary['decision'])}</p>
  <div class="cards">{cards}</div>
  <section>
    <table>
      <thead><tr><th>map</th><th>cells</th><th>exact resource roots</th><th>commands inside exact roots</th><th>palette near scene</th><th>assessment</th></tr></thead>
      <tbody>{''.join(rows)}</tbody>
    </table>
  </section>
  <section><pre id="raw"></pre></section>
</main>
<script>
const DATA = {raw};
document.getElementById('raw').textContent = JSON.stringify(DATA, null, 2);
</script>
"""


def main() -> None:
    OUT.mkdir(exist_ok=True)
    WEB.mkdir(exist_ok=True)
    report = build()
    (OUT / "map_animation_resource_binding_probe.json").write_text(
        json.dumps(report, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    html_text = render_html(report)
    print("map_animation_resource_binding_probe ok")


if __name__ == "__main__":
    main()
