#!/usr/bin/env python3
"""
Full-ROM builder for bit Generations: Orbital (GBA).

Assembles the 16MB ROM from decomp sources + extracted assets:

  header  assets/header.bin              (0x000000-0x0000C0, byte-exact copy)
  code    asm/all_stubs.s assembled+linked (0x0000C0-0x01F30C, built from code)
  tail    assets/rodata.bin + chunks/...  (0x01F30C-0x1000000, extracted data)

The code section is genuinely built: all_stubs.s (generated from
function_list_v2.txt by tools/gen_stubs.py) is assembled with
arm-none-eabi-as, linked at 0x08000000 with gba.ld, and the .text output
must equal the reference code bytes -- otherwise the build fails. The
header/tail are copy-through in v1 (moddable rebuild is the next step:
replace a blob, keep its offset/size, rebuild, verify).

Success = output sha1 equals the reference ROM sha1
(437e3093928ce9b0705476053a059d70f9f84ae3).

Usage:
  python3 tools/extract_assets.py        # first (provides assets/)
  python3 tools/build_rom.py [--rom PATH] [--out build/orbital.gba]
"""
import argparse
import hashlib
import json
import os
import subprocess
import sys
import tempfile
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
DEFAULT_ROM = ROOT / "bit Generations - Orbital (Japan) (En).gba"
STUB = ROOT / "asm" / "all_stubs.s"
LINKER = ROOT / "gba.ld"
TB = str(ROOT / "arm-gnu-toolchain-13.3.rel1-darwin-arm64-arm-none-eabi" / "bin")
AS = os.path.join(TB, "arm-none-eabi-as")
LD = os.path.join(TB, "arm-none-eabi-ld")
OBJCOPY = os.path.join(TB, "arm-none-eabi-objcopy")

CODE_START = 0xC0
CODE_END = 0x1F30C
CODE_SIZE = CODE_END - CODE_START


def run(cmd):
    r = subprocess.run(cmd, capture_output=True, text=True)
    return r.returncode, r.stderr


def build_code_section(tmp: Path) -> bytes:
    o = tmp / "code.o"
    elf = tmp / "code.elf"
    binp = tmp / "code.bin"
    rc, err = run([AS, "-mcpu=arm7tdmi", str(STUB), "-o", str(o)])
    if rc != 0:
        sys.exit(f"ASM FAIL:\n{err.strip()[:2000]}")
    rc, err = run([LD, "-T", str(LINKER), str(o), "-o", str(elf)])
    if rc != 0:
        sys.exit(f"LINK FAIL:\n{err.strip()[:2000]}")
    rc, err = run([OBJCOPY, "-O", "binary", "-j", ".text", str(elf), str(binp)])
    if rc != 0:
        sys.exit(f"OBJCOPY FAIL:\n{err.strip()[:2000]}")
    data = binp.read_bytes()
    if len(data) < CODE_SIZE:
        sys.exit(f"built code too small: {len(data)} < {CODE_SIZE}")
    return data[:CODE_SIZE]


def main() -> int:
    ap = argparse.ArgumentParser(description="Build full Orbital ROM")
    ap.add_argument("--rom", default=str(DEFAULT_ROM),
                    help="reference ROM (verify target)")
    ap.add_argument("--assets", default=str(ROOT / "assets"))
    ap.add_argument("--out", default=str(ROOT / "build" / "orbital.gba"))
    args = ap.parse_args()
    assets = Path(args.assets)
    man_path = assets / "manifest.json"
    if not man_path.exists():
        sys.exit("assets/manifest.json missing; run tools/extract_assets.py first")
    if not STUB.exists():
        sys.exit("asm/all_stubs.s missing; run tools/gen_stubs.py first")
    man = json.loads(man_path.read_text())
    by_name = {r["name"]: r for r in man["regions"]}

    with tempfile.TemporaryDirectory() as td:
        code = build_code_section(Path(td))
    ref_code = (assets / "code.bin").read_bytes()
    if code != ref_code:
        diffs = sum(1 for a, b in zip(code, ref_code) if a != b)
        sys.exit(f"built code differs from extracted code.bin: {diffs} diffs; "
                 f"regen stubs (tools/gen_stubs.py) and re-extract")
    print(f"code: built {len(code)} bytes from asm/all_stubs.s, matches code.bin")

    rom = bytearray()
    rom += (assets / "header.bin").read_bytes()
    assert len(rom) == CODE_START, "header size drift"
    rom += code
    assert len(rom) == CODE_END, "code size drift"
    tail_regs = [r for r in man["regions"]
                 if int(r["rom_off"], 16) >= CODE_END and not r.get("overlap")]
    tail_regs.sort(key=lambda r: int(r["rom_off"], 16))
    cursor = CODE_END
    for r in tail_regs:
        off = int(r["rom_off"], 16)
        if off != cursor:
            sys.exit(f"tail gap/overlap at {r['name']}: {off:X} != {cursor:X}")
        data = (assets / r["name"]).read_bytes()
        if len(data) != r["size"]:
            sys.exit(f"size drift: {r['name']}")
        rom += data
        cursor += len(data)

    out = Path(args.out)
    out.parent.mkdir(parents=True, exist_ok=True)
    out.write_bytes(bytes(rom))
    got = hashlib.sha1(bytes(rom)).hexdigest()
    print(f"wrote {out} ({len(rom)} bytes) sha1={got}")

    ref = Path(args.rom)
    if ref.exists():
        want = hashlib.sha1(ref.read_bytes()).hexdigest()
        if got == want:
            print(f"BYTE-IDENTICAL to {ref.name}")
            (out.parent / "rom_sha1.txt").write_text(got + "\n")
            return 0
        sys.exit(f"MISMATCH: got {got} want {want}")
    print("no reference ROM to verify against; wrote output only")
    return 0


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