"""スライド用「シミュ vs 暗室実機」形状比較パネルを生成する.

シミュレーション輝度マップと暗室撮影 DNG (現行キャリブ TARGET) を，
**各画像を独立に [0, 100] へ正規化**して共有 colorbar で左右に並べる比較図.
正規化の基準は 20 ビン動径プロファイルの最小値→0・最大値→100 で，
絶対的な明るさの違い (mean) も全体コントラストの強弱もキャンセルし，
「照明の形 (空間パターン)」だけを直接比較できるようにする.

これにより:
  - 各画像がフル 0〜100 レンジを使うので「量の違い」ではなく「形」が見える
  - 動径 min/max を基準にするため画素単位のノイズ・外れ値に影響されにくい
  - 形状指標 (CoV / 勾配 / C/P / 動径 min/max) はオフセット込み正規化で値が
    変わるため，指標は **物理 ROI のまま** 算出して title に表示する
    (表示用配列だけを 0〜100 正規化する)

実機側 (暗室 DNG) の評価は SmTIAS-Evaluation の正規手順を再現する:
  load_image_dng(rgb_float32, output_color=raw, linear)
    → meta.json lscMap を順方向適用 (レンズビネット除去)
    → Rec.709 luma × 255 → rot90(-1) で portrait 向き合わせ
    → 白板 ROI 抽出
これは reference_scripts/run_uniformity_dng.py と同一 (mean=207.82 を再現).

シミュ側は output/baseline/luminance.npy を SIM_ROI で抽出.

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

出力:
    output/_slide_panels/sim_vs_darkroom_normalized.png

ドキュメント:
    docs/06_TEST/TEST_01_キャリブレーション検証履歴.md (§Step 8-13 実機 DNG 突合)
    docs/05_TECH/TECH_07_LSC取り扱い方針.md (LSC 順方向適用の役割分担)
"""

from __future__ import annotations

import json
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np

from sm_tias_lightsim.evaluation.dng_loader import load_image_dng
from sm_tias_lightsim.evaluation.radial import (
    calc_radial_min_max_ratio,
    calc_radial_profile,
)
from sm_tias_lightsim.evaluation.roi_presets import REAL_ROI, SIM_ROI, extract_roi_xywh
from sm_tias_lightsim.evaluation.zones import (
    calc_zone_stats,
    classify_zones,
    compute_normalized_distance_map,
)

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

# Rec.709 luma 係数 (uniformity.py / dng_loader.py と一致)
_REC709 = (0.2126, 0.7152, 0.0722)

# 実機 暗室 DNG (Step 8-13 以降の正規 baseline)
REAL_DNG = ROOT / "reference_measurement" / "quantitative" / "MiniTIAS_QM_20260601_124500.dng"
REAL_META = (
    ROOT / "reference_measurement" / "quantitative" / "MiniTIAS_QM_20260601_124500.meta.json"
)
# REAL_ROI（白板 portrait）/ SIM_ROI は roi_presets に集約（configs と整合）.

# シミュ baseline (max_depth=16, R=0.85, spp=1024)
SIM_NPY = ROOT / "output" / "baseline" / "luminance.npy"

OUT_PATH = ROOT / "output" / "_slide_panels" / "sim_vs_darkroom_normalized.png"


def _rec709_luma(rgb: np.ndarray) -> np.ndarray:
    """(H, W, 3) RGB → (H, W) Rec.709 luma."""
    return _REC709[0] * rgb[..., 0] + _REC709[1] * rgb[..., 1] + _REC709[2] * rgb[..., 2]


def _upsample(grid: np.ndarray, shape: tuple[int, int]) -> np.ndarray:
    """(rows, cols) グリッドを (H, W) へバイリニア補間する (run_uniformity_dng.py と同一)."""
    rows, cols = grid.shape
    h, w = shape
    ys = np.linspace(0, rows - 1, h)
    xs = np.linspace(0, cols - 1, w)
    y0 = np.floor(ys).astype(int)
    y1 = np.minimum(y0 + 1, rows - 1)
    x0 = np.floor(xs).astype(int)
    x1 = np.minimum(x0 + 1, cols - 1)
    wy = (ys - y0)[:, None]
    wx = (xs - x0)[None, :]
    top = grid[y0][:, x0] * (1 - wx) + grid[y0][:, x1] * wx
    bot = grid[y1][:, x0] * (1 - wx) + grid[y1][:, x1] * wx
    return top * (1 - wy) + bot * wy


def _lsc_gain_rgb(meta_path: Path, shape: tuple[int, int]) -> np.ndarray:
    """meta.json lscMap (rows, cols, 4=[R,Geven,Godd,B]) → (H, W, 3) RGB ゲイン."""
    meta = json.loads(meta_path.read_text(encoding="utf-8"))
    rows, cols = meta["lscMapRowCount"], meta["lscMapColumnCount"]
    m = np.array(meta["lscMap"], dtype=np.float64).reshape(rows, cols, 4)
    gr = _upsample(m[:, :, 0], shape)
    gg = _upsample((m[:, :, 1] + m[:, :, 2]) / 2.0, shape)
    gb = _upsample(m[:, :, 3], shape)
    return np.stack([gr, gg, gb], axis=2)


def _load_real_darkroom() -> np.ndarray:
    """暗室 DNG → linear raw → LSC 順適用 → Rec.709×255 → portrait → 白板 ROI (物理値)."""
    rgb = load_image_dng(str(REAL_DNG), output_format="rgb_float32")  # linear landscape
    rgb = rgb * _lsc_gain_rgb(REAL_META, rgb.shape[:2])  # LSC 順方向適用 (ビネット除去)
    luma = _rec709_luma(rgb) * 255.0  # 0〜255 linear スケール
    luma = np.rot90(luma, -1)  # portrait (PNG / ROI と同じ向き)
    return extract_roi_xywh(luma, REAL_ROI)


def _load_sim() -> np.ndarray:
    """シミュ luminance.npy → ROI (物理値)."""
    arr = np.load(SIM_NPY)
    if arr.ndim == 3:
        arr = _rec709_luma(arr)
    return extract_roi_xywh(arr, SIM_ROI)


def _minmax_normalize(roi: np.ndarray) -> np.ndarray:
    """各画像を実画素の最小値→0・最大値→100 へ線形正規化する.

    元の輝度マップ (expected_results の luminance_map.png) はデータ実 min/max へ
    自動スケールして描かれるため，実画素 min/max を用いると左パネル (実機) が
    元画像と同じ見た目になる．mean・全体コントラストの違いはキャンセルされ，
    空間パターン (形) だけが共有 0〜100 スケールで比較できる．
    クリップは起きない (両端が実 min/max に一致するため)．
    注: 動径 min/max と違い画素ノイズの外れ値にレンジ端が引っ張られうる．
    """
    vmin = float(roi.min())
    vmax = float(roi.max())
    return (roi - vmin) / (vmax - vmin) * 100.0


def _compute_metrics(roi: np.ndarray) -> dict[str, float]:
    """正規評価関数で CoV / 勾配% / C/P / 動径 min/max を算出する (CLI と同一定義)."""
    mean = float(roi.mean())
    cov = float(roi.std() / mean)
    dist = compute_normalized_distance_map(*roi.shape)
    zone_stats = calc_zone_stats(roi, classify_zones(dist))
    rmm = calc_radial_min_max_ratio(calc_radial_profile(roi))
    return {
        "CoV": cov,
        "grad": zone_stats["gradient_magnitude"],
        "CP": zone_stats["center_periphery_ratio"],
        "rmm": rmm["radial_min_max_ratio"],
    }


def _title(name: str, subtitle: str, m: dict[str, float]) -> str:
    return (
        f"{name}\n{subtitle}\n"
        f"CoV={m['CoV']:.4f}  grad={m['grad']:.2f}%  "
        f"C/P={m['CP']:.3f}  r.min/max={m['rmm']:.3f}"
    )


def main() -> None:
    real_raw = _load_real_darkroom()
    sim_raw = _load_sim()

    # 指標は物理 ROI のまま算出 (CoV/勾配/C/P/動径 min/max はオフセット込み正規化で変わるため)
    real_m = _compute_metrics(real_raw)
    sim_m = _compute_metrics(sim_raw)

    # 表示用配列だけ「実画素 min→0 / max→100」で各画像独立に正規化
    # (左パネルが元の輝度マップと同じ見た目になる)
    real_disp = _minmax_normalize(real_raw)
    sim_disp = _minmax_normalize(sim_raw)

    # 共有スケール: 0〜100 固定 (各画像の実 min/max がこの両端に対応)
    vmin, vmax = 0.0, 100.0

    fig, axes = plt.subplots(1, 2, figsize=(15, 7))

    axes[0].imshow(real_disp, cmap="hot", vmin=vmin, vmax=vmax, aspect="auto")
    axes[0].set_box_aspect(1.0)  # パネルを正方形に固定
    axes[0].set_axis_off()
    axes[0].set_title(
        _title(
            "Real (dark-room DNG, TARGET)",
            "linear + LSC forward, white-board ROI",
            real_m,
        ),
        fontsize=11,
        loc="left",
    )

    last_im = axes[1].imshow(sim_disp, cmap="hot", vmin=vmin, vmax=vmax, aspect="auto")
    axes[1].set_box_aspect(1.0)  # パネルを正方形に固定
    axes[1].set_axis_off()
    axes[1].set_title(
        _title(
            "Simulation (baseline)",
            "R=0.85, d=16, spp=1024, linear",
            sim_m,
        ),
        fontsize=11,
        loc="left",
    )

    cbar = fig.colorbar(
        last_im,
        ax=axes.tolist(),
        shrink=0.75,
        label="Normalized intensity (pixel min -> 0, max -> 100, per image)",
    )
    cbar.ax.tick_params(labelsize=9)

    fig.suptitle(
        "Simulation vs Dark-room Measurement "
        "(shape-focused: each map normalized to its pixel min/max -> 0/100)",
        fontsize=14,
        fontweight="bold",
    )

    OUT_PATH.parent.mkdir(parents=True, exist_ok=True)
    plt.savefig(OUT_PATH, dpi=120, bbox_inches="tight")
    print(f"Saved: {OUT_PATH}")
    print("  Display normalization: per-image pixel min->0, max->100 (vmin/vmax=0/100)")
    print(f"  Real : CoV={real_m['CoV']:.4f} grad={real_m['grad']:.2f}% "
          f"C/P={real_m['CP']:.3f} r.min/max={real_m['rmm']:.3f}")
    print(f"  Sim  : CoV={sim_m['CoV']:.4f} grad={sim_m['grad']:.2f}% "
          f"C/P={sim_m['CP']:.3f} r.min/max={sim_m['rmm']:.3f}")


if __name__ == "__main__":
    main()
