#!/usr/bin/env python3
"""Score a Ch357 ZSCHED framebuffer dump.

The z/paint/replay owner modes compare against the dump-derived texture oracle.
The replay-color mode is the hard board/RTL content check: it replays the emitted
fragment trace colors through the same clamp16 GEQUAL persistent-Z rule and
compares the framebuffer colors directly.

Usage:
  tools/analyze_zsched_fb.py sim/data/top_psmct32_raster_demo/sh3_zsched_board_fb.mem
  tools/analyze_zsched_fb.py <fb.mem> --owner paint
  tools/analyze_zsched_fb.py <fb.mem> --owner replay --frags sim/traces/rtl/zsched_frags.txt
  tools/analyze_zsched_fb.py <fb.mem> --owner replay-color --frags sim/traces/rtl/zsched_frags.txt
  tools/analyze_zsched_fb.py <fb.mem> --owner replay --radius 8
  tools/analyze_zsched_fb.py <fb.mem> --maps
"""
import os
import re
import sys

HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.normpath(os.path.join(HERE, ".."))
DATA = os.path.join(ROOT, "sim", "data", "top_psmct32_raster_demo")


def load_mem(path):
    vals = []
    with open(path) as f:
        for ln in f:
            s = ln.strip()
            if not s or s.startswith("//"):
                continue
            vals.append(int(s, 16) & 0xFFFFFFFF)
    return vals


def parse_params(tag):
    path = os.path.join(DATA, f"sh3_{tag}_params.vh")
    txt = open(path).read()
    out = {}
    for key in ("FBPXW", "FBH", "TW", "TH", "N_EPOCHS"):
        m = re.search(rf"localparam\s+int\s+{key}\s*=\s*([0-9]+)\s*;", txt)
        if not m:
            raise SystemExit(f"[analyze] missing {key} in {path}")
        out[key] = int(m.group(1))
    return out


def cell(idx, pal, tw, u, v):
    lin = v * tw + u
    w = idx[lin // 4]
    ix = (w >> (8 * (lin % 4))) & 0xFF
    return pal[ix] & 0xFFFFFF


def match_radius(fbc, idx, pal, tw, th, tu, tv, max_radius):
    for rad in range(max_radius + 1):
        for du in range(-rad, rad + 1):
            for dv in range(-rad, rad + 1):
                if max(abs(du), abs(dv)) != rad:
                    continue
                uu = tu + du
                vv = tv + dv
                if 0 <= uu < tw and 0 <= vv < th and fbc == cell(idx, pal, tw, uu, vv):
                    return rad
    return None


def write_maps(prefix, w, h, fb, status):
    try:
        from PIL import Image
    except Exception:
        print("[analyze] PIL not available; skipping maps")
        return

    colors = {
        0: (0, 0, 0),       # uncovered
        1: (0, 180, 0),     # owner exact
        2: (170, 170, 0),   # owner <=1 texel
        3: (0, 80, 220),    # wrong owner/other covering epoch
        4: (220, 0, 0),     # no covering epoch match
        5: (255, 0, 255),   # no palette
    }
    img = Image.new("RGB", (w, h))
    for o, st in enumerate(status):
        img.putpixel((o % w, o // w), colors.get(st, (255, 255, 255)))
    out = f"{prefix}_class.png"
    img.resize((w * 3, h * 3), Image.NEAREST).save(out)
    print(f"[analyze] wrote {out}")

    diff = Image.new("RGB", (w, h))
    for o, px in enumerate(fb[:w * h]):
        if status[o] in (4, 5):
            diff.putpixel((o % w, o // w), (255, 0, 0))
        else:
            diff.putpixel((o % w, o // w), (px & 0xFF, (px >> 8) & 0xFF, (px >> 16) & 0xFF))
    out = f"{prefix}_bad_overlay.png"
    diff.resize((w * 3, h * 3), Image.NEAREST).save(out)
    print(f"[analyze] wrote {out}")


def replay_owner(frags, npx, w):
    zbuf = [-1] * npx
    z_owner = [-1] * npx
    paint_owner = [-1] * npx
    cov = [0] * npx
    with open(frags) as f:
        for ln in f:
            p = ln.split()
            if len(p) < 5:
                continue
            ep = int(p[0])
            x = int(p[1])
            y = int(p[2])
            z = int(p[3])
            o = y * w + x
            if o < 0 or o >= npx:
                continue
            zq = 0xFFFF if z > 0xFFFF else (0 if z < 0 else z)
            cov[o] = 1
            paint_owner[o] = ep
            if zq >= zbuf[o]:
                zbuf[o] = zq
                z_owner[o] = ep
    return cov, z_owner, paint_owner, zbuf


def replay_color(frags, npx, w):
    zbuf = [-1] * npx
    cov = [0] * npx
    col = [0] * npx
    frag_count = 0
    pass_count = 0
    with open(frags) as f:
        for ln in f:
            p = ln.split()
            if len(p) < 5:
                continue
            x = int(p[1])
            y = int(p[2])
            z = int(p[3])
            c = int(p[4], 16) & 0xFFFFFF
            o = y * w + x
            if o < 0 or o >= npx:
                continue
            frag_count += 1
            zq = 0xFFFF if z > 0xFFFF else (0 if z < 0 else z)
            if zq >= zbuf[o]:
                zbuf[o] = zq
                cov[o] = 1
                col[o] = c
                pass_count += 1
    return cov, col, frag_count, pass_count


def replay_color_check(fb_path, fb, frags, w, h):
    npx = w * h
    cov, exp, frag_count, pass_count = replay_color(frags, npx, w)
    covered = sum(cov)
    mismatches = []
    stray = 0
    for o in range(npx):
        got = fb[o] & 0xFFFFFF
        want = exp[o] & 0xFFFFFF
        if cov[o]:
            if got != want:
                if len(mismatches) < 12:
                    mismatches.append((o, got, want, "covered"))
        elif got != 0:
            stray += 1
            if len(mismatches) < 12:
                mismatches.append((o, got, want, "uncovered"))

    mismatch_count = 0
    for o in range(npx):
        got = fb[o] & 0xFFFFFF
        want = exp[o] & 0xFFFFFF
        if (cov[o] and got != want) or ((not cov[o]) and got != 0):
            mismatch_count += 1

    print(f"[analyze] fb={fb_path}")
    print(f"[analyze] owner=replay-color FB={w}x{h}")
    print(f"[analyze] replay_frags={frags}")
    print(f"[analyze] replay fragments={frag_count} zpass_updates={pass_count} covered={covered} stray_uncovered={stray}")
    print(f"[analyze] replay-color exact {npx - mismatch_count}/{npx} ({100.0 * (npx - mismatch_count) / npx:.2f}%) mismatches={mismatch_count}")
    for o, got, want, kind in mismatches:
        print(f"[analyze] bad {kind}: px={o} x={o % w} y={o // w} got={got:06x} exp={want:06x}")
    return 0 if mismatch_count == 0 else 1


def main(argv):
    args = argv[1:]
    fb_path = args[0] if args and not args[0].startswith("--") else os.path.join(DATA, "sh3_zsched_board_fb.mem")
    tag = args[args.index("--tag") + 1] if "--tag" in args else "zsched"
    owner_mode = args[args.index("--owner") + 1] if "--owner" in args else "z"
    frags = args[args.index("--frags") + 1] if "--frags" in args else os.path.join(ROOT, "sim", "traces", "rtl", "zsched_frags.txt")
    max_radius = int(args[args.index("--radius") + 1]) if "--radius" in args else 1
    emit_maps = "--maps" in args
    if owner_mode not in ("z", "paint", "replay", "replay-color"):
        raise SystemExit("[analyze] --owner must be z, paint, replay, or replay-color")

    p = parse_params(tag)
    w, h, tw, th, ne = p["FBPXW"], p["FBH"], p["TW"], p["TH"], p["N_EPOCHS"]
    npx = w * h

    fb = load_mem(fb_path)
    if len(fb) != npx:
        raise SystemExit(f"[analyze] {fb_path}: {len(fb)} words != expected {npx}")

    if owner_mode == "replay-color":
        return replay_color_check(fb_path, fb, frags, w, h)

    idx = [load_mem(os.path.join(DATA, f"sh3_{tag}{e}_idx.mem")) for e in range(ne)]
    pal = [load_mem(os.path.join(DATA, f"sh3_{tag}{e}_pal.mem")) for e in range(ne)]
    ref = load_mem(os.path.join(DATA, f"sh3_{tag}_refmap.mem"))
    rep = [load_mem(os.path.join(DATA, f"sh3_{tag}{e}_refmap.mem")) for e in range(ne)]
    zowner_path = os.path.join(DATA, f"sh3_{tag}_zowner.mem")
    zowner = load_mem(zowner_path) if os.path.exists(zowner_path) else None
    if owner_mode == "z" and zowner is None:
        raise SystemExit(f"[analyze] missing {zowner_path}; use --owner paint or regenerate z oracle")
    replay = replay_owner(frags, npx, w) if owner_mode == "replay" else None

    palette_set = set()
    for e in range(ne):
        palette_set.update(x & 0xFFFFFF for x in pal[e])

    totals = {
        "covered": 0, "interior": 0, "multi": 0, "reject": 0,
        "owner_r0": 0, "owner_r1": 0, "any_r1": 0,
        "interior_owner_r1": 0, "multi_owner_r1": 0, "reject_owner_r1": 0,
        "palette_bad": 0, "owner_missing_ref": 0,
    }
    status = [0] * npx
    owner_counts = [0] * ne
    by_owner = [{"cov": 0, "r0": 0, "r1": 0, "multi": 0, "multi_r1": 0, "reject": 0, "reject_r1": 0} for _ in range(ne)]
    radius_hist = [0] * (max_radius + 1)
    bad_examples = []

    for o in range(npx):
        if owner_mode == "z":
            zw = zowner[o]
            cov = (zw >> 31) & 1
            if not cov:
                continue
            owner = ((zw >> 24) & 0xF) - 1
            paint_owner = (zw & 0xF) - 1
            reject = owner != paint_owner
            multi = (ref[o] >> 28) & 1
            interior = (rep[owner][o] >> 30) & 1 if 0 <= owner < ne else 0
        elif owner_mode == "replay":
            covs, owners, paint_owners, _zq = replay
            if not covs[o]:
                continue
            owner = owners[o]
            paint_owner = paint_owners[o]
            reject = owner != paint_owner
            multi = (ref[o] >> 28) & 1
            interior = (rep[owner][o] >> 30) & 1 if 0 <= owner < ne else 0
        else:
            rw = ref[o]
            cov = (rw >> 31) & 1
            if not cov:
                continue
            owner = (rw >> 24) & 0x7
            reject = False
            multi = (rw >> 28) & 1
            interior = (rw >> 30) & 1

        totals["covered"] += 1
        totals["interior"] += int(interior)
        totals["multi"] += int(multi)
        totals["reject"] += int(reject)
        if 0 <= owner < ne:
            owner_counts[owner] += 1
            by_owner[owner]["cov"] += 1
            by_owner[owner]["multi"] += int(multi)
            by_owner[owner]["reject"] += int(reject)

        fbc = fb[o] & 0xFFFFFF
        if fbc not in palette_set:
            totals["palette_bad"] += 1
            status[o] = 5
            if len(bad_examples) < 12:
                bad_examples.append((o, owner, fbc, "palette"))
            continue

        owner_rad = None
        if 0 <= owner < ne and (rep[owner][o] >> 31):
            rm = rep[owner][o]
            tu = (rm >> 9) & 0x1FF
            tv = rm & 0x1FF
            owner_rad = match_radius(fbc, idx[owner], pal[owner], tw, th, tu, tv, max_radius)
        else:
            totals["owner_missing_ref"] += 1

        if owner_rad == 0:
            radius_hist[0] += 1
            totals["owner_r0"] += 1
            totals["owner_r1"] += 1
            totals["interior_owner_r1"] += int(interior)
            totals["multi_owner_r1"] += int(multi)
            totals["reject_owner_r1"] += int(reject)
            if 0 <= owner < ne:
                by_owner[owner]["r0"] += 1
                by_owner[owner]["r1"] += 1
                by_owner[owner]["multi_r1"] += int(multi)
                by_owner[owner]["reject_r1"] += int(reject)
            status[o] = 1
            continue
        if owner_rad == 1:
            radius_hist[1] += 1
            totals["owner_r1"] += 1
            totals["interior_owner_r1"] += int(interior)
            totals["multi_owner_r1"] += int(multi)
            totals["reject_owner_r1"] += int(reject)
            if 0 <= owner < ne:
                by_owner[owner]["r1"] += 1
                by_owner[owner]["multi_r1"] += int(multi)
                by_owner[owner]["reject_r1"] += int(reject)
            status[o] = 2
            continue

        if owner_rad is not None:
            radius_hist[owner_rad] += 1
            totals["owner_r1"] += 1
            totals["interior_owner_r1"] += int(interior)
            totals["multi_owner_r1"] += int(multi)
            totals["reject_owner_r1"] += int(reject)
            if 0 <= owner < ne:
                by_owner[owner]["r1"] += 1
                by_owner[owner]["multi_r1"] += int(multi)
                by_owner[owner]["reject_r1"] += int(reject)
            status[o] = 2
            continue

        any_ok = False
        for e in range(ne):
            if not (rep[e][o] >> 31):
                continue
            rm = rep[e][o]
            tu = (rm >> 9) & 0x1FF
            tv = rm & 0x1FF
            if match_radius(fbc, idx[e], pal[e], tw, th, tu, tv, max_radius) is not None:
                any_ok = True
                break
        if any_ok:
            totals["any_r1"] += 1
            status[o] = 3
        else:
            status[o] = 4
            if len(bad_examples) < 12:
                bad_examples.append((o, owner, fbc, "nomatch"))

    def pct(num, den):
        return 100.0 * num / den if den else 0.0

    print(f"[analyze] fb={fb_path}")
    print(f"[analyze] tag={tag} owner={owner_mode} FB={w}x{h} epochs={ne} radius={max_radius}")
    if owner_mode == "replay":
        print(f"[analyze] replay_frags={frags}")
    print(f"[analyze] oracle covered={totals['covered']} interior={totals['interior']} multi={totals['multi']} reject={totals['reject']}")
    print("[analyze] owner counts: " + " ".join(f"e{e}={owner_counts[e]}" for e in range(ne)))
    for e in range(ne):
        b = by_owner[e]
        rlbl = "<=1" if max_radius == 1 else f"<=R{max_radius}"
        print(f"[analyze] e{e}: {rlbl} {b['r1']}/{b['cov']} ({pct(b['r1'], b['cov']):.2f}%)  "
              f"multi{rlbl} {b['multi_r1']}/{b['multi']} ({pct(b['multi_r1'], b['multi']):.2f}%)  "
              f"reject{rlbl} {b['reject_r1']}/{b['reject']} ({pct(b['reject_r1'], b['reject']):.2f}%)")
    print(f"[analyze] owner exact      {totals['owner_r0']}/{totals['covered']} ({pct(totals['owner_r0'], totals['covered']):.2f}%)")
    if max_radius == 1:
        print(f"[analyze] owner <=1 texel  {totals['owner_r1']}/{totals['covered']} ({pct(totals['owner_r1'], totals['covered']):.2f}%)")
        print(f"[analyze] interior <=1     {totals['interior_owner_r1']}/{totals['interior']} ({pct(totals['interior_owner_r1'], totals['interior']):.2f}%)")
        print(f"[analyze] multi <=1        {totals['multi_owner_r1']}/{totals['multi']} ({pct(totals['multi_owner_r1'], totals['multi']):.2f}%)")
    else:
        print(f"[analyze] owner <=R texel  {totals['owner_r1']}/{totals['covered']} ({pct(totals['owner_r1'], totals['covered']):.2f}%)")
        print(f"[analyze] interior <=R     {totals['interior_owner_r1']}/{totals['interior']} ({pct(totals['interior_owner_r1'], totals['interior']):.2f}%)")
        print(f"[analyze] multi <=R        {totals['multi_owner_r1']}/{totals['multi']} ({pct(totals['multi_owner_r1'], totals['multi']):.2f}%)")
        print("[analyze] owner radius hist: " + " ".join(f"r{r}={radius_hist[r]}" for r in range(max_radius + 1)))
    if owner_mode == "z":
        print(f"[analyze] reject <=1       {totals['reject_owner_r1']}/{totals['reject']} ({pct(totals['reject_owner_r1'], totals['reject']):.2f}%)")
    print(f"[analyze] wrong-owner <=1  {totals['any_r1']}/{totals['covered']} ({pct(totals['any_r1'], totals['covered']):.2f}%)")
    print(f"[analyze] palette_bad={totals['palette_bad']} owner_missing_ref={totals['owner_missing_ref']}")
    if bad_examples:
        for o, owner, fbc, kind in bad_examples:
            print(f"[analyze] bad {kind}: px={o} x={o % w} y={o // w} owner=e{owner} fb={fbc:06x}")

    if emit_maps:
        prefix = os.path.splitext(fb_path)[0] + f"_{owner_mode}oracle"
        write_maps(prefix, w, h, fb, status)

    return 0 if totals["palette_bad"] == 0 else 1


if __name__ == "__main__":
    raise SystemExit(main(sys.argv))
