#!/usr/bin/env python3
"""retroDE_ps2 — Ch341 textured-triangle translator (declared affine ST/Q surrogate).

Extracts the earliest contiguous single-TEX0 textured-triangle subsegment that FITS staging (<=27 tris)
from a GS dump, derives faithful per-vertex texel coords u=S/Q, v=T/Q (FST=0 perspective ST), applies
the declared 256->64 downscale, and emits a ps2_feeder textured scene (tex0 + tritex) — BUT ONLY if the
honesty gate passes: the affine interpolation of vertex (S/Q,T/Q) must stay within MAX_ERR texels of the
true perspective interpolation over the triangle. Otherwise it FAILS CLOSED (Ch342 = real ST/Q).

This is NOT faithful perspective-correct GS texturing. It is authentic cube geometry + authentic
extracted texels through the affine-UV feeder, with a DECLARED affine substitute for perspective ST/Q
on a tiny-span segment. Sprites stay unsupported. Reports are aggregate; scene text is dump-derived.

Usage: gs_translate_tex.py <dump> [--tbp 64] [--dst 64] [--maxtri 27] [--scene out.txt] [--report r.txt]
"""
import sys, os, struct
sys.path.insert(0, os.path.dirname(__file__))
import gs_parse

MAX_ERR = 0.5   # texels, on the 64x64 fixture
def f32(bits): return struct.unpack("<f", struct.pack("<I", bits & 0xFFFFFFFF))[0]
def bit(v,b): return (v>>b)&1

def extract(events, dst, maxtri):
    """Walk events; return (segment tris, meta). Each tri: dict(scr=[(x,y)*3], z, stq=[(S,T,Q)*3], rgb)."""
    st_S=st_T=0.0; q=1.0; rgb=(255,255,255); tex0=0; fst=0; ptype=7; xyoff=0; clamp=0
    prim_val=0; test_val=0
    uploads_to={}   # TBP -> count of IMAGE uploads seen so far (to detect re-upload crossing)
    bitbltbuf=0
    vbuf=[]  # pending verts
    tris=[]  # each: dict
    seg=[]; seg_tex0=None; seg_clamp=None; seg_prim=None; seg_test=None; started=False; stop_reason=None
    first=None; last=None
    for e in events:
        if e.kind=="IMAGE":
            dbp=(bitbltbuf>>32)&0x3FFF
            uploads_to[dbp]=uploads_to.get(dbp,0)+1
            if started and seg_tex0 is not None and ((seg_tex0&0x3FFF)==dbp):
                stop_reason=f"re-upload to bound TBP {dbp} at event #{e.idx}"; break
            continue
        if e.kind!="GSREG": continue
        r=e.reg; v=e.value
        if   r=="BITBLTBUF": bitbltbuf=v
        elif r=="PRIM": ptype=v&7; fst=bit(v,8); prim_val=v; vbuf=[]
        elif r=="TEST_1": test_val=v
        elif r=="PRMODE": fst=bit(v,8)
        elif r=="ST":
            st_S=f32(v&0xFFFFFFFF); st_T=f32((v>>32)&0xFFFFFFFF)
            if "q_stq" in e.info: q=f32(e.info["q_stq"])   # PACKED ST routes Q -> RGBAQ.Q
        elif r=="RGBAQ": rgb=(v&0xFF,(v>>8)&0xFF,(v>>16)&0xFF); q=f32((v>>32)&0xFFFFFFFF)
        elif r=="TEX0_1": tex0=v
        elif r in ("CLAMP_1",): clamp=v
        elif r in ("XYOFFSET_1",): xyoff=v
        elif r in ("XYZ2","XYZ3","XYZF2","XYZF3"):
            x=(v&0xFFFF)/16.0 - (xyoff&0xFFFF)/16.0
            y=((v>>16)&0xFFFF)/16.0 - ((xyoff>>16)&0xFFFF)/16.0
            kick = e.reg in ("XYZ2","XYZF2")
            vbuf.append(dict(x=x,y=y,z=(v>>32)&0xFFFFFFFF,S=st_S,T=st_T,Q=q,rgb=rgb,tex0=tex0,fst=fst,clamp=clamp))
            if kick and ptype==3 and len(vbuf)>=3:
                t=vbuf[-3:]; vbuf=[]
                if not all(vv["fst"]==0 for vv in t):   # only FST=0 textured tris in this rung
                    if started: stop_reason="prim left FST=0 ST mode"; break
                    continue
                tt = t[0]["tex0"]
                if not started:
                    seg_tex0=tt; seg_clamp=t[0]["clamp"]; seg_prim=prim_val; seg_test=test_val; started=True; first=e.idx
                if tt!=seg_tex0:
                    stop_reason=f"TEX0 changed at event #{e.idx}"; break
                if t[0]["clamp"]!=seg_clamp:
                    stop_reason=f"CLAMP changed at event #{e.idx}"; break
                seg.append(t); last=e.idx
                if len(seg)>=maxtri:
                    stop_reason=f"staging cap ({maxtri} tris)"; break
    return seg, dict(tex0=seg_tex0, clamp=seg_clamp, prim=seg_prim, test=seg_test, first=first, last=last, stop=stop_reason)

def reconstruct_all_textured(events):
    """Every FST=0 textured triangle (3 verts) with its active tex0/clamp + texture-upload epoch
    (uploads-to-its-TBP seen so far). For the closeout all-window scan — NOT the mechanical earliest
    selection (extract() above is that). Documentation only, per Codex."""
    st_S=st_T=0.0; q=1.0; rgb=(255,255,255); tex0=0; fst=0; ptype=7; xyoff=0; clamp=0; bitbltbuf=0
    epoch={}; vbuf=[]; out=[]
    for e in events:
        if e.kind=="IMAGE":
            dbp=(bitbltbuf>>32)&0x3FFF; epoch[dbp]=epoch.get(dbp,0)+1; continue
        if e.kind!="GSREG": continue
        r=e.reg; v=e.value
        if   r=="BITBLTBUF": bitbltbuf=v
        elif r=="PRIM": ptype=v&7; fst=(v>>8)&1; vbuf=[]
        elif r=="PRMODE": fst=(v>>8)&1
        elif r=="ST":
            st_S=f32(v&0xFFFFFFFF); st_T=f32((v>>32)&0xFFFFFFFF)
            if "q_stq" in e.info: q=f32(e.info["q_stq"])   # PACKED ST routes Q -> RGBAQ.Q
        elif r=="RGBAQ": rgb=(v&0xFF,(v>>8)&0xFF,(v>>16)&0xFF); q=f32((v>>32)&0xFFFFFFFF)
        elif r=="TEX0_1": tex0=v
        elif r=="CLAMP_1": clamp=v
        elif r=="XYOFFSET_1": xyoff=v
        elif r in ("XYZ2","XYZ3","XYZF2","XYZF3"):
            kick=e.reg in ("XYZ2","XYZF2")
            vbuf.append(dict(S=st_S,T=st_T,Q=q))
            if kick and ptype==3 and len(vbuf)>=3:
                t=vbuf[-3:]; vbuf=[]
                if fst==0:
                    out.append(dict(v=t, tex0=tex0, clamp=clamp, epoch=epoch.get(tex0&0x3FFF,0)))
    return out

def scan_windows(tris, dst, maxtri):
    """Slide a <=maxtri window from every start; a window breaks on tex0/clamp/epoch change.
    Returns (n_windows, n_pass<=0.5, min_window_error)."""
    n=len(tris); npass=0; best=1e9; total=0
    for i in range(n):
        win=[tris[i]]
        for j in range(i+1, min(n, i+maxtri)):
            a,b=tris[i],tris[j]
            if a["tex0"]!=b["tex0"] or a["clamp"]!=b["clamp"] or a["epoch"]!=b["epoch"]: break
            win.append(tris[j])
        me=max(tri_error(t["v"], dst)[0] for t in win)
        total+=1; best=min(best,me)
        if me<=MAX_ERR: npass+=1
    return total, npass, best

def bary_samples():
    pts=[(1/3,1/3,1/3)]
    for a in (0.25,0.5,0.75):
        pts += [(a,(1-a)/2,(1-a)/2),((1-a)/2,a,(1-a)/2),((1-a)/2,(1-a)/2,a)]
    return pts

def tri_error(t, dst):
    """max |affine - perspective| texel error over the triangle, for u and v (dst-sized texture)."""
    SQ=[(v["S"]/v["Q"] if v["Q"] else 0.0, v["T"]/v["Q"] if v["Q"] else 0.0) for v in t]
    uv=[(sq[0]*dst, sq[1]*dst) for sq in SQ]
    me=0.0
    for (b0,b1,b2) in bary_samples():
        Sb=b0*t[0]["S"]+b1*t[1]["S"]+b2*t[2]["S"]; Tb=b0*t[0]["T"]+b1*t[1]["T"]+b2*t[2]["T"]
        Qb=b0*t[0]["Q"]+b1*t[1]["Q"]+b2*t[2]["Q"]
        if Qb==0: continue
        up=(Sb/Qb)*dst; vp=(Tb/Qb)*dst
        ua=b0*uv[0][0]+b1*uv[1][0]+b2*uv[2][0]; va=b0*uv[0][1]+b1*uv[1][1]+b2*uv[2][1]
        me=max(me, abs(up-ua), abs(vp-va))
    return me, uv

def main(argv):
    if len(argv)<2: print(__doc__); return 2
    path=argv[1]
    def opt(n,d=None): return argv[argv.index(n)+1] if n in argv else d
    tbp=int(opt("--tbp","64")); dst=int(opt("--dst","64")); maxtri=int(opt("--maxtri","27"))
    h, events = gs_parse.parse_dump(path)
    fst0 = sum(1 for e in events if e.kind=="GSREG" and e.reg=="PRIM" and ((e.value>>8)&1)==0)
    seg, meta = extract(events, dst, maxtri)
    R=[f"# Ch341 textured-triangle translation  (source {os.path.basename(path)}; aggregate facts + dump-derived scene)"]
    R.append(f"FST=0 (perspective ST) PRIM submissions: {fst0}")
    if not seg:
        R.append(f"NO textured-tri segment selected (stop: {meta['stop']}).")
        print("\n".join(R)); return 0

    # Ch342 — FAITHFUL perspective ST/Q emit (no affine, no error gate; the Ch301 path is exact).
    # Packs the Ch301 fixed-point contract: S_fp=round(S*4096), T_fp=round(T*4096), Q_fp=round(Q*4096)
    # (24-bit FRAC=12). The 256->64 downscale is in the TEXTURE (TW=6); ST/Q stay normalized — the GS
    # computes texel=(S/Q)*2^TW=(S/Q)*64. Fails closed on Q<=0 or fixed-point overflow.
    if "--perspective" in argv:
        xs=[v["x"] for t in seg for v in t]; ys=[v["y"] for t in seg for v in t]
        x0,x1,y0,y1=min(xs),max(xs),min(ys),max(ys)
        FB=64; sc=min((FB-2)/max(1.0,x1-x0),(FB-2)/max(1.0,y1-y0))
        fx=lambda x:max(0,min(FB-1,1+int((x-x0)*sc))); fy=lambda y:max(0,min(FB-1,1+int((y-y0)*sc)))
        L=[f"# Ch342 authentic cube subsegment ({len(seg)} tris) — FAITHFUL perspective ST/Q (Ch301 fixed-point, FRAC=12)",
           f"# events #{meta['first']}..#{meta['last']}; screen ({x0:.0f},{y0:.0f})..({x1:.0f},{y1:.0f}) viewport-fit -> {FB}x{FB}; texture 256->{dst}",
           f"tex0 {tbp} 1 6 6 0", "persp"]
        for t in seg:
            a=[]
            for v in t:
                if v["Q"]<=0:
                    R.append(f"GATE FAILED: Q<=0 ({v['Q']}) at event #{meta['first']} — fail closed."); print("\n".join(R)); return 0
                sfp=round(v["S"]*4096); tfp=round(v["T"]*4096); qfp=round(v["Q"]*4096)
                if not (0<=sfp<=0xFFFFFF and 0<=tfp<=0xFFFFFF and 0<qfp<=0xFFFFFF):
                    R.append(f"GATE FAILED: ST/Q fixed-point out of 24-bit range -> fail closed."); print("\n".join(R)); return 0
                a += [fx(v["x"]), fy(v["y"]), sfp, tfp, qfp]
            (r,g,b)=t[-1]["rgb"]; z=t[-1]["z"]
            L.append("persptri "+" ".join(map(str,a+[z,r,g,b])))
        L.append("go")
        Qs=[v["Q"] for t in seg for v in t]
        pv=meta["prim"] or 0; tv=meta["test"] or 0
        R.append(f"segment PRIM: type={pv&7}(TRI=3) IIP={(pv>>3)&1} TME={(pv>>4)&1} FGE={(pv>>5)&1} ABE={(pv>>6)&1} FST={(pv>>8)&1}"
                 f"  TEST_1: ZTE={(tv>>16)&1} ZTST={(tv>>17)&3}(GEQ=2)")
        if ((pv>>6)&1):
            R.append("WARN: segment ABE=1 -> routes to the combined-TAZ path (perspective there is a known follow-on bug), NOT the proven S1 path. Do NOT flip ABE.")
        R.append("S1 perspective path honors TME+FST=0 + ZTE/ZTST GEQUAL; cube segment is ABE=0 (S1 path).")
        R.append(f"FAITHFUL PERSPECTIVE: {len(seg)} tris, TEX0->TBP={tbp} TW=6 TH=6 TFX=0; S_fp/T_fp/Q_fp=round(*4096); Q span {min(Qs):.4f}..{max(Qs):.4f}")
        R.append(f"staging words: {7+9*len(seg)} (perspective format word0[32]=1, no rects)")
        print("\n".join(R)+"\n")
        sp=opt("--scene")
        if sp: open(sp,"w").write("\n".join(L)+"\n"); print(f"[wrote {len(seg)}-tri FAITHFUL perspective scene -> {sp}]")
        if opt("--report"): open(opt("--report"),"w").write("\n".join(R)+"\n")
        return 0

    # span + Q + error
    Qs=[v["Q"] for t in seg for v in t]; Ss=[v["S"] for t in seg for v in t]; Ts=[v["T"] for t in seg for v in t]
    maxerr=0.0
    for t in seg:
        me,_=tri_error(t,dst); maxerr=max(maxerr,me)
    tx=tbp; tex0=meta["tex0"]
    R.append(f"selected segment: {len(seg)} triangles, events #{meta['first']}..#{meta['last']}, stop after: {meta['stop']}")
    R.append(f"active TEX0 (orig): TBP0={tex0&0x3FFF} TW={(tex0>>26)&0xF} TH={(tex0>>30)&0xF} TFX={(tex0>>35)&3}")
    R.append(f"relocated TEX0 (fixture): TBP0={tbp} TBW=1 TW=6 TH=6 TFX=0  (downscale 256->{dst}, UV scale /{256//dst})")
    R.append(f"Q span: {min(Qs):.4f}..{max(Qs):.4f}   S span {min(Ss):.4f}..{max(Ss):.4f}   T span {min(Ts):.4f}..{max(Ts):.4f}")
    R.append(f"perspective-vs-affine max error: {maxerr:.4f} texels (threshold {MAX_ERR})")
    ok = maxerr <= MAX_ERR
    staging_words = 7 + 9*len(seg)
    R.append(f"staging words: {staging_words} (<=256: {'ok' if staging_words<=256 else 'OVERFLOW'})")
    if not ok:
        R.append("GATE FAILED: affine ST/Q surrogate exceeds error threshold -> FAIL CLOSED. Ch342 = real ST/Q through the feeder.")
    else:
        R.append(f"DECLARED APPROXIMATION: perspective ST/Q rendered as affine UV; max_error={maxerr:.4f} texels (NOT faithful perspective-correct texturing).")
    print("\n".join(R)+"\n")
    if opt("--report"):     # write the report in BOTH branches (the failure report is the useful one)
        open(opt("--report"),"w").write("\n".join(R)+"\n"); print(f"[wrote report -> {opt('--report')}]")
    if not ok:
        return 0
    sp=opt("--scene")
    if sp:
        # viewport-fit the segment's screen bbox into [1,dst-2] (declared, like Ch340)
        xs=[v["x"] for t in seg for v in t]; ys=[v["y"] for t in seg for v in t]
        x0,x1,y0,y1=min(xs),max(xs),min(ys),max(ys)
        FB=64; s=min((FB-2)/max(1.0,x1-x0),(FB-2)/max(1.0,y1-y0))
        def fx(x): return max(0,min(FB-1,1+int((x-x0)*s)))
        def fy(y): return max(0,min(FB-1,1+int((y-y0)*s)))
        L=[f"# Ch341 authentic cube subsegment ({len(seg)} tris) — DECLARED affine ST/Q surrogate, max_error={maxerr:.3f} texels",
           f"# screen bbox ({x0:.0f},{y0:.0f})..({x1:.0f},{y1:.0f}) viewport-fit -> {FB}x{FB}; texture 256->{dst} downscale",
           f"tex0 {tbp} 1 6 6 0"]
        for t in seg:
            a=[]
            for v in t:
                u=max(0,min(dst-1,int(round((v["S"]/v["Q"])*dst)))) if v["Q"] else 0
                w=max(0,min(dst-1,int(round((v["T"]/v["Q"])*dst)))) if v["Q"] else 0
                a += [fx(v["x"]), fy(v["y"]), u, w]
            (r,g,b)=t[-1]["rgb"]; z=t[-1]["z"]
            L.append("tritex "+" ".join(map(str,a+[z,r,g,b])))
        L.append("go")
        open(sp,"w").write("\n".join(L)+"\n"); print(f"[wrote {len(seg)}-tri textured scene -> {sp}]")
    return 0

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