# pyright: reportCallIssue=none
# Mitsuba 3 は variant ベースの動的属性解決を行うため Pylance/Pyright が
# mi.* を object 型と推論し reportCallIssue を誤検知する（GUIDE_06 の方針）．
"""sm-visualize コマンドラインエントリポイント．

YAML シーン定義を読み込み，3D 照度ベイクと PyVista インタラクティブビューアを提供する．

処理フロー:
    1. YAML シーン定義を読み込み SceneConfig を得る．
    2. --render が指定された場合，luminance.npy を読み込む
       （sm-visualize 自身はレンダリングを実行しない）．
    3. --render 未指定かつ --no-bake 指定がない場合，bake_irradiance でベイクを計算する．
       既定（render 無し）: **全メッシュ（評価板含む）**を path / direct ベイクで統一着色．
       --render 指定時（任意）: 評価板はテクスチャ投影・壁は path ベイク（ハイブリッドモード）．
         --render + --bake-mode path: 評価板＝投影・壁＝ベイク着色（ハイブリッド）．
         --render + --bake-mode direct: 投影のみ（direct ベイクはしない、従来どおり）．
    4. --bake-cache 指定時はキャッシュ npz を読み書きする（path ベイクのみ有効）．
       キャッシュが存在すれば Mitsuba 起動なしで即時読込，なければベイク後に npz 保存．
    5. show_scene で PyVista 表示（または screenshot 保存）を行う．
    6. --output 指定があれば per-face irradiance を npz 形式で保存する．
"""

from __future__ import annotations

import argparse
import sys
from pathlib import Path


def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
    """コマンドライン引数をパースして返す．

    Args:
        argv: 引数リスト．None のとき sys.argv[1:] を使用．

    Returns:
        パース済み Namespace．
    """
    parser = argparse.ArgumentParser(
        prog="sm-visualize",
        description=(
            "YAML シーン定義から 3D 照度ベイクを実行し，PyVista でインタラクティブ表示する．"
            " --render を指定すると sm-render 出力の 2D 輝度マップを評価板に投影表示する"
            "（レンダリングは実行しない）．"
        ),
    )
    parser.add_argument(
        "--scene",
        "-s",
        required=True,
        type=Path,
        metavar="YAML_PATH",
        help="シーン定義 YAML ファイルのパス．",
    )
    parser.add_argument(
        "--render",
        type=Path,
        default=None,
        metavar="LUMINANCE_NPY",
        help=(
            "sm-render が出力した luminance.npy のパス（任意オプション）．"
            "指定時は評価板メッシュに 2D 輝度マップを orthographic 投影して表示する"
            "（sm-visualize 自身はレンダリングを実行しない）．"
            "未指定（既定）: 全メッシュ（評価板含む）を bake_irradiance で統一着色する．"
            "--render + --bake-mode path: ハイブリッドモード（評価板=投影・壁=ベイク）．"
            "--render + --bake-mode direct: 投影のみ（direct ベイクはスキップ）．"
        ),
    )
    parser.add_argument(
        "--clip-percentile",
        nargs=2,
        type=float,
        default=[1.0, 99.0],
        metavar=("LOW", "HIGH"),
        help=(
            "スカラ表示の percentile クリップ範囲（%%）．"
            "高ダイナミックレンジ潰れ対策．既定: 1 99 ．"
        ),
    )
    parser.add_argument(
        "--no-bake",
        action="store_true",
        default=False,
        help="照度ベイクをスキップしてシーンレイアウトのみ表示する．",
    )
    parser.add_argument(
        "--bake-mode",
        choices=["direct", "path"],
        default="direct",
        metavar="MODE",
        help=(
            "照度ベイクのモード．"
            "'direct'（既定）: shadow ray による直接照度のみ（後方互換）．"
            "'path': コサイン重点サンプリング + Mitsuba path tracer による多重反射込み照度．"
            " scalar_rgb variant を使用（LLVM / DRJIT_LIBLLVM_PATH 不要）．"
            " --render 未指定時（既定）: 全メッシュ（評価板含む）をベイクで統一着色する．"
            " --render + path: ハイブリッドモード（評価板=投影・壁=ベイク）．"
            " --render + direct: direct ベイクはスキップし投影のみ（従来どおり）．"
        ),
    )
    parser.add_argument(
        "--bake-samples",
        type=int,
        default=64,
        metavar="N",
        help=(
            "--bake-mode path 時の face あたりサンプル数．"
            "大きいほど低ノイズ・高コスト．既定: 64．"
            "高速確認は 16 程度，本番は 256 以上推奨．"
            "--bake-mode direct では無視される．"
        ),
    )
    parser.add_argument(
        "--bake-cache",
        type=Path,
        default=None,
        metavar="NPZ_PATH",
        help=(
            "path ベイク結果のキャッシュ npz ファイルパス（任意）．"
            " 指定パスが存在すれば npz を読み込んでベイクをスキップ（Mitsuba 起動なし・即時）．"
            " 存在しなければ path ベイクを実行し結果を npz に保存して次回以降キャッシュを利用．"
            " --bake-mode path のときのみ有効（direct では無視）．"
        ),
    )
    parser.add_argument(
        "--screenshot",
        type=Path,
        default=None,
        metavar="PNG_PATH",
        help="スクリーンショット保存先 PNG パス．指定時はウィンドウを開かず off_screen で保存する",
    )
    parser.add_argument(
        "--output",
        "-o",
        type=Path,
        default=None,
        metavar="OUTPUT_DIR",
        help="ベイク結果（per-face irradiance）を npz 形式で保存するディレクトリ（任意）．",
    )
    parser.add_argument(
        "--off-screen",
        action="store_true",
        default=False,
        help="ウィンドウを開かない（screenshot 専用モード）．",
    )
    parser.add_argument(
        "--opacity",
        type=float,
        default=0.5,
        metavar="O",
        help=(
            "ベイク着色モードのメッシュ不透明度（0=透明〜1=不透明、既定 0.5）．"
            " 1.0 指定で完全不透明になる．"
            " --render 投影モードには影響しない．"
        ),
    )
    parser.add_argument(
        "--hide-below",
        type=float,
        default=0.0,
        metavar="VALUE",
        help=(
            "ベイク着色で irradiance がこの値以下の面を非表示にする"
            "（既定 0.0＝光が当たっていない面を描かない）．"
            " 負値指定で全面表示（ベイク着色の全 face を描画）．"
            " 投影モード・ベイク計算ロジックには影響しない．"
        ),
    )
    parser.add_argument(
        "--bake-smooth",
        type=int,
        default=4,
        metavar="N",
        help=(
            "ベイク着色メッシュのスカラ Laplacian 平滑化イテレーション数（既定 4）．"
            " N>0 のとき cell↔point_data 変換を N 回往復してノイズを拡散・抑制する"
            "（per-face モンテカルロノイズによる三角形モザイクを軽減）．"
            " N=0 で平滑化なし（cell_data → point_data 変換のみ行い Gouraud 補間は有効）．"
            " 負値は 0 にクランプ．"
            " 投影モード・ベイク計算ロジックには影響しない．"
        ),
    )
    parser.add_argument(
        "--bake-scale",
        choices=["linear", "log"],
        default="log",
        metavar="SCALE",
        help=(
            "ベイク着色の輝度正規化．"
            "'log'（既定）: log10 変換後に共有 clim / percentile を算出する．"
            " 高ダイナミックレンジ潰れを防ぎ壁面にコントラストが付く．"
            "'linear': 従来どおり（後方互換）．"
            " 評価板の実レンダ投影には影響しない．"
        ),
    )
    parser.add_argument(
        "--bake-max-depth",
        type=int,
        default=16,
        metavar="N",
        help=(
            "path ベイクの ray 最大反射回数（render 設定から独立、既定 16）．"
            " 大きいほど多重反射の均一化を正確に積分する"
            "（SPEC_03: depth≥12 推奨．depth=8 は後段多重反射を取りこぼし勾配を過大評価）．"
            " --bake-mode direct では無視される（direct ベイクは path integrator を使わない）．"
        ),
    )
    # BooleanOptionalAction が Windows CP932 環境で --help 表示時に ↔ を出力し
    # UnicodeEncodeError になる問題を回避するため、store_true / store_false で実装する．
    _cull_group = parser.add_mutually_exclusive_group()
    _cull_group.add_argument(
        "--cull-outward",
        action="store_true",
        default=True,
        dest="cull_outward",
        help=(
            "外向き面（シーン外を向く ≈0 の外殻）を描画しない．"
            " 暗殻の濁り/見かけ段差を解消する．既定有効．"
            " --render の評価板テクスチャには影響しない（壁ベイク側にのみ適用）．"
        ),
    )
    _cull_group.add_argument(
        "--no-cull-outward",
        action="store_false",
        dest="cull_outward",
        help="外向き面を含む全面を描画する（後方互換）．",
    )
    parser.add_argument(
        "--no-sensor-marker",
        action="store_true",
        default=False,
        help=(
            "センサマーカー（青い球＋視線矢印）のみ非表示にする（デモ撮影用）．"
            " LED マーカーは残す．メッシュ着色とカラーバーは影響しない．"
        ),
    )
    parser.add_argument(
        "--no-led-markers",
        action="store_true",
        default=False,
        help=(
            "LED マーカー（黄色球・AreaLight plane・矢印等）のみ非表示にする（デモ撮影用）．"
            " センサマーカーは残す．メッシュ着色とカラーバーは影響しない．"
        ),
    )
    parser.add_argument(
        "--no-markers",
        action="store_true",
        default=False,
        help=(
            "LED/センサ両方のマーカー・矢印を非表示にするショートカット（デモ撮影用）．"
            " --no-sensor-marker --no-led-markers を同時指定したのと等価．"
            " メッシュ着色とカラーバーは残す．"
        ),
    )
    return parser.parse_args(argv)


def main(argv: list[str] | None = None) -> int:
    """sm-visualize コマンドのメインエントリポイント．

    処理フロー:
        1. コマンドライン引数をパースする．
        2. YAML シーン定義を load_scene_yaml で読み込み，SceneConfig を得る．
        3. --render が指定された場合，luminance.npy を読み込む（レンダリング実行なし）．
        4. --no-bake がなければ bake_irradiance で照度を計算する．
           --render 未指定（既定）: 全メッシュ（評価板含む）を統一ベイク着色．
           --render + path: ハイブリッド（評価板=投影・壁=ベイク；評価板をベイクから除外）．
           --render + direct: ベイクしない（投影のみ、従来どおり）．
        5. --output が指定されていれば per-face irradiance を npz で保存する．
        6. show_scene で PyVista 表示（または screenshot 保存）を行う．

    Args:
        argv: 引数リスト．None のとき sys.argv[1:] を使用．

    Returns:
        終了コード（成功時 0，失敗時 1）．
    """
    args = _parse_args(argv)

    scene_path: Path = args.scene.resolve()
    render_path: Path | None = args.render.resolve() if args.render else None
    screenshot: Path | None = args.screenshot.resolve() if args.screenshot else None
    output_dir: Path | None = args.output.resolve() if args.output else None
    clip_low: float = float(args.clip_percentile[0])
    clip_high: float = float(args.clip_percentile[1])
    bake_mode: str = args.bake_mode  # "direct" または "path"
    bake_samples: int = int(args.bake_samples)
    bake_cache_path: Path | None = args.bake_cache.resolve() if args.bake_cache else None
    bake_max_depth: int = int(args.bake_max_depth)
    cull_outward: bool = bool(args.cull_outward)
    # --no-markers は --no-sensor-marker / --no-led-markers 両方の互換ショートカット
    hide_sensor_marker: bool = bool(args.no_sensor_marker) or bool(args.no_markers)
    hide_led_markers: bool = bool(args.no_led_markers) or bool(args.no_markers)
    # モード判定:
    #   render 無し（既定）        → 全メッシュ統一ベイク（評価板も含む）
    #   --render + --bake-mode path  → ハイブリッドモード（評価板=投影・壁=ベイク）
    #   --render + --bake-mode direct → ベイクしない（投影のみ、従来どおり）
    is_hybrid = (render_path is not None) and (bake_mode == "path")
    no_bake: bool = args.no_bake or (render_path is not None and not is_hybrid)
    off_screen: bool = args.off_screen
    bake_opacity: float = float(args.opacity)
    hide_below: float = float(args.hide_below)
    bake_smooth: int = int(args.bake_smooth)
    bake_scale: str = args.bake_scale

    # YAML 読み込み
    try:
        from sm_tias_lightsim.scene.schema import load_scene_yaml

        config = load_scene_yaml(scene_path)
    except FileNotFoundError as exc:
        print(f"エラー: シーン YAML ファイルが見つかりません — {exc}", file=sys.stderr)
        return 1
    except Exception as exc:  # noqa: BLE001
        print(f"エラー: シーン YAML の読み込みに失敗しました — {exc}", file=sys.stderr)
        return 1

    # project_root: YAML ファイルの親ディレクトリを基点とする
    project_root = scene_path.parent

    # luminance.npy 読み込み（投影モード）
    luminance_map = None
    if render_path is not None:
        print(f"投影モード: luminance.npy を読み込みます: {render_path}")
        try:
            import numpy as np

            luminance_map = np.load(str(render_path))
            print(
                f"  shape={luminance_map.shape}, min={luminance_map.min():.3f},"
                f" max={luminance_map.max():.3f}, std={luminance_map.std():.3f}"
            )
        except FileNotFoundError:
            print(f"エラー: luminance.npy が見つかりません: {render_path}", file=sys.stderr)
            return 1
        except Exception as exc:  # noqa: BLE001
            print(f"エラー: luminance.npy の読み込みに失敗しました — {exc}", file=sys.stderr)
            return 1

    # 照度ベイク
    irradiance: dict | None = None
    if not no_bake:
        import numpy as np

        if bake_mode == "direct":
            if is_hybrid:
                mode_label = "直接照度（shadow ray）- ハイブリッド（評価板除外）"
            else:
                mode_label = "直接照度（shadow ray）- 全メッシュ統一"
        else:
            if is_hybrid:
                mode_label = (
                    f"path ベイク（ハイブリッドモード・壁のみ,"
                    f" samples={bake_samples}, max_depth={bake_max_depth}）"
                )
            else:
                mode_label = (
                    f"path ベイク（多重反射込み・全メッシュ統一,"
                    f" samples={bake_samples}, max_depth={bake_max_depth}）"
                )
        print(f"照度ベイクを開始します（{mode_label}）...")
        if bake_mode == "path":
            print("  path モードは計算が重いため時間がかかります．")
        if is_hybrid:
            print(
                "  ハイブリッドモード（--render 指定）: "
                "評価板メッシュはベイクから除外します（投影で着色）．"
            )
        else:
            print(
                "  全メッシュ統一ベイク（--render 未指定）: "
                "評価板を含むすべてのメッシュをベイクします．"
            )

        # ハイブリッドモード時のみ評価板メッシュをベイクから除外する
        # render 未指定（全面統一ベイク）時は評価板を除外しない
        exclude_ids: set[str] = set()
        if is_hybrid:
            from sm_tias_lightsim.visualize.projection import find_eval_board_mesh_id

            eval_board_id_for_bake = find_eval_board_mesh_id(config)
            exclude_ids = {eval_board_id_for_bake}
            print(
                f"  評価板メッシュ '{eval_board_id_for_bake}' を"
                "ベイクから除外します（投影着色のため）．"
            )

        # キャッシュ対応（path モードのみ）
        cache_loaded = False
        if bake_mode == "path" and bake_cache_path is not None:
            if bake_cache_path.exists():
                print(f"キャッシュを読み込みます（ベイク再計算なし）: {bake_cache_path}")
                try:
                    npz = np.load(str(bake_cache_path))
                    irradiance = {k: npz[k] for k in npz.files}
                    # 形状・型の簡易チェック
                    for k, v in irradiance.items():
                        if v.ndim != 1:
                            msg = f"キャッシュ '{k}' の配列形状が不正です: ndim={v.ndim}"
                            raise ValueError(msg)
                    cache_loaded = True
                    print(f"キャッシュ読み込み完了．メッシュ数={len(irradiance)}")
                    for mesh_id, irr in irradiance.items():
                        print(
                            f"  {mesh_id}: shape={irr.shape}  min={irr.min():.4e}  "
                            f"max={irr.max():.4e}"
                        )
                except Exception as exc:  # noqa: BLE001
                    print(
                        "警告: キャッシュの読み込みに失敗しました"
                        f"（再計算にフォールバック）— {exc}",
                        file=sys.stderr,
                    )
                    irradiance = None
                    cache_loaded = False

        if not cache_loaded:
            try:
                from sm_tias_lightsim.visualize.baker import bake_irradiance

                if bake_mode == "path":
                    irradiance = bake_irradiance(
                        config,
                        project_root,
                        mode="path",
                        samples=bake_samples,
                        exclude_mesh_ids=exclude_ids,
                        max_depth=bake_max_depth,
                    )
                else:
                    irradiance = bake_irradiance(
                        config,
                        project_root,
                        mode="direct",
                        exclude_mesh_ids=exclude_ids,
                    )

                # path ベイク後にキャッシュを保存する
                if bake_mode == "path" and bake_cache_path is not None and irradiance:
                    try:
                        bake_cache_path.parent.mkdir(parents=True, exist_ok=True)
                        np.savez(str(bake_cache_path), **irradiance)
                        print(f"ベイクキャッシュを保存しました: {bake_cache_path}")
                    except Exception as exc:  # noqa: BLE001
                        print(
                            f"警告: キャッシュの保存に失敗しました — {exc}",
                            file=sys.stderr,
                        )
            except Exception as exc:  # noqa: BLE001
                print(f"エラー: 照度ベイクに失敗しました — {exc}", file=sys.stderr)
                return 1

        if irradiance:
            all_vals = np.concatenate([v for v in irradiance.values() if len(v) > 0])
            print(
                f"照度ベイク完了．全メッシュ: min={all_vals.min():.4e}  "
                f"max={all_vals.max():.4e}  std={all_vals.std():.4e}"
            )
            for mesh_id, irr in irradiance.items():
                print(
                    f"  {mesh_id}: min={irr.min():.4e}  max={irr.max():.4e}  "
                    f"std={irr.std():.4e}  nonzero={int((irr > 0).sum())}/{len(irr)}"
                )
        else:
            print("照度ベイク完了（結果なし）．")
    elif render_path is None:
        print("--no-bake: 照度ベイクをスキップします．シーンレイアウトのみ表示します．")

    # ベイク結果を npz で保存
    if irradiance is not None and output_dir is not None:
        try:
            output_dir.mkdir(parents=True, exist_ok=True)
            import numpy as np

            npz_path = output_dir / f"{config.name}_irradiance.npz"
            np.savez(npz_path, **irradiance)
            print(f"照度データ保存: {npz_path}")
        except Exception as exc:  # noqa: BLE001
            print(f"エラー: 照度データの保存に失敗しました — {exc}", file=sys.stderr)
            return 1

    # PyVista 表示
    try:
        from sm_tias_lightsim.visualize.viewer import show_scene

        show_scene(
            config,
            project_root,
            irradiance=irradiance,
            luminance_map=luminance_map,
            off_screen=off_screen,
            screenshot=screenshot,
            clip_low=clip_low,
            clip_high=clip_high,
            bake_opacity=bake_opacity,
            hide_below=hide_below,
            bake_smooth=bake_smooth,
            bake_scale=bake_scale,
            cull_outward=cull_outward,
            hide_sensor_marker=hide_sensor_marker,
            hide_led_markers=hide_led_markers,
        )
    except Exception as exc:  # noqa: BLE001
        print(f"エラー: PyVista 表示に失敗しました — {exc}", file=sys.stderr)
        return 1

    if screenshot is not None:
        print(f"スクリーンショット保存: {screenshot}")

    return 0
