#!/usr/bin/env python3
"""
Compile gate for readable/ — every readable/*.c must compile cleanly with
agbcc (the game's compiler). This checks syntax/types only; readable C is
NOT required to be byte-exact (that is src/matched/'s job).

Usage: python3 tools/check_readable.py
Exit 1 if any file fails to compile.
"""
import subprocess
import re
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
AGBCC = str(ROOT / "agbcc" / "agbcc")
READABLE = ROOT / "readable"
INCLUDE = ROOT / "include"
FUNC_LIST = ROOT / "function_list_v2.txt"


def load_list():
    """name.lower() -> '0xADDR' from the boundary map."""
    out = {}
    for line in FUNC_LIST.read_text().splitlines():
        m = re.search(r"([0-9A-Fa-f]{6,8})\s+(\d+)\s+(.+)", line)
        if m:
            out.setdefault(m.group(3).strip().lower(),
                           "0x" + m.group(1).upper().rjust(8, "0"))
    return out


def check_headers(listmap):
    """Every readable/*.c header address must match the boundary map
    (or the address embedded in its own stem). Returns failure count."""
    fails = 0
    for f in sorted(READABLE.glob("*.c")):
        head = "\n".join(f.read_text(errors="replace").splitlines()[:15])
        m = re.search(r"@ (0x[0-9A-Fa-f]+)", head)
        if not m:
            print(f"ADDR-FAIL {f.name}: no header address")
            fails += 1
            continue
        hdr = m.group(1).upper()
        exp = listmap.get(f.stem.lower())
        if exp is None:
            em = re.search(r"_(080[0-9A-Fa-f]{5})$", f.stem, re.I)
            if em:
                exp = "0x" + em.group(1).upper()
        if exp is None:
            exp = {"sound_noop_08011ef0": "0X08011EF0",
                   "level_veneer470c": "0X0800470C",
                   "mem_bootmodeflag": "0X08014F88"}.get(f.stem.lower())
        if exp is None or exp.upper() != hdr:
            print(f"ADDR-FAIL {f.name}: header {m.group(1)} vs list {exp}")
            fails += 1
    return fails


def main() -> int:
    files = sorted(READABLE.glob("*.c"))
    if not files:
        print("no readable/*.c files yet")
        return 0
    fails = []
    for f in files:
        r = subprocess.run(
            [AGBCC, "-O2", "-mthumb-interwork", "-Wimplicit", "-Wparentheses",
             "-Wreturn-type", "-I", str(INCLUDE), "-S", str(f),
             "-o", "/dev/stdout"],
            capture_output=True, text=True)
        # agbcc prints spurious "Invalid option -S" with rc=1 but still
        # emits output; treat "no output" as the real failure signal.
        if not r.stdout.strip():
            fails.append((f.name, (r.stderr or r.stdout)[:300]))
            print(f"FAIL {f.name}: {(r.stderr or 'no output')[:200]}")
    print(f"\nREADABLE: {len(files) - len(fails)}/{len(files)} files compile")
    if fails:
        for name, err in fails[:10]:
            print(f"  {name}: {err[:160]}")
        return 1
    afails = check_headers(load_list())
    print(f"ADDR: {len(files) - afails}/{len(files)} headers match function_list")
    return 1 if afails else 0


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