"""
line_detector
カメラ画像から黒線の位置を検出するモジュール
近方・遠方の2領域で重心 x 座標を算出する
"""

from dataclasses import dataclass

import cv2
import numpy as np

from common import config

# ガウシアンブラーのカーネルサイズ
BLUR_KERNEL_SIZE: int = 5

# 二値化の閾値（黒線検出用，この値以下を黒とみなす）
BINARY_THRESHOLD: int = 80

# 横方向クロージングのカーネルサイズ（反射による途切れを補間）
MORPH_CLOSE_WIDTH: int = 25
MORPH_CLOSE_HEIGHT: int = 3

# 近方領域の y 範囲（画像下部）
NEAR_Y_START: int = int(config.FRAME_HEIGHT * 0.7)
NEAR_Y_END: int = config.FRAME_HEIGHT

# 遠方領域の y 範囲（画像上部）
FAR_Y_START: int = int(config.FRAME_HEIGHT * 0.3)
FAR_Y_END: int = int(config.FRAME_HEIGHT * 0.5)


@dataclass
class LineDetectResult:
    """線検出の結果を格納するデータクラス

    Attributes:
        near_x: 近方領域の線の重心 x 座標（未検出時は None）
        far_x: 遠方領域の線の重心 x 座標（未検出時は None）
        near_error: 近方偏差（画像中心からのずれ）
        far_error: 遠方偏差（画像中心からのずれ）
        binary_image: 二値化後の画像（デバッグ用）
    """
    near_x: float | None
    far_x: float | None
    near_error: float
    far_error: float
    binary_image: np.ndarray | None


def detect_line(frame: np.ndarray) -> LineDetectResult:
    """画像から黒線の位置を検出する

    Args:
        frame: BGR 形式のカメラ画像

    Returns:
        線検出の結果
    """
    # グレースケール変換
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

    # ガウシアンブラー
    blurred = cv2.GaussianBlur(
        gray,
        (BLUR_KERNEL_SIZE, BLUR_KERNEL_SIZE),
        0,
    )

    # 固定閾値で二値化（黒線を白，背景を黒に反転）
    _, binary = cv2.threshold(
        blurred, BINARY_THRESHOLD, 255,
        cv2.THRESH_BINARY_INV,
    )

    # 横方向クロージング（反射で途切れた線を左右からつなぐ）
    kernel = cv2.getStructuringElement(
        cv2.MORPH_ELLIPSE,
        (MORPH_CLOSE_WIDTH, MORPH_CLOSE_HEIGHT),
    )
    binary = cv2.morphologyEx(
        binary, cv2.MORPH_CLOSE, kernel,
    )

    # 画像の中心 x 座標
    center_x = config.FRAME_WIDTH / 2.0

    # 近方領域の重心算出
    near_roi = binary[NEAR_Y_START:NEAR_Y_END, :]
    near_x = _calc_centroid_x(near_roi)

    # 遠方領域の重心算出
    far_roi = binary[FAR_Y_START:FAR_Y_END, :]
    far_x = _calc_centroid_x(far_roi)

    # 偏差の計算
    near_error = 0.0
    if near_x is not None:
        near_error = (center_x - near_x) / center_x

    far_error = 0.0
    if far_x is not None:
        far_error = (center_x - far_x) / center_x

    return LineDetectResult(
        near_x=near_x,
        far_x=far_x,
        near_error=near_error,
        far_error=far_error,
        binary_image=binary,
    )


def _calc_centroid_x(
    roi: np.ndarray,
) -> float | None:
    """ROI 内の白ピクセルの重心 x 座標を算出する

    Args:
        roi: 二値化された領域画像

    Returns:
        重心の x 座標，白ピクセルがない場合は None
    """
    moments = cv2.moments(roi)
    if moments["m00"] == 0:
        return None
    return moments["m10"] / moments["m00"]
