"""ROI（関心領域）の平均色算出．

SPEC_02 (0) を実装する．旧実装（`org/src` の `CtonguediagnosisMFC2Dlg::getRGB()` と
`OnBnClickedButtonCalc()`）と同じく，クリック点を中心とする 51×51 px の矩形 ROI の
平均 BGR を求め，複数点クリック時は各 ROI 平均をさらに平均する．

※ 画像は numpy 配列（OpenCV の BGR 画像）として受け取り，GUI には依存しない
（`core` の依存方向は project-structure.md を参照）．
"""

from __future__ import annotations

import logging
from collections.abc import Sequence

import numpy as np
from numpy.typing import NDArray

logger = logging.getLogger(__name__)

# --- 定数（SPEC_02 (0) / SPEC_01「ROI 指定」）---

#: ROI の一辺の長さ（px．旧実装 `cv::Rect(x - 25, y - 25, 51, 51)`）
ROI_SIZE = 51
#: ROI 中心から左上までのオフセット（px．旧実装の -25）
ROI_OFFSET = 25
#: クリック可能な最大点数（SPEC_01．5 点目はエラー）
MAX_ROI_POINTS = 4
#: BGR 画像のチャンネル数
CHANNEL_COUNT = 3
#: BGR 画像の軸数（高さ・幅・チャンネルの 3 軸．`CHANNEL_COUNT` とは意味が異なる）
_IMAGE_NDIM = 3


def roi_mean_bgr(image: NDArray[np.generic], point: tuple[int, int]) -> tuple[float, float, float]:
    """クリック点を中心とする 51×51 px の ROI の平均 BGR を返す．

    Args:
        image: BGR 画像（形状 (高さ, 幅, 3)．OpenCV の `imread` と同じ並び）．
        point: クリック点 (x, y)．画像座標（クロップ後・縮小前）で指定する．

    Returns:
        (B, G, R) の平均値（float．丸めは行わない）．

    Raises:
        ValueError: 画像が BGR 3 チャンネルでない場合，
            または ROI が画像の範囲外にはみ出す場合．
    """
    array = _as_bgr_image(image)
    x, y = _as_point(point)

    left = x - ROI_OFFSET
    top = y - ROI_OFFSET
    height, width = array.shape[:2]
    # 旧実装は範囲チェックを持たず，はみ出すと OpenCV が例外を投げる．
    # 新実装では原因の分かるエラーにして GUI 側でガードできるようにする
    if left < 0 or top < 0 or left + ROI_SIZE > width or top + ROI_SIZE > height:
        raise ValueError(
            f"ROI が画像の範囲外です: 中心 ({x}, {y})・ROI {ROI_SIZE}x{ROI_SIZE}・"
            f"画像 {width}x{height}"
        )

    region = array[top : top + ROI_SIZE, left : left + ROI_SIZE]
    # 旧実装の `cv::mean(roiBGR)` に対応する（各チャンネルの単純平均）
    totals = region.reshape(-1, CHANNEL_COUNT).sum(axis=0, dtype=np.float64)
    means = totals / float(ROI_SIZE * ROI_SIZE)
    return float(means[0]), float(means[1]), float(means[2])


def mean_bgr(
    image: NDArray[np.generic], points: Sequence[tuple[int, int]]
) -> tuple[float, float, float]:
    """複数のクリック点の ROI 平均 BGR を，さらに平均して返す（SPEC_02 (0)）．

    旧実装の `m_meanColors += meanBGR` → `m_meanColors /= m_numClick` に対応する
    （各 ROI 平均の合算をクリック数で割る）．

    Args:
        image: BGR 画像（形状 (高さ, 幅, 3)）．
        points: クリック点 (x, y) の並び（1 点以上）．

    Returns:
        (B, G, R) の平均値．

    Raises:
        ValueError: 点が 1 つも無い場合，画像が不正な場合，
            または ROI が画像の範囲外にはみ出す場合．
    """
    if len(points) == 0:
        raise ValueError("ROI の平均色には 1 点以上のクリック点が必要です")

    totals = np.zeros(CHANNEL_COUNT, dtype=np.float64)
    for point in points:
        totals += np.asarray(roi_mean_bgr(image, point), dtype=np.float64)
    means = totals / float(len(points))

    logger.info(
        "ROI 平均色: %d 点 -> BGR=(%.6f, %.6f, %.6f)",
        len(points),
        means[0],
        means[1],
        means[2],
    )
    return float(means[0]), float(means[1]), float(means[2])


def mean_rgb(
    image: NDArray[np.generic], points: Sequence[tuple[int, int]]
) -> tuple[float, float, float]:
    """複数のクリック点の ROI 平均色を (R, G, B) の順で返す．

    `core.color.rgb_to_lab` の入力に合わせた並べ替えのみを行う（値は `mean_bgr` と同じ）．

    Args:
        image: BGR 画像（形状 (高さ, 幅, 3)）．
        points: クリック点 (x, y) の並び（1 点以上）．

    Returns:
        (R, G, B) の平均値．

    Raises:
        ValueError: 点が 1 つも無い場合，画像が不正な場合，
            または ROI が画像の範囲外にはみ出す場合（`mean_bgr` と同じ）．
    """
    blue, green, red = mean_bgr(image, points)
    return red, green, blue


def _as_bgr_image(image: NDArray[np.generic]) -> NDArray[np.generic]:
    """入力が BGR 3 チャンネル画像であることを検証して返す．"""
    array = np.asarray(image)
    if array.ndim != _IMAGE_NDIM or array.shape[2] != CHANNEL_COUNT:
        raise ValueError(
            f"画像は形状 (高さ, 幅, {CHANNEL_COUNT}) の BGR 画像が必要です: {array.shape}"
        )
    if array.shape[0] == 0 or array.shape[1] == 0:
        raise ValueError("画像が空です")
    return array


def _as_point(point: tuple[int, int]) -> tuple[int, int]:
    """クリック点を整数座標 (x, y) に整える．"""
    values = tuple(point)
    if len(values) != 2:
        raise ValueError(f"クリック点は (x, y) の 2 要素が必要です: {point!r}")
    return int(values[0]), int(values[1])
