#!/usr/bin/env python3
"""Check (and optionally repair) Amiga kickstart ROMs in a RetroArch/PUAE BIOS dir.

PUAE reads kickstarts from the RetroArch system directory (the bios/BIOS folder).
Corrupt kickstarts there make Amiga games black-screen. A clean, correctly-sized
set usually sits in a `<bios>/amiga files/` subfolder that PUAE does NOT scan.

Usage:
  check_kickstarts.py <bios_dir>            # report CRC32 + size, flag corrupt
  check_kickstarts.py <bios_dir> --fix      # back up top-level kick*, then copy the
                                            # good set from `<bios>/amiga files/` over it

Corrupt = size < 200000 bytes, or a kickXXXXX.* whose CRC isn't a known-good one.
Always backs up before writing. Verifies by CRC32 after.
"""
import os, sys, zlib, shutil, argparse, datetime

# known-good CRC32 (lowercase) -> label
GOOD = {
    0x891e9a29: "KS1.3 A500 (256K)", 0xc4f0f55f: "KS1.3 A500 (alt)",
    0xc3bdb240: "KS2.04 37.175 A500", 0x83028fb5: "KS2.05 37.350 A600",
    0x6c9b07d2: "KS3.0 39.106 A1200", 0x9e6ac152: "KS3.0 39.106 A4000",
    0x1483a091: "KS3.1 40.68 A1200", 0xfc24ae0d: "KS3.1 A600",
    0xd6bae334: "KS3.1/3.0 A4000", 0x1e62d4a5: "CD32 40.60",
    0x87746be2: "CD32 ext", 0x42baa124: "CDTV ext",
}
# the standard PUAE kickstart filenames to repair from `amiga files/`
STD = ["kick33180.A500","kick34005.A500","kick34005.CDTV","kick37175.A500",
       "kick37350.A600","kick39106.A1200","kick39106.A4000","kick40060.CD32",
       "kick40060.CD32.ext","kick40063.A600","kick40068.A1200","kick40068.A4000"]

def crc(path):
    return zlib.crc32(open(path, "rb").read()) & 0xffffffff

def is_kick(f):
    return f.lower().startswith("kick") and not f.endswith(".bonus")

def report(bios):
    bad = []
    for f in sorted(os.listdir(bios)):
        p = os.path.join(bios, f)
        if not os.path.isfile(p) or not is_kick(f):
            continue
        sz = os.path.getsize(p); c = crc(p)
        corrupt = sz < 200000 or (c not in GOOD)
        tag = GOOD.get(c, "unrecognized CRC")
        flag = "  <-- CORRUPT/WRONG" if sz < 200000 else ("" if c in GOOD else "  <-- unknown CRC")
        print(f"{f:26s} {sz:>8d}  crc={c:08x}  {tag}{flag}")
        if sz < 200000:
            bad.append(f)
    return bad

def fix(bios):
    src = os.path.join(bios, "amiga files")
    if not os.path.isdir(src):
        sys.exit(f"no clean set found at '{src}' — supply kickstarts from Amiga Forever instead")
    bk = os.path.join(bios, "_kickstart_backup_" + datetime.datetime.now().strftime("%F_%H%M"))
    os.makedirs(bk, exist_ok=True)
    for f in os.listdir(bios):
        if is_kick(f) and os.path.isfile(os.path.join(bios, f)):
            shutil.copy2(os.path.join(bios, f), os.path.join(bk, f))
    print(f"backed up top-level kickstarts -> {bk}")
    n = 0
    for f in STD:
        s = os.path.join(src, f)
        if os.path.isfile(s):
            # --remove-destination breaks hardlinks so we never write through
            d = os.path.join(bios, f)
            if os.path.exists(d):
                os.remove(d)
            shutil.copy2(s, d); n += 1
            print(f"  updated {f}")
    print(f"repaired {n} kickstarts")

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("bios_dir")
    ap.add_argument("--fix", action="store_true", help="backup + repair from 'amiga files/' subfolder")
    a = ap.parse_args()
    if not os.path.isdir(a.bios_dir):
        sys.exit(f"not a directory: {a.bios_dir}")
    print("=== kickstarts (before) ===")
    bad = report(a.bios_dir)
    if a.fix:
        print("\n=== repairing ===")
        fix(a.bios_dir)
        print("\n=== kickstarts (after) ===")
        bad = report(a.bios_dir)
    print(f"\nremaining under-size (corrupt) kickstarts: {len(bad)}")

if __name__ == "__main__":
    main()
