#!/usr/bin/env python3
"""Offline cockpit-shell renderer (warp-style iteration harness).
Loads a *_COP.BGF via bgf_parse and rasterizes it from a cockpit eye with a
z-buffer over a placeholder world (sky+ground), so we can iterate the frame/
window reconstruction against the cabinet reference WITHOUT a full game build.

Hypothesis under test: the PUNCH patch is the windshield -> DROP it (transparent);
keep the non-punch frame patches (dark 'softer'-ramp gray).
Env knobs (iterate the camera): EX EY EZ EZOFF TZ FOV ; KEEP_PUNCH=1 keeps it.
"""
import sys, os, math
import numpy as np
from PIL import Image
from bgf_parse import parse

W, H = 480, 360
def envf(k,d):
    v=os.getenv(k); return float(v) if v not in (None,"") else d

def render(path, out):
    P = parse(path)
    tris=[]
    for pt in P:
        V=pt["verts"]
        for (a,b,c) in pt["faces"]:
            if max(a,b,c)>=len(V): continue
            tris.append((np.array(V[a][:3]),np.array(V[b][:3]),np.array(V[c][:3]),pt["punch"]))
    allv=np.array([v[:3] for pt in P for v in pt["verts"]])
    lo,hi=allv.min(0),allv.max(0); cen=(lo+hi)/2
    drop_punch = os.getenv("KEEP_PUNCH") is None

    eye=np.array([envf("EX",0.0), envf("EY",float(cen[1])), envf("EZ",float(hi[2])+envf("EZOFF",0.4))])
    target=np.array([eye[0], eye[1], envf("TZ",float(lo[2])-1.0)])
    up=np.array([0.0,1.0,0.0])
    f=target-eye; f/=np.linalg.norm(f)+1e-9
    r=np.cross(f,up); r/=np.linalg.norm(r)+1e-9
    u=np.cross(r,f)
    fov=math.radians(envf("FOV",75.0)); tanf=math.tan(fov/2); aspect=W/H

    img=np.zeros((H,W,3),np.float32)
    for y in range(H):
        t=y/H
        img[y,:]= (np.array([0.55,0.52,0.68])+(0.30*(0.5-t))*np.array([0.7,0.7,0.6])) if t<0.5 \
                  else (np.array([0.72,0.68,0.78])-(0.15*(t-0.5))*np.array([0.5,0.5,0.4]))
    zb=np.full((H,W),1e9,np.float32)
    frame=np.array([0.28,0.24,0.34])

    def proj(p):
        d=p-eye; cx=np.dot(d,r); cy=np.dot(d,u); cz=np.dot(d,f)
        if cz<=0.01: return None
        return np.array([(cx/(cz*tanf*aspect))*0.5*W+0.5*W, -(cy/(cz*tanf))*0.5*H+0.5*H, cz])

    drawn=0
    for (p0,p1,p2,punch) in tris:
        if drop_punch and punch: continue
        s=[proj(p0),proj(p1),proj(p2)]
        if any(x is None for x in s): continue
        s0,s1,s2=s
        col=frame*(1.0-0.05*min(1.0,(s0[2]+s1[2]+s2[2])/12.0))
        raster(img,zb,s0,s1,s2,col); drawn+=1
    Image.fromarray((np.clip(img,0,1)*255).astype(np.uint8)).save(out)
    print(f"{os.path.basename(path)} -> {out} eye={eye.round(2)} tgt={target.round(2)} "
          f"fov={math.degrees(fov):.0f} drop_punch={drop_punch} drawn={drawn}/{len(tris)}")

def raster(img,zb,s0,s1,s2,col):
    xs=[s0[0],s1[0],s2[0]]; ys=[s0[1],s1[1],s2[1]]
    x0=max(0,int(min(xs))); x1=min(W-1,int(max(xs))); y0=max(0,int(min(ys))); y1=min(H-1,int(max(ys)))
    if x1<x0 or y1<y0: return
    ax,ay=s0[0],s0[1]; bx,by=s1[0],s1[1]; cx,cy=s2[0],s2[1]
    den=(by-cy)*(ax-cx)+(cx-bx)*(ay-cy)
    if abs(den)<1e-6: return
    for py in range(y0,y1+1):
        for px in range(x0,x1+1):
            w0=((by-cy)*(px+.5-cx)+(cx-bx)*(py+.5-cy))/den
            w1=((cy-ay)*(px+.5-cx)+(ax-cx)*(py+.5-cy))/den
            w2=1-w0-w1
            if w0<0 or w1<0 or w2<0: continue
            z=w0*s0[2]+w1*s1[2]+w2*s2[2]
            if z<zb[py,px]: zb[py,px]=z; img[py,px]=col

if __name__=="__main__":
    path=sys.argv[1] if len(sys.argv)>1 else "content/VIDEO/GEO/MAX_COP.BGF"
    out=sys.argv[2] if len(sys.argv)>2 else "scratchpad/cop_offline.png"
    render(path,out)
