#!/usr/bin/env python3
"""Ch343 — build the AUTHENTIC cube board fixtures (LOCAL, dump-derived).

Inputs (local, from the cubes .gs dump pipeline):
  captures/gs/cubes/extracted/cube_tex_64.bin    64x64 PSMCT32 cube texture (downscaled)
  captures/gs/cubes/extracted/cube_persp.scene.txt  translated perspective scene (ps2_feeder)

Outputs into sim/data/top_psmct32_raster_demo/ (gitignored — game-derived content):
  payload_cube_setup.mem   boot GIF payload: upload the 64x64 cube texture @ TBP=64 (32 KiB EE RAM)
  bios_cube_setup.mem      one-shot bootlet, QWC = payload qword count
  feeder_cube_persp.mem    cube perspective staging (ps2_feeder --dump-file cube_persp.scene)

Reuses bake.py's GIF packers + bootlet builder so the framing matches the proven feeder_persp path
exactly; only the texture size (64x64 vs 16x16) and the RAM/QWC budget differ. ABE=0 S1 perspective.
This GENERATOR is committable; its .mem outputs are not (provenance: authentic GS dump content)."""

import os
import sys
import struct
import subprocess

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, DATA)
import bake  # noqa: E402  (reuse giftag/aplusd/*_pack/bootlet/write_bios_mem)

TEX_BIN = os.path.join(ROOT, "captures", "gs", "cubes", "extracted", "cube_tex_64.bin")
SCENE   = os.path.join(ROOT, "captures", "gs", "cubes", "extracted", "cube_persp.scene.txt")

# VRAM layout (64 KiB / 16384 words) — three 64x64 surfaces must NOT alias:
#   FB   FBP=0  -> words 0..4095        ZBP units = 2048 words
#   Z    ZBP=2  -> words 4096..8191     (ps2_feeder hardcodes zbuf1_pack(2) + GEQUAL)
#   TEX  TBP=128-> words 8192..12287    TBP0 units = 64 words
# The scene's translator picked TBP=64 (word 4096), which ALIASES the Z buffer: the render then reads
# texel values (~0xFF000000) as Z and GEQUAL rejects the whole cube. Relocate the texture past Z.
TBP = 128         # word 8192 — clear of FB (0..4095) and Z (4096..8191)
TBW = 1           # 64-wide -> TBW=1 (64-px stride); matches the scene's tex0 stride
W = H = 64
RAM_QWORDS = 2048 # 32 KiB / 16  (EE RAM payload; unrelated to VRAM)

for p in (TEX_BIN, SCENE):
    if not os.path.exists(p):
        sys.exit(f"missing local input (run the cubes .gs extraction first): {p}")

# --- 64x64 PSMCT32 texels, raster order (ty*64 + tx), little-endian u32 ---
raw = open(TEX_BIN, "rb").read()
if len(raw) != W * H * 4:
    sys.exit(f"{TEX_BIN}: expected {W*H*4} bytes, got {len(raw)}")
texels = list(struct.unpack(f"<{W*H}I", raw))

# --- boot GIF payload: BITBLTBUF/TRXPOS/TRXREG/TRXDIR (A+D) + IMAGE upload ---
qw = []
qw.append(bake.giftag(1, 0, 0, 4, 0x0000_0000_0000_EEEE))      # 4x A+D
qw.append(bake.aplusd(bake.R_BITBLTBUF, bake.bitbltbuf_pack(TBP, TBW, 0)))
qw.append(bake.aplusd(bake.R_TRXPOS,    bake.trxpos_pack(0, 0)))
qw.append(bake.aplusd(bake.R_TRXREG,    bake.trxreg_pack(W, H)))
qw.append(bake.aplusd(bake.R_TRXDIR,    bake.trxdir_pack(0)))  # host -> local
n_image = (W * H) // 4                                         # 4 texels / qword = 1024
qw.append(bake.giftag(n_image, 1, 2, 0, 0))                   # IMAGE, EOP=1 (setup-only payload)
for i in range(n_image):
    word = 0
    for lane in range(4):
        word |= (texels[i * 4 + lane] & 0xFFFFFFFF) << (32 * lane)
    qw.append(word)

qwc = len(qw)                                                  # 6 + 1024 = 1030
if 16 + qwc > RAM_QWORDS:
    sys.exit(f"payload {qwc} qw + 16 lead > {RAM_QWORDS} (bump RAM_SIZE_BYTES)")

payload_path = os.path.join(DATA, "payload_cube_setup.mem")
with open(payload_path, "w") as f:
    f.write("// Ch343 LOCAL authentic-cube texture boot payload (64x64 PSMCT32 @ TBP=64).\n")
    f.write("// game-derived (cubes .gs) -> gitignored. qw 0..15 zero; qw 16.. = GIF upload.\n")
    for _ in range(16):
        f.write(f"{0:032x}\n")
    for x in qw:
        f.write(f"{x & ((1 << 128) - 1):032x}\n")
    for _ in range(RAM_QWORDS - 16 - qwc):
        f.write(f"{0:032x}\n")

# --- one-shot bootlet (same shape as feeder_persp setup; QWC chosen for the cube payload) ---
bake.write_bios_mem(
    "bios_cube_setup.mem",
    bake.build_textured_demo_bootlet_disp(qwc, bake.CAP_DISPLAY1_HI, bake.CAP_FBW),
    f"Ch343 LOCAL authentic-cube setup bootlet (QWC={qwc}). game-derived -> gitignored.")

# --- cube perspective staging via ps2_feeder --dump-file ---
# The translator bound TEX0 TBP0 to a word that aliases the Z buffer; rewrite the tex0 line so the
# render samples the relocated texture (TBP) instead. Single source of truth = the TBP above.
# The translator grabbed the EARLIEST ≤27-tri textured run, which is a WIDE-SHORT strip of ~4 tiny
# cubes (source 280x75 viewport-fit to 64x64 -> squashed to a y:[0..17] band of speckle). Isolate ONE
# cube (CUBE_TRIS contiguous tris) and re-fit it to fill the frame: a legitimate viewport zoom. The
# perspective ST/Q are interpolated linearly in SCREEN space, so a uniform 2D scale of the screen verts
# preserves per-pixel u=S/Q,v=T/Q exactly — faithful, just bigger. The first 6 tris (lines 5..10) are a
# corner-view cube: 3 faces meeting at center vertex (59,13).
CUBE_FIRST = 0     # index of the first persptri of the chosen cube
CUBE_TRIS  = 6     # 3 faces x 2 tris
MARGIN     = 2     # leave a 2px border in the 64x64 frame
SPAN_PX    = (W - 1) - 2*MARGIN

# Two faithful coordinate conversions applied to the selected cube:
#   (a) ST normalized->texel: retroDE's gs_persp_uv recovers the TEXEL index directly (expects S/Q in
#       0..TW, as bake.py persp_attrs emits S=u_texel/w). The dump's ST are NORMALIZED (0..1) so S/Q
#       lands in [0,1] -> samples only texel (0,0). Scale S by TW, T by TH (what real GS does internally).
#   (b) TEX0 -> DECAL (TFX=1): emit the authentic texels directly. The dump's per-vertex color came
#       through as a uniform (128,0,0) (translator artifact); MODULATE with it masks G/B. DECAL matches
#       the proven Ch342 checkerboard. (TODO: backport (a)+(b) into gs_translate_tex.py --perspective.)
scene_lines = open(SCENE).read().splitlines()
header = []                      # comments + persp + tex0 (patched)
tris   = []                      # token-lists of persptri lines (len 20)
for ln in scene_lines:
    tok = ln.split()
    if not tok or tok[0] == "go":
        continue
    if tok[0] == "persptri" and len(tok) == 20:
        tris.append(tok)
    else:
        if tok[0] == "tex0":
            tok[1] = str(TBP)    # tex0 <TBP> <TBW> <TW> <TH> <TFX>
            tok[5] = "1"         # DECAL
            ln = " ".join(tok)
        header.append(ln)

sel = tris[CUBE_FIRST:CUBE_FIRST + CUBE_TRIS]
VTX = ((1, 2, 3, 4), (6, 7, 8, 9), (11, 12, 13, 14))   # (X,Y,S,T) token indices per vertex
xs = [int(t[xi]) for t in sel for (xi, _, _, _) in VTX]
ys = [int(t[yi]) for t in sel for (_, yi, _, _) in VTX]
minx, maxx, miny, maxy = min(xs), max(xs), min(ys), max(ys)
scale = SPAN_PX / max(maxx - minx, maxy - miny, 1)     # uniform -> preserve cube proportions
offx  = MARGIN + (SPAN_PX - (maxx - minx) * scale) / 2.0
offy  = MARGIN + (SPAN_PX - (maxy - miny) * scale) / 2.0
for t in sel:
    for (xi, yi, si, ti) in VTX:
        t[xi] = str(int(round(offx + (int(t[xi]) - minx) * scale)))   # re-fit screen X
        t[yi] = str(int(round(offy + (int(t[yi]) - miny) * scale)))   # re-fit screen Y
        t[si] = str(int(round(int(t[si]) * W)))                       # ST normalized -> texel
        t[ti] = str(int(round(int(t[ti]) * H)))
patched = header + [" ".join(t) for t in sel] + ["go"]
N_TRI_OUT = len(sel)
scene_tmp = os.path.join(DATA, ".cube_persp.tbp.scene.txt")
with open(scene_tmp, "w") as f:
    f.write("\n".join(patched) + "\n")

psf = os.path.join(HERE, "ps2_feeder")
subprocess.run(["gcc", "-O2", "-o", psf, os.path.join(HERE, "ps2_feeder.c")], check=True)
stg = subprocess.run([psf, "--dump-file", scene_tmp],
                     capture_output=True, text=True, check=True).stdout
os.remove(scene_tmp)
stg_words = [ln for ln in stg.splitlines() if ln.strip() and not ln.lstrip().startswith("//")]
staging_path = os.path.join(DATA, "feeder_cube_persp.mem")
with open(staging_path, "w") as f:
    f.write("// Ch343 LOCAL cube perspective staging (ps2_feeder --dump-file cube_persp.scene).\n")
    f.write("// game-derived (cubes .gs) -> gitignored. ABE=0 S1 perspective path.\n")
    f.write(stg)
    if not stg.endswith("\n"):
        f.write("\n")

# --- raster-order texel hex for the smoke TB's VRAM round-trip check (LOCAL) ---
tex_ref_path = os.path.join(DATA, "feeder_cube_tex.mem")
with open(tex_ref_path, "w") as f:
    f.write("// Ch343 LOCAL cube texels, raster order (vram word TBP*64+i). game-derived -> gitignored.\n")
    for t in texels:
        f.write(f"{t & 0xFFFFFFFF:08x}\n")

print(f"[Ch343] payload_cube_setup.mem  : {qwc} qw  (QWC={qwc}, 32 KiB EE RAM, TBP={TBP} TBW={TBW})")
print(f"[Ch343] bios_cube_setup.mem     : bootlet QWC={qwc}")
print(f"[Ch343] feeder_cube_persp.mem   : {len(stg_words)} staging words from cube_persp.scene")
print(f"[Ch343] outputs -> {DATA} (LOCAL / gitignored)")
