import struct, math, numpy as np
from PIL import Image

data=open("content/VIDEO/GEO/TSPHERE.BGF","rb").read()
verts=[]; faces=[]
def parse(p,end):
    while p+3<=end:
        tw=struct.unpack_from("<H",data,p)[0]; p+=2
        cid=tw&0x2fff; lw=4 if(tw&0x8000)else(2 if(tw&0x4000)else 1)
        if p+lw>end:return
        ln=data[p] if lw==1 else(struct.unpack_from("<H",data,p)[0] if lw==2 else struct.unpack_from("<I",data,p)[0])
        p+=lw
        if p+ln>end:return
        if cid==0x0089:
            for i in range(ln//32):
                o=p+i*32; x,y,z=struct.unpack_from("<fff",data,o); u,v=struct.unpack_from("<ff",data,o+24)
                verts.append((x,y,z,u,v))
        elif cid==0x0047:
            n=ln//4; idx=[struct.unpack_from("<I",data,p+i*4)[0] for i in range(n)]
            for f in range(0,(n//3)*3,3): faces.append((idx[f],idx[f+1],idx[f+2]))
        if cid in {0x3,0x70,0x40,0x41,0x42,0x46,0x48,0x10,0x20,0x30}: parse(p,p+ln)
        p+=ln
        if cid==0x5: break
parse(8,len(data))
V=[list(v[:3]) for v in verts]; UV=[list(v[3:]) for v in verts]; F=list(faces)

def proj_cone(m):
    x,y,z=m; t=min(abs(z)/21.0,1.0); rI=10.5*(1-t); dy=y-8.25; r=math.hypot(x,dy)
    if r>1e-5: sc=rI/r; x*=sc; y=8.25+dy*sc
    return [x,y,z]
def tess(V,UV,F,passes):
    for _ in range(passes):
        nF=[]; mid={}
        def gm(a,b):
            k=(min(a,b),max(a,b))
            if k in mid:return mid[k]
            pm=proj_cone([(V[a][0]+V[b][0])/2,(V[a][1]+V[b][1])/2,(V[a][2]+V[b][2])/2])
            um=[(UV[a][0]+UV[b][0])/2,(UV[a][1]+UV[b][1])/2]
            V.append(pm); UV.append(um); mid[k]=len(V)-1; return len(V)-1
        for (a,b,c) in F:
            ab=gm(a,b); bc=gm(b,c); ca=gm(c,a)
            nF+= [(a,ab,ca),(ab,b,bc),(ca,bc,c),(ab,bc,ca)]
        F=nF
    return V,UV,F
V,UV,F=tess(V,UV,F,3)
Va=np.array(V); UVa=np.array(UV)
print("after tess: verts",len(V),"faces",len(F))

bd=open("content/VIDEO/TEX/BINTA.VTX","rb").read()
w=h=64; off=57
tex=np.frombuffer(bd[off:off+w*h*3],dtype=np.uint8).astype(float).reshape(h,w,3)/255.0
texL=0.299*tex[:,:,0]+0.587*tex[:,:,1]+0.114*tex[:,:,2]
def sample(u,v):
    return texL[np.clip(((v%1.0)*h).astype(int),0,h-1),np.clip(((u%1.0)*w).astype(int),0,w-1)]

def clip_near(poly,zn=0.1):
    out=[];n=len(poly)
    for i in range(n):
        A=poly[i];B=poly[(i+1)%n]
        if A[2]>=zn: out.append(A)
        if (A[2]>=zn)!=(B[2]>=zn):
            t=(zn-A[2])/(B[2]-A[2]); out.append(tuple(A[k]+(B[k]-A[k])*t for k in range(5)))
    return out

def render(eyeY=0.0,spin=0.0,fov=75.0,contrast=1.0,twist=0.0,
           LO=(0.15,0.12,0.35),HI=(0.98,0.94,1.0),W=320,H=240,radfreq=1.0):
    LO=np.array(LO);HI=np.array(HI)
    c,s=math.cos(spin),math.sin(spin)
    Vx=Va[:,0]*c-Va[:,1]*s; Vy=Va[:,0]*s+Va[:,1]*c
    P=np.stack([Vx,Vy-eyeY,Va[:,2]],axis=1)
    f=(H/2)/math.tan(math.radians(fov)/2)
    img=np.zeros((H,W,3)); zb=np.full((H,W),1e18)
    for (a,b,cc) in F:
        tri=[(P[i][0],P[i][1],P[i][2],UVa[i][0]+twist*UVa[i][1],UVa[i][1]*radfreq) for i in (a,b,cc)]
        cp=clip_near(tri)
        if len(cp)<3:continue
        for k in range(1,len(cp)-1):
            A,B,C=cp[0],cp[k],cp[k+1]
            def pr(p):return(W/2+f*p[0]/p[2],H/2-f*p[1]/p[2],p[2],p[3],p[4])
            xa,ya,za,ua,vaa=pr(A);xb,yb,zb2,ub,vbb=pr(B);xc,yc,zc,uc,vcc=pr(C)
            minx=max(int(min(xa,xb,xc)),0);maxx=min(int(max(xa,xb,xc))+1,W-1)
            miny=max(int(min(ya,yb,yc)),0);maxy=min(int(max(ya,yb,yc))+1,H-1)
            if minx>=maxx or miny>=maxy:continue
            den=((yb-yc)*(xa-xc)+(xc-xb)*(ya-yc))
            if abs(den)<1e-9:continue
            ys,xs=np.mgrid[miny:maxy+1,minx:maxx+1];xf=xs+.5;yf=ys+.5
            l1=((yb-yc)*(xf-xc)+(xc-xb)*(yf-yc))/den
            l2=((yc-ya)*(xf-xc)+(xa-xc)*(yf-yc))/den
            l3=1-l1-l2; m=(l1>=0)&(l2>=0)&(l3>=0)
            if not m.any():continue
            iz=l1/za+l2/zb2+l3/zc; z=1/iz
            u=(l1*ua/za+l2*ub/zb2+l3*uc/zc)*z; v=(l1*vaa/za+l2*vbb/zb2+l3*vcc/zc)*z
            lum=np.clip((sample(u,v)-0.5)*contrast+0.5,0,1)
            col=LO[None,:]+(HI-LO)[None,:]*lum[...,None]
            yi=ys[m];xi=xs[m];zz=z[m];cl=zz<zb[yi,xi]
            zb[yi[cl],xi[cl]]=zz[cl]; img[yi[cl],xi[cl]]=col[m][cl]
    return (np.clip(img,0,1)*255).astype(np.uint8)

tgt=Image.open("scratchpad/target.png").convert("RGB").resize((320,240))
cols=[np.array(tgt)]
for eyeY in (0.0,2.0,4.0,6.0,8.25):
    cols.append(render(eyeY=eyeY,spin=0.5))
Image.fromarray(np.concatenate(cols,axis=1)).save("scratchpad/cmp_eye.png")
print("saved scratchpad/cmp_eye.png [TARGET | eyeY=0,2,4,6,8.25]")

# --- bilinear sampler + focused sweep around on-axis ---
def sample_bilin(u,v):
    uu=(u%1.0)*w-0.5; vv=(v%1.0)*h-0.5
    x0=np.floor(uu).astype(int); y0=np.floor(vv).astype(int)
    fx=uu-x0; fy=vv-y0
    x0m=x0%w; x1m=(x0+1)%w; y0m=y0%h; y1m=(y0+1)%h
    c00=texL[y0m,x0m]; c10=texL[y0m,x1m]; c01=texL[y1m,x0m]; c11=texL[y1m,x1m]
    return (c00*(1-fx)*(1-fy)+c10*fx*(1-fy)+c01*(1-fx)*fy+c11*fx*fy)

def render2(eyeY,spin,twist,contrast,fov=75.0,LO=(0.15,0.12,0.35),HI=(0.98,0.94,1.0),W=320,H=240,radfreq=1.0):
    LO=np.array(LO);HI=np.array(HI)
    c,s=math.cos(spin),math.sin(spin)
    Vx=Va[:,0]*c-Va[:,1]*s; Vy=Va[:,0]*s+Va[:,1]*c
    P=np.stack([Vx,Vy-eyeY,Va[:,2]],axis=1)
    f=(H/2)/math.tan(math.radians(fov)/2)
    img=np.zeros((H,W,3)); zbf=np.full((H,W),1e18)
    for (a,b,cc) in F:
        tri=[(P[i][0],P[i][1],P[i][2],UVa[i][0]+twist*UVa[i][1],UVa[i][1]*radfreq) for i in (a,b,cc)]
        cp=clip_near(tri)
        if len(cp)<3:continue
        for k in range(1,len(cp)-1):
            A,B,C=cp[0],cp[k],cp[k+1]
            def pr(p):return(W/2+f*p[0]/p[2],H/2-f*p[1]/p[2],p[2],p[3],p[4])
            xa,ya,za,ua,vaa=pr(A);xb,yb,zb2,ub,vbb=pr(B);xc,yc,zc,uc,vcc=pr(C)
            minx=max(int(min(xa,xb,xc)),0);maxx=min(int(max(xa,xb,xc))+1,W-1)
            miny=max(int(min(ya,yb,yc)),0);maxy=min(int(max(ya,yb,yc))+1,H-1)
            if minx>=maxx or miny>=maxy:continue
            den=((yb-yc)*(xa-xc)+(xc-xb)*(ya-yc))
            if abs(den)<1e-9:continue
            ys,xs=np.mgrid[miny:maxy+1,minx:maxx+1];xf=xs+.5;yf=ys+.5
            l1=((yb-yc)*(xf-xc)+(xc-xb)*(yf-yc))/den
            l2=((yc-ya)*(xf-xc)+(xa-xc)*(yf-yc))/den
            l3=1-l1-l2; m=(l1>=0)&(l2>=0)&(l3>=0)
            if not m.any():continue
            iz=l1/za+l2/zb2+l3/zc; z=1/iz
            u=(l1*ua/za+l2*ub/zb2+l3*uc/zc)*z; v=(l1*vaa/za+l2*vbb/zb2+l3*vcc/zc)*z
            lum=np.clip((sample_bilin(u,v)-0.5)*contrast+0.5,0,1)
            col=LO[None,:]+(HI-LO)[None,:]*lum[...,None]
            yi=ys[m];xi=xs[m];zz=z[m];cl=zz<zbf[yi,xi]
            zbf[yi[cl],xi[cl]]=zz[cl]; img[yi[cl],xi[cl]]=col[m][cl]
    return (np.clip(img,0,1)*255).astype(np.uint8)

rows=[]
labels=[]
for eyeY in (7.0,8.25,10.0):
    row=[render2(eyeY,0.5,tw,1.0) for tw in (-0.3,0.0,0.3)]
    rows.append(np.concatenate(row,axis=1))
grid=np.concatenate(rows,axis=0)
# put target strip on the left, resized to grid height
tg=np.array(Image.open("scratchpad/target.png").convert("RGB").resize((320,grid.shape[0])))
Image.fromarray(np.concatenate([tg,grid],axis=1)).save("scratchpad/cmp_twist.png")
print("saved cmp_twist.png [TARGET | rows eyeY=7,8.25,10 ; cols twist=-0.3,0,0.3]")

# --- FINAL confirmation: proposed game params, 2 spins, vs target ---
final=[render2(8.25,sp,0.0,1.0,LO=(0.15,0.12,0.35),HI=(0.98,0.94,1.0),W=400,H=300,radfreq=1.0) for sp in (0.3,1.2,2.4)]
grid=np.concatenate(final,axis=1)
tg=np.array(Image.open("scratchpad/target.png").convert("RGB").resize((400,grid.shape[0])))
Image.fromarray(np.concatenate([tg,grid],axis=1)).save("scratchpad/cmp_final.png")
print("saved cmp_final.png [TARGET | spin=0.3,1.2,2.4 @ eyeUp8.25 twist0 contrast1 wideRamp]")

# --- render3: EXACT game transform order (eyeUp translate BEFORE spin) ---
def render3(eyeUp,spin,twist,contrast,fov=75.0,LO=(0.15,0.12,0.35),HI=(0.98,0.94,1.0),W=400,H=300,radfreq=1.0):
    LO=np.array(LO);HI=np.array(HI)
    qx=Va[:,0]; qy=Va[:,1]-eyeUp; qz=Va[:,2]           # eyeUp translate first
    c,s=math.cos(spin),math.sin(spin)
    rx=qx*c-qy*s; ry=qx*s+qy*c; rz=qz                   # spin about Z in-place
    P=np.stack([rx,ry,rz],axis=1)
    f=(H/2)/math.tan(math.radians(fov)/2)
    img=np.zeros((H,W,3)); zbf=np.full((H,W),1e18)
    for (a,b,cc) in F:
        tri=[(P[i][0],P[i][1],P[i][2],UVa[i][0]+twist*UVa[i][1],UVa[i][1]*radfreq) for i in (a,b,cc)]
        cp=clip_near(tri)
        if len(cp)<3:continue
        for k in range(1,len(cp)-1):
            A,B,C=cp[0],cp[k],cp[k+1]
            def pr(p):return(W/2+f*p[0]/p[2],H/2-f*p[1]/p[2],p[2],p[3],p[4])
            xa,ya,za,ua,vaa=pr(A);xb,yb,zb2,ub,vbb=pr(B);xc,yc,zc,uc,vcc=pr(C)
            minx=max(int(min(xa,xb,xc)),0);maxx=min(int(max(xa,xb,xc))+1,W-1)
            miny=max(int(min(ya,yb,yc)),0);maxy=min(int(max(ya,yb,yc))+1,H-1)
            if minx>=maxx or miny>=maxy:continue
            den=((yb-yc)*(xa-xc)+(xc-xb)*(ya-yc))
            if abs(den)<1e-9:continue
            ys,xs=np.mgrid[miny:maxy+1,minx:maxx+1];xf=xs+.5;yf=ys+.5
            l1=((yb-yc)*(xf-xc)+(xc-xb)*(yf-yc))/den
            l2=((yc-ya)*(xf-xc)+(xa-xc)*(yf-yc))/den
            l3=1-l1-l2; m=(l1>=0)&(l2>=0)&(l3>=0)
            if not m.any():continue
            iz=l1/za+l2/zb2+l3/zc; z=1/iz
            u=(l1*ua/za+l2*ub/zb2+l3*uc/zc)*z; v=(l1*vaa/za+l2*vbb/zb2+l3*vcc/zc)*z
            lum=np.clip((sample_bilin(u,v)-0.5)*contrast+0.5,0,1)
            col=LO[None,:]+(HI-LO)[None,:]*lum[...,None]
            yi=ys[m];xi=xs[m];zz=z[m];cl=zz<zbf[yi,xi]
            zbf[yi[cl],xi[cl]]=zz[cl]; img[yi[cl],xi[cl]]=col[m][cl]
    return (np.clip(img,0,1)*255).astype(np.uint8)

final=[render3(8.25,sp,0.0,1.0) for sp in (0.0,1.0,2.0,3.0)]
grid=np.concatenate(final,axis=1)
tg=np.array(Image.open("scratchpad/target.png").convert("RGB").resize((400,grid.shape[0])))
Image.fromarray(np.concatenate([tg,grid],axis=1)).save("scratchpad/cmp_final2.png")
print("saved cmp_final2.png [TARGET | game-order spin=0,1,2,3 @ eyeUp8.25 twist0]")

# --- FOV test: does a narrow FOV (zoom into apex) turn rings into spokes? ---
fovs=[render3(8.25,0.5,0.0,1.0,fov=fv) for fv in (30,45,60,90)]
grid=np.concatenate(fovs,axis=1)
tg=np.array(Image.open("scratchpad/target.png").convert("RGB").resize((400,grid.shape[0])))
Image.fromarray(np.concatenate([tg,grid],axis=1)).save("scratchpad/cmp_fov.png")
print("saved cmp_fov.png [TARGET | fov=30,45,60,90]")

# --- which eyeUp matches the in-game capture? ---
ig=np.array(Image.open("scratchpad/ingame_warp2.png").convert("RGB").resize((400,300)))
row=[ig]+[render3(eu,0.5,0.0,1.0,W=400,H=300) for eu in (0.0,4.0,8.25,12.0)]
Image.fromarray(np.concatenate(row,axis=1)).save("scratchpad/cmp_ingame.png")
print("saved cmp_ingame.png [IN-GAME | eyeUp=0,4,8.25,12]")
