#!/usr/bin/env python3
"""Expand save scene selector resource references with pointer paths."""
from __future__ import annotations

import argparse
import html
import json
import re
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))

from parse_savedata import dword_window
from probe_exe_scene_tables import find_cns_strings, read_sections


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
FIELD_MAP_RE = re.compile(r"map\d+_\d+[a-z]\.cns$")


def hex_va(value: int) -> str:
    return f"0x{value:08x}"


def selector_label(row: dict) -> str:
    return f"{row['group']}:{row['slot']}"


def manifest_index(records: list[dict] | None) -> dict[tuple[int, str], dict]:
    if not records:
        return {}
    return {
        (record["recordVa"], record["map"]): record
        for record in records
        if isinstance(record.get("recordVa"), int) and isinstance(record.get("map"), str)
    }


def expand_selector_references(
    exe_data: bytes,
    selectors: list[dict],
    *,
    manifest: list[dict] | None = None,
    max_depth: int = 2,
    count: int = 64,
    max_nodes: int = 24,
) -> list[dict]:
    sections = read_sections(exe_data)
    strings = find_cns_strings(exe_data, sections)
    scenes_by_ref = manifest_index(manifest)
    rows = []

    for selector in sorted(selectors, key=lambda row: (row["group"], row["slot"])):
        root = selector.get("selectedPointer")
        if not isinstance(root, int):
            continue
        seen: set[int] = set()

        def visit(va: int, depth: int, path: list[int]) -> None:
            if len(seen) >= max_nodes or va in seen:
                return
            seen.add(va)
            for item in dword_window(exe_data, sections, strings, va, count=count):
                name = item.get("string")
                if isinstance(name, str) and name.endswith(".cns"):
                    resource = name[:-4]
                    scene = scenes_by_ref.get((item["va"], resource))
                    rows.append(
                        {
                            "group": selector["group"],
                            "slot": selector["slot"],
                            "label": selector_label(selector),
                            "selectedPointer": root,
                            "selectedPointerHex": hex_va(root),
                            "path": path,
                            "pathHex": [hex_va(value) for value in path],
                            "depth": len(path) - 1,
                            "refVa": item["va"],
                            "refVaHex": item["vaHex"],
                            "resourcePointer": item["value"],
                            "resourcePointerHex": item["valueHex"],
                            "filename": name,
                            "resource": resource,
                            "kind": "fieldMap" if FIELD_MAP_RE.fullmatch(name) else "cns",
                            "sceneMatched": bool(scene),
                            "sceneIdHex": scene.get("sceneIdHex") if scene else None,
                            "sceneId": scene.get("sceneId") if scene else None,
                            "tilesets": scene.get("tilesets") if scene else [],
                        }
                    )
                value = item.get("value")
                if (
                    depth > 0
                    and item.get("pointer")
                    and isinstance(value, int)
                    and 0x00400000 <= value <= 0x00600000
                ):
                    visit(value, depth - 1, [*path, value])

        visit(root, max_depth, [root])
    return rows


def grouped_field_maps(rows: list[dict]) -> dict[str, list[str]]:
    grouped: dict[str, list[str]] = {}
    for row in rows:
        if row.get("kind") != "fieldMap":
            continue
        grouped.setdefault(row["label"], [])
        if row["resource"] not in grouped[row["label"]]:
            grouped[row["label"]].append(row["resource"])
    return grouped


def markdown(rows: list[dict]) -> str:
    field_rows = [row for row in rows if row.get("kind") == "fieldMap"]
    lines = [
        "# Save Scene Selector References",
        "",
        "Generated by expanding each save selector's selected pointer through pointer dwords.",
        "",
        f"Resource refs: {len(rows)}.",
        f"Field map refs: {len(field_rows)}.",
        "",
        "## Field Map Summary",
        "",
        "| selector | field maps |",
        "| --- | --- |",
    ]
    for label, maps in grouped_field_maps(rows).items():
        links = ", ".join(f"[{name}](../web/game.html?map={name})" for name in maps)
        lines.append(f"| `{label}` | {links or '-'} |")
    lines.extend(
        [
            "",
            "## Field Map Reference Rows",
            "",
        "| selector | resource | scene | tilesets | ref VA | depth | pointer path | open |",
        "| --- | --- | --- | --- | --- | ---: | --- | --- |",
        ]
    )
    for row in field_rows:
        lines.append(
            "| {label} | {resource} | {scene} | {tilesets} | `{ref}` | {depth} | {path} | [open](../web/game.html?map={resource}) |".format(
                label=f"`{row['label']}`",
                resource=row["resource"],
                scene=row.get("sceneIdHex") or "-",
                tilesets=", ".join(row.get("tilesets") or []) or "-",
                ref=row["refVaHex"],
                depth=row["depth"],
                path=" -> ".join(f"`{value}`" for value in row["pathHex"]),
            )
        )
    lines.append("")
    return "\n".join(lines)


def html_page(rows: list[dict]) -> str:
    field_rows = [row for row in rows if row.get("kind") == "fieldMap"]
    body = []
    for row in field_rows:
        path = " -> ".join(row["pathHex"])
        body.append(
            "\n".join(
                [
                    "<tr>",
                    f"  <td><code>{html.escape(row['label'])}</code></td>",
                    f'  <td><a href="../web/game.html?map={html.escape(row["resource"])}">{html.escape(row["resource"])}</a></td>',
                    f"  <td>{html.escape(row.get('sceneIdHex') or '-')}</td>",
                    f"  <td>{html.escape(', '.join(row.get('tilesets') or []) or '-')}</td>",
                    f"  <td><code>{html.escape(row['refVaHex'])}</code></td>",
                    f"  <td>{row['depth']}</td>",
                    f"  <td><code>{html.escape(path)}</code></td>",
                    "</tr>",
                ]
            )
        )
    return "\n".join(
        [
            "<!doctype html>",
            '<html lang="en">',
            "<head>",
            '  <meta charset="utf-8">',
            '  <meta name="viewport" content="width=device-width, initial-scale=1">',
            "  <title>Save Scene Selector References</title>",
            "  <style>",
            "    body { margin: 24px; background: #101010; color: #eee; font: 14px system-ui, sans-serif; }",
            "    table { border-collapse: collapse; width: 100%; }",
            "    th, td { border: 1px solid #333; padding: 6px 8px; vertical-align: top; }",
            "    th { background: #1d1d1d; position: sticky; top: 0; }",
            "    a { color: #9bd4ff; }",
            "    code { color: #ffd447; }",
            "  </style>",
            "</head>",
            "<body>",
            "  <h1>Save Scene Selector References</h1>",
            "  <p>Expanded selected-pointer resource refs. Pointer paths identify where each field map is attached.</p>",
            f"  <p>Resource refs: {len(rows)}. Field map refs: {len(field_rows)}.</p>",
            "  <table>",
            "    <thead><tr><th>selector</th><th>resource</th><th>scene</th><th>tilesets</th><th>ref VA</th><th>depth</th><th>pointer path</th></tr></thead>",
            "    <tbody>",
            *body,
            "    </tbody>",
            "  </table>",
            "</body>",
            "</html>",
            "",
        ]
    )


def write_outputs(rows: list[dict], out_dir: Path = OUT) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "save_scene_selector_references.json").write_text(
        json.dumps(rows, ensure_ascii=False, separators=(",", ":")) + "\n",
        encoding="utf-8",
    )


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--exe", type=Path, default=Path("Hwanse2.exe"))
    parser.add_argument("--selectors", type=Path, default=OUT / "save_scene_selectors.json")
    parser.add_argument("--manifest", type=Path, default=OUT / "scene_manifest.json")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    args = parser.parse_args()
    manifest = json.loads(args.manifest.read_text(encoding="utf-8")) if args.manifest.exists() else None
    rows = expand_selector_references(
        args.exe.read_bytes(),
        json.loads(args.selectors.read_text(encoding="utf-8")),
        manifest=manifest,
    )
    write_outputs(rows, args.out_dir)
    print(f"wrote {len(rows)} save scene selector refs -> {args.out_dir / 'save_scene_selector_references.json'}")


if __name__ == "__main__":
    main()
