"""ρ×ω×α スイープの輝度マップを「目で判断する」ための可視化.

指標（CoV / NCC 等）は分布を集約した量であり，形そのものの判断材料にはならない
（TEST_03 の主題）．本スクリプトは summary.csv の順位ではなく **輝度マップそのもの**
を並べ，どのパラメータで形がどう変わるか／変わらないかを目視できるようにする．

生成物:
  1. bsdf_grid_contact_sheet.png -- 全 60 条件 + 実機のコンタクトシート
     （行 = ρ，列 = (ω, α)．全セル共通カラースケールなので差がそのまま見える）
  2. bsdf_grid_rho_series.png    -- ω=0 純 diffuse の ρ 系列 + 実機．
     上段 = 輝度マップ，下段 = 実機との相対残差マップ [%]
  3. bsdf_grid_top_residual.png  -- スコア上位条件の残差マップ（発散カラーマップ）

残差の定義: 平均で正規化したうえで (sim - real) / mean(real) [%]．
正（赤）= シミュが明るすぎる領域，負（青）= 暗すぎる領域．

実行方法:
    uv run python scripts/build_bsdf_grid_visual.py [--top 4]
"""

from __future__ import annotations

import argparse
import csv
import sys
from pathlib import Path

import matplotlib
import matplotlib.pyplot as plt
import numpy as np
from scipy.ndimage import gaussian_filter, median_filter
from skimage.transform import resize

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

from sm_tias_lightsim.evaluation.roi_presets import SIM_ROI, extract_roi_xywh  # noqa: E402

matplotlib.rcParams["font.family"] = ["Meiryo", "Yu Gothic", "DejaVu Sans"]

ROOT = Path(__file__).resolve().parents[1]
SUMMARY_CSV = ROOT / "output" / "bsdf_grid_fix" / "summary.csv"
OUTPUT_BASE = ROOT / "output" / "bsdf_grid_fix"
REFERENCE_DIR = ROOT / "output" / "full_base_fix"
REAL_FULL = ROOT / "lightsim" / "luminance_maps" / "full_MiniTIAS_QM_20260601_124500.npy"
PANEL_DIR = ROOT / "output" / "_slide_panels"

RESIDUAL_LIMIT = 12.0  # 残差カラーバーの上下限 [%]


def load_rows() -> list[dict]:
    """summary.csv を数値化して読み込む."""
    if not SUMMARY_CSV.exists():
        raise SystemExit(f"エラー: {SUMMARY_CSV} がありません（スイープ未実行）")
    rows = []
    with SUMMARY_CSV.open(encoding="utf-8") as fh:
        for raw in csv.DictReader(fh):
            row: dict = {"job": raw["job"], "stage": raw["stage"]}
            for key, value in raw.items():
                if key not in ("job", "stage"):
                    row[key] = float(value) if value not in ("", None) else float("nan")
            rows.append(row)
    return rows


def _smooth(arr: np.ndarray, is_sim: bool) -> np.ndarray:
    """他パネルと同一の平滑化（median5 → gauss σ4）と ROI 抽出."""
    if arr.ndim == 3:
        arr = 0.2126 * arr[..., 0] + 0.7152 * arr[..., 1] + 0.0722 * arr[..., 2]
    arr = arr.astype(np.float64)
    if is_sim:
        arr = extract_roi_xywh(arr, SIM_ROI)
    return gaussian_filter(median_filter(arr, size=5), sigma=4)


def _job_map(job: str) -> np.ndarray:
    """ジョブ名から平滑化・mean 正規化済みの輝度マップを返す."""
    path = OUTPUT_BASE / job / "luminance.npy"
    if not path.exists():
        path = REFERENCE_DIR / "luminance.npy"
    img = _smooth(np.load(path), True)
    return img / img.mean()


def _real_map(shape: tuple[int, int]) -> np.ndarray:
    """実機マップを指定 shape に合わせ mean 正規化して返す."""
    real = _smooth(np.load(REAL_FULL), False)
    real = resize(real, shape, anti_aliasing=True)
    return real / real.mean()


def _residual(sim_norm: np.ndarray, real_norm: np.ndarray) -> np.ndarray:
    """mean 正規化済みどうしの相対残差 [%]（正＝シミュが明るすぎ）."""
    return 100.0 * (sim_norm - real_norm)


def contact_sheet(rows: list[dict]) -> Path:
    """全条件を 行=ρ × 列=(ω, α) で並べた一覧（共通カラースケール）."""
    rho_values = sorted({r["rho"] for r in rows})
    combos = sorted({(r["omega"], r["alpha"]) for r in rows})
    lookup = {(r["rho"], r["omega"], r["alpha"]): r for r in rows}

    ncols = len(combos) + 1  # 右端に実機列
    fig, axes = plt.subplots(len(rho_values), ncols,
                             figsize=(1.75 * ncols, 1.95 * len(rho_values)),
                             constrained_layout=True)
    sample = _job_map(rows[0]["job"])
    real = _real_map(sample.shape)
    vmin, vmax = np.percentile(real, [1, 99])

    for row_idx, rho in enumerate(rho_values):
        for col_idx, (omega, alpha) in enumerate(combos):
            ax = axes[row_idx][col_idx]
            entry = lookup.get((rho, omega, alpha))
            if entry is None:
                ax.axis("off")
                continue
            ax.imshow(_job_map(entry["job"]), cmap="hot", vmin=vmin, vmax=vmax)
            ax.set_title(f"NCC={entry['ncc_smooth']:.3f}", fontsize=6.5, pad=2)
            ax.set_xticks([])
            ax.set_yticks([])
            if col_idx == 0:
                ax.set_ylabel(f"ρ={rho:g}", fontsize=9)
            if row_idx == 0:
                label = "ω=0" if omega == 0 else f"ω={omega:g}\nα={alpha:g}"
                ax.text(0.5, 1.35, label, transform=ax.transAxes,
                        ha="center", va="bottom", fontsize=8)
        ax_real = axes[row_idx][-1]
        im = ax_real.imshow(real, cmap="hot", vmin=vmin, vmax=vmax)
        ax_real.set_xticks([])
        ax_real.set_yticks([])
        ax_real.set_title("実機", fontsize=7, pad=2)
        if row_idx == 0:
            ax_real.text(0.5, 1.35, "参照", transform=ax_real.transAxes,
                         ha="center", va="bottom", fontsize=8)

    fig.colorbar(im, ax=axes, shrink=0.5, label="mean 正規化輝度（全セル共通スケール）")
    fig.suptitle("全 60 条件の輝度マップ一覧 — 行=ρ，列=(ω, α)，右端=実機"
                 "（全セル共通カラースケール・mean 正規化）", fontsize=13)
    PANEL_DIR.mkdir(parents=True, exist_ok=True)
    path = PANEL_DIR / "bsdf_grid_contact_sheet.png"
    fig.savefig(path, dpi=140, bbox_inches="tight")
    plt.close(fig)
    return path


def rho_series(rows: list[dict]) -> Path:
    """ω=0 の ρ 系列を輝度マップ（上段）と残差マップ（下段）で並べる."""
    series = sorted([r for r in rows if r["omega"] == 0], key=lambda r: r["rho"])
    sample = _job_map(series[0]["job"])
    real = _real_map(sample.shape)
    vmin, vmax = np.percentile(real, [1, 99])

    ncols = len(series) + 1
    fig, axes = plt.subplots(2, ncols, figsize=(2.55 * ncols, 5.9),
                             constrained_layout=True)
    for col, entry in enumerate(series):
        sim = _job_map(entry["job"])
        axes[0][col].imshow(sim, cmap="hot", vmin=vmin, vmax=vmax)
        axes[0][col].set_title(f"ρ={entry['rho']:g}\nCoV={entry['cov']:.4f}  "
                               f"NCC={entry['ncc_smooth']:.3f}", fontsize=9)
        res = axes[1][col].imshow(_residual(sim, real), cmap="RdBu_r",
                                  vmin=-RESIDUAL_LIMIT, vmax=RESIDUAL_LIMIT)
        rms = float(np.sqrt(np.mean(_residual(sim, real) ** 2)))
        axes[1][col].set_title(f"残差 RMS={rms:.2f}%", fontsize=9)
    top = axes[0][-1].imshow(real, cmap="hot", vmin=vmin, vmax=vmax)
    axes[0][-1].set_title("実機 全灯\nCoV=0.0695", fontsize=9)
    axes[1][-1].axis("off")
    for ax in axes.ravel():
        if ax.axison:
            ax.set_xticks([])
            ax.set_yticks([])

    fig.colorbar(top, ax=axes[0], shrink=0.8, label="mean 正規化輝度")
    fig.colorbar(res, ax=axes[1], shrink=0.8, label="相対残差 [%]（赤=シミュが明るい）")
    fig.suptitle("純 diffuse（ω=0）の ρ 系列 — 上段: 輝度マップ / 下段: 実機との残差",
                 fontsize=13)
    PANEL_DIR.mkdir(parents=True, exist_ok=True)
    path = PANEL_DIR / "bsdf_grid_rho_series.png"
    fig.savefig(path, dpi=150, bbox_inches="tight")
    plt.close(fig)
    return path


def top_residual(rows: list[dict], top: int) -> Path:
    """スコア上位条件の残差マップを並べる（形の判断はここが主戦場）."""
    best = sorted(rows, key=lambda r: r["score"])[:top]
    sample = _job_map(best[0]["job"])
    real = _real_map(sample.shape)

    fig, axes = plt.subplots(1, top, figsize=(3.7 * top, 4.5), constrained_layout=True)
    for ax, entry in zip(np.atleast_1d(axes), best, strict=True):
        residual = _residual(_job_map(entry["job"]), real)
        im = ax.imshow(residual, cmap="RdBu_r", vmin=-RESIDUAL_LIMIT, vmax=RESIDUAL_LIMIT)
        rms = float(np.sqrt(np.mean(residual ** 2)))
        p95 = float(np.percentile(np.abs(residual), 95))
        ax.set_title(f"{entry['job']}\nRMS={rms:.2f}%  P95={p95:.2f}%  "
                     f"NCC={entry['ncc_smooth']:.3f}", fontsize=10)
        ax.set_xticks([])
        ax.set_yticks([])
    fig.colorbar(im, ax=axes, shrink=0.8, label="相対残差 [%]（赤=シミュが明るい / 青=暗い）")
    fig.suptitle("スコア上位条件の残差マップ — 赤青の構造が「材質では埋まらない形の差」",
                 fontsize=13)
    PANEL_DIR.mkdir(parents=True, exist_ok=True)
    path = PANEL_DIR / "bsdf_grid_top_residual.png"
    fig.savefig(path, dpi=150, bbox_inches="tight")
    plt.close(fig)
    return path


def main() -> int:
    """3 種の可視化を生成するメインエントリポイント."""
    parser = argparse.ArgumentParser(description="ρ×ω×α スイープの輝度マップ可視化")
    parser.add_argument("--top", type=int, default=4, help="残差マップに並べる上位条件数")
    args = parser.parse_args()

    rows = load_rows()
    print(f"読み込み: {len(rows)} 条件")
    print(f"一覧:     {contact_sheet(rows)}")
    print(f"ρ 系列:   {rho_series(rows)}")
    print(f"残差:     {top_residual(rows, args.top)}")
    return 0


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