"""シミュ評価板 STL の上に ROI 矩形を 3D で重ねて表示するスクリプト．

使い方:
    uv run python scripts/visualize_roi_3d.py [--white-board configs/white_board_bbox.json] \
        [--real-roi-x 539 --real-roi-y 1015 --real-roi-w 1400 --real-roi-h 1409] \
        [--screenshot output/roi_3d.png]

意図:
    実測 ROI が白色板内で占める「相対位置」を、シミュの評価板（同じ形状の白色板を想定）の
    物理サイズに変換し、評価板 STL の表面に 3D 矩形として重ねる．
    人間が見て「シミュの ROI が評価板のここに対応する」と確認するため．

備考:
    白色板の物理サイズは「評価板の bbox（X=99, Y=102 mm）」を使う
    （白色板 = 評価板の有効測定面と仮定）．
"""

from __future__ import annotations

import argparse
import json
from pathlib import Path

import numpy as np
import pyvista as pv
import trimesh


def main() -> None:
    parser = argparse.ArgumentParser(description="Visualize ROI on eval board (3D)")
    parser.add_argument("--white-board", type=Path, default=Path("configs/white_board_bbox.json"))
    parser.add_argument("--real-roi-x", type=int, default=539)
    parser.add_argument("--real-roi-y", type=int, default=1015)
    parser.add_argument("--real-roi-w", type=int, default=1400)
    parser.add_argument("--real-roi-h", type=int, default=1409)
    parser.add_argument(
        "--screenshot", type=Path, default=None, help="出力 PNG．未指定なら GUI を開く"
    )
    parser.add_argument("--off-screen", action="store_true")
    parser.add_argument(
        "--flip-y",
        action="store_true",
        default=True,
        # 実機の物理上下と画像 y の向きが逆の場合に反転する（デフォルト ON）
        help="実測 PNG の y 軸を反転して物理 Y にマップ．デフォルト ON",
    )
    parser.add_argument(
        "--no-flip-y",
        dest="flip_y",
        action="store_false",
        help="y 軸を反転しない",
    )
    parser.add_argument(
        "--flip-x",
        action="store_true",
        default=False,
        help="実測 PNG の x 軸を反転して物理 X にマップ（実機の物理左右と画像 x の向きが逆の場合）",
    )
    args = parser.parse_args()

    if not args.white_board.exists():
        msg = (
            f"白色板 bbox JSON が見つかりません: {args.white_board}\n"
            "先に `uv run python scripts/select_white_board.py` を実行してください．"
        )
        raise FileNotFoundError(msg)

    wb = json.loads(args.white_board.read_text(encoding="utf-8"))
    wb_x, wb_y, wb_w, wb_h = wb["x"], wb["y"], wb["width"], wb["height"]
    print(f"白色板 bbox (real PNG): x={wb_x}, y={wb_y}, w={wb_w}, h={wb_h}")

    rx, ry, rw, rh = args.real_roi_x, args.real_roi_y, args.real_roi_w, args.real_roi_h
    print(f"実測 ROI: x={rx}, y={ry}, w={rw}, h={rh}")

    # ROI の相対位置（白色板内 0〜1）
    rel_x = (rx - wb_x) / wb_w
    rel_y = (ry - wb_y) / wb_h
    rel_w = rw / wb_w
    rel_h = rh / wb_h
    print(f"  相対位置: x={rel_x:.3f}, y={rel_y:.3f}, w={rel_w:.3f}, h={rel_h:.3f}")

    # 評価板 STL を読み込み
    eval_stl = Path("configs/meshes/照明ムラ補正ボード.stl")
    m = trimesh.load(eval_stl, force="mesh")
    print(f"\n評価板 STL bbox (mm): min={m.bounds[0]}, max={m.bounds[1]}")
    board_xmin, board_xmax = m.bounds[0, 0], m.bounds[1, 0]
    board_ymin, board_ymax = m.bounds[0, 1], m.bounds[1, 1]
    board_w = board_xmax - board_xmin
    board_h = board_ymax - board_ymin

    # ROI の評価板内 mm 範囲
    if args.flip_x:
        flipped_x_start = 1.0 - (rel_x + rel_w)
        roi_xmin_mm = board_xmin + flipped_x_start * board_w
        roi_xmax_mm = roi_xmin_mm + rel_w * board_w
        print(f"  X 反転適用: rel_x_orig={rel_x:.3f} → flipped={flipped_x_start:.3f}")
    else:
        roi_xmin_mm = board_xmin + rel_x * board_w
        roi_xmax_mm = roi_xmin_mm + rel_w * board_w
    if args.flip_y:
        # 実測 PNG の y 増加（画像下方向）= 物理 Y 減少と仮定して反転
        # 白色板内 y 相対位置 [rel_y, rel_y+rel_h] を 1.0 から引いて反転
        flipped_y_start = 1.0 - (rel_y + rel_h)
        roi_ymin_mm = board_ymin + flipped_y_start * board_h
        roi_ymax_mm = roi_ymin_mm + rel_h * board_h
        print(f"  Y 反転適用: rel_y_orig={rel_y:.3f} → flipped={flipped_y_start:.3f}")
    else:
        roi_ymin_mm = board_ymin + rel_y * board_h
        roi_ymax_mm = roi_ymin_mm + rel_h * board_h

    # 評価板の裏面（平坦面、-Z 法線、Z=最小）に ROI を重ねる
    # 表面は凹凸が多くて視認しにくいため平坦な裏面を採用
    z_bottom = m.bounds[0, 2]
    z_offset = -1.0  # 裏面のさらに -Z 方向に矩形を浮かせて視認性確保

    print("\nROI の評価板上 mm 範囲:")
    print(f"  X: [{roi_xmin_mm:.2f}, {roi_xmax_mm:.2f}]")
    print(f"  Y: [{roi_ymin_mm:.2f}, {roi_ymax_mm:.2f}]")
    print(f"  Z: {z_bottom + z_offset:.2f}（裏面 Z={z_bottom:.2f} のさらに {z_offset}mm 下）")

    # PyVista で表示
    plotter = pv.Plotter(
        off_screen=args.off_screen or args.screenshot is not None,
        window_size=(1400, 1000),
    )

    # ----- シーン全体のメッシュを追加 -----
    scene_meshes = [
        ("照明ムラ補正ボード.stl", "lightgray", 0.85, "評価板"),
        ("前側.stl", "white", 0.20, "前側筐体"),
        ("後側.stl", "white", 0.20, "後側筐体"),
        ("スマホホルダ.stl", "tan", 0.30, "スマホホルダ"),
        ("LED基板カバー.stl", "lightblue", 0.40, "LED 基板カバー"),
        ("SH-02M.stl", "dimgray", 0.30, "スマホ"),
    ]
    for stl_name, color, opacity, label in scene_meshes:
        stl_path = Path(f"configs/meshes/{stl_name}")
        if not stl_path.exists():
            continue
        mesh = trimesh.load(stl_path, force="mesh")
        v = mesh.vertices * 0.001
        f = np.hstack([np.full((len(mesh.faces), 1), 3), mesh.faces]).flatten()
        plotter.add_mesh(pv.PolyData(v, f), color=color, opacity=opacity, label=label)

    # LED 18 個を黄色い小球で表示
    import yaml
    cfg = yaml.safe_load(Path("configs/scene_baseline.yaml").read_text(encoding="utf-8"))
    for light in cfg.get("lights", []):
        if light.get("type") == "area":
            cx, cy, cz = light["center"]
            pos = np.array([cx, cy, cz]) * 0.001
            plotter.add_mesh(pv.Sphere(radius=0.003, center=pos), color="yellow")

    # ROI 矩形（評価板の裏面 -Z 側に浮かせて表示）
    roi_corners_mm = np.array([
        [roi_xmin_mm, roi_ymin_mm, z_bottom + z_offset],
        [roi_xmax_mm, roi_ymin_mm, z_bottom + z_offset],
        [roi_xmax_mm, roi_ymax_mm, z_bottom + z_offset],
        [roi_xmin_mm, roi_ymax_mm, z_bottom + z_offset],
    ])
    roi_corners_m = roi_corners_mm * 0.001
    # 矩形を 2 三角形で
    faces = np.array([[3, 0, 1, 2], [3, 0, 2, 3]]).flatten()
    roi_poly = pv.PolyData(roi_corners_m, faces)
    plotter.add_mesh(roi_poly, color="red", opacity=0.5, label="ROI（実測対応）")
    # 矩形の枠線も
    for i in range(4):
        a = roi_corners_m[i]
        b = roi_corners_m[(i + 1) % 4]
        line = pv.Line(a, b)
        plotter.add_mesh(line, color="red", line_width=5)

    plotter.add_axes()
    annotation = (
        f"Red = ROI on eval board BACK surface (flat, -Z side)\n"
        f"Range (mm): X=[{roi_xmin_mm:.1f},{roi_xmax_mm:.1f}]"
        f" Y=[{roi_ymin_mm:.1f},{roi_ymax_mm:.1f}]"
    )
    plotter.add_text(annotation, position="upper_left", font_size=10, color="black")

    # 座標軸（X+/Y+/Z+ の向きが見える）
    plotter.add_axes(interactive=True)
    plotter.show_bounds(
        grid="back",
        location="outer",
        xtitle="X [m]",
        ytitle="Y [m]",
        ztitle="Z [m]",
        all_edges=True,
    )

    # デフォルトの視点（screenshot 時のみ．GUI 時はマウスでぐりぐり回せる）
    if args.screenshot is not None:
        plotter.camera_position = [(0.15, -0.20, 0.05), (0, 0, -0.05), (0, 0, 1)]

    if args.screenshot is not None:
        args.screenshot.parent.mkdir(parents=True, exist_ok=True)
        plotter.show(screenshot=str(args.screenshot))
        print(f"\n保存: {args.screenshot}")
    else:
        plotter.show()


if __name__ == "__main__":
    main()
