#!/usr/bin/env python3
"""Visual QA for every ME1a PLY component and all CSV defect marks."""
from __future__ import annotations

import json
from pathlib import Path

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import numpy as np
from PIL import Image
from scipy.spatial import cKDTree

from audit_ply_fusion import ply_info, sample_ply, full_scan_check

plt.rcParams["font.family"]=["Apple SD Gothic Neo","DejaVu Sans"]
plt.rcParams["axes.unicode_minus"]=False

PROJECT=Path(__file__).resolve().parents[1]
ROOT=PROJECT/"experiments/ME1a_DT"
OUT=ROOT/"results/inspection"


def main() -> None:
    manifest=json.loads((ROOT/"analysis/results/source_manifest.json").read_text())
    defect=json.loads((ROOT/"analysis/results/defects.json").read_text())
    fused=np.load(ROOT/"analysis/results/14h06_all_merge_pointcloud_full.npz")["points"]
    tree=cKDTree(fused)
    paths=[PROJECT/manifest["fused_ply"]]+[PROJECT/p for p in manifest["component_ply"]]
    rows=[]
    fig,axes=plt.subplots(2,3,figsize=(16,8),constrained_layout=True)
    for ax,path in zip(axes.ravel(),paths):
        info=ply_info(path)
        points,_=sample_ply(info,65000)
        exact=manifest["fused_check"] if path==paths[0] else full_scan_check(info)
        distance=tree.query(points,workers=-1)[0]
        rows.append({"file":str(path.relative_to(PROJECT)),"vertices":info.count,
                     "full_bounds_min_mm":exact["min_xyz"],"full_bounds_max_mm":exact["max_xyz"],
                     "nonfinite_vertices":exact["nonfinite_vertices"],
                     "sample_to_fused_p50_mm":float(np.median(distance)),
                     "sample_to_fused_p95_mm":float(np.quantile(distance,.95)),
                     "sample_within_5mm_fraction":float(np.mean(distance<=5))})
        shown=points[::max(1,len(points)//16000)]
        ax.scatter(shown[:,0],shown[:,1],s=.3,c=shown[:,2],cmap="viridis",rasterized=True)
        ax.set_aspect("equal");ax.set_title(path.stem+f"\n{info.count:,} points")
        ax.set_xlabel("scanner X (mm)");ax.set_ylabel("scanner Y (mm)")
    fig.suptitle("ME1a fused and component PLY: native scanner frame, color = height")
    fig.savefig(OUT/"ply_components.png",dpi=150);plt.close(fig)

    image_path=paths[0].with_name("Merged_Image.png")
    img=Image.open(image_path).convert("RGB")
    factor=8
    small=img.resize((img.width//factor,img.height//factor))
    fig,ax=plt.subplots(figsize=(10,15),constrained_layout=True)
    ax.imshow(small);ax.axis("off")
    for row in defect:
        x,y=float(row["X"])/factor,float(row["Y"])/factor
        w,h=row.get("W",""),row.get("H","")
        ax.scatter([x],[y],s=35,c="red",edgecolors="white",linewidths=.8)
        ax.annotate(f"{row['No']} {row['Error']}",(x,y),xytext=(x+8,y-5),
                    color="white",fontsize=9,bbox={"facecolor":"#15202b","alpha":.8,"pad":2})
        if w and h:
            ax.add_patch(patches.Rectangle((x,y),float(w)/factor,float(h)/factor,
                         fill=False,edgecolor="#ff5353",linewidth=1.3))
    ax.set_title("ME1a merged image with six supplied CSV marks; pixel origin unconfirmed")
    fig.savefig(OUT/"csv_defects_on_merged_image.png",dpi=150);plt.close(fig)
    (OUT/"component_audit.json").write_text(json.dumps({"rows":rows,
        "comparison":"sampled component points to 350,000-point fused cache; not an independent sample count",
        "csv_marks":defect,"merged_image_size_xy":list(img.size)},ensure_ascii=False,indent=2))
    print(json.dumps({"files":len(rows),"defect_marks":len(defect),
                      "component_p95_mm":[round(r["sample_to_fused_p95_mm"],3) for r in rows]}))


if __name__=="__main__":main()
