#!/usr/bin/env python3
"""Compare captured RTL Z fragments against the fixture's software Z planes.

This is intentionally per-fragment, not per-final-frame. It reconstructs the
same clipped triangles used by the ZSCHED feeder fixture, then asks whether each
captured RTL fragment's Z is close to the expected screen-linear interpolated Z
at that pixel.

Usage:
  tools/diagnose_zsched_frag_z.py [sim/traces/rtl/zsched_frags.txt]
"""
import os
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")
sys.path.insert(0, HERE)

import gs_make_sh3_multidraw_fixture as MD


def load_epochs(tag="zsched"):
    path = os.path.join(DATA, f"sh3_{tag}_epochs.txt")
    out = []
    with open(path) as f:
        for ln in f:
            s = ln.strip()
            if not s or s.startswith("#") or s.startswith("META"):
                continue
            p = s.split()
            out.append(int(p[1]))
    return out


def find_dump():
    import glob
    c = glob.glob(os.path.join(ROOT, "captures", "gs", "silenthill3", "*224139*.gs.zst"))
    if not c:
        raise SystemExit("[fragz] no SH3 224139 dump found")
    return c[0]


def edge(ax, ay, bx, by, px, py):
    return (px - ax) * (by - ay) - (py - ay) * (bx - ax)


def clip_rect_z(tri, w, h):
    def lerp(p1, p2, al):
        return {k: (p1[k] + al * (p2[k] - p1[k])) for k in ("x", "y", "z", "s", "t", "q")}

    def clip_edge(poly, inside, isect):
        out = []
        n = len(poly)
        for i in range(n):
            a = poly[i]
            b = poly[(i + 1) % n]
            ina = inside(a)
            inb = inside(b)
            if ina:
                out.append(a)
            if ina != inb:
                out.append(isect(a, b))
        return out

    poly = [dict(x=v["x"], y=v["y"], z=v.get("z", 0.0), s=v["s"], t=v["t"], q=v["q"]) for v in tri]
    poly = clip_edge(poly, lambda p: p["x"] >= 0.0, lambda a, b: lerp(a, b, (0.0 - a["x"]) / (b["x"] - a["x"])))
    if not poly:
        return []
    poly = clip_edge(poly, lambda p: p["x"] <= w, lambda a, b: lerp(a, b, (w - a["x"]) / (b["x"] - a["x"])))
    if not poly:
        return []
    poly = clip_edge(poly, lambda p: p["y"] >= 0.0, lambda a, b: lerp(a, b, (0.0 - a["y"]) / (b["y"] - a["y"])))
    if not poly:
        return []
    poly = clip_edge(poly, lambda p: p["y"] <= h, lambda a, b: lerp(a, b, (h - a["y"]) / (b["y"] - a["y"])))
    if len(poly) < 3:
        return []
    return [(poly[0], poly[k], poly[k + 1]) for k in range(1, len(poly) - 1)]


def tri_z_at(tri, x, y):
    v0, v1, v2 = tri
    ar = edge(v0["x"], v0["y"], v1["x"], v1["y"], v2["x"], v2["y"])
    if abs(ar) < 1e-9:
        return None
    inv = 1.0 / ar
    w0 = edge(v1["x"], v1["y"], v2["x"], v2["y"], x, y) * inv
    w1 = edge(v2["x"], v2["y"], v0["x"], v0["y"], x, y) * inv
    w2 = 1.0 - w0 - w1
    if w0 < -0.001 or w1 < -0.001 or w2 < -0.001:
        return None
    return w0 * v0["z"] + w1 * v1["z"] + w2 * v2["z"]


def build_epoch_tris(draw_idxs, fbw, fbh):
    got, _ = MD.load_draws(find_dump(), draw_idxs)
    eps = [got[i] for i in draw_idxs]
    ox = int(min(min(v["x"] for v in e["verts"]) for e in eps))
    oy = int(min(min(v["y"] for v in e["verts"]) for e in eps))
    out = []
    for e in eps:
        fv = [dict(x=v["x"] - ox, y=v["y"] - oy, z=v["z"], s=v["s"], t=v["t"], q=v["q"]) for v in e["verts"]]
        raw = [(fv[i - 2], fv[i - 1], fv[i]) for i in range(2, len(fv))]
        tris = []
        for tri in raw:
            tris.extend(clip_rect_z(tri, fbw, fbh))
        out.append(tris)
    return out


def best_expected(tris, x, y, sample_center):
    sx = x + 0.5 if sample_center else float(x)
    sy = y + 0.5 if sample_center else float(y)
    vals = []
    for tri in tris:
        z = tri_z_at(tri, sx, sy)
        if z is not None:
            vals.append(z)
    return vals


def main(argv):
    frags = argv[1] if len(argv) > 1 else os.path.join(ROOT, "sim", "traces", "rtl", "zsched_frags.txt")
    draw_idxs = load_epochs("zsched")
    fbw, fbh = 256, 210
    tris_by_ep = build_epoch_tris(draw_idxs, fbw, fbh)

    stats = {
        "n": 0, "nocov_center": 0, "nocov_corner": 0,
        "center_le0": 0, "center_le1": 0, "center_le16": 0, "center_le256": 0,
        "corner_le0": 0, "corner_le1": 0, "corner_le16": 0, "corner_le256": 0,
    }
    abs_sum_center = 0.0
    abs_sum_corner = 0.0
    ratios = []
    examples = []

    with open(frags) as f:
        for ln in f:
            p = ln.split()
            if len(p) < 5:
                continue
            ep, x, y, rz = int(p[0]), int(p[1]), int(p[2]), int(p[3])
            stats["n"] += 1
            cvals = best_expected(tris_by_ep[ep], x, y, True)
            kvals = best_expected(tris_by_ep[ep], x, y, False)
            if not cvals:
                stats["nocov_center"] += 1
                cdiff = None
            else:
                cz = min(cvals, key=lambda z: abs(rz - z))
                cdiff = abs(rz - cz)
                abs_sum_center += cdiff
                if cz:
                    ratios.append(rz / cz)
                if cdiff <= 0.5:
                    stats["center_le0"] += 1
                if cdiff <= 1:
                    stats["center_le1"] += 1
                if cdiff <= 16:
                    stats["center_le16"] += 1
                if cdiff <= 256:
                    stats["center_le256"] += 1
            if not kvals:
                stats["nocov_corner"] += 1
                kdiff = None
            else:
                kz = min(kvals, key=lambda z: abs(rz - z))
                kdiff = abs(rz - kz)
                abs_sum_corner += kdiff
                if kdiff <= 0.5:
                    stats["corner_le0"] += 1
                if kdiff <= 1:
                    stats["corner_le1"] += 1
                if kdiff <= 16:
                    stats["corner_le16"] += 1
                if kdiff <= 256:
                    stats["corner_le256"] += 1
            if len(examples) < 16 and ((cdiff is None or cdiff > 256) and (kdiff is None or kdiff > 256)):
                examples.append((ep, x, y, rz, cdiff, kdiff, cvals[:3], kvals[:3]))

    n = stats["n"]
    def pct(k):
        return 100.0 * stats[k] / n if n else 0.0

    print(f"[fragz] fragments={n} draw_idxs={draw_idxs}")
    print(f"[fragz] center coverage misses={stats['nocov_center']} corner coverage misses={stats['nocov_corner']}")
    print(f"[fragz] center absdiff: <=0.5 {stats['center_le0']} ({pct('center_le0'):.2f}%)  <=1 {stats['center_le1']} ({pct('center_le1'):.2f}%)  <=16 {stats['center_le16']} ({pct('center_le16'):.2f}%)  <=256 {stats['center_le256']} ({pct('center_le256'):.2f}%)")
    print(f"[fragz] corner absdiff: <=0.5 {stats['corner_le0']} ({pct('corner_le0'):.2f}%)  <=1 {stats['corner_le1']} ({pct('corner_le1'):.2f}%)  <=16 {stats['corner_le16']} ({pct('corner_le16'):.2f}%)  <=256 {stats['corner_le256']} ({pct('corner_le256'):.2f}%)")
    if n - stats["nocov_center"] > 0:
        print(f"[fragz] mean center absdiff={abs_sum_center / (n - stats['nocov_center']):.2f}")
    if n - stats["nocov_corner"] > 0:
        print(f"[fragz] mean corner absdiff={abs_sum_corner / (n - stats['nocov_corner']):.2f}")
    if ratios:
        ratios.sort()
        mid = ratios[len(ratios) // 2]
        print(f"[fragz] raw/expected center ratio: p10={ratios[len(ratios)//10]:.4f} median={mid:.4f} p90={ratios[(len(ratios)*9)//10]:.4f}")
    for ep, x, y, rz, cdiff, kdiff, cvals, kvals in examples:
        print(f"[fragz] bad ep{ep} ({x},{y}) rtl_z={rz} center_diff={cdiff} corner_diff={kdiff} center_z={[round(v,1) for v in cvals]} corner_z={[round(v,1) for v in kvals]}")
    return 0


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