# pyright: reportCallIssue=none
# Mitsuba 3 は variant ベースの動的属性解決を行うため Pylance/Pyright が
# mi.* を object 型と推論し reportCallIssue を誤検知する（coding-style.md の方針）．
"""SceneConfig 全体を Mitsuba load_dict 互換 dict に変換する統合モジュール．"""

from __future__ import annotations

import shutil
import tempfile
from pathlib import Path

import trimesh

from sm_tias_lightsim.geometry.loader import LoadedMesh, load_stl
from sm_tias_lightsim.geometry.transforms import scale_vertices
from sm_tias_lightsim.scene.lights import build_light_dict
from sm_tias_lightsim.scene.materials import build_material_dict
from sm_tias_lightsim.scene.schema import SceneConfig
from sm_tias_lightsim.scene.sensors import build_sensor_dict

# mm→m の変換係数
_MM_TO_M = 0.001


def _position_scale(unit: str) -> float:
    """単位文字列に対応する座標スケール係数を返す．

    YAML 内の座標値はすべて同一単位で記述されているため，
    scene 全体での単位を sensor/light の位置変換に統一して使う．
    現状は mm と m のみサポート．

    Args:
        unit: "mm" または "m"．

    Returns:
        座標をメートルに変換するスケール係数．
    """
    if unit == "mm":
        return _MM_TO_M
    if unit == "m":
        return 1.0
    msg = f"Unsupported position unit: {unit!r}"
    raise ValueError(msg)


def _save_loaded_mesh_to_ply(mesh: LoadedMesh, path: Path) -> None:
    """LoadedMesh を PLY ファイルとして書き出す．

    trimesh 経由で書き出すことで Mitsuba の PLY パーサに渡せる形式にする．

    Args:
        mesh: 書き出す LoadedMesh（頂点・面が既に目的単位に変換済みであること）．
        path: 書き出し先の PLY ファイルパス．
    """
    tmesh = trimesh.Trimesh(vertices=mesh.vertices, faces=mesh.faces, process=False)
    tmesh.export(str(path), file_type="ply")


def build_scene(
    config: SceneConfig,
    project_root: Path | None = None,
    *,
    exclude_emitters: bool = False,
) -> tuple[dict, Path]:
    """SceneConfig 全体を Mitsuba load_dict 互換の dict に変換する．

    STL メッシュは trimesh で読み込み，一時ディレクトリに PLY として書き出して
    Mitsuba の "ply" shape type に渡す．

    呼び出し側（runner.py）は返された tempdir を try/finally で shutil.rmtree すること．
    Mitsuba の load_dict が PLY ファイルを読むまでファイルを保持する必要があるため，
    builder 内では tempdir を削除しない．

    Args:
        config: 検証済み SceneConfig．
        project_root: STL 相対パスの基点ディレクトリ．None なら CWD を使用．
        exclude_emitters: True にすると AreaLight の rectangle 形状をシーンに登録しない．
            shadow ray 評価専用シーンを構築する際に使用する．PointLight / DirectionalLight は
            emitter dict を持たないため常に除外されない．

    Returns:
        (scene_dict, tempdir) のタプル．
        - scene_dict: mi.load_dict() に渡せる dict．
        - tempdir: 一時 PLY ファイルを格納したディレクトリ（呼び出し側が削除する）．

    Raises:
        FileNotFoundError: STL ファイルが見つからない場合．
        ValueError: 未対応の設定値が含まれる場合．
    """
    from sm_tias_lightsim.scene.schema import AreaLight

    root = project_root if project_root is not None else Path.cwd()

    # YAML 内の座標値の単位は sensor.position の単位と一致しているため，
    # sensor の位置座標を基準に scale を決める
    # SPEC_02: lights の position も mm 単位で記述する前提
    # ここでは固定で mm→m スケールを使う（YAML 上の座標は常に mm 基準）
    pos_scale = _MM_TO_M

    tempdir = Path(tempfile.mkdtemp(prefix="sm_tias_scene_"))
    try:
        scene_dict: dict = {"type": "scene"}

        # integrator
        scene_dict["integrator"] = {
            "type": config.render.integrator,
            "max_depth": config.render.max_depth,
        }

        # sensor
        scene_dict["camera"] = build_sensor_dict(config.sensor, position_scale=pos_scale)

        # materials（トップレベルに配置し mesh から id 参照する）
        for name, mat in config.materials.items():
            mat_dict = build_material_dict(mat)
            mat_dict["id"] = f"mat_{name}"
            scene_dict[f"mat_{name}"] = mat_dict

        # meshes
        for mesh_cfg in config.geometry.meshes:
            raw_path = Path(mesh_cfg.path)
            stl_path = raw_path if raw_path.is_absolute() else root / mesh_cfg.path
            loaded = load_stl(stl_path, mesh_cfg.unit)
            scaled = scale_vertices(loaded, target_unit="m")
            ply_path = tempdir / f"{mesh_cfg.id}.ply"
            _save_loaded_mesh_to_ply(scaled, ply_path)
            scene_dict[f"mesh_{mesh_cfg.id}"] = {
                "type": "ply",
                "filename": str(ply_path),
                "bsdf": {"type": "ref", "id": f"mat_{mesh_cfg.material}"},
            }

        # lights
        # exclude_emitters=True のとき AreaLight（rectangle 形状）はシーンに登録しない．
        # shadow ray 用シーンで AreaLight 自身が遮蔽物になるのを防ぐためのオプション．
        for light_cfg in config.lights:
            if exclude_emitters and isinstance(light_cfg, AreaLight):
                continue
            scene_dict[f"light_{light_cfg.id}"] = build_light_dict(
                light_cfg, position_scale=pos_scale
            )

        return scene_dict, tempdir

    except Exception:
        shutil.rmtree(tempdir, ignore_errors=True)
        raise
