"""Render what neighbour.py did layer by layer: the rings each sweep accepted (up: green, down: purple dashed), the
merged layer (red, thick), the slicer's rings (blue dotted), GT (black) and the points (grey). Each layer: the whole
slice and a zoom on a rejected ring of either sweep (or the slice). Writes ../report/neighbour_<name>_<cloud>_<a>-<b>.html.

    env -u PYTHONPATH PYTHONWARNINGS=ignore .venv/bin/python ~/code/data/meshes/fcp_2710_tests/scripts/neighbour_log.py loaf_pan clean_10000 107-115
"""
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))
import neighbour as nb  # noqa: E402
import splits as sp  # noqa: E402
from fcp._slice import centres  # noqa: E402
from parts_report import CSS, OBJ, REPORT, ROOT, UP  # noqa: E402


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


def summary(es, tag):
    mine = [e for e in es if e["sweep"] == tag]
    why = {}
    for e in mine:
        if e["why"]:
            why[e["why"]] = why.get(e["why"], 0) + 1
    held = sum(1 for e in mine if e.get("held"))
    return (f"{tag}: {sum(not e['why'] for e in mine)} ok"
            + (f", rejected {', '.join(f'{v} {k}' for k, v in why.items())}" if why else "")
            + (f" ({held} held)" if held else ""))


def main(name, cloud, a, b):
    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))
    d = nb.encode_neighbour(P, up=up, parts="tracked")
    sc = d.pose.scale.astype(float)
    X = (P - d.pose.center) @ d.pose.rotation
    c = centres(d.layers)
    z = c * sc[2]
    s = float(d.provenance["spacing"])
    ks = list(range(a, b + 1))
    merged = {e["k"]: e for e in nb.LOG if e.get("merged")}
    fig, axs = plt.subplots(len(ks), 2, figsize=(13, 6.2 * len(ks)), dpi=80, squeeze=False)
    allxy = np.vstack([np.vstack(merged[k]["slicer_rings"]) for k in ks if merged[k]["slicer_rings"]])
    lo, hi = allxy.min(0), allxy.max(0)
    m = 0.05 * (hi - lo).max()
    for row, k in zip(axs, ks):
        es = [e for e in nb.LOG if e.get("k") == k and "why" in e]
        me = merged[k]
        near = np.abs(X[:, 2] - z[k]) <= nb.SLAB * s
        bad = next((e for e in es if e["why"]), None)
        focus = tuple(bad["ring"][0]) if bad is not None else tuple(allxy[0])
        for ax in row:
            ax.scatter(X[near, 0], X[near, 1], s=3, c="#a8a8a8", lw=0, zorder=1)
            for L in sp.gt_section(g, d.pose, c[k]):
                L = L * sc[:2]
                ax.plot(L[:, 0], L[:, 1], c="k", lw=1.6, zorder=2)
            for L in me["slicer_rings"]:
                ax.plot(*closed(L).T, c="#5b8fd6", lw=1.0, ls=":", zorder=3)
            for L in me["up"]:
                ax.plot(*closed(L).T, c="#2a9d4b", lw=1.1, zorder=4)
            for L in me["down"]:
                ax.plot(*closed(L).T, c="#9b5de5", lw=1.1, ls="--", zorder=4)
            for L in me["final"]:
                ax.plot(*closed(L).T, c="#d1495b", lw=2.4, alpha=0.4, zorder=5)
        if bad is not None:
            for ax in row:
                ax.plot(*closed(bad["ring"]).T, c="#f08a24", lw=1.0, zorder=6)
        row[0].set_xlim(lo[0] - m, hi[0] + m); row[0].set_ylim(lo[1] - m, hi[1] + m)
        w = 6 * s
        row[1].set_xlim(focus[0] - w, focus[0] + w); row[1].set_ylim(focus[1] - w, focus[1] + w)
        for ax in row:
            ax.set_aspect("equal"); ax.tick_params(labelsize=7)
        title = (f"layer {k}: {summary(es, 'up')}; {summary(es, 'down')}\n"
                 f"merged: {me['both']} carried by both sweeps, {me['one']} by one; uses "
                 f"{', '.join(me['accepted']) or 'no refined ring'}; slicer {', '.join(me['slicer'])}, {me['replaced']} replaced"
                 + (f", {me['conflicts']} conflicts" if me["conflicts"] else "")
                 + (" — region check failed: slicer kept" if me["grown"] else ""))
        row[0].set_title(title, fontsize=8, loc="left")
        row[1].set_title("zoom (±6 spacings)" + (f": orange, a rejected {bad['sweep']} ring ({bad['why']})" if bad else ""),
                         fontsize=8)
    fig.text(0.5, 0.002, "black: GT · grey: points (±1 spacing) · green: up-sweep rings accepted · purple dashed: down-sweep "
             "rings accepted · red, thick: the merged layer · blue dotted: the slicer's rings · orange: a rejected ring",
             ha="center", fontsize=9)
    fig.tight_layout(rect=(0, 0.01, 1, 1))
    tag = f"neighbour_{name}_{cloud}_{a}-{b}"
    fig.savefig(REPORT / f"img/{tag}.png")
    plt.close(fig)
    (REPORT / f"{tag}.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>{name}: neighbour refinement {a}-{b}</title><style>{CSS}</style></head><body>
<p><a href="index.html">← report</a> · <a href="slices_{name}_{cloud}_refined.html">refined slices</a></p>
<h1>{name} ({cloud.replace('_', ' ')}): the neighbour refinement, layers {a}–{b}</h1>
<p class="mut">Prototype <code>scripts/neighbour.py</code>: two independent sweeps (up, down) from the slicer's rings, merged per layer
(a ring both sweeps carried is used; one carried by one sweep only when backed ≥ {nb.ONE_VALID:g} and within {nb.ONE_FIT:g} s).
FIT {nb.FIT} s, MIN_VALID {nb.MIN_VALID}, EXPLAIN {nb.EXPLAIN} s for {nb.EXPLAIN_SHARE:g} of the vertices; vertices move by side points
(|n_z| ≤ {nb.VERT}) and are judged against all points; failed rings held half the layers they were carried ({nb.HOLD}–{nb.HOLD_MAX});
small loops cut; region check {nb.GROW} s. Metric canonical frame (x right, y up), spacing s = {s:.2f}.
Generated {time.strftime('%Y-%m-%d %H:%M')} by <code>scripts/neighbour_log.py</code>.</p>
<img src="img/{tag}.png">
</body></html>""")
    print(tag)


if __name__ == "__main__":
    a, b = map(int, sys.argv[3].split("-"))
    main(sys.argv[1], sys.argv[2], a, b)
