"""
_blur_detector.py — Motion blur detection via Laplacian variance (no ROS2 deps).

Algorithm
---------
1. Convert the BGR image to greyscale.
2. Apply the discrete Laplacian operator (cv2.Laplacian, ksize=3).
3. Compute the variance of the Laplacian response across the entire image.
4. A sharp image has high-contrast edges → high variance.
   A blurred image smears edges → low variance.
5. Compare the variance against a configurable threshold:
     blurred = (variance < threshold)

The result is returned as a BlurResult NamedTuple.  All image-free helpers
(is_blurred, etc.) work on pre-computed variance values so callers can cache
expensive computation.

Optional ROI
------------
If roi (row_start, row_end, col_start, col_end) is given, only that crop is
analysed.  Useful for ignoring the sky or far-field clutter that would
artificially inflate variance.

Public API
----------
  BlurResult(variance, is_blurred, threshold)
  laplacian_variance(bgr, roi=None) -> float
  detect_blur(bgr, threshold=100.0, roi=None) -> BlurResult
"""

from __future__ import annotations

from typing import NamedTuple, Optional, Tuple


# ── Data types ────────────────────────────────────────────────────────────────

class BlurResult(NamedTuple):
    """Result of a single blur detection evaluation."""
    variance:   float   # Laplacian variance of the (cropped) image
    is_blurred: bool    # True when variance < threshold
    threshold:  float   # threshold used for this evaluation


# ── Public API ────────────────────────────────────────────────────────────────

def laplacian_variance(
    bgr: 'np.ndarray',
    roi: Optional[Tuple[int, int, int, int]] = None,
) -> float:
    """
    Compute the variance of the Laplacian of a BGR image.

    Parameters
    ----------
    bgr : (H, W, 3) or (H, W) uint8 ndarray
    roi : optional (row_start, row_end, col_start, col_end) — analyse this crop only

    Returns
    -------
    float — Laplacian variance (≥ 0); higher = sharper
    """
    import cv2
    import numpy as np

    if bgr.ndim == 3:
        grey = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY)
    else:
        grey = np.asarray(bgr, dtype=np.uint8)

    if roi is not None:
        r0, r1, c0, c1 = roi
        grey = grey[r0:r1, c0:c1]

    if grey.size == 0:
        return 0.0

    lap = cv2.Laplacian(grey, cv2.CV_64F, ksize=3)
    return float(lap.var())


def detect_blur(
    bgr:       'np.ndarray',
    threshold: float = 100.0,
    roi:       Optional[Tuple[int, int, int, int]] = None,
) -> BlurResult:
    """
    Detect whether an image is motion-blurred.

    Parameters
    ----------
    bgr       : (H, W, 3) uint8 BGR ndarray (or greyscale (H, W))
    threshold : Laplacian variance below which the image is considered blurred
    roi       : optional (row_start, row_end, col_start, col_end) crop

    Returns
    -------
    BlurResult(variance, is_blurred, threshold)
    """
    var = laplacian_variance(bgr, roi=roi)
    return BlurResult(
        variance=var,
        is_blurred=var < threshold,
        threshold=threshold,
    )
