"""バッフルが評価板への直接光を実際に塞いでいるかをレイキャストで直接検証する.

「指標が良くなったから効いている」という論法を取らないための検証スクリプト．
評価板上の多数の点から LED 発光面上の多数の点へレイを飛ばし，**LED に到達する前に
別の物体（バッフル）に遮られること**を Mitsuba の交差計算で実測する．

判定:
    board_baffle 構成 : 全レイが遮蔽されていること（blocked == total）が合格条件．
                        1 本でも通っていればバッフル寸法が不足している．
    axial_wall  構成  : LED が板と反対向きなので「到達はするが発光面の裏側」．
                        本スクリプトは遮蔽本数だけを報告し，合否判定は行わない．

実行方法:
    uv run python scripts/verify_baffle_occlusion.py \
        --scene configs/_tmp_sphere_radius/scene_board_baffle_r100.yaml
    uv run python scripts/verify_baffle_occlusion.py --all-radii
"""

from __future__ import annotations

import argparse
import shutil
import sys
from pathlib import Path

import numpy as np

sys.path.insert(0, str(Path(__file__).resolve().parent))
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))

from sweep_sphere_radius import (  # noqa: E402
    BOARD_SURFACE_Z,
    CAVITIES,
    RADII,
    TMP_DIR,
    job_name,
)

from sm_tias_lightsim.runtime.llvm_init import ensure_drjit_libllvm_path  # noqa: E402
from sm_tias_lightsim.scene.builder import build_scene  # noqa: E402
from sm_tias_lightsim.scene.schema import AreaLight, load_scene_yaml  # noqa: E402

ROOT = Path(__file__).resolve().parents[1]
MM_TO_M = 0.001

# 評価板の実体半幅（configs/meshes/照明ムラ補正ボード.stl と一致）
BOARD_HALF = (49.5, 51.0)
# レイ起点を板面から少しキャビティ側へ浮かせる量（mm）．板自身への自己交差を避ける
SURFACE_OFFSET = 0.05
# レイが LED 発光面そのものに当たったと誤判定しないための許容量（m）
HIT_EPSILON = 1.0e-5
# LED 外周サンプルの位相オフセット（rad）．対称面上にサンプルを置かないための任意値
LED_RING_PHASE = 0.1234


def board_samples(grid: int) -> np.ndarray:
    """評価板のキャビティ側の面を格子状にサンプリングした点（mm）を返す.

    z は板面（BOARD_SURFACE_Z）から SURFACE_OFFSET だけキャビティ側へ浮かせる．
    面上に厳密に置くと自分自身の面に交差して「遮蔽された」と誤判定するため．
    四隅は必ず含める（見込み角が最大＝最悪条件になる）．
    """
    gx = np.linspace(-BOARD_HALF[0], BOARD_HALF[0], grid)
    gy = np.linspace(-BOARD_HALF[1], BOARD_HALF[1], grid)
    mesh_x, mesh_y = np.meshgrid(gx, gy)
    z = np.full(mesh_x.size, BOARD_SURFACE_Z - SURFACE_OFFSET)
    return np.stack([mesh_x.ravel(), mesh_y.ravel(), z], axis=1)


def led_samples(light: AreaLight, count: int) -> np.ndarray:
    """LED 円板上のサンプル点（mm）を返す（中心 + 外周リング．外周が最悪条件）.

    リングは light.direction を法線とする円板の平面上に張る（オフ軸 LED＝TEST_17 で
    円板が斜めを向くため．軸沿い direction=(0,0,±1) では従来どおり XY 平面になる）．
    位相を LED_RING_PHASE だけずらすのは従来どおり＝サンプル点が XZ / YZ 対称面に
    乗り，メッシュのエッジ上を走るレイになって交差判定が数値的に不安定になるのを防ぐ．
    """
    center = np.asarray(light.center, dtype=float)
    radius = float(getattr(light, "radius", 0.0) or 0.0)
    normal = np.asarray(getattr(light, "direction", None) or [0.0, 0.0, -1.0], dtype=float)
    normal = normal / np.linalg.norm(normal)
    axis = np.array([0.0, 1.0, 0.0]) if abs(normal[1]) < 0.9 else np.array([0.0, 0.0, 1.0])
    u = np.cross(normal, axis)
    u /= np.linalg.norm(u)
    v = np.cross(normal, u)
    angles = LED_RING_PHASE + np.linspace(0.0, 2.0 * np.pi, count, endpoint=False)
    ring = center + radius * (np.outer(np.cos(angles), u) + np.outer(np.sin(angles), v))
    return np.vstack([center[None, :], ring])


def check_occlusion(scene_yaml: Path, board_grid: int, led_count: int) -> dict[str, float]:
    """板 × LED の全組合せについて，LED 到達前に遮蔽されるレイの割合を測る.

    Returns:
        total / blocked / clearance_min（遮蔽点から LED までの最短余裕・mm）を含む dict．
    """
    ensure_drjit_libllvm_path()
    import mitsuba as mi

    config = load_scene_yaml(scene_yaml)
    if mi.variant() is None:
        mi.set_variant(config.render.variant)

    scene_dict, tempdir = build_scene(config, project_root=scene_yaml.parent)
    try:
        scene = mi.load_dict(scene_dict)
        lights = [light for light in config.lights if isinstance(light, AreaLight)]
        if not lights:
            msg = f"{scene_yaml}: AreaLight が見つかりません"
            raise ValueError(msg)

        origins_mm = board_samples(board_grid)
        targets_mm = np.vstack([led_samples(light, led_count) for light in lights])
        # 全組合せを 1 回の交差計算にまとめる（llvm_* で 1 本ずつ呼ぶと JIT が不安定）
        origins = np.repeat(origins_mm, len(targets_mm), axis=0) * MM_TO_M
        targets = np.tile(targets_mm, (len(origins_mm), 1)) * MM_TO_M
        delta = targets - origins
        distance = np.linalg.norm(delta, axis=1)
        direction = delta / distance[:, None]

        ray = mi.Ray3f(
            mi.Point3f(origins[:, 0], origins[:, 1], origins[:, 2]),
            mi.Vector3f(direction[:, 0], direction[:, 1], direction[:, 2]),
        )
        si = scene.ray_intersect(ray)
        hit_t = np.asarray(si.t).reshape(-1)
        valid = np.asarray(si.is_valid()).reshape(-1)
        # LED 発光面そのものへの命中は「遮蔽なし」．それより手前で何かに当たれば遮蔽
        blocked = valid & (hit_t < distance - HIT_EPSILON)
        clearance = (distance - hit_t)[blocked]
        return {
            "total": float(len(distance)),
            "blocked": float(blocked.sum()),
            "clearance_min_mm": float(clearance.min() / MM_TO_M) if blocked.any() else float("nan"),
        }
    finally:
        shutil.rmtree(tempdir, ignore_errors=True)


def main() -> int:
    """指定シーン（または全半径）のバッフル遮蔽を検証する."""
    parser = argparse.ArgumentParser(description="バッフルの遮蔽をレイキャストで検証する")
    parser.add_argument("--scene", type=Path, default=None, help="検証するシーン YAML")
    parser.add_argument("--all-radii", action="store_true",
                        help="_tmp_sphere_radius の board_baffle 全半径を検証する")
    parser.add_argument("--cavity", choices=CAVITIES, default="similar_g2",
                        help="--all-radii のときに検証するキャビティ形状")
    parser.add_argument("--board-grid", type=int, default=41,
                        help="評価板の 1 辺あたりサンプル数（既定 41 = 1681 点）")
    parser.add_argument("--led-samples", type=int, default=32,
                        help="LED 円板の外周サンプル数（既定 32）")
    args = parser.parse_args()

    if args.all_radii:
        scenes = [TMP_DIR / f"scene_board_baffle_{args.cavity}_{job_name(r)}.yaml"
                  for r in RADII]
    elif args.scene is not None:
        scenes = [args.scene]
    else:
        parser.error("--scene か --all-radii のいずれかを指定してください")

    failures = 0
    print(f"{'シーン':>44} {'レイ本数':>10} {'遮蔽':>10} {'判定':>8} {'最小余裕mm':>11}")
    for scene_yaml in scenes:
        if not scene_yaml.exists():
            print(f"{scene_yaml.name:>44}  (未生成のためスキップ)")
            continue
        result = check_occlusion(scene_yaml, args.board_grid, args.led_samples)
        ok = result["blocked"] == result["total"]
        failures += 0 if ok else 1
        print(f"{scene_yaml.name:>44} {result['total']:10.0f} {result['blocked']:10.0f} "
              f"{'OK' if ok else '不合格':>8} {result['clearance_min_mm']:11.2f}")

    if failures:
        print(f"\n!! {failures} 件で直接光が通っています．バッフル寸法を見直してください")
        return 1
    print("\n全シーンで評価板から LED への直線は完全に遮蔽されています")
    return 0


if __name__ == "__main__":
    sys.exit(main())
