#!/usr/bin/env python3
"""
Build and match decompiled code against original ROM.
Strategy:
1. Extract each function's bytes from original ROM
2. Try to compile C version with agbcc
3. If no C available, try to assemble matching Thumb code
4. Compare byte-by-byte
5. Report overall accuracy
"""
import os
import re
import struct
import subprocess
import tempfile
import sys

ROM_PATH = "bit Generations - Orbital (Japan) (En).gba"
ROM_BASE = 0x08000000
AGBCC = "agbcc/agbcc"
AS = "arm-gnu-toolchain-13.3.rel1-darwin-arm64-arm-none-eabi/bin/arm-none-eabi-as"
OBJCOPY = "arm-gnu-toolchain-13.3.rel1-darwin-arm64-arm-none-eabi/bin/arm-none-eabi-objcopy"

def read_rom():
    with open(ROM_PATH, "rb") as f:
        return f.read()

def rom_to_file(addr):
    return addr - ROM_BASE

def try_agbcc(c_code, opt_flags="-O2"):
    """Compile C with agbcc, return assembly"""
    with tempfile.NamedTemporaryFile(suffix=".c", mode="w", delete=False) as f:
        f.write(c_code)
        c_path = f.name
    
    try:
        result = subprocess.run(
            [AGBCC, opt_flags, c_path],
            capture_output=True, text=True, timeout=30
        )
        s_path = c_path.replace(".c", ".s")
        if os.path.exists(s_path):
            with open(s_path) as f:
                asm = f.read()
            os.unlink(s_path)
            return asm, None
        return None, result.stderr or "No output"
    except Exception as e:
        return None, str(e)
    finally:
        os.unlink(c_path)

def assemble_thumb(asm_text, label="func"):
    """Assemble Thumb code and return raw bytes"""
    with tempfile.NamedTemporaryFile(suffix=".s", mode="w", delete=False) as f:
        f.write(f".syntax unified\n.thumb\n.text\n.global {label}\n.thumb_func\n{label}:\n")
        f.write(asm_text)
        f.write(f"\n.size {label}, .-{label}\n")
        s_path = f.name
    
    o_path = s_path.replace(".s", ".o")
    bin_path = s_path.replace(".s", ".bin")
    
    try:
        result = subprocess.run(
            [AS, "-mcpu=arm7tdmi", "-mthumb-interwork", s_path, "-o", o_path],
            capture_output=True, text=True, timeout=30
        )
        if result.returncode != 0:
            return None, result.stderr
        
        result = subprocess.run(
            [OBJCOPY, "-O", "binary", o_path, bin_path],
            capture_output=True, text=True, timeout=30
        )
        if result.returncode != 0:
            return None, result.stderr
        
        with open(bin_path, "rb") as f:
            return f.read(), None
    except Exception as e:
        return None, str(e)
    finally:
        for p in [s_path, o_path, bin_path]:
            if os.path.exists(p):
                os.unlink(p)

def match_bytes(our, orig):
    """Check exact byte match"""
    if len(our) != len(orig):
        return False, 0.0
    
    matching = sum(1 for a, b in zip(our, orig) if a == b)
    rate = matching / len(orig) if orig else 1.0
    return matching == len(orig), rate

def extract_function_asm(asm_text, func_name):
    """Extract just the function body from agbcc output"""
    lines = asm_text.split('\n')
    body = []
    in_func = False
    
    for line in lines:
        stripped = line.strip()
        # Start at function label
        if stripped == f"{func_name}:" or stripped.startswith(f"{func_name}:"):
            in_func = True
            continue
        # End at size directive or next function
        if in_func and (stripped.startswith('.Lfe') or stripped.startswith('.size') or 
                       (stripped.endswith(':') and not stripped.startswith('.'))):
            break
        if in_func:
            # Skip assembler directives
            if not stripped.startswith('.') and not stripped.startswith('@'):
                body.append(line)
    
    return '\n'.join(body)

def main():
    print("=" * 70)
    print("GBA Binary Accuracy Builder")
    print("=" * 70)
    
    rom = read_rom()
    
    # Read function list
    with open("function_list_full.txt") as f:
        func_list = []
        for line in f:
            line = line.strip()
            if not line:
                continue
            parts = line.split(None, 2)
            addr = int(parts[0], 16)
            size = int(parts[1])
            name = parts[2] if len(parts) > 2 else f"func_{addr:08X}"
            func_list.append((addr, size, name))
    
    print(f"Functions: {len(func_list)}")
    
    # Read decompiled C source
    with open("src/orbital_complete.c") as f:
        c_source = f.read()
    
    # Extract individual function C code
    func_c_codes = {}
    # Pattern: /* ---- FuncName @ 0xADDR ---- */\n... next such comment or end
    pattern = r'/\*\s*----\s+(\w+)\s+@\s+(0x[0-9A-Fa-f]+)\s+----\s*\*/(.*?)(?=/\*\s*----|\Z)'
    matches = re.findall(pattern, c_source, re.DOTALL)
    
    for name, addr, body in matches:
        # Clean up the C code
        body = body.strip()
        # Remove includes and forward declarations (keep only the function)
        lines = body.split('\n')
        func_lines = []
        skip = True
        for line in lines:
            stripped = line.strip()
            # Start capturing at the function definition
            if not stripped.startswith('#') and not stripped.startswith('/*') and not stripped.startswith('*') and not stripped.startswith('extern'):
                skip = False
            if not skip:
                func_lines.append(line)
        
        func_c_codes[name] = '\n'.join(func_lines)
    
    print(f"Extracted {len(func_c_codes)} C function bodies")
    
    # Statistics
    total_bytes = 0
    matched_bytes = 0
    matched_count = 0
    partial_count = 0
    failed_count = 0
    no_source = 0
    
    results = []
    
    for addr, size, name in func_list:
        total_bytes += size
        offset = rom_to_file(addr)
        orig_bytes = rom[offset:offset+size]
        
        if name in func_c_codes:
            c_code = func_c_codes[name]
            
            # Try compiling with agbcc
            asm, err = try_agbcc(c_code)
            
            if asm:
                # Extract function body
                func_asm = extract_function_asm(asm, name)
                
                if func_asm.strip():
                    # Assemble
                    binary, err2 = assemble_thumb(func_asm, name)
                    
                    if binary:
                        exact, rate = match_bytes(binary, orig_bytes)
                        
                        if exact:
                            matched_bytes += size
                            matched_count += 1
                            results.append((addr, size, name, "MATCH"))
                            print(f"  ✅ {name:40s} 0x{addr:08X} {size:4d}B MATCH")
                        elif rate > 0.9:
                            matched_bytes += int(size * rate)
                            partial_count += 1
                            results.append((addr, size, name, f"PARTIAL {rate*100:.0f}%"))
                            print(f"  🔶 {name:40s} 0x{addr:08X} {size:4d}B {rate*100:.0f}%")
                        else:
                            failed_count += 1
                            results.append((addr, size, name, f"MISMATCH {rate*100:.0f}%"))
                    else:
                        failed_count += 1
                        results.append((addr, size, name, "ASM_FAIL"))
                else:
                    no_source += 1
                    results.append((addr, size, name, "NO_BODY"))
            else:
                no_source += 1
                results.append((addr, size, name, "COMPILE_FAIL"))
        else:
            no_source += 1
            results.append((addr, size, name, "NO_C_SOURCE"))
    
    # Summary
    print("\n" + "=" * 70)
    print("RESULTS")
    print("=" * 70)
    print(f"Total functions: {len(func_list)}")
    print(f"  MATCH:     {matched_count:4d} ({matched_count/len(func_list)*100:5.1f}%)")
    print(f"  PARTIAL:   {partial_count:4d} ({partial_count/len(func_list)*100:5.1f}%)")
    print(f"  FAILED:    {failed_count:4d} ({failed_count/len(func_list)*100:5.1f}%)")
    print(f"  NO SOURCE: {no_source:4d} ({no_source/len(func_list)*100:5.1f}%)")
    print(f"\nTotal bytes: {total_bytes}")
    print(f"Matched bytes: {matched_bytes} ({matched_bytes/total_bytes*100:.1f}%)")
    print(f"\nOverall binary accuracy: {matched_bytes/total_bytes*100:.1f}%")
    
    # Save detailed results
    with open("build/matching_results.txt", "w") as f:
        for addr, size, name, status in results:
            f.write(f"0x{addr:08X} {size:4d} {name:40s} {status}\n")
    
    print(f"\nDetailed results: build/matching_results.txt")

if __name__ == "__main__":
    os.makedirs("build", exist_ok=True)
    main()
