"""TEST_03 用「輝度マップ形状追跡」比較パネルを生成する.

各改善 Step での輝度マップの「形」を 2×2 グリッドで並べる.
各画像は ROI 抽出後に mean を target_mean (=146.7146) に揃える線形ゲインで正規化し,
共有 colorbar (実機 ROI の min-max ベース) で描画する.

これにより:
  - 線形ゲインなので形状指標 (CoV / 勾配 / C/P / max-min) は不変
  - 絶対値スケールが全画像で揃い「形の異同」が直接比較できる
  - 共有 colorbar で色の濃淡が画像間で物理的に同じ意味を持つ

使い方:
    uv run python scripts/build_luminance_evolution_panel.py

出力:
    docs/06_TEST/figures/luminance_map_evolution.png

ドキュメント:
    docs/06_TEST/TEST_03_輝度マップ形状追跡.md（観察・解釈・LSC モデル化の意義）
    docs/06_TEST/TEST_01_キャリブレーション検証履歴.md（Step 毎の改善履歴）

新しい改善 Step を追加する場合は PANELS リストに 1 エントリ追加するだけ.
"""

from __future__ import annotations

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np
from PIL import Image

from sm_tias_lightsim.evaluation.normalize import apply_mean_anchor
from sm_tias_lightsim.evaluation.oetf import apply_srgb_oetf, solve_exposure
from sm_tias_lightsim.evaluation.roi_presets import REAL_ROI, SIM_ROI, extract_roi_xywh

ROOT = Path(__file__).resolve().parents[1]
TARGET_MEAN = 146.7146

# 共有 colorbar の値域決定方針: 実機 ROI のパーセンタイル
# 5〜95 パーセンタイルで外れ値（センサノイズ・最暗最明画素）を除く．
REAL_VMIN_PERCENTILE = 5.0
REAL_VMAX_PERCENTILE = 95.0


def _rec709_luma(rgb: np.ndarray) -> np.ndarray:
    """RGB 配列を Rec.709 luma に変換する (H, W, 3) → (H, W)."""
    return 0.2126 * rgb[..., 0] + 0.7152 * rgb[..., 1] + 0.0722 * rgb[..., 2]


def _load_real() -> np.ndarray:
    """実機 PNG → Rec.709 luma → ROI 抽出 → mean アンカー（恒等近い）."""
    png = (
        ROOT / "reference_measurement" / "MiniTIAS_20260408_144434" / "MiniTIAS_20260408_144434.png"
    )
    img = np.array(Image.open(png))
    if img.ndim == 3:
        img = img[..., :3]  # 念のため RGBA → RGB
        luma = _rec709_luma(img.astype(np.float64))
    else:
        luma = img.astype(np.float64)
    roi = extract_roi_xywh(luma, REAL_ROI)
    # 実機は既に mean ≈ 146.71 なので alpha ≈ 1.0（揺れ吸収）
    normalized, _ = apply_mean_anchor(roi, TARGET_MEAN)
    return normalized


def _load_sim_linear(npy_path: Path) -> np.ndarray:
    """シミュ luminance.npy → ROI 抽出 → mean アンカー正規化（linear ドメイン）."""
    arr = np.load(npy_path)
    if arr.ndim == 3:
        arr = _rec709_luma(arr)
    roi = extract_roi_xywh(arr, SIM_ROI)
    normalized, _ = apply_mean_anchor(roi, TARGET_MEAN)
    return normalized


def _load_sim_oetf(npy_path: Path) -> np.ndarray:
    """シミュ luminance.npy → ROI 抽出 → OETF 順方向適用（sRGB ドメイン, uint8 量子化）."""
    arr = np.load(npy_path)
    if arr.ndim == 3:
        arr = _rec709_luma(arr)
    roi = extract_roi_xywh(arr, SIM_ROI)
    scale, _ = solve_exposure(roi, TARGET_MEAN)
    return apply_srgb_oetf(roi, scale, quantize_uint8=True)


# パネル定義．新規 Step を追加する場合はここに 1 エントリ追加するだけ．
PANELS = [
    {
        "loader": _load_real,
        "args": (),
        "title": "Real Measurement (TARGET)",
        "subtitle": "central peak, near-symmetric (camera optics anisotropy)",
    },
    {
        "loader": _load_sim_linear,
        "args": (ROOT / "output" / "baseline" / "luminance.npy",),
        "title": "Step 8-1: Starting Point",
        "subtitle": "R=0.85, d=8, spp=1024, linear + mean normalization",
    },
    {
        "loader": _load_sim_oetf,
        "args": (ROOT / "output" / "baseline" / "luminance.npy",),
        "title": "Step 8-3: After OETF Forward Application",
        "subtitle": "R=0.85, d=8, spp=1024, sRGB domain (mean-anchored)",
    },
    {
        "loader": _load_sim_linear,
        "args": (ROOT / "output" / "emitter_real" / "luminance.npy",),
        "title": "Step 8-7: Real-chip Emitter Size",
        "subtitle": "size 1.5x1.5 mm (Φe preserved), linear + mean normalization",
    },
    {
        "loader": _load_sim_linear,
        "args": (ROOT / "output" / "baseline_v3_leds" / "luminance.npy",),
        "title": "Step 8-9: LED Positions from Fusion STL",
        "subtitle": "Accurate LED layout (3+gap+3 per board), linear + mean normalization",
    },
    {
        "loader": _load_sim_linear,
        "args": (ROOT / "output" / "baseline_spot" / "luminance.npy",),
        "title": "Step 8-10: Spot Emitter (Lambertian-equivalent)",
        "subtitle": "cutoff=90, beam=30 (FWHM 120deg), flux-preserved",
    },
    {
        "loader": _load_sim_linear,
        "args": (ROOT / "output" / "baseline_datasheet" / "luminance.npy",),
        "title": "Step 8-11: Data-sheet Faithful (cos^1.2 spot + R=0.89)",
        "subtitle": "cutoff=85, beam=26.72 (FWHM 112deg), I_0 from 17.5 lm/280 lm/W",
    },
]


def _compute_indicators(roi: np.ndarray) -> dict[str, float]:
    """ROI 配列から主要指標 (mean / CoV / 勾配 % / C/P) を計算する.

    勾配 % と C/P は ROI 中心からの正規化楕円距離 d で 3 ゾーン分類した値．
    """
    mean = float(roi.mean())
    cov = float(roi.std() / mean)

    h, w = roi.shape
    ys, xs = np.mgrid[0:h, 0:w]
    cy, cx = (h - 1) / 2.0, (w - 1) / 2.0
    d = np.sqrt(((xs - cx) / (w / 2.0)) ** 2 + ((ys - cy) / (h / 2.0)) ** 2)
    center_mask = d < 0.33
    periphery_mask = d >= 0.66
    center_mean = float(roi[center_mask].mean())
    periphery_mean = float(roi[periphery_mask].mean())
    cp = center_mean / periphery_mean
    grad = (center_mean - periphery_mean) / center_mean * 100.0
    return {"mean": mean, "CoV": cov, "grad": grad, "CP": cp}


def main() -> None:
    # 全画像をロード
    panels_data = []
    for panel in PANELS:
        roi = panel["loader"](*panel["args"])
        metrics = _compute_indicators(roi)
        panels_data.append((panel, roi, metrics))

    # 共有 vmin/vmax を実機 ROI のパーセンタイルで決定（外れ値を除く）
    real_roi = panels_data[0][1]
    vmin = float(np.percentile(real_roi, REAL_VMIN_PERCENTILE))
    vmax = float(np.percentile(real_roi, REAL_VMAX_PERCENTILE))

    # PANELS 数に応じてグリッドを自動決定
    n = len(panels_data)
    if n <= 2:
        rows, cols = 1, n
    elif n <= 4:
        rows, cols = 2, 2
    elif n <= 6:
        rows, cols = 2, 3
    elif n <= 9:
        rows, cols = 3, 3
    else:
        cols = 4
        rows = (n + cols - 1) // cols

    fig, axes = plt.subplots(rows, cols, figsize=(cols * 7, rows * 6.5))
    axes_flat = axes.flatten() if hasattr(axes, "flatten") else [axes]

    last_im = None
    for ax, (panel, roi, m) in zip(axes_flat, panels_data, strict=False):
        last_im = ax.imshow(roi, cmap="hot", vmin=vmin, vmax=vmax, aspect="auto")
        ax.set_axis_off()
        title = (
            f"{panel['title']}\n"
            f"{panel['subtitle']}\n"
            f"mean={m['mean']:.2f}  CoV={m['CoV']:.4f}  "
            f"grad={m['grad']:.2f}%  C/P={m['CP']:.3f}"
        )
        ax.set_title(title, fontsize=10, loc="left")

    # 余ったマス目は非表示
    for ax in axes_flat[n:]:
        ax.set_axis_off()

    # 共有 colorbar
    cbar = fig.colorbar(
        last_im,
        ax=axes_flat.tolist() if hasattr(axes_flat, "tolist") else list(axes_flat),
        shrink=0.7,
        label=f"Luminance (mean-anchored to {TARGET_MEAN})",
    )
    cbar.ax.tick_params(labelsize=9)

    fig.suptitle(
        "Luminance Map Shape Tracking (shared colorbar, mean-anchored)",
        fontsize=14,
        fontweight="bold",
    )

    out_dir = ROOT / "docs" / "06_TEST" / "figures"
    out_dir.mkdir(parents=True, exist_ok=True)
    out_path = out_dir / "luminance_map_evolution.png"
    plt.savefig(out_path, dpi=120, bbox_inches="tight")
    print(f"Saved: {out_path}")
    print(f"  Shared colorbar range: [{vmin:.2f}, {vmax:.2f}] "
          f"({REAL_VMIN_PERCENTILE}-{REAL_VMAX_PERCENTILE} percentile of real ROI)")


if __name__ == "__main__":
    main()
