# pyright: reportCallIssue=none
# Mitsuba 3 は variant ベースの動的属性解決を行うため Pylance/Pyright が
# mi.* を object 型と推論し reportCallIssue を誤検知する（GUIDE_06 の方針）．
"""各メッシュ face 中心での直接照度ベイクモジュール．

shadow ray + コサイン項 + 距離減衰による直接照度（W/m²）を計算し，
{mesh_id: irradiance_per_face (N_f,) float32} の形式で返す．

多重反射（インターリフレクション）は Phase A では無視する．
将来的に mode="path" を追加することで path tracing ベースの計算に切り替え可能．
"""

from __future__ import annotations

import logging
import shutil
from pathlib import Path
from typing import Literal

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,
)

_logger = logging.getLogger(__name__)

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

# shadow ray の原点オフセット（face 法線方向に少しずらして自己交差を防ぐ）
_SHADOW_RAY_EPSILON = 1e-4

# DirectionalLight の shadow ray の最大距離（無限遠をシミュレート）
_DIRECTIONAL_MAXT = 1e9

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


def _to_scalar_intensity(value: float | tuple[float, float, float]) -> float:
    """スカラまたは RGB タプルを輝度スカラに変換する．

    RGB の場合は Rec.709 係数で輝度に変換する．
    スカラの場合はそのまま返す．

    Args:
        value: スカラまたは RGB タプル．

    Returns:
        輝度スカラ値（float）．
    """
    if isinstance(value, (int, float)):
        return float(value)
    r, g, b = value
    return 0.2126 * r + 0.7152 * g + 0.0722 * b


def _shadow_ray_blocked(
    scene: object,
    origin: np.ndarray,
    direction: np.ndarray,
    maxt: float,
) -> bool:
    """shadow ray が遮蔽されているかを判定する．

    origin → direction 方向に距離 maxt まで ray を飛ばし，
    途中に交差があれば True（遮蔽あり）を返す．

    Args:
        scene: Mitsuba の Scene オブジェクト（load_dict 済み）．
        origin: ray の起点（m 単位）．
        direction: ray の方向（単位ベクトル）．
        maxt: 最大ヒット距離（m 単位）．

    Returns:
        遮蔽あり（途中に交差あり）なら True．
    """
    import mitsuba as mi

    o = mi.Point3f(float(origin[0]), float(origin[1]), float(origin[2]))
    d = mi.Vector3f(float(direction[0]), float(direction[1]), float(direction[2]))
    ray = mi.Ray3f(o, d)  # type: ignore[call-arg]
    ray.maxt = maxt  # type: ignore[assignment]
    si = scene.ray_intersect_preliminary(ray)  # type: ignore[attr-defined]
    return bool(si.is_valid())


def _compute_face_centroids_and_normals(
    vertices: np.ndarray, faces: np.ndarray
) -> tuple[np.ndarray, np.ndarray]:
    """face の重心と法線ベクトルを計算する．

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

    Returns:
        (centroids, normals) のタプル．
        - centroids: face 重心 (N_f, 3)．
        - normals: 単位法線ベクトル (N_f, 3)．長さ 0 の面はゼロベクトルになる．
    """
    v0 = vertices[faces[:, 0]]
    v1 = vertices[faces[:, 1]]
    v2 = vertices[faces[:, 2]]

    centroids = (v0 + v1 + v2) / 3.0

    edge1 = v1 - v0
    edge2 = v2 - v0
    raw_normals = np.cross(edge1, edge2)  # (N_f, 3)

    norms = np.linalg.norm(raw_normals, axis=1, keepdims=True)  # (N_f, 1)
    norms = np.where(norms == 0.0, 1.0, norms)
    normals = raw_normals / norms

    return centroids, normals


def _accumulate_finite_light(
    irradiance: np.ndarray,
    centroids: np.ndarray,
    normals: np.ndarray,
    scene: object,
    light_pos_m: np.ndarray,
    intensity: float,
    emit_dir: np.ndarray | None = None,
) -> None:
    """距離減衰のある光源（PointLight / AreaLight）の照度を face ごとに累積する．

    PointLight と AreaLight は「光源位置 → face 距離」「cos_theta」「shadow ray」の
    計算構造が共通のため，このヘルパーに集約する．
    AreaLight に固有の「裏面スキップ」は emit_dir が指定されたときのみ評価する．

    Args:
        irradiance: 累積先の照度配列 (N_f,)，in-place で更新する．
        centroids: face 重心 (N_f, 3)．
        normals: face 単位法線 (N_f, 3)．
        scene: Mitsuba Scene オブジェクト．
        light_pos_m: 光源位置（m 単位）．
        intensity: 光源の等価スカラ強度（W/sr 相当）．
        emit_dir: AreaLight の放射方向単位ベクトル（正規化済み）．
            None のとき裏面チェックをスキップする（PointLight 用）．
    """
    for fi in range(len(centroids)):
        centroid = centroids[fi]
        normal = normals[fi]

        to_light = light_pos_m - centroid
        distance = float(np.linalg.norm(to_light))
        if distance < _SHADOW_RAY_EPSILON:
            continue

        dir_n = to_light / distance
        cos_theta = float(np.dot(normal, dir_n))
        if cos_theta <= 0.0:
            continue

        # AreaLight の裏面には放射しない（放射方向との角度チェック）
        # emit_dir が face から光源への方向と逆向きの場合はスキップ
        if emit_dir is not None:
            emit_cos = float(np.dot(-dir_n, emit_dir))
            if emit_cos <= 0.0:
                continue

        # shadow ray: origin を法線方向にわずかにオフセットし，
        # maxt は光源手前までに制限して自己遮蔽と光源貫通を防ぐ
        origin = centroid + _SHADOW_RAY_EPSILON * normal
        if _shadow_ray_blocked(scene, origin, dir_n, distance - _SHADOW_RAY_EPSILON):
            continue

        irradiance[fi] += float(intensity * cos_theta / (distance**2))


def bake_irradiance(
    scene_config: SceneConfig,
    project_root: Path,
    *,
    mode: Literal["direct"] = "direct",
) -> dict[str, np.ndarray]:
    """各メッシュの face 中心で照度（W/m²）を計算する（直接照度のみ）．

    Phase A: shadow ray + コサイン項 + 距離減衰による直接照度のみを計算する．
    多重反射は無視する（Phase B で mode="path" を追加予定）．

    shadow ray の評価には Mitsuba の `scene.ray_intersect_preliminary` を利用する．
    遮蔽された face は寄与をスキップする．

    Args:
        scene_config: 検証済み SceneConfig．
        project_root: STL 相対パスの基点ディレクトリ．
        mode: 計算モード．現在は "direct" のみサポート．

    Returns:
        {mesh_id: irradiance_per_face (N_f,) float32} の辞書．
        irradiance の単位は W/m²（intensity の単位に依存）．

    Raises:
        NotImplementedError: mode が "direct" 以外の場合．
        FileNotFoundError: STL ファイルが見つからない場合．
    """
    if mode != "direct":
        msg = f"mode={mode!r} は未サポートです．現在は 'direct' のみ実装しています．"
        raise NotImplementedError(msg)

    import mitsuba as mi

    from sm_tias_lightsim.scene.builder import build_scene

    # Mitsuba variant を初期化（scalar_rgb を使う：shadow ray 評価に最適）
    # runner.py と同じ variant が既に設定されている場合はそのまま続行する
    desired_variant = "scalar_rgb"
    try:
        mi.set_variant(desired_variant)
    except Exception:
        _logger.warning(
            "Mitsuba variant の切り替えに失敗しました．現在の variant で続行します．"
        )

    # shadow ray 評価専用シーン（AreaLight の rectangle 形状を除外）を構築する．
    # AreaLight を含む通常シーンでは，面光源自身の rectangle にヒットして
    # 正面メッシュへの shadow ray が常に遮蔽されてしまう問題を回避するため，
    # exclude_emitters=True で AreaLight をシーンから除外する．
    scene_dict, tempdir = build_scene(
        scene_config, project_root=project_root, exclude_emitters=True
    )
    try:
        scene = mi.load_dict(scene_dict)
        return _bake_direct(scene_config, project_root, scene)
    finally:
        shutil.rmtree(tempdir, ignore_errors=True)


def _bake_direct(
    scene_config: SceneConfig,
    project_root: Path,
    scene: object,
) -> dict[str, np.ndarray]:
    """直接照度ベイクの内部実装．

    各メッシュの face 中心に対して，全光源からの直接照度を shadow ray で計算する．

    Args:
        scene_config: 検証済み SceneConfig．
        project_root: STL 相対パスの基点ディレクトリ．
        scene: Mitsuba Scene オブジェクト（load_dict 済み）．

    Returns:
        {mesh_id: irradiance_per_face (N_f,) float32}．
    """
    result: dict[str, np.ndarray] = {}

    for mesh_cfg in scene_config.geometry.meshes:
        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")

        centroids, normals = _compute_face_centroids_and_normals(
            scaled.vertices, scaled.faces
        )
        n_faces = len(centroids)
        irradiance = np.zeros(n_faces, dtype=np.float32)

        for light in scene_config.lights:
            if isinstance(light, PointLight):
                intensity = _to_scalar_intensity(light.intensity)
                light_pos_m = np.array(light.position, dtype=np.float64) * _MM_TO_M
                _accumulate_finite_light(
                    irradiance, centroids, normals, scene, light_pos_m, intensity
                )

            elif isinstance(light, AreaLight):
                # 面光源: 中心点を点光源として近似する（TODO: 面サンプリングで精度向上可能）
                # 近似の精度を上げるには center を N 点サンプリングして平均する方法がある．
                # 現時点ではシンプルに中心点のみで計算する．
                radiance = _to_scalar_intensity(light.radiance)
                area = (light.size[0] * _MM_TO_M) * (light.size[1] * _MM_TO_M)
                # 点光源近似の等価強度: radiance × area（Lambertian 放射源の近似）
                equiv_intensity = radiance * area

                center_m = np.array(light.center, dtype=np.float64) * _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:
                    continue
                emit_dir = dir_vec / dir_norm

                _accumulate_finite_light(
                    irradiance, centroids, normals, scene, center_m, equiv_intensity, emit_dir
                )

            elif isinstance(light, DirectionalLight):
                # YAML の direction は「光の進行方向」なので，face への入射方向は逆転する
                irradiance_val = _to_scalar_intensity(light.irradiance)
                dir_vec = np.array(light.direction, dtype=np.float64)
                dir_norm = np.linalg.norm(dir_vec)
                if dir_norm < _DIR_NORM_EPSILON:
                    continue
                incident_dir = -dir_vec / dir_norm

                for fi in range(n_faces):
                    centroid = centroids[fi]
                    normal = normals[fi]

                    cos_theta = float(np.dot(normal, incident_dir))
                    if cos_theta <= 0.0:
                        continue

                    origin = centroid + _SHADOW_RAY_EPSILON * normal
                    if _shadow_ray_blocked(
                        scene, origin, incident_dir, float(_DIRECTIONAL_MAXT)
                    ):
                        continue

                    irradiance[fi] += float(irradiance_val * cos_theta)

        result[mesh_cfg.id] = irradiance

    return result
