"""PyVista 3D インタラクティブビューアモジュール．

シーンのメッシュ・LED・センサを PyVista で可視化する．
照度ベイク結果 (irradiance) があればメッシュを照度で着色する．
"""

from __future__ import annotations

import logging
from pathlib import Path
from typing import TYPE_CHECKING

import numpy as np

from sm_tias_lightsim.geometry.loader import load_stl
from sm_tias_lightsim.geometry.transforms import scale_vertices
from sm_tias_lightsim.scene.schema import (
    AreaLight,
    DirectionalLight,
    PointLight,
    SceneConfig,
)

if TYPE_CHECKING:
    import pyvista as pv

_logger = logging.getLogger(__name__)

# YAML 座標 (mm) → m 変換係数
_MM_TO_M = 0.001

# 方向ベクトルの長さがこれ以下なら縮退とみなしてスキップする
_DIR_NORM_EPSILON = 1e-9

# 光源・センサを示す球の半径（m 単位）
_MARKER_SPHERE_RADIUS = 0.005

# AreaLight の放射方向矢印のスケール（m 単位）
_AREA_LIGHT_ARROW_SCALE = 0.02

# DirectionalLight の矢印をシーン対角線長に対して拡大する比率
_DIRECTIONAL_ARROW_SCALE_RATIO = 0.3

# センサの視線矢印をシーン対角線長に対してクランプする比率
_SENSOR_ARROW_SCALE_RATIO = 0.2


def _build_pyvista_polydata(vertices: np.ndarray, faces: np.ndarray) -> pv.PolyData:
    """頂点・面配列から PyVista PolyData を構築する．

    PyVista の faces 形式は各行が [n_vertices, v0, v1, v2, ...] の 1D フラット配列．

    Args:
        vertices: 頂点配列 (N_v, 3)，m 単位．
        faces: 三角形面配列 (N_f, 3)，頂点インデックス．

    Returns:
        PyVista PolyData オブジェクト．
    """
    import pyvista as pv

    n_faces = len(faces)
    # PyVista 形式: [3, v0, v1, v2, 3, v0, v1, v2, ...]
    pv_faces = np.hstack([np.full((n_faces, 1), 3, dtype=np.int64), faces]).flatten()
    return pv.PolyData(vertices.astype(np.float32), pv_faces)


def _get_scene_bbox_center_and_diag(
    scene_config: SceneConfig, project_root: Path
) -> tuple[np.ndarray, float]:
    """シーン全メッシュのバウンディングボックス中心と対角線長を返す．

    DirectionalLight の矢印位置を決めるために使う．
    メッシュが空なら原点と 1.0 を返す．

    Args:
        scene_config: SceneConfig．
        project_root: STL 相対パスの基点ディレクトリ．

    Returns:
        (center (3,), diagonal_length) のタプル．
    """
    all_vertices: list[np.ndarray] = []
    for mesh_cfg in scene_config.geometry.meshes:
        try:
            raw_path = Path(mesh_cfg.path)
            stl_path = raw_path if raw_path.is_absolute() else project_root / mesh_cfg.path
            loaded = load_stl(stl_path, mesh_cfg.unit)
            scaled = scale_vertices(loaded, target_unit="m")
            all_vertices.append(scaled.vertices)
        except Exception as exc:
            _logger.warning("バウンディングボックス計算でメッシュを読み込めませんでした: %s", exc)

    if not all_vertices:
        return np.zeros(3), 1.0

    combined = np.vstack(all_vertices)
    mins = combined.min(axis=0)
    maxs = combined.max(axis=0)
    center = (mins + maxs) / 2.0
    diagonal = float(np.linalg.norm(maxs - mins))
    return center, max(diagonal, 1e-6)


def show_scene(
    scene_config: SceneConfig,
    project_root: Path,
    irradiance: dict[str, np.ndarray] | None = None,
    *,
    off_screen: bool = False,
    screenshot: Path | None = None,
) -> None:
    """PyVista でシーンとベイク結果を表示する．

    メッシュ・LED・センサ・座標軸を含む 3D インタラクティブビューアを開く．
    irradiance が指定されていれば，メッシュを照度で着色する（viridis カラーマップ）．

    off_screen=True のときは GUI ウィンドウを開かず，screenshot が指定されていれば保存する．
    screenshot=None かつ off_screen=True のときは何も出力しない（no-op）．

    Args:
        scene_config: 検証済み SceneConfig．
        project_root: STL 相対パスの基点ディレクトリ．
        irradiance: {mesh_id: irradiance_per_face (N_f,)} の辞書．None なら着色なし．
        off_screen: True のとき GUI ウィンドウを開かない（screenshot 専用）．
        screenshot: 保存先 PNG パス．指定時は off_screen で screenshot を保存する．
    """
    import pyvista as pv

    # screenshot 指定があれば off_screen を強制する
    if screenshot is not None:
        off_screen = True

    plotter = pv.Plotter(off_screen=off_screen)

    # ---- メッシュ追加 ----
    for mesh_cfg in scene_config.geometry.meshes:
        try:
            raw_path = Path(mesh_cfg.path)
            stl_path = raw_path if raw_path.is_absolute() else project_root / mesh_cfg.path
            loaded = load_stl(stl_path, mesh_cfg.unit)
            scaled = scale_vertices(loaded, target_unit="m")
        except Exception as exc:
            _logger.warning("メッシュ '%s' を読み込めませんでした: %s", mesh_cfg.id, exc)
            continue

        polydata = _build_pyvista_polydata(scaled.vertices, scaled.faces)

        if irradiance is not None and mesh_cfg.id in irradiance:
            irr = irradiance[mesh_cfg.id]
            polydata.cell_data["irradiance"] = irr.astype(np.float32)
            plotter.add_mesh(
                polydata,
                scalars="irradiance",
                cmap="viridis",
                show_scalar_bar=True,
                scalar_bar_args={"title": "Irradiance (W/m²)"},
                label=mesh_cfg.id,
            )
        else:
            plotter.add_mesh(polydata, color="lightgray", opacity=0.5, label=mesh_cfg.id)

    # ---- シーン全体のバウンディングボックス（DirectionalLight 用）----
    bbox_center, bbox_diag = _get_scene_bbox_center_and_diag(scene_config, project_root)

    # ---- LED / 光源表示 ----
    for light in scene_config.lights:
        if isinstance(light, PointLight):
            pos_m = [c * _MM_TO_M for c in light.position]
            sphere = pv.Sphere(radius=_MARKER_SPHERE_RADIUS, center=pos_m)
            plotter.add_mesh(sphere, color="yellow", label=f"LED:{light.id}")

        elif isinstance(light, AreaLight):
            center_m = [c * _MM_TO_M for c in light.center]
            size_m = [light.size[0] * _MM_TO_M, light.size[1] * _MM_TO_M]

            dir_vec = np.array(light.direction, dtype=np.float64)
            dir_norm = np.linalg.norm(dir_vec)
            if dir_norm > _DIR_NORM_EPSILON:
                dir_vec = dir_vec / dir_norm

            plane = pv.Plane(
                center=center_m,
                direction=dir_vec.tolist(),
                i_size=size_m[0],
                j_size=size_m[1],
            )
            plotter.add_mesh(plane, color="orange", opacity=0.7, label=f"AreaLight:{light.id}")

            arrow = pv.Arrow(
                start=center_m,
                direction=dir_vec.tolist(),
                scale=_AREA_LIGHT_ARROW_SCALE,
            )
            plotter.add_mesh(arrow, color="orange", label=f"AreaLight:{light.id}:dir")

        elif isinstance(light, DirectionalLight):
            # バウンディングボックス中心から矢印 1 本で進行方向を示す
            dir_vec = np.array(light.direction, dtype=np.float64)
            dir_norm = np.linalg.norm(dir_vec)
            if dir_norm < _DIR_NORM_EPSILON:
                continue
            dir_vec = dir_vec / dir_norm

            arrow_scale = bbox_diag * _DIRECTIONAL_ARROW_SCALE_RATIO
            arrow = pv.Arrow(
                start=bbox_center.tolist(),
                direction=dir_vec.tolist(),
                scale=arrow_scale,
            )
            plotter.add_mesh(arrow, color="lightyellow", label=f"DirLight:{light.id}")

    # ---- センサ表示 ----
    sensor_pos_m = [c * _MM_TO_M for c in scene_config.sensor.position]
    sensor_target_m = [c * _MM_TO_M for c in scene_config.sensor.target]

    sensor_sphere = pv.Sphere(radius=_MARKER_SPHERE_RADIUS, center=sensor_pos_m)
    plotter.add_mesh(sensor_sphere, color="blue", label="sensor")

    sensor_pos_arr = np.array(sensor_pos_m)
    sensor_target_arr = np.array(sensor_target_m)
    view_dir = sensor_target_arr - sensor_pos_arr
    view_len = np.linalg.norm(view_dir)
    if view_len > _DIR_NORM_EPSILON:
        view_dir_n = view_dir / view_len
        sensor_arrow = pv.Arrow(
            start=sensor_pos_m,
            direction=view_dir_n.tolist(),
            scale=min(float(view_len), bbox_diag * _SENSOR_ARROW_SCALE_RATIO),
        )
        plotter.add_mesh(sensor_arrow, color="blue", label="sensor:view")

    # ---- 座標軸と境界表示 ----
    plotter.add_axes()
    plotter.show_bounds(grid="back", location="outer")

    # ---- 表示 / 保存 ----
    if screenshot is not None:
        plotter.show(screenshot=str(screenshot))
    elif not off_screen:
        plotter.show()
    # off_screen=True かつ screenshot=None の場合は no-op
