#!/usr/bin/env python3
"""Interactive viewer for the molding-defect point cloud, with NG label info.

Usage:
    pixi run python view_pointcloud.py            # small/web-sized cloud (fast)
    pixi run python view_pointcloud.py --full      # full-resolution original
    pixi run python view_pointcloud.py path/to.ply # any other .ply file

Controls (Open3D window):
    Left mouse drag     - rotate
    Ctrl + drag          - pan
    Scroll / right drag  - zoom
    Q or Esc             - quit
"""
import csv
import re
import sys
from pathlib import Path

import open3d as o3d

DATA_DIR = Path(__file__).parent / "datas"
FULL_PLY = DATA_DIR / "all_merge_pointcloud_full.ply"
WEB_PLY = DATA_DIR / "all_merge_pointcloud_web.ply"

# Korean defect-code glossary (extend as more codes show up in the data).
DEFECT_GLOSSARY = {
    "찍힘": "press/dent mark (surface indentation, often from ejector pin or handling)",
}


def find_label_csv():
    """Find the NG-labeled CSV that travels alongside the point cloud."""
    if not DATA_DIR.exists():
        return None
    candidates = sorted(DATA_DIR.glob("*.csv"))
    return candidates[0] if candidates else None


def parse_label_info(csv_path: Path):
    """Pull part name / production time / NG status from the filename, and
    the defect rows (location + defect type) from the CSV body."""
    name = csv_path.stem  # e.g. "AC28_DT RR UPR STD 2_20260827 18시01분 생산_NG"
    m = re.match(
        r"(?P<part>.+?)_(?P<date>\d{8}) (?P<hour>\d{2})시(?P<minute>\d{2})분 생산_(?P<result>\w+)",
        name,
    )
    info = {"part": name, "when": None, "result": None, "defects": []}
    if m:
        d = m.groupdict()
        info["part"] = d["part"]
        info["when"] = f"{d['date'][:4]}-{d['date'][4:6]}-{d['date'][6:]} {d['hour']}:{d['minute']}"
        info["result"] = d["result"]

    with open(csv_path, encoding="utf-8-sig") as f:
        for row in csv.DictReader(f):
            err = (row.get("Error") or "").strip()
            if not err:
                continue
            info["defects"].append(
                {
                    "no": row.get("No"),
                    "x": row.get("X"),
                    "y": row.get("Y"),
                    "error": err,
                    "meaning": DEFECT_GLOSSARY.get(err, "(unknown defect code)"),
                }
            )
    return info


def print_label_summary(info):
    print("=" * 60)
    print(f"Part:       {info['part']}")
    print(f"Produced:   {info['when']}")
    print(f"Result:     {info['result']}")
    for d in info["defects"]:
        print(f"Defect #{d['no']}: {d['error']} -> {d['meaning']}")
        print(f"  (inspection-image location, not 3D scan coords: x={d['x']}, y={d['y']})")
    print("=" * 60)


def label_text(info) -> str:
    if not info["defects"]:
        return f"{info['part']} | {info['result']}"
    d = info["defects"][0]
    return f"{info['part']} | {info['result']}: {d['error']} ({d['meaning'].split(' (')[0]})"


def main():
    args = sys.argv[1:]
    path = None
    if args and args[0] not in ("--full",):
        path = Path(args[0])
    elif "--full" in args:
        path = FULL_PLY
    else:
        path = WEB_PLY if WEB_PLY.exists() else FULL_PLY

    if not path.exists():
        print(f"File not found: {path}")
        sys.exit(1)

    csv_path = find_label_csv()
    info = parse_label_info(csv_path) if csv_path else None
    if info:
        print_label_summary(info)

    print(f"Loading {path.name} ...")
    pcd = o3d.io.read_point_cloud(str(path))
    print(f"Loaded {len(pcd.points):,} points")

    title = path.name
    if info:
        title = f"{label_text(info)}  [{path.name}]"

    # Use the newer O3DVisualizer so we can drop a text label into the scene.
    app = o3d.visualization.gui.Application.instance
    app.initialize()
    vis = o3d.visualization.O3DVisualizer(title, 1400, 900)
    vis.add_geometry(path.stem, pcd)
    vis.reset_camera_to_default()

    if info:
        bb = pcd.get_axis_aligned_bounding_box()
        # Anchor the marker/label at a corner, offset clear of the cloud,
        # so it never sits hidden inside the geometry from some angles.
        anchor = bb.max_bound.copy()
        anchor[0] += bb.get_extent()[0] * 0.08
        anchor[2] += bb.get_extent()[2] * 0.5

        marker = o3d.geometry.TriangleMesh.create_sphere(
            radius=max(bb.get_extent()) * 0.01
        )
        marker.translate(anchor)
        marker.paint_uniform_color([1.0, 0.15, 0.0])  # red -> "defective"
        vis.add_geometry("ng_marker", marker)
        vis.add_3d_label(anchor, label_text(info))

        # Exact defect spot isn't known in 3D (CSV gives 2D inspection-image
        # pixels, not scan coords), so make that explicit rather than
        # pretending the marker/label pinpoints it.
        d = info["defects"][0] if info["defects"] else None
        detail_lines = [
            f"Part:      {info['part']}",
            f"Produced:  {info['when']}",
            f"Result:    {info['result']}",
        ]
        if d:
            detail_lines += [
                f"Defect:    {d['error']} - {d['meaning']}",
                f"Inspection-image location: x={d['x']}, y={d['y']}",
                "(This is a 2D camera-image pixel coordinate, not a 3D scan",
                " coordinate -- no calibration links the two, so the red",
                " marker/label above is a whole-part NG summary, not the",
                " exact defect spot on the mesh.)",
            ]
        app.add_window(vis)
        vis.show_message_box("NG Defect Info", "\n".join(detail_lines))
    else:
        app.add_window(vis)

    app.run()


if __name__ == "__main__":
    main()
