#!/usr/bin/env python3
"""retroDE_ps2 — Ch341 Brick 1: host-side texture state analysis + extraction from a GS dump.

Decodes the texture UPLOADS (BITBLTBUF / TRXREG / TRXPOS / TRXDIR + IMAGE) and TEX0 BINDS in a dump,
matches the dominant textured-TRIANGLE primitives to the texture they sample, and finds the EARLIEST
contiguous textured-triangle segment that uses a SINGLE TEX0 bind (the no-RTL Ch341 v1 target: one
scene-level TEX0 + per-vertex real UV). Reports aggregate facts (committable). With --extract it
writes the matched texture blob + a generated-fixture descriptor LOCALLY (gitignored, per provenance).

Usage:
  gs_texture.py <dump.gs[.xz|.zst]> [--report out.txt] [--extract outdir]
"""
import sys, os, json
sys.path.insert(0, os.path.dirname(__file__))
import gs_parse, gs_translate

PSM = {0x00:"PSMCT32",0x01:"PSMCT24",0x02:"PSMCT16",0x0A:"PSMCT16S",0x13:"PSMT8",0x14:"PSMT4",
       0x1B:"PSMT8H",0x24:"PSMT4HL",0x2C:"PSMT4HH",0x30:"PSMZ32",0x31:"PSMZ24",0x32:"PSMZ16",0x3A:"PSMZ16S"}
BPP = {0x00:32,0x01:24,0x02:16,0x0A:16,0x13:8,0x14:4,0x1B:8,0x24:4,0x2C:4}

def dec_bitbltbuf(v): return dict(SBP=v&0x3FFF,SBW=(v>>16)&0x3F,SPSM=(v>>24)&0x3F,DBP=(v>>32)&0x3FFF,DBW=(v>>48)&0x3F,DPSM=(v>>56)&0x3F)
def dec_trxreg(v):    return dict(RRW=v&0xFFF, RRH=(v>>32)&0xFFF)
def dec_trxpos(v):    return dict(DSAX=v&0x7FF, DSAY=(v>>16)&0x7FF)
def dec_tex0(v):      return dict(TBP0=v&0x3FFF,TBW=(v>>14)&0x3F,PSM=(v>>20)&0x3F,TW=(v>>26)&0xF,TH=(v>>30)&0xF,TCC=(v>>34)&1,TFX=(v>>35)&3)

def analyze(path):
    d = gs_parse.read_dump_bytes(path)
    h, events = gs_parse.parse_dump(path)
    # --- pass 1: texture uploads (transfer state machine) ---
    st = dict(bitbltbuf=0, trxreg=0, trxpos=0)
    uploads = []   # each: dict(dbp,dbw,dpsm,w,h,bytes,data_off,event_idx)
    for e in events:
        if e.kind=="GSREG":
            if e.reg=="BITBLTBUF": st["bitbltbuf"]=e.value
            elif e.reg=="TRXREG":  st["trxreg"]=e.value
            elif e.reg=="TRXPOS":  st["trxpos"]=e.value
        elif e.kind=="IMAGE":
            bb=dec_bitbltbuf(st["bitbltbuf"]); rr=dec_trxreg(st["trxreg"]); tp=dec_trxpos(st["trxpos"])
            uploads.append(dict(dbp=bb["DBP"],dbw=bb["DBW"],dpsm=bb["DPSM"],w=rr["RRW"],h=rr["RRH"],
                                dsax=tp["DSAX"],dsay=tp["DSAY"],bytes=e.info.get("bytes",0),
                                data_off=e.byte_off,event_idx=e.idx))
    # --- pass 2: triangles + their active TEX0 ---
    prims,_ = gs_translate.reconstruct_prims(events)
    # track TEX0 active at each triangle by re-walking (reconstruct doesn't keep tex0)
    tex0_at = {}
    cur_tex0 = 0
    pi = 0
    # rebuild active TEX0 per primitive index, matching reconstruct's order
    # (simple: re-run the kick model tracking tex0)
    cur_tex0=0; vcount=0; ptype=7; idxmap=[]
    for e in events:
        if e.kind=="GSREG":
            if e.reg=="TEX0_1": cur_tex0=e.value
            elif e.reg=="PRIM": ptype=e.value&7; vcount=0
            elif e.reg in ("XYZ2","XYZ3","XYZF2","XYZF3"):
                need={0:1,1:2,2:2,3:3,4:3,5:3,6:2}.get(ptype,99)
                kick = e.reg in ("XYZ2","XYZF2")
                vcount+=1
                if kick and vcount>=need:
                    idxmap.append(cur_tex0)
                    if ptype in (3,6): vcount=0
    tris = [(p, idxmap[i] if i < len(idxmap) else 0) for i,p in enumerate(prims) if p.type==3]
    # --- earliest contiguous textured-triangle segment with a SINGLE TEX0 ---
    seg=[]; seg_tex0=None
    for (p,t) in tris:
        c,_=gs_translate.classify(p)   # textured tri -> unsupported in v0 envelope, but here we WANT textured
        if not p.tme:
            if seg: break
            else: continue
        if seg_tex0 is None: seg_tex0=t; seg=[p]
        elif t==seg_tex0: seg.append(p)
        else: break                    # crossed a TEX0 bind -> stop (single-TEX0 segment)
    return h, uploads, tris, seg, seg_tex0

def match_upload(uploads, tex0):
    tx=dec_tex0(tex0)
    for u in uploads:
        if u["dbp"]==tx["TBP0"] and u["dpsm"]==tx["PSM"]:
            return u
    # fall back: TBP match only
    for u in uploads:
        if u["dbp"]==tx["TBP0"]:
            return u
    return None

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
    h, uploads, tris, seg, seg_tex0 = analyze(path)
    R=[]
    R.append(f"# Ch341 Brick 1 texture analysis  (source {os.path.basename(path)} serial={h.serial!r}; aggregate facts only)")
    R.append(f"texture uploads: {len(uploads)}")
    seen=set()
    for u in uploads:
        k=(u["dbp"],u["dpsm"],u["w"],u["h"])
        if k in seen: continue
        seen.add(k)
        R.append(f"  TBP={u['dbp']} DBW={u['dbw']} PSM={PSM.get(u['dpsm'],hex(u['dpsm']))} "
                 f"{u['w']}x{u['h']} bytes={u['bytes']} (expect {u['w']*u['h']*BPP.get(u['dpsm'],0)//8})")
    R.append(f"textured triangles: {sum(1 for p,_ in tris if p.tme)} / {len(tris)} total triangles")
    if seg:
        tx=dec_tex0(seg_tex0)
        R.append("")
        R.append(f"EARLIEST single-TEX0 textured-tri segment: {len(seg)} triangles")
        R.append(f"  TEX0: TBP0={tx['TBP0']} TBW={tx['TBW']} PSM={PSM.get(tx['PSM'],hex(tx['PSM']))} "
                 f"TW={tx['TW']}({1<<tx['TW']}px) TH={tx['TH']}({1<<tx['TH']}px) TFX={tx['TFX']}")
        u=match_upload(uploads, seg_tex0)
        if u:
            R.append(f"  -> matched upload: TBP={u['dbp']} {u['w']}x{u['h']} {PSM.get(u['dpsm'],hex(u['dpsm']))} "
                     f"bytes={u['bytes']} data@0x{u['data_off']:x}")
            R.append(f"  VERDICT: single scene-level TEX0 + per-vertex UV — NO RTL feeder change needed for v1.")
        else:
            R.append(f"  !! no matching upload found for TBP0={tx['TBP0']} — texture may be CLUT/region or uploaded elsewhere.")
    else:
        R.append("NO single-TEX0 textured-triangle segment found (every textured run crosses TEX0 binds).")
    report="\n".join(R)+"\n"
    print(report)
    if opt("--report"):
        open(opt("--report"),"w").write(report); print(f"[wrote report -> {opt('--report')}]")
    outdir=opt("--extract")
    if outdir and seg:
        u=match_upload(uploads, seg_tex0)
        if u:
            os.makedirs(outdir, exist_ok=True)
            d = gs_parse.read_dump_bytes(path)
            blob = d[u["data_off"]:u["data_off"]+u["bytes"]]
            bp=os.path.join(outdir,"tex0_blob.bin"); open(bp,"wb").write(blob)
            desc=dict(schema=1, tbp0=dec_tex0(seg_tex0)["TBP0"], tbw=dec_tex0(seg_tex0)["TBW"],
                      psm=u["dpsm"], psm_name=PSM.get(u["dpsm"]), w=u["w"], h=u["h"], bytes=u["bytes"],
                      tw=dec_tex0(seg_tex0)["TW"], th=dec_tex0(seg_tex0)["TH"], tfx=dec_tex0(seg_tex0)["TFX"],
                      provenance="cubes_demo (MIT, glampert/ps2-homebrew) — LOCAL only")
            open(os.path.join(outdir,"tex0_desc.json"),"w").write(json.dumps(desc,indent=2))
            print(f"[extracted {len(blob)}-byte texture blob + descriptor -> {outdir}/ (LOCAL, gitignored)]")
    return 0

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