#!/usr/bin/env python3
"""Dump Radium command opcode table entries from the native game binaries.

The command stream runtime uses a 16-byte table entry per opcode:
handler address, then payload byte length. Static Ghidra analysis of
FUN_007a46e0 confirms the preload scan indexes the second qword directly
(`table_base + 8`) to skip unknown/non-sound opcodes.
"""

from __future__ import annotations

import csv
import struct
from pathlib import Path


ROOT = Path(__file__).resolve().parents[2]
OPCODE_COUNT = 0x13

TARGETS = [
    {
        "image": "main",
        "binary": ROOT / "games/pokemon_pro/game",
        "table_offset": 0x3079E70,
        "histogram": ROOT / "analysis/radium-image/command_opcode_histogram.tsv",
        "payload_summary": ROOT / "analysis/radium-image/command_opcode_payload_summary.tsv",
        "out": ROOT / "analysis/radium-image/opcode_handler_table.tsv",
    },
    {
        "image": "spike_menu",
        "binary": ROOT / "games/pokemon_pro/spike3/spike_menu/game",
        "table_offset": 0x410CE0,
        "histogram": ROOT / "analysis/radium-spike-menu-image/command_opcode_histogram.tsv",
        "payload_summary": ROOT / "analysis/radium-spike-menu-image/command_opcode_payload_summary.tsv",
        "out": ROOT / "analysis/radium-spike-menu-image/opcode_handler_table.tsv",
    },
]


def load_tsv_by_opcode(path: Path) -> dict[int, dict[str, str]]:
    if not path.exists():
        return {}
    with path.open(newline="") as f:
        rows = {}
        for row in csv.DictReader(f, delimiter="\t"):
            opcode = row.get("opcode", "")
            if opcode == "":
                continue
            rows[int(opcode)] = row
        return rows


def main() -> int:
    fieldnames = [
        "image",
        "opcode",
        "opcode_hex",
        "table_offset",
        "entry_offset",
        "handler_address",
        "payload_length",
        "observed_command_count",
        "observed_stream_count",
        "observed_sound_ref_count",
        "unique_payload_count",
        "top_payload_hex",
    ]
    for target in TARGETS:
        data = target["binary"].read_bytes()
        hist = load_tsv_by_opcode(target["histogram"])
        summary = load_tsv_by_opcode(target["payload_summary"])
        rows = []
        for opcode in range(OPCODE_COUNT):
            entry_offset = target["table_offset"] + opcode * 0x10
            handler_address, payload_length = struct.unpack_from("<QQ", data, entry_offset)
            hist_row = hist.get(opcode, {})
            summary_row = summary.get(opcode, {})
            rows.append(
                {
                    "image": target["image"],
                    "opcode": opcode,
                    "opcode_hex": f"0x{opcode:02x}",
                    "table_offset": f"0x{target['table_offset']:x}",
                    "entry_offset": f"0x{entry_offset:x}",
                    "payload_length": payload_length,
                    "handler_address": f"0x{handler_address:x}" if handler_address else "",
                    "observed_command_count": hist_row.get("command_count", "0"),
                    "observed_stream_count": hist_row.get("stream_count", "0"),
                    "observed_sound_ref_count": hist_row.get("sound_ref_count", "0"),
                    "unique_payload_count": summary_row.get("unique_payload_count", "0"),
                    "top_payload_hex": summary_row.get("top_payload_hex", ""),
                }
            )
        target["out"].parent.mkdir(parents=True, exist_ok=True)
        with target["out"].open("w", newline="") as f:
            writer = csv.DictWriter(f, fieldnames=fieldnames, delimiter="\t")
            writer.writeheader()
            writer.writerows(rows)
        print(f"wrote {target['out']} ({len(rows)} rows)")
    return 0


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