"""The thin-wall prototype (thinwall.py) against the package and the mesh route: per case, a slice sheet (mesh route,
package, thinwall at the same layers; the flagged thin-wall points marked) and the three part renders.
Writes ../report/thinwall.html.

    env -u PYTHONPATH PYTHONWARNINGS=ignore .venv/bin/python ~/code/data/meshes/fcp_2710_tests/scripts/thinwall_view.py
"""
import sys
import time
from pathlib import Path

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import trimesh

sys.path.insert(0, str(Path(__file__).parent))
sys.path.insert(0, "/home/joao/code/fcp_devel/api")
import splits as sp  # noqa: E402
import fcp  # noqa: E402
import thinwall as tw  # noqa: E402
from fcp._slice import centres  # noqa: E402
from parts_report import CSS, OBJ, REPORT, ROOT, UP, render, stats  # noqa: E402

CASES = [("loaf_pan", "clean_10000"), ("loaf_pan", "clean_2048"), ("shoe", "clean_10000"), ("bowl_gso", "clean_2048"),
         ("mug", "clean_2048"), ("dino", "clean_10000")]
LAYERS = (20, 44, 64, 88, 110)


def closed(R):
    return np.vstack([R, R[:1]])


def rings_of(S, k, to_xy):
    return [(to_xy(np.asarray(S.rings.ring(r), float)), bool(S.hole[r])) for r in np.nonzero(S.h == k)[0]]


def case(name, cloud):
    g = trimesh.load(OBJ[name], force="mesh")
    P = np.asarray(trimesh.load(ROOT / "pointclouds" / f"{name}_{cloud}.ply").vertices)
    up = UP.get(name, (0, 0, 1))
    ref = np.asarray(g.sample(50_000, seed=1))
    out = {}
    for lab, f in (("mesh route", lambda: fcp.encode(g, device="cuda", up=up)),
                   ("package", lambda: fcp.encode(P, device="cuda", up=up)),
                   ("thinwall", lambda: tw.encode_thinwall(P, up=up))):
        d = f()
        S = sp.CAP["S"]
        m = fcp.decode(d, device="cuda")
        mean, p95 = stats(d, ref, m) if m is not None else (float("nan"), float("nan"))
        img = f"thinwall_{name}_{cloud}_{lab.replace(' ', '_')}.png"
        render(d, m, REPORT / "img" / img, f"{name} · {cloud.replace('_', ' ')} · {lab}: {len(d.parts)} parts, mean/diag {mean:.4f}")
        out[lab] = dict(d=d, S=S, mean=mean, p95=p95, img=img)
    thin, Xt = tw.LAST["thin"], tw.LAST["X"]
    d0 = out["package"]["d"]
    sc = d0.pose.scale.astype(float)
    X = (P - d0.pose.center) @ d0.pose.rotation
    s = float(d0.provenance["spacing"])
    c = centres(d0.layers)
    pk = out["package"]["S"]
    worst = int(np.bincount(pk.h).argmax()) if len(pk.h) else 64
    layers = sorted(set(LAYERS) | {worst})
    md = out["mesh route"]["d"]
    cm = centres(md.layers)
    zw = lambda pose, z: pose.to_world(np.array([[0.0, 0.0, z]]))[0] @ np.asarray(up, float)  # noqa: E731
    fig, axs = plt.subplots(len(layers), 3, figsize=(15, 5 * len(layers)), dpi=78, squeeze=False)
    for row, k in zip(axs, layers):
        gt = [L * sc[:2] for L in sp.gt_section(g, d0.pose, c[k])]
        near = np.abs(X[:, 2] - c[k] * sc[2]) <= s
        tnear = thin & (np.abs(Xt[:, 2] - c[k] * sc[2]) <= tw.SLAB * s)
        km = int(np.argmin([abs(zw(md.pose, z) - zw(d0.pose, c[k])) for z in cm]))
        to_cloud = lambda R: ((md.pose.to_world(np.c_[R, np.full(len(R), cm[km])]) - d0.pose.center) @ d0.pose.rotation)[:, :2]  # noqa: E731
        sets = [("mesh route", rings_of(out["mesh route"]["S"], km, to_cloud), f"its layer {km}"),
                ("package", rings_of(out["package"]["S"], k, lambda R: R * sc[:2]), f"layer {k}"),
                ("thinwall", rings_of(out["thinwall"]["S"], k, lambda R: R * sc[:2]), f"layer {k}")]
        allxy = np.vstack(gt) if gt else (X[near, :2] if near.any() else np.zeros((1, 2)))
        lo, hi = allxy.min(0), allxy.max(0)
        mg = 0.08 * (hi - lo).max() + 2 * s
        for ax, (lab, rings, where) in zip(row, sets):
            ax.scatter(X[near, 0], X[near, 1], s=3, c="#a8a8a8", lw=0, zorder=1)
            if lab == "thinwall" and tnear.any():
                ax.scatter(Xt[tnear, 0], Xt[tnear, 1], s=7, c="#f08a24", lw=0, zorder=2)
            for L in gt:
                ax.plot(L[:, 0], L[:, 1], c="k", lw=1.5, zorder=3)
            for R, h in rings:
                ax.plot(*closed(R).T, c="#2e86ab" if h else "#d1495b", lw=1.2, ls="--" if h else "-", zorder=4)
            ax.set_xlim(lo[0] - mg, hi[0] + mg); ax.set_ylim(lo[1] - mg, hi[1] + mg); ax.set_aspect("equal")
            ax.tick_params(labelsize=6)
            ax.set_title(f"{lab} · {where}{' (package: most rings)' if k == worst else ''}: {sum(not h for _, h in rings)} solid, "
                         f"{sum(h for _, h in rings)} holes" + (f"; {int(tnear.sum())} thin points (orange)" if lab == "thinwall" else ""),
                         fontsize=8)
    fig.tight_layout()
    simg = f"thinwall_{name}_{cloud}_slices.png"
    fig.savefig(REPORT / "img" / simg)
    plt.close(fig)
    rows = "".join(f"<tr><td>{lab}</td><td>{len(r['d'].parts)}</td><td>{sum(x < 0 for x, _, _ in r['d'].parts)}</td>"
                   f"<td>{1e3 * r['mean']:.2f}</td><td>{1e3 * r['p95']:.2f}</td></tr>" for lab, r in out.items())
    imgs = "".join(f"<h4>{lab}</h4><img src='img/{r['img']}' loading='lazy'>" for lab, r in out.items())
    print(name, cloud, {lab: len(r["d"].parts) for lab, r in out.items()}, flush=True)
    return f"""<h2 id='{name}_{cloud}'>{name} · {cloud.replace('_', ' ')}</h2>
<p class="mut">{100 * thin.mean():.1f} % of the points flagged as thin wall.</p>
<table><tr><th></th><th>parts</th><th>holes</th><th>mean/diag ×10⁻³</th><th>p95/diag ×10⁻³</th></tr>{rows}</table>
<h3>Slices</h3><img src="img/{simg}" loading="lazy"><h3>Parts</h3>{imgs}"""


def main():
    secs = [case(n, c) for n, c in CASES]
    nav = " · ".join(f"<a href='#{n}_{c}'>{n} {c.replace('_', ' ')}</a>" for n, c in CASES)
    (REPORT / "thinwall.html").write_text(f"""<!doctype html><html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1"><title>Thin walls in the slicer's field</title><style>{CSS}</style></head><body>
<p><a href="index.html">← report</a> · <a href="synthesis.html">synthesis</a> · <a href="field_loaf_pan_clean_10000.html">the field: loaf pan</a> ·
<a href="field_bowl_gso_clean_10000.html">the field: GSO bowl</a></p>
<h1>Thin walls in the slicer's field (prototype)</h1>
<p><b>Diagnosis</b> (the field pages): a wall thinner than about a spacing is oriented as one sheet, both faces with the outer face's
normal, so the inner face reads the cavity beside it as inside: the slicer's inside band sits next to the wall, not in it, and its
zero line breaks (fragments, C-rings).</p>
<p><b>Prototype</b> (<code>scripts/thinwall.py</code>, the slicer's kernels untouched): a point is on a thin wall when at least
{tw.NEED} points with a parallel normal (|cos| ≥ {tw.PAR}) lie {tw.LO}–{tw.HI} spacings along its normal, within {tw.LAT} of its
normal line; the flag spreads along the sheet ({tw.SPREAD:.0%} of {tw.SPREAD_K} neighbours, {tw.SPREAD_ROUNDS} rounds) and groups under
{tw.MIN_GROUP} points are dropped. In each layer, within {tw.REACH} spacings of the thin points (|dz| ≤ {tw.SLAB} spacings), the field
becomes min(field, d − {tw.R} spacings): the wall's band is added to the inside.</p>
<p class="mut">Columns: the mesh route (its layer nearest in height), the package, thinwall. Black: GT; grey: points within a spacing;
orange: the thin-wall points the band is built from; red: solid rings, blue dashed: holes. Generated {time.strftime('%Y-%m-%d %H:%M')}
by <code>scripts/thinwall_view.py</code>.</p>
<p>{nav}</p>{''.join(secs)}</body></html>""")


if __name__ == "__main__":
    main()
