#!/usr/bin/env python3
"""Scan scene records for pointer fields that look like coordinate tables.

This is intentionally broader than extract_scene_events.py. Many hits are
script/object argument tables rather than confirmed map triggers, but the output
is useful when comparing repeated scene records and looking for true transitions.
"""
from __future__ import annotations

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

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

from extract_scene_events import load_map_sizes, read_point_table
from probe_exe_scene_tables import find_cns_strings, offset_to_va, read_sections


MAP_RE = re.compile(r"map\d+_\d+[a-z]\.cns$")


def dword_at(data: bytes, offset: int) -> int:
    return struct.unpack_from("<I", data, offset)[0]


def looks_like_scene_id(value: int) -> bool:
    return 0 < value < 0x100000 and (value & 0xFF) in {0x18, 0x00}


def next_map_field_index(data: bytes, strings: dict[int, str], hit: int, max_dwords: int) -> int:
    for index in range(3, max_dwords):
        off = hit + index * 4
        if off + 4 > len(data):
            break
        name = strings.get(dword_at(data, off))
        if name and MAP_RE.fullmatch(name):
            return index
    return max_dwords


def extract_candidates(data: bytes, extract_dir: Path, max_dwords: int = 40) -> list[dict]:
    sections = read_sections(data)
    strings = find_cns_strings(data, sections)
    map_sizes = load_map_sizes(extract_dir)
    records = []

    for map_va, filename in strings.items():
        if not MAP_RE.fullmatch(filename):
            continue
        name = filename[:-4]
        size = map_sizes.get(name)
        if not size:
            continue

        needle = struct.pack("<I", map_va)
        search = 0
        while True:
            hit = data.find(needle, search)
            if hit < 0:
                break
            search = hit + 1
            if hit + 12 > len(data):
                continue

            scene_id = dword_at(data, hit + 4)
            zero = dword_at(data, hit + 8)
            if zero != 0 or not looks_like_scene_id(scene_id):
                continue

            scan_limit = next_map_field_index(data, strings, hit, max_dwords)
            candidates = []
            for index in range(3, scan_limit):
                off = hit + index * 4
                if off + 4 > len(data):
                    break
                ptr_va = dword_at(data, off)
                if not (0x00400000 <= ptr_va <= 0x00600000):
                    continue
                raw_points, points = read_point_table(data, sections, ptr_va, *size)
                if len(points) < 2:
                    continue
                candidates.append(
                    {
                        "fieldIndex": index,
                        "fieldVa": offset_to_va(sections, off),
                        "pointTableVa": ptr_va,
                        "rawPointCount": len(raw_points),
                        "pointCount": len(points),
                        "points": points,
                    }
                )

            if not candidates:
                continue
            record_va = offset_to_va(sections, hit)
            if record_va is None:
                continue
            records.append(
                {
                    "map": name,
                    "sceneId": scene_id,
                    "sceneIdHex": f"0x{scene_id:04x}",
                    "recordVa": record_va,
                    "recordFileOffset": hit,
                    "candidates": candidates,
                }
            )

    records.sort(key=lambda item: (item["recordVa"], item["map"]))
    return records


def markdown(records: list[dict]) -> str:
    lines = [
        "# Scene Coordinate Candidates",
        "",
        "Generated by scanning scene records for pointer fields that decode as in-bounds coordinate tables.",
        "",
        "These are broad candidates, not confirmed transition triggers. Many short tables are script/object arguments.",
        "",
        "| map | scene | record | fields | first points |",
        "| --- | --- | --- | --- | --- |",
    ]
    for item in records:
        fields = ", ".join(
            f"i{candidate['fieldIndex']} n={candidate['pointCount']}"
            for candidate in item["candidates"][:8]
        )
        points = []
        for candidate in item["candidates"][:3]:
            coords = " ".join(
                f"{point['x']},{point['y']}"
                for point in candidate["points"][:5]
            )
            points.append(f"i{candidate['fieldIndex']}: {coords}")
        lines.append(
            f"| {item['map']} | {item['sceneIdHex']} | "
            f"`0x{item['recordVa']:08x}` | {fields} | {'; '.join(points)} |"
        )
    lines.append("")
    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("--extract-dir", type=Path, default=Path("extract_fld"))
    parser.add_argument("--out", type=Path, help="write JSON output")
    parser.add_argument("--md-out", type=Path, help="write Markdown summary")
    parser.add_argument("--max-dwords", type=int, default=40)
    parser.add_argument("--map", dest="map_filter", help="only print records for one map stem")
    args = parser.parse_args()

    records = extract_candidates(args.exe.read_bytes(), args.extract_dir, args.max_dwords)
    if args.out:
        args.out.parent.mkdir(parents=True, exist_ok=True)
        args.out.write_text(json.dumps(records, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    if args.md_out:
        args.md_out.parent.mkdir(parents=True, exist_ok=True)
        args.md_out.write_text(markdown(records), encoding="utf-8")

    shown = [item for item in records if not args.map_filter or item["map"] == args.map_filter]
    print(f"found {len(records)} scene records with coordinate-like pointer candidates")
    for item in shown[:120]:
        summary = ", ".join(
            f"i{candidate['fieldIndex']} n={candidate['pointCount']} "
            f"ptr=0x{candidate['pointTableVa']:08x}"
            for candidate in item["candidates"][:6]
        )
        print(
            f"{item['sceneIdHex']} {item['map']} "
            f"record=0x{item['recordVa']:08x} {summary}"
        )
    if len(shown) > 120:
        print(f"... {len(shown) - 120} more")


if __name__ == "__main__":
    main()
