#!/usr/bin/env python3
"""Extract savedat scene selector table entries from Hwanse2.exe."""
from __future__ import annotations

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

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

from parse_savedata import SCENE_SELECTOR_TABLE_VA, collect_linked_cns, dword_window
from probe_exe_scene_tables import find_cns_strings, read_sections, va_to_offset


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


def read_u32(data: bytes, sections: list[dict], va: int) -> int | None:
    offset = va_to_offset(sections, va)
    if offset is None or offset + 4 > len(data):
        return None
    return struct.unpack_from("<I", data, offset)[0]


def extract_selectors(
    data: bytes,
    *,
    table_va: int = SCENE_SELECTOR_TABLE_VA,
    max_groups: int = 64,
    max_slots: int = 32,
) -> list[dict]:
    sections = read_sections(data)
    strings = find_cns_strings(data, sections)
    rows = []
    for group in range(max_groups):
        row_pointer_va = table_va + group * 4
        row_pointer = read_u32(data, sections, row_pointer_va)
        if not isinstance(row_pointer, int) or not (0x00400000 <= row_pointer <= 0x00600000):
            continue
        if va_to_offset(sections, row_pointer) is None:
            continue
        for slot in range(max_slots):
            selected_pointer_va = row_pointer + slot * 4
            selected_pointer = read_u32(data, sections, selected_pointer_va)
            if not isinstance(selected_pointer, int) or not (0x00400000 <= selected_pointer <= 0x00600000):
                continue
            if va_to_offset(sections, selected_pointer) is None:
                continue
            linked = collect_linked_cns(data, sections, strings, selected_pointer)
            if not linked.get("linkedCns"):
                continue
            first_dwords = dword_window(data, sections, strings, selected_pointer, count=8)
            rows.append(
                {
                    "group": group,
                    "slot": slot,
                    "selectorBytes": {"groupOffset": 2, "slotOffset": 3},
                    "rowPointerVa": row_pointer_va,
                    "rowPointerVaHex": f"0x{row_pointer_va:08x}",
                    "rowPointer": row_pointer,
                    "rowPointerHex": f"0x{row_pointer:08x}",
                    "selectedPointerVa": selected_pointer_va,
                    "selectedPointerVaHex": f"0x{selected_pointer_va:08x}",
                    "selectedPointer": selected_pointer,
                    "selectedPointerHex": f"0x{selected_pointer:08x}",
                    "fieldMaps": linked.get("fieldMaps", []),
                    "linkedCns": linked.get("linkedCns", []),
                    "linkedScanDepth": linked.get("scanDepth"),
                    "linkedScannedPointers": linked.get("scannedPointers"),
                    "firstDwords": first_dwords,
                }
            )
    return rows


def markdown(rows: list[dict]) -> str:
    lines = [
        "# Save Scene Selectors",
        "",
        "Generated from the original loader's scene selector table.",
        "",
        "Save bytes `0x0002` and `0x0003` select `group` and `slot`. The loader uses table `0x00442d35` and stores the selected pointer in `0x0059de30`.",
        "",
        f"Rows: {len(rows)}.",
        "",
        "| group | slot | selected pointer | field maps | linked CNS |",
        "| ---: | ---: | --- | --- | --- |",
    ]
    for row in rows:
        lines.append(
            f"| {row['group']} | {row['slot']} | `{row['selectedPointerHex']}` | "
            f"{', '.join(row['fieldMaps']) or '-'} | {', '.join(row['linkedCns'][:10]) or '-'} |"
        )
    lines.append("")
    return "\n".join(lines)


def selector_html(rows: list[dict]) -> str:
    lines = [
        "<!doctype html>",
        '<html lang="en">',
        "<head>",
        '  <meta charset="utf-8">',
        '  <meta name="viewport" content="width=device-width, initial-scale=1">',
        "  <title>Save Scene Selectors</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; }",
        "    .links { display: flex; flex-wrap: wrap; gap: 6px 10px; }",
        "  </style>",
        "</head>",
        "<body>",
        "  <h1>Save Scene Selectors</h1>",
        "  <p>Save bytes <code>0x0002</code>/<code>0x0003</code> select group/slot. Links open the web runtime without loading all maps at startup.</p>",
        f"  <p>Rows: {len(rows)}.</p>",
        "  <table>",
        "    <thead><tr><th>group</th><th>slot</th><th>open selector</th><th>field maps</th><th>selected pointer</th><th>linked CNS</th></tr></thead>",
        "    <tbody>",
    ]
    for row in rows:
        selector_href = f"../web/game.html?saveGroup={row['group']}&saveSlot={row['slot']}"
        field_links = []
        for name in row.get("fieldMaps") or []:
            field_links.append(f'<a href="../web/game.html?map={html.escape(name)}">{html.escape(name)}</a>')
        cns = ", ".join(html.escape(name) for name in (row.get("linkedCns") or [])[:12]) or "-"
        lines.extend(
            [
                "      <tr>",
                f"        <td>{row['group']}</td>",
                f"        <td>{row['slot']}</td>",
                f'        <td><a href="{html.escape(selector_href)}">saveGroup={row["group"]}&amp;saveSlot={row["slot"]}</a></td>',
                f'        <td><div class="links">{"".join(field_links) or "-"}</div></td>',
                f"        <td><code>{html.escape(row['selectedPointerHex'])}</code></td>",
                f"        <td>{cns}</td>",
                "      </tr>",
            ]
        )
    lines.extend(["    </tbody>", "  </table>", "</body>", "</html>", ""])
    return "\n".join(lines)


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--exe", type=Path, default=Path("Hwanse2.exe"))
    parser.add_argument("--json-out", type=Path, default=OUT / "save_scene_selectors.json")
    args = parser.parse_args()

    rows = extract_selectors(args.exe.read_bytes())
    args.json_out.parent.mkdir(parents=True, exist_ok=True)
    args.json_out.write_text(json.dumps(rows, ensure_ascii=False, separators=(",", ":")) + "\n", encoding="utf-8")
    print(f"wrote {len(rows)} save scene selectors -> {args.json_out}")


if __name__ == "__main__":
    main()
