"""YAML 材料定義 → Mitsuba BSDF dict 変換モジュール．"""

from __future__ import annotations

from sm_tias_lightsim.scene.schema import (
    BlendBSDFConfig,
    DielectricMaterial,
    DiffuseMaterial,
    MaterialConfig,
    RoughConductorBSDFConfig,
    ThinDielectricMaterial,
)


def _to_rgb(value: float | tuple[float, float, float]) -> list[float]:
    """スカラまたは RGB タプルを 3 要素の float リストに正規化する．

    lights.py / materials.py 共通のヘルパ．このモジュールが正本で，
    lights.py はここから import して重複を回避する．

    Args:
        value: スカラ（全チャンネル同値）または RGB タプル．

    Returns:
        3 要素の float リスト．
    """
    if isinstance(value, (int, float)):
        return [float(value)] * 3
    return list(value)


def _build_bsdf_dict(material: object) -> dict:
    """1 つの BSDF config を Mitsuba BSDF dict に再帰変換する（内部ヘルパ）．

    BlendBSDFConfig の components は再帰的に変換する．

    Args:
        material: DiffuseMaterial / DielectricMaterial / ThinDielectricMaterial /
                  RoughConductorBSDFConfig / BlendBSDFConfig のいずれか．

    Returns:
        Mitsuba BSDF dict．

    Raises:
        ValueError: 未対応の材料タイプが指定された場合．
    """
    if isinstance(material, DiffuseMaterial):
        return {
            "type": "diffuse",
            "reflectance": {
                "type": "rgb",
                "value": _to_rgb(material.reflectance),
            },
        }

    if isinstance(material, DielectricMaterial):
        return {
            "type": "dielectric",
            "int_ior": material.int_ior,
            "ext_ior": material.ext_ior,
        }

    if isinstance(material, ThinDielectricMaterial):
        return {
            "type": "thindielectric",
            "int_ior": material.int_ior,
            "ext_ior": material.ext_ior,
        }

    if isinstance(material, RoughConductorBSDFConfig):
        # material="none" で eta/k なし（完全反射体）として扱う
        return {
            "type": "roughconductor",
            "alpha": material.alpha,
            "distribution": material.distribution,
            "material": "none",
        }

    if isinstance(material, BlendBSDFConfig):
        # components は必ず 2 要素（スキーマで min_length=2, max_length=2 を保証）
        return {
            "type": "blendbsdf",
            "weight": material.weight,
            "bsdf_0": _build_bsdf_dict(material.components[0]),
            "bsdf_1": _build_bsdf_dict(material.components[1]),
        }

    msg = f"Unsupported material type: {type(material).__name__}"
    raise ValueError(msg)


def build_material_dict(material: MaterialConfig) -> dict:
    """1 MaterialConfig を Mitsuba BSDF dict に変換する．

    Args:
        material: SceneConfig.materials の値（DiffuseMaterial / BlendBSDFConfig 等）．

    Returns:
        Mitsuba BSDF dict．
        - diffuse → {"type": "diffuse", "reflectance": {"type":"rgb","value":[r,g,b]}}
        - dielectric → {"type": "dielectric", "int_ior": ..., "ext_ior": ...}
        - thindielectric → {"type": "thindielectric", "int_ior": ..., "ext_ior": ...}
        - roughconductor → {"type": "roughconductor", "alpha": ..., "distribution": ...,
          "material": "none"}
        - blendbsdf → {"type": "blendbsdf", "weight": ..., "bsdf_0": ..., "bsdf_1": ...}

    Raises:
        ValueError: 未対応の材料タイプが指定された場合．
    """
    return _build_bsdf_dict(material)


def _is_opaque(material: object) -> bool:
    """不透明な反射専用 BSDF かどうかを判定する（内部ヘルパ）．

    誘電体（dielectric / thindielectric）は透過を伴い両面挙動が本質なので
    twosided でラップしてはならない．拡散・粗面導体は片面 BSDF なので
    ラップ対象になる．blendbsdf は全構成要素が不透明なときのみ不透明とみなす．

    Args:
        material: MaterialConfig のいずれか．

    Returns:
        twosided でラップしてよい不透明反射材なら True．
    """
    if isinstance(material, (DiffuseMaterial, RoughConductorBSDFConfig)):
        return True

    if isinstance(material, BlendBSDFConfig):
        return all(_is_opaque(c) for c in material.components)

    # DielectricMaterial / ThinDielectricMaterial および未知型は非対象
    return False


def build_scene_bsdf_dict(material: MaterialConfig) -> dict:
    """シーン登録用の BSDF dict を返す（不透明反射材は twosided でラップする）．

    `build_material_dict` が返す素の BSDF dict をシーンにそのまま登録すると，
    Mitsuba の diffuse / roughconductor は**片面 BSDF** であるため，
    法線が裏返ったメッシュ面は入射光を一切反射せず**エラーも警告もなく真っ黒**になる．
    間接照明のみで評価板を照らす SmTIAS の構成では，内壁が 1 面でも黒化すると
    輝度分布が静かに壊れる（TEST_12 のシェーディング法線バグと同じ「気づけない」故障モード）．

    そこで不透明反射材を `twosided` でラップし，面の向きに依存しない物理的に正しい
    挙動にする．積分球の解析解による検証では，法線が正しい場合はラップの有無で
    結果が完全に一致し（無影響），法線が裏返った場合のみ黒化を救済することを確認済み．

    透過を伴う誘電体（dielectric / thindielectric）は両面挙動が本質なのでラップしない．

    Args:
        material: SceneConfig.materials の値．

    Returns:
        Mitsuba BSDF dict．不透明反射材は
        {"type": "twosided", "bsdf": <素の BSDF dict>} の形になる．
    """
    bsdf = build_material_dict(material)
    if _is_opaque(material):
        return {"type": "twosided", "bsdf": bsdf}
    return bsdf
