"""sRGB OETF（光電変換関数）順方向適用モジュール．

linear radiance → sRGB 符号化空間への標準変換を提供する．
シミュの linear 値を実測 sRGB PNG と同じドメインへ揃えることで，
均一性指標の直接比較を正当化する（SPEC_03「手法訂正」参照）．

無彩色シーン（LED・材料が RGB 等価）では luminance への直接 OETF 適用は
チャネル別適用後 Rec.709 luma と厳密一致する．これはシミュのシーンが
R=G=B の条件を満たすためであり，本モジュールはその前提を明記した上で
luminance への直接適用パスを提供する（参照実装: output/sweep_reflectance/apply_oetf.py）．

将来カラー LED・色付き材料を扱う場合は，cli/evaluate.py での
「チャネル別 OETF → Rec.709 luma」順を使うこと（そちらが一般式）．
"""

from __future__ import annotations

import numpy


def srgb_oetf(c: numpy.ndarray) -> numpy.ndarray:
    """sRGB OETF（IEC 61966-2-1）．線形 [0, 1] → 符号化 [0, 1] へ変換する．

    入力は [0, 1] にクリップしてから変換する．
    トイ部（c ≤ 0.0031308）は線形 12.92c，それ以外は 1.055·c^(1/2.4) − 0.055．

    Args:
        c: 線形 radiance 配列．値域は任意（内部でクリップ）．

    Returns:
        sRGB 符号化値（float64, 値域 [0, 1]）．

    References:
        IEC 61966-2-1:1999 — sRGB
    """
    c = numpy.clip(c, 0.0, 1.0)
    lo = c <= 0.0031308
    out = numpy.empty_like(c)
    out[lo] = 12.92 * c[lo]
    out[~lo] = 1.055 * numpy.power(c[~lo], 1.0 / 2.4) - 0.055
    return out


def solve_exposure(
    lin_roi: numpy.ndarray,
    target_mean: float,
) -> tuple[float, float]:
    """OETF 後 ROI mean が target_mean になる線形スケールを対数二分で求める．

    OETF は単調増加 → encode_with_exposure(lin_roi, scale).mean() も scale に
    対して単調増加 → 二分法で一意解が存在する．
    対数二分（初期区間 [1e-12, 1e6]，200 回反復）で収束する．

    実測 mean=146.71 は ROI 上の uint8 mean であるため，露出ソルバも
    ROI 切り出し後の配列 lin_roi を渡すこと．

    Args:
        lin_roi: linear 輝度 ROI 配列（H x W, float64）．
        target_mean: 目標 ROI mean（uint8 スケール [0, 255]）．

    Returns:
        (scale, clipped_fraction) のタプル．
        scale: 線形ゲイン（lin_roi に掛ける値）．
        clipped_fraction: OETF 入力が 1.0 を超えるピクセルの割合 [0, 1]．
    """
    lo, hi = 1e-12, 1e6
    for _ in range(200):
        mid = (lo * hi) ** 0.5  # 対数二分（乗法的中点）
        m = (srgb_oetf(lin_roi * mid) * 255.0).mean()
        if m < target_mean:
            lo = mid
        else:
            hi = mid
    scale = (lo * hi) ** 0.5
    clipped = float(numpy.mean(lin_roi * scale >= 1.0))
    return scale, clipped


def apply_srgb_oetf(
    lin: numpy.ndarray,
    scale: float,
    *,
    quantize_uint8: bool = True,
) -> numpy.ndarray:
    """linear 配列に露出スケールを掛けて sRGB OETF を適用し，[0, 255] に変換する．

    内部演算は float64 で行い，quantize_uint8=True のとき
    ``np.clip(np.round(x * 255), 0, 255)`` で uint8 量子化する（実測 PNG 同等）．

    Args:
        lin: linear 輝度配列（任意次元 float64）．
        scale: 線形ゲイン（solve_exposure で取得した値を渡す）．
        quantize_uint8: True のとき uint8 量子化を適用し float64 で返す．

    Returns:
        OETF 適用後の配列（float64, quantize_uint8=True なら [0, 255] の整数値）．
    """
    encoded = srgb_oetf(lin * scale) * 255.0
    if quantize_uint8:
        encoded = numpy.clip(numpy.round(encoded), 0.0, 255.0)
    return encoded
