"""Colored judge renders — tessellate each part with its OWN color.

Root cause found in r58: the STEP pipeline drops per-part colors on Compound
children (0 COLOUR entities), so every judged render was grey-on-grey — the
judge could not tell prongs from stone from band. This renderer walks the
generator's Compound children directly, tessellates each with its build123d
.color, and paints a Lambert-shaded schematic at the same azimuth:elevation
camera the pipeline uses.

  .cad-venv/bin/python harness/render.py <generator.py> <out.png> [azim:elev]
"""

from __future__ import annotations

import importlib.util
import math
import sys
from pathlib import Path

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt  # noqa: E402
import numpy as np  # noqa: E402
from mpl_toolkits.mplot3d.art3d import Poly3DCollection  # noqa: E402

BG = "#efece6"          # warm light — matches reference packshots + studio theme


def _load_parts(gen_path: Path, supports: bool = False):
    spec = importlib.util.spec_from_file_location("gen_mod", gen_path)
    mod = importlib.util.module_from_spec(spec)
    sys.path.insert(0, str(gen_path.parent))
    spec.loader.exec_module(mod)
    if hasattr(mod, "gen_parts"):                  # raw list keeps .color; a
        parts = mod.gen_parts()                    # Compound re-wraps and drops it
    else:
        comp = mod.gen_step()
        parts = list(comp.children) or [comp]
    if supports and hasattr(mod, "gen_supports"):  # green sprue + stems
        parts = list(parts) + list(mod.gen_supports())
    return parts


def _tris(shape, tol=0.15):
    verts, faces = shape.tessellate(tol)
    v = np.array([(p.X, p.Y, p.Z) for p in verts])
    f = np.array(faces, dtype=int)
    return v[f]                                    # (n, 3, 3)


def render(gen_path: Path, out_png: Path, camera: str = "25:20", size=1600,
           head: bool = False, supports: bool = False) -> None:
    """head=True frames only the top of the ring (the setting) — the close-up
    view the judge uses to verify prong contact, accents, and connectivity.
    supports=True adds the green casting sprue/stems to the frame."""
    children = _load_parts(gen_path, supports=supports)
    azim, elev = (float(x) for x in camera.split(":"))
    az, el = math.radians(azim), math.radians(elev)
    view = np.array([math.cos(el) * math.cos(az), math.cos(el) * math.sin(az),
                     math.sin(el)])
    light = view + np.array([0.3, 0.2, 0.8])
    light /= np.linalg.norm(light)

    # orthographic camera frame: view direction + screen axes
    up = np.array([0.0, 0.0, 1.0])
    right = np.cross(up, view)
    right /= np.linalg.norm(right)
    vup = np.cross(view, right)

    tris_all, cols_all = [], []
    for child in children:
        c = getattr(child, "color", None)
        rgb = np.array(tuple(c)[:3] if c is not None else (0.75, 0.75, 0.78))
        tri = _tris(child)
        tris_all.append(tri)
        cols_all.append(np.repeat(rgb[None, :], len(tri), axis=0))
    tri = np.concatenate(tris_all)
    base_rgb = np.concatenate(cols_all)

    # per-face shading: ambient + Lambert + Blinn specular
    n = np.cross(tri[:, 1] - tri[:, 0], tri[:, 2] - tri[:, 0])
    norm = np.linalg.norm(n, axis=1, keepdims=True)
    n = np.divide(n, np.where(norm == 0, 1, norm))
    n = np.where((n @ view)[:, None] < 0, -n, n)
    half = light + view
    half /= np.linalg.norm(half)
    lam = np.clip(n @ light, 0, 1)
    spec = np.clip(n @ half, 0, 1) ** 40
    face_rgb = np.clip(base_rgb * (0.28 + 0.72 * lam[:, None])
                       + 0.40 * spec[:, None], 0, 1)

    # project to screen (orthographic), z-buffer rasterize
    W, H = size, int(size * 0.75)
    pts = tri.reshape(-1, 3)
    sx_all = pts @ right
    sy_all = pts @ vup
    sz_all = pts @ view
    if head:
        # frame only the setting: everything in the top third of the model's z
        z_world = pts[:, 2]
        keep = z_world > z_world.min() + (z_world.max() - z_world.min()) * 0.62
        if keep.sum() < 3:
            keep = np.ones(len(pts), bool)
        lo = np.array([sx_all[keep].min(), sy_all[keep].min()])
        hi = np.array([sx_all[keep].max(), sy_all[keep].max()])
    else:
        lo = np.array([sx_all.min(), sy_all.min()])
        hi = np.array([sx_all.max(), sy_all.max()])
    mid, span = (lo + hi) / 2, (hi - lo).max() * 1.12
    scale_px = min(W, H) / span
    X = ((sx_all - mid[0]) * scale_px + W / 2).reshape(-1, 3)
    Y = (H / 2 - (sy_all - mid[1]) * scale_px).reshape(-1, 3)
    Z = sz_all.reshape(-1, 3)

    import matplotlib.colors as mcolors
    img = np.full((H, W, 3), np.array(mcolors.to_rgb(BG)))
    zbuf = np.full((H, W), -np.inf)
    for i in range(len(X)):
        x, y, z = X[i], Y[i], Z[i]
        x0, x1 = int(max(0, x.min())), int(min(W - 1, math.ceil(x.max())))
        y0, y1 = int(max(0, y.min())), int(min(H - 1, math.ceil(y.max())))
        if x1 < x0 or y1 < y0:
            continue
        gx, gy = np.meshgrid(np.arange(x0, x1 + 1), np.arange(y0, y1 + 1))
        d = (x[1] - x[0]) * (y[2] - y[0]) - (x[2] - x[0]) * (y[1] - y[0])
        if abs(d) < 1e-9:
            continue
        w0 = ((x[1] - gx) * (y[2] - gy) - (x[2] - gx) * (y[1] - gy)) / d
        w1 = ((x[2] - gx) * (y[0] - gy) - (x[0] - gx) * (y[2] - gy)) / d
        w2 = 1 - w0 - w1
        mask = (w0 >= -1e-6) & (w1 >= -1e-6) & (w2 >= -1e-6)
        if not mask.any():
            continue
        zi = w0 * z[0] + w1 * z[1] + w2 * z[2]
        sub_z = zbuf[y0:y1 + 1, x0:x1 + 1]
        upd = mask & (zi > sub_z)
        if not upd.any():
            continue
        sub_z[upd] = zi[upd]
        img[y0:y1 + 1, x0:x1 + 1][upd] = face_rgb[i]

    plt.imsave(out_png, np.clip(img, 0, 1))


def main() -> int:
    args = [a for a in sys.argv[1:] if a not in ("--head", "--supports")]
    head = "--head" in sys.argv
    supports = "--supports" in sys.argv
    gen, out = Path(args[0]), Path(args[1])
    camera = args[2] if len(args) > 2 else "25:20"
    render(gen, out, camera, head=head, supports=supports)
    print(f"colored render: {out}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
