#!/usr/bin/env python3
"""
Comprehensive batch matcher for Orbital.

Matches as many functions as possible using pattern detection and
agbcc compilation. Tracks progress and saves matched C to src/matched/.

Usage: python3 tools/batch_match.py
"""

import json
import os
import re
import struct
import subprocess
import sys
import tempfile
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
ROM_PATH = ROOT / "bit Generations - Orbital (Japan) (En).gba"
FUNC_LIST = ROOT / "function_list_v2.txt"
AGBCC = str(ROOT / "agbcc" / "agbcc")
TB = str(ROOT / "arm-gnu-toolchain-13.3.rel1-darwin-arm64-arm-none-eabi" / "bin")
AS = os.path.join(TB, "arm-none-eabi-as")
OBJCOPY = os.path.join(TB, "arm-none-eabi-objcopy")
ROM_BASE = 0x08000000
CODE_START = 0x080000C0
CODE_END = 0x0801F30C
MATCHED_DIR = ROOT / "src" / "matched"

def mask_bl(data):
    m = bytearray(data)
    i = 0
    while i < len(m)-3:
        h1 = int.from_bytes(m[i:i+2],'little')
        h2 = int.from_bytes(m[i+2:i+4],'little')
        if (h1>>11)==0b11110 and (h2>>11)==0b11111:
            h1&=0xF800; h2&=0xF800
            m[i]=h1&0xFF; m[i+1]=h1>>8; m[i+2]=h2&0xFF; m[i+3]=h2>>8
            i+=4; continue
        i+=2
    return m

def last_ret_offset(data):
    i = 0; last = -1
    while i < len(data)-1:
        hw = int.from_bytes(data[i:i+2],'little')
        if hw == 0x4770 or ((hw>>8)==0xBD and (hw&0x100)):
            last = i
        if (hw>>11) in (0b11101, 0b11110, 0b11111): i += 4
        else: i += 2
    return last

def try_compile_check(c_code, addr, size, rom):
    with tempfile.TemporaryDirectory() as td:
        c_path = f'{td}/f.c'
        s_path = f'{td}/f.s'
        o_path = f'{td}/f.o'
        b_path = f'{td}/f.bin'
        Path(c_path).write_text(c_code)
        subprocess.run([AGBCC,'-O2','-mthumb-interwork','-S',c_path,'-o',s_path],
                      capture_output=True,timeout=10)
        if not Path(s_path).exists(): return False, 0
        subprocess.run([AS,'-mcpu=arm7tdmi',s_path,'-o',o_path],
                      capture_output=True,timeout=10)
        if not Path(o_path).exists(): return False, 0
        subprocess.run([OBJCOPY,'-O','binary','-j','.text',o_path,b_path],
                      capture_output=True,timeout=10)
        if not Path(b_path).exists(): return False, 0
        compiled = Path(b_path).read_bytes()
        off = addr - ROM_BASE
        rb = rom[off:off+size]
        lr = last_ret_offset(compiled)
        code_end = (lr + 2) if lr >= 0 else len(compiled)
        lr_r = last_ret_offset(rb)
        rom_end = (lr_r + 2) if lr_r >= 0 else size
        n = min(code_end, rom_end, size)
        if n == 0: return False, 0
        if mask_bl(compiled[:n]) == mask_bl(rb[:n]):
            return True, code_end
        return False, code_end

def hw16(rom, addr):
    return struct.unpack_from('<H', rom, addr - ROM_BASE)[0]

def generate_candidates(rom, addr, size, name):
    """Generate C code candidates based on function patterns."""
    off = addr - ROM_BASE
    fb = rom[off:off+size]
    candidates = []
    
    h1 = hw16(rom, addr)
    
    # Empty function (bx lr only)
    if size == 2 and h1 == 0x4770:
        candidates.append("void f(void) {}\n")
        return candidates
    
    # movs r0, #N; bx lr
    if size >= 4 and (h1 & 0xF800) == 0x2000 and (h1 & 0x700) == 0:
        h2 = hw16(rom, addr + 2)
        if h2 == 0x4770:
            val = h1 & 0xFF
            candidates.append(f"int f(void) {{ return {val}; }}\n")
            return candidates
    
    # swi #N; bx lr
    if size >= 4 and (h1 & 0xFF00) == 0xDF00:
        h2 = hw16(rom, addr + 2)
        if h2 == 0x4770:
            swi = h1 & 0xFF
            candidates.append(f"void f(void) {{ __asm__ volatile(\"swi #0x{swi:02X}\"); }}\n")
            return candidates
    
    # Simple store pattern: str r1,[r0,#off]; str r2,[r0,#off2]; ...; bx lr
    if size >= 6 and size <= 32:
        last = hw16(rom, addr + size - 2)
        if last == 0x4770:
            stores = []
            valid = True
            for j in range(0, size - 2, 2):
                hh = hw16(rom, addr + j)
                if (hh & 0xF800) == 0x6000:  # str rX, [rY, #imm*4]
                    rd = hh & 7
                    rn = (hh >> 3) & 7
                    off_val = ((hh >> 6) & 0x1F) * 4
                    stores.append((rd, rn, off_val))
                else:
                    valid = False
                    break
            if valid and stores:
                max_off = max(s[2] for s in stores)
                fields = []
                for o in range(0, max_off + 4, 4):
                    fields.append(f"    int f{o};")
                struct_def = "struct S {\n" + "\n".join(fields) + "\n};\n"
                params = {}
                assigns = []
                for rd, rn, off_val in stores:
                    if rn != 0:
                        valid = False
                        break
                    if rd not in params:
                        params[rd] = f"p{rd}"
                    assigns.append(f"    p->f{off_val} = {params[rd]};")
                if valid:
                    param_list = ", ".join(f"int {v}" for _, v in sorted(params.items()))
                    func = f"void f(struct S *p{', ' + param_list if param_list else ''}) {{\n"
                    func += "\n".join(assigns) + "\n}\n"
                    candidates.append(struct_def + "\n" + func)
                    return candidates
    
    # push {lr}; ldr r0, [pc, #N]; ldr r0, [r0]; bl ...; pop {r0}; bx r0
    # (simple global load + call pattern)
    if (h1 & 0xFF00) == 0xB500 and size >= 12:
        # Try: push {lr}; <body>; pop {r0}; bx r0
        last_hw = hw16(rom, addr + size - 2)
        penult_hw = hw16(rom, addr + size - 4) if size >= 4 else 0
        if penult_hw == 0xBC01 and last_hw == 0x4700:  # pop {r0}; bx r0
            # Look for ldr r0, [pc, #N] at offset 2
            h2 = hw16(rom, addr + 2)
            if (h2 >> 11) == 0x9:  # ldr rN, [pc, #imm]
                # This is a "load global and call" pattern
                # Generate: extern void g(void); void f(void) { g(); }
                # But we need to know what 'g' is
                pass
    
    return candidates

def main():
    rom = ROM_PATH.read_bytes()
    funcs = []
    for line in open(FUNC_LIST):
        parts = line.strip().split(None, 2)
        if len(parts) >= 2:
            a = int(parts[0], 16); s = int(parts[1])
            n = parts[2] if len(parts) > 2 else f"sub_{a:X}"
            funcs.append((a, s, n))
    funcs.sort()
    
    matched = []
    total_bytes = 0
    
    for addr, size, name in funcs:
        if not (CODE_START <= addr < CODE_END) or size < 2:
            continue
        total_bytes += size
        
        candidates = generate_candidates(rom, addr, size, name)
        
        for c_code in candidates:
            ok, code_len = try_compile_check(c_code, addr, size, rom)
            if ok:
                matched.append((addr, size, name, code_len, c_code))
                break
    
    matched_bytes = sum(code_len for _, _, _, code_len, _ in matched)
    
    MATCHED_DIR.mkdir(parents=True, exist_ok=True)
    for addr, size, name, code_len, c_code in matched:
        (MATCHED_DIR / f"{name}.c").write_text(c_code)
    
    # Save report
    report = {
        "matched_functions": len(matched),
        "matched_bytes": matched_bytes,
        "total_bytes": total_bytes,
        "accuracy_pct": round(100 * matched_bytes / total_bytes, 1) if total_bytes else 0,
        "functions": [
            {"addr": f"0x{a:08X}", "size": s, "name": n, "code_bytes": cl}
            for a, s, n, cl, _ in matched
        ]
    }
    (ROOT / "build" / "batch_match_report.json").write_text(json.dumps(report, indent=2))
    
    print(f"\n{'='*60}")
    print(f"MATCHED: {len(matched)} functions, {matched_bytes} bytes")
    print(f"TOTAL:   {total_bytes} bytes in code section")
    print(f"ACCURACY: {100*matched_bytes/total_bytes:.1f}%")
    print(f"\nTop matched by bytes:")
    for addr, size, name, cl, _ in sorted(matched, key=lambda x: -x[3])[:20]:
        print(f"  0x{addr:08X} {cl:5d}B  {name}")

if __name__ == "__main__":
    main()
