#!/usr/bin/env python3
"""Render every mech's *_COP canopy from its AUTHORED eye position (SKL [jointeye]
translation, same torso-local frame as the _cop mesh), single-sided with per-face
INWARD winding -- the port's exact treatment -- with faces coloured by patch +
punch flag so we can see WHICH geometry blocks each mech's view.

  frame colours: punch patches = warm (red/orange/yellow), non-punch = cool (blue/teal)
  world (holes) = lavender background
Env: FWD=+z|-z (look direction, default -z), CULL=0 disables single-siding,
DROP_PUNCH=1 drops punch patches entirely.
"""
import re, os, math, sys
import numpy as np
from PIL import Image, ImageDraw
import importlib.util
spec = importlib.util.spec_from_file_location("bgf_parse", os.path.join(os.path.dirname(__file__), "bgf_parse.py"))
bgf = importlib.util.module_from_spec(spec); spec.loader.exec_module(bgf)

ROOT = r"C:\git\bt411"
MECHS = {  # model: (outside skl prefix, X-variant cop prefix)
    "madcat": ("MAD", "MAX"), "bhk1": ("BLH", "BLX"), "thor": ("THR", "THX"),
    "owens": ("OWN", "OWX"), "sunder": ("SND", "SNX"), "loki": ("LOK", "LOX"),
    "vulture": ("VUL", "VUX"), "avatar": ("AVA", "AVX"),
}
W, H = 300, 225
PUNCH_COLS = [(200,60,50),(230,140,40),(230,210,60)]     # warm = punch
FRAME_COLS = [(70,90,200),(60,170,180),(120,80,200)]     # cool = non-punch

def jointeye(skl_path):
    t = open(skl_path, encoding="latin1").read()
    m = re.search(r"\[jointeye\]", t)
    if not m: return None
    blk = t[m.start():m.start()+400]
    g = lambda k: float((re.search(rf"{k}=([-\d.e]+)", blk) or [None,"0"])[1])
    return (g("tranx"), g("trany"), g("tranz"))

def render(model, out_path, fwd_sign=-1.0, cull=True, drop_punch=False):
    pfx, xpfx = MECHS[model]
    eye = jointeye(os.path.join(ROOT, "content", "VIDEO", pfx + ".SKL"))
    P = bgf.parse(os.path.join(ROOT, "content", "VIDEO", "GEO", xpfx + "_COP.BGF"))
    allv = np.array([v[:3] for pt in P for v in pt["verts"]])
    cen = allv.mean(0)
    e = np.array(eye, float)
    f = np.array([0.0, 0.0, fwd_sign]); u = np.array([0.0, 1.0, 0.0])
    r = np.cross(u, f); r /= np.linalg.norm(r); u2 = np.cross(f, r)
    tanf = math.tan(math.radians(75)/2); aspect = W/H

    img = np.zeros((H, W, 3), np.uint8)
    for y in range(H):                                   # lavender world bg
        t = y/H
        img[y,:] = (150,148,205) if t < 0.5 else (185,178,200)
    zb = np.full((H, W), 1e9)

    npi = nfi = 0
    for pi, pt in enumerate(P):
        V = pt["verts"]
        if pt["punch"]:
            col = PUNCH_COLS[npi % 3]; npi += 1
            if drop_punch: continue
        else:
            col = FRAME_COLS[nfi % 3]; nfi += 1
        for (a, b, c) in pt["faces"]:
            if max(a, b, c) >= len(V): continue
            tri = np.array([V[a][:3], V[b][:3], V[c][:3]])
            n = np.cross(tri[1]-tri[0], tri[2]-tri[0]); nl = np.linalg.norm(n)
            if nl < 1e-9: continue
            n /= nl
            # per-face INWARD orientation (the loader's rule): normal toward mesh centroid
            fc = tri.mean(0)
            inward = n if np.dot(n, cen - fc) >= 0 else -n
            if cull and np.dot(inward, e - fc) <= 0:     # single-sided: cull faces facing away from the eye
                continue
            s = []; ok = True
            for p in tri:
                d = p - e; cx = np.dot(d, r); cy = np.dot(d, u2); cz = np.dot(d, f)
                if cz <= 0.02: ok = False; break
                s.append((cx/(cz*tanf*aspect)*0.5*W + 0.5*W, -cy/(cz*tanf)*0.5*H + 0.5*H, cz))
            if not ok: continue
            shade = 0.55 + 0.45*abs(np.dot(inward, f))
            cc = tuple(min(255, int(ch*shade)) for ch in col)
            raster(img, zb, s, cc)
    im = Image.new("RGB", (W, H+22), (18,18,22)); im.paste(Image.fromarray(img), (0,22))
    d = ImageDraw.Draw(im)
    d.text((4,5), f"{model} eye=({eye[0]:+.2f},{eye[1]:.2f},{eye[2]:+.2f}) fwd={'-Z' if fwd_sign<0 else '+Z'}", fill=(235,235,240))
    im.save(out_path)
    return eye

def raster(img, zb, s, col):
    (ax,ay,az),(bx,by,bz),(cx,cy,cz) = s
    x0=max(0,int(min(ax,bx,cx))); x1=min(W-1,int(max(ax,bx,cx)))
    y0=max(0,int(min(ay,by,cy))); y1=min(H-1,int(max(ay,by,cy)))
    den=(by-cy)*(ax-cx)+(cx-bx)*(ay-cy)
    if abs(den)<1e-6 or x1<x0 or y1<y0: 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*az+w1*bz+w2*cz
            if z<zb[py,px]: zb[py,px]=z; img[py,px]=col

if __name__ == "__main__":
    fwd = -1.0 if os.getenv("FWD","-z") == "-z" else 1.0
    cull = os.getenv("CULL","1") != "0"
    dropp = os.getenv("DROP_PUNCH","0") == "1"
    tag = sys.argv[1] if len(sys.argv) > 1 else "eyeview"
    tiles = []
    for m in MECHS:
        p = os.path.join(ROOT, "scratchpad", f"ev_{m}.png")
        render(m, p, fwd, cull, dropp)
        tiles.append(np.array(Image.open(p)))
    g = np.full((tiles[0].shape[0], 4, 3), 255, np.uint8)
    rows = []
    for i in range(0, len(tiles), 4):
        row = []
        for t in tiles[i:i+4]: row.append(t); row.append(g)
        rows.append(np.concatenate(row[:-1], axis=1))
    wmax = max(r.shape[1] for r in rows)
    rows = [np.pad(r, ((0,0),(0,wmax-r.shape[1]),(0,0)), constant_values=18) for r in rows]
    vg = np.full((4, wmax, 3), 255, np.uint8)
    out = []
    for r in rows: out.append(r); out.append(vg)
    outp = os.path.join(ROOT, "scratchpad", f"{tag}.png")
    Image.fromarray(np.concatenate(out[:-1], axis=0)).save(outp)
    print("saved", outp)
