"""Fusion 360 から出力した LED ユニット STL から LED 位置を抽出する.

STL ファイルに含まれる連結成分を解析し，カバー（最大の連結成分）と LED 群
（小さい連結成分）を識別する．各 LED の中心座標・サイズ・法線方向（発光方向）を
抽出して scene_baseline.yaml の lights セクションに組み込める形式で出力する.

使い方:
    # 1 ファイル（複数ユニット含む）
    uv run python scripts/extract_led_positions_from_stl.py \\
        configs/meshes/LEDユニット_all.stl

    # 複数 STL ファイル（各ユニット別ファイル）
    uv run python scripts/extract_led_positions_from_stl.py \\
        configs/meshes/LEDユニット下.stl \\
        configs/meshes/LEDユニット右.stl \\
        configs/meshes/LEDユニット左.stl

    # 出力を YAML ファイルに保存
    uv run python scripts/extract_led_positions_from_stl.py \\
        configs/meshes/LEDユニット_all.stl \\
        --output configs/leds_extracted.yaml

仕様:
- 連結成分のうち面数最大をカバー（光らない），残りを LED（発光面）と識別
- 各 LED の法線方向は extents 最小軸（板の厚み方向）から推定
- 法線の符号は「カバー中心 → LED 中心」のベクトルに基づき外向きに決定
- LED 位置は (Y, X, Z) 順で sort（下基板は X 順，右・左基板は Y 順になる想定）
"""

from __future__ import annotations

import argparse
from collections import defaultdict
from pathlib import Path
from typing import Any

import numpy as np
import trimesh
import yaml

# 既存 scene_baseline.yaml の LED radiance（Step 8-6 で校正済み）．新規 YAML 生成時に使う．
DEFAULT_RADIANCE = 2807.2469840551203


def _split_connected_components(mesh: trimesh.Trimesh) -> list[list[int]]:
    """頂点共有で連結する面群をグループ化（union-find）．

    Returns:
        各グループは face index のリスト．
    """
    n_verts = len(mesh.vertices)
    parent = list(range(n_verts))

    def find(x: int) -> int:
        while parent[x] != x:
            parent[x] = parent[parent[x]]
            x = parent[x]
        return x

    def union(a: int, b: int) -> None:
        ra, rb = find(a), find(b)
        if ra != rb:
            parent[ra] = rb

    for face in mesh.faces:
        union(int(face[0]), int(face[1]))
        union(int(face[1]), int(face[2]))

    groups: dict[int, list[int]] = defaultdict(list)
    for fi, face in enumerate(mesh.faces):
        groups[find(int(face[0]))].append(fi)
    return list(groups.values())


def _component_stats(mesh: trimesh.Trimesh, face_indices: list[int]) -> dict[str, Any]:
    """連結成分の統計（頂点・面数・bbox 中心・extents）を計算する．

    `face_indices` は STL 内の元 face index リストで，後で独立 STL として
    書き出す際に頂点・面の再マッピングに使う．
    """
    face_verts_idx = np.unique(mesh.faces[face_indices].flatten())
    verts = mesh.vertices[face_verts_idx]
    bmin = verts.min(axis=0)
    bmax = verts.max(axis=0)
    return {
        "n_verts": len(face_verts_idx),
        "n_faces": len(face_indices),
        "center": (bmin + bmax) / 2.0,
        "extents": bmax - bmin,
        "bmin": bmin,
        "bmax": bmax,
        "face_indices": face_indices,
    }


def _export_component_as_stl(mesh: trimesh.Trimesh, face_indices: list[int],
                              output_path: Path) -> None:
    """連結成分を独立 STL ファイルとして書き出す．

    元メッシュの face_indices に対応する頂点・面を抽出し，頂点 index を再マッピング
    してから新規 Trimesh として書き出す．元の座標系（ワールド）は保持する．
    """
    face_verts_idx = np.unique(mesh.faces[face_indices].flatten())
    old_to_new = {int(old): new for new, old in enumerate(face_verts_idx)}
    new_vertices = mesh.vertices[face_verts_idx]
    new_faces = np.array(
        [[old_to_new[int(v)] for v in mesh.faces[fi]] for fi in face_indices]
    )
    sub_mesh = trimesh.Trimesh(vertices=new_vertices, faces=new_faces, process=False)
    output_path.parent.mkdir(parents=True, exist_ok=True)
    sub_mesh.export(str(output_path), file_type="stl")


def _infer_led_direction(led_center: np.ndarray, led_extents: np.ndarray,
                        cover_center: np.ndarray) -> np.ndarray:
    """LED 発光面の法線方向を推定する．

    LED は薄板状（extents の最小軸が板の厚み）．発光方向はその最小軸方向で，
    カバー中心から LED 中心への差分の符号で外向きを決める．
    """
    min_axis = int(np.argmin(led_extents))
    direction = np.zeros(3)
    diff = led_center - cover_center
    sign = 1.0 if diff[min_axis] >= 0 else -1.0
    direction[min_axis] = sign
    return direction


def _led_size_xy(led_extents: np.ndarray) -> tuple[float, float]:
    """LED 発光面のサイズ（厚み以外の 2 軸）を取得する．

    パッケージ命名規則どおり (2.8, 3.5) mm 等が想定される．
    返り値は (短辺, 長辺) ではなく (X 方向幅, Y 方向幅) を意図しない．
    AreaLight の size は (width, height) の 2 値で，向きは direction で別途指定する．
    """
    min_axis = int(np.argmin(led_extents))
    other = [i for i in range(3) if i != min_axis]
    return (float(led_extents[other[0]]), float(led_extents[other[1]]))


def _classify_components(comps: list[dict]) -> tuple[list[dict], list[dict]]:
    """連結成分を「LED 候補」「カバー候補」に分類する．

    分類基準（SMD 2835 LED と LED 基板カバーの想定形状に基づく）:
      - LED 候補: extents の最大 ≤ 4mm かつ最小 ≤ 1.5mm（板状で 2.8×3.5×1mm 想定）
      - カバー候補: extents の最大 > 30mm かつ最小 ≤ 3mm（細長い板状、LED 基板カバー想定）

    上記いずれにも該当しない大きな成分（筐体、評価板、スマホ等）は除外する．
    """
    led_candidates = []
    cover_candidates = []
    for c in comps:
        e = c["extents"]
        if max(e) <= 4.0 and min(e) <= 1.5:
            led_candidates.append(c)
        elif max(e) > 30.0 and min(e) <= 3.0:
            cover_candidates.append(c)
    return led_candidates, cover_candidates


def extract_leds_from_mesh(mesh: trimesh.Trimesh) -> tuple[list[dict], list[dict]]:
    """STL メッシュから LED 群とカバー群を抽出する．

    複数の LED 基板カバー（下・右・左）を含む複合 STL に対応．
    各 LED について「最寄りカバー」を探し，そのカバー中心からの差分で
    発光方向（direction）の符号を決定する．

    Returns:
        (leds, covers) のタプル．
        - leds: 各 LED の {"center", "size", "direction", "extents", "nearest_cover_idx"} のリスト
        - covers: 各カバーの統計（center, extents 等）のリスト
    """
    groups = _split_connected_components(mesh)
    if len(groups) < 2:
        msg = f"STL に連結成分が {len(groups)} 個しかない．カバー + LED 群が必要"
        raise ValueError(msg)

    comps = [_component_stats(mesh, g) for g in groups]
    led_components, cover_components = _classify_components(comps)

    if len(cover_components) == 0:
        msg = "カバー候補が見つからない（細長い板状の連結成分が必要）"
        raise ValueError(msg)
    if len(led_components) == 0:
        msg = "LED 候補が見つからない（2.8x3.5x1mm 想定の小さな板状成分が必要）"
        raise ValueError(msg)

    cover_centers = np.array([c["center"] for c in cover_components])
    leds = []
    for c in led_components:
        # 最寄りカバー（中心間距離が最小）を選ぶ
        distances = np.linalg.norm(cover_centers - c["center"], axis=1)
        nearest_idx = int(np.argmin(distances))
        nearest_cover = cover_components[nearest_idx]

        size = _led_size_xy(c["extents"])
        direction = _infer_led_direction(c["center"], c["extents"], nearest_cover["center"])
        leds.append({
            "center": c["center"].tolist(),
            "size": list(size),
            "direction": direction.tolist(),
            "extents": c["extents"].tolist(),
            "nearest_cover_idx": nearest_idx,
        })
    return leds, cover_components


def _classify_cover_position(cover_center: np.ndarray) -> str:
    """カバー中心位置から `bottom` / `right` / `left` / `top` を判定する．

    判定基準: |Y| > |X| なら上下軸（Y 符号で bottom/top），そうでなければ左右軸（X 符号で left/right）．
    MiniTIAS の 3 基板配置 (下基板 Y<0, 右基板 X>0, 左基板 X<0) に対応．
    """
    x, y, _ = cover_center
    if abs(y) > abs(x):
        return "bottom" if y < 0 else "top"
    return "right" if x > 0 else "left"


def _assign_led_ids(leds: list[dict], covers: list[dict]) -> list[dict]:
    """各 LED に id を割り当てて並べ替える．

    - 各 LED の所属基板を cover の位置（bottom/right/left/top）で判定
    - 同基板内では「下基板: X 昇順」「右/左基板: Y 昇順」「上基板: X 昇順」で並べる
    - id 命名: `led_{label}_{index}` 例: `led_bottom_0`, `led_right_5`
    """
    cover_labels = [_classify_cover_position(np.array(c["center"])) for c in covers]
    grouped: dict[str, list[dict]] = defaultdict(list)
    for led in leds:
        label = cover_labels[led["nearest_cover_idx"]]
        grouped[label].append(led)

    # 並べ替えキー: bottom/top は X 昇順，right/left は Y 昇順
    sort_keys = {
        "bottom": lambda l: l["center"][0],
        "top": lambda l: l["center"][0],
        "right": lambda l: l["center"][1],
        "left": lambda l: l["center"][1],
    }
    # 出力順: bottom → right → left → top（読みやすい固定順）
    label_order = ["bottom", "right", "left", "top"]
    result = []
    for label in label_order:
        if label not in grouped:
            continue
        group = sorted(grouped[label], key=sort_keys[label])
        for i, led in enumerate(group):
            led_with_id = dict(led)
            led_with_id["assigned_id"] = f"led_{label}_{i}"
            result.append(led_with_id)
    return result


def _format_led_yaml_entries(leds: list[dict], radiance: float = DEFAULT_RADIANCE) -> list[dict]:
    """LED リストを scene_baseline.yaml の lights セクション形式に整形する．

    `assigned_id` フィールド（_assign_led_ids で付与）が無い場合は単純な番号で代用．
    """
    entries = []
    for i, led in enumerate(leds):
        led_id = led.get("assigned_id", f"led_{i:02d}")
        entries.append({
            "id": led_id,
            "type": "area",
            "shape": "rectangle",
            "center": [round(c, 4) for c in led["center"]],
            "size": [round(s, 4) for s in led["size"]],
            "direction": [round(d, 4) for d in led["direction"]],
            "radiance": [radiance, radiance, radiance],
        })
    return entries


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument("stl_files", nargs="+", type=Path,
                        help="解析する STL ファイル（複数可）")
    parser.add_argument("--output", type=Path, default=None,
                        help="出力 YAML パス（省略時は標準出力）")
    parser.add_argument("--radiance", type=float, default=DEFAULT_RADIANCE,
                        help=f"各 LED の radiance 値（既定 {DEFAULT_RADIANCE}）")
    parser.add_argument("--export-covers", type=Path, default=None, metavar="DIR",
                        help=(
                            "LED 基板カバーを bottom/right/left ごとに独立 STL として書き出す．"
                            " 指定したディレクトリに LED基板カバー{bottom,right,left}.stl を保存．"
                        ))
    parser.add_argument("--summary", action="store_true",
                        help="LED 位置サマリーを stderr に表示")
    args = parser.parse_args()

    all_leds: list[dict] = []
    for stl in args.stl_files:
        if not stl.is_file():
            msg = f"STL ファイルが見つかりません: {stl}"
            raise FileNotFoundError(msg)
        print(f"# Processing {stl}", flush=True)
        mesh = trimesh.load_mesh(str(stl))
        leds, covers = extract_leds_from_mesh(mesh)
        print(f"  detected {len(covers)} cover(s):", flush=True)
        for i, cover in enumerate(covers):
            label = _classify_cover_position(cover["center"])
            print(f"    cover[{i}] label={label:<6} center=({cover['center'][0]:+.2f}, "
                  f"{cover['center'][1]:+.2f}, {cover['center'][2]:+.2f}) "
                  f"extents=({cover['extents'][0]:.2f}, {cover['extents'][1]:.2f}, "
                  f"{cover['extents'][2]:.2f})", flush=True)

        # ID 付与（bottom/right/left/top + 並び順）
        leds_with_ids = _assign_led_ids(leds, covers)
        print(f"  detected {len(leds_with_ids)} LED(s) (id assigned):", flush=True)
        for led in leds_with_ids:
            print(f"    {led['assigned_id']:<16} "
                  f"center=({led['center'][0]:+.2f}, {led['center'][1]:+.2f}, "
                  f"{led['center'][2]:+.2f}) size={led['size']} dir={led['direction']}",
                  flush=True)
        all_leds.extend(leds_with_ids)

        # カバー書き出し（--export-covers 指定時のみ）．
        # ファイル名は既存 `LED基板カバー.stl` と整合させて日本語化（下/右/左）．
        jp_labels = {"bottom": "下", "right": "右", "left": "左", "top": "上"}
        if args.export_covers is not None:
            for cover in covers:
                label = _classify_cover_position(cover["center"])
                jp = jp_labels.get(label, label)
                out_path = args.export_covers / f"LED基板カバー{jp}.stl"
                _export_component_as_stl(mesh, cover["face_indices"], out_path)
                print(f"  exported cover [{label}] → {out_path}", flush=True)

    print(f"\n# Total: {len(all_leds)} LED(s) from {len(args.stl_files)} STL file(s)", flush=True)

    # YAML 形式で出力
    entries = _format_led_yaml_entries(all_leds, radiance=args.radiance)
    yaml_str = yaml.safe_dump(entries, default_flow_style=False,
                              allow_unicode=True, sort_keys=False)

    if args.output is None:
        print("\n# --- lights セクション（scene_baseline.yaml にコピペ可） ---")
        print(yaml_str)
    else:
        args.output.write_text(yaml_str, encoding="utf-8")
        print(f"\n# Wrote {len(entries)} LED entries to {args.output}", flush=True)


if __name__ == "__main__":
    main()
