#!/usr/bin/env python3
"""Extract native GodotSpikeBridge sound registrations from the game binary.

This is a static AArch64 pattern extractor for the registration table
identified in Ghidra as FUN_005d6b90 -> FUN_006ec060(name, id, -1).
It does not execute the target binary.
"""

from __future__ import annotations

import argparse
import csv
import struct
from pathlib import Path


GHIDRA_BASE_DELTA = 0x100000
GHIDRA_REGISTER_SOUND = 0x006EC060
TEXT_START = 0x00460000
TEXT_END = 0x0258A7B0


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


def decode_bl_target(pc: int, insn: int) -> int | None:
    if insn & 0xFC000000 != 0x94000000:
        return None
    imm = insn & 0x03FFFFFF
    if imm & 0x02000000:
        imm -= 0x04000000
    return pc + imm * 4


def decode_movz_w2(insn: int) -> int | None:
    if insn & 0x7F80001F != 0x52800002:
        return None
    shift = (insn >> 21) & 0x3
    if shift != 0:
        return None
    return (insn >> 5) & 0xFFFF


def decode_adrp(pc: int, insn: int) -> tuple[int, int] | None:
    if insn & 0x9F000000 != 0x90000000:
        return None
    reg = insn & 0x1F
    immlo = (insn >> 29) & 0x3
    immhi = (insn >> 5) & 0x7FFFF
    imm = (immhi << 2) | immlo
    if imm & (1 << 20):
        imm -= 1 << 21
    return reg, (pc & ~0xFFF) + imm * 0x1000


def decode_add_imm(insn: int) -> tuple[int, int, int] | None:
    if insn & 0xFFC00000 != 0x91000000:
        return None
    rd = insn & 0x1F
    rn = (insn >> 5) & 0x1F
    shift = (insn >> 22) & 0x3
    imm = (insn >> 10) & 0xFFF
    if shift == 1:
        imm <<= 12
    elif shift != 0:
        return None
    return rd, rn, imm


def read_c_string(data: bytes, offset: int) -> str:
    end = data.find(b"\0", offset)
    if end < 0:
        end = min(len(data), offset + 256)
    return data[offset:end].decode("ascii", errors="replace")


def extract(data: bytes, target_file_addr: int) -> list[dict[str, object]]:
    rows: list[dict[str, object]] = []
    start = min(TEXT_START, len(data))
    end = min(TEXT_END, len(data))
    for pc in range(start, end, 4):
        insn = u32(data, pc)
        if decode_bl_target(pc, insn) != target_file_addr:
            continue
        sound_id = None
        string_addr = None
        for cursor in range(pc - 4, max(TEXT_START, pc - 0x100), -4):
            prior = u32(data, cursor)
            if sound_id is None:
                sound_id = decode_movz_w2(prior)
            add_info = decode_add_imm(prior)
            if add_info is not None and cursor >= TEXT_START + 4:
                rd, rn, add_imm = add_info
                adrp_info = decode_adrp(cursor - 4, u32(data, cursor - 4))
                if adrp_info is not None:
                    adrp_reg, adrp_addr = adrp_info
                    if rd == rn and rn == adrp_reg:
                        string_addr = adrp_addr + add_imm
            if sound_id is not None and string_addr is not None:
                break
        rows.append(
            {
                "name": read_c_string(data, string_addr) if string_addr is not None else "",
                "native_sound_id": sound_id if sound_id is not None else "",
                "register_call_ghidra": f"0x{pc + GHIDRA_BASE_DELTA:08x}",
                "register_call_file_offset": f"0x{pc:08x}",
                "string_ghidra": f"0x{string_addr + GHIDRA_BASE_DELTA:08x}" if string_addr is not None else "",
                "string_file_offset": f"0x{string_addr:08x}" if string_addr is not None else "",
                "register_function_ghidra": f"0x{GHIDRA_REGISTER_SOUND:08x}",
            }
        )
    return rows


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--game-binary", type=Path, default=Path("games/pokemon_pro/game"))
    parser.add_argument("--out", type=Path, default=Path("analysis/radium-image/native_sound_id_map.tsv"))
    args = parser.parse_args()

    data = args.game_binary.read_bytes()
    rows = extract(data, GHIDRA_REGISTER_SOUND - GHIDRA_BASE_DELTA)
    args.out.parent.mkdir(parents=True, exist_ok=True)
    fields = [
        "name",
        "native_sound_id",
        "register_call_ghidra",
        "register_call_file_offset",
        "string_ghidra",
        "string_file_offset",
        "register_function_ghidra",
    ]
    with args.out.open("w", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=fields, delimiter="\t")
        writer.writeheader()
        writer.writerows(rows)
    print(f"wrote {args.out} rows={len(rows)}")
    return 0


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