"""
_sky_detector.py — Sky detection via HSV thresholding + horizon estimation (no ROS2 deps).

Algorithm
---------
Sky pixels are identified by two HSV bands (OpenCV convention: H∈[0,180], S/V∈[0,255]):

  1. Blue sky  — H∈[90,130], S∈[40,255], V∈[80,255]
                 (captures clear blue sky from cyan-blue through sky-blue to blue-violet)

  2. Overcast  — S∈[0,50],   V∈[185,255]
                 (very bright, near-zero saturation: white/grey sky on cloudy days)

The two masks are OR-combined into a single sky mask.

Sky fraction
------------
Computed over the top *scan_frac* of the image (default 60 %).  This concentrates
sensitivity on the region where sky is expected to appear and avoids penalising ground
reflections or obstacles in the lower frame.

    sky_fraction = sky_pixels_in_top_scan_frac / pixels_in_top_scan_frac

Horizon line
------------
For each image row, compute the row-level sky fraction (sky pixels / row width).
The horizon is the **bottommost row** where that fraction ≥ *row_threshold* (default 0.30).
This represents the lower boundary of continuous sky content.

  horizon_y = max { r : row_sky_frac[r] ≥ row_threshold }   or  -1  if no sky found.

Returns -1 when no sky is detected at all (indoors, underground, etc.).

Public API
----------
  SkyResult(sky_fraction, horizon_y, sky_mask)
  detect_sky(bgr, scan_frac=0.60, row_threshold=0.30) -> SkyResult
"""

from __future__ import annotations

from typing import NamedTuple

import numpy as np


# ── HSV sky bands ─────────────────────────────────────────────────────────────

# Blue sky (OpenCV H ∈ [0, 180])
_BLUE_SKY_LO = np.array([90,  40,  80], dtype=np.uint8)
_BLUE_SKY_HI = np.array([130, 255, 255], dtype=np.uint8)

# White / grey overcast sky (any hue, very bright, very desaturated)
_GREY_SKY_LO = np.array([0,   0,   185], dtype=np.uint8)
_GREY_SKY_HI = np.array([180, 50,  255], dtype=np.uint8)


# ── Data type ─────────────────────────────────────────────────────────────────

class SkyResult(NamedTuple):
    """Sky detection result for a single frame."""
    sky_fraction: float        # fraction of top scan_frac region classified as sky [0, 1]
    horizon_y:    int          # pixel row of horizon (-1 = no sky detected)
    sky_mask:     np.ndarray   # (H, W) uint8 binary mask (255 = sky pixel)


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

def detect_sky(
    bgr:           np.ndarray,
    scan_frac:     float = 0.60,
    row_threshold: float = 0.30,
) -> SkyResult:
    """
    Detect sky pixels and estimate the horizon row.

    Parameters
    ----------
    bgr           : (H, W, 3) uint8 BGR ndarray
    scan_frac     : fraction of image height to use for sky_fraction computation
                    (top of frame, where sky is expected)
    row_threshold : minimum per-row sky fraction to count a row as sky for
                    horizon estimation

    Returns
    -------
    SkyResult(sky_fraction, horizon_y, sky_mask)
    """
    import cv2

    bgr = np.asarray(bgr, dtype=np.uint8)
    h, w = bgr.shape[:2]

    if h == 0 or w == 0:
        empty = np.zeros((h, w), dtype=np.uint8)
        return SkyResult(sky_fraction=0.0, horizon_y=-1, sky_mask=empty)

    hsv = cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV)

    # Build combined sky mask (blue OR overcast)
    mask_blue = cv2.inRange(hsv, _BLUE_SKY_LO, _BLUE_SKY_HI)
    mask_grey = cv2.inRange(hsv, _GREY_SKY_LO, _GREY_SKY_HI)
    sky_mask  = cv2.bitwise_or(mask_blue, mask_grey)   # (H, W) uint8, 255 = sky

    # ── sky_fraction: top scan_frac rows ──────────────────────────────────────
    scan_rows = max(1, int(h * min(float(scan_frac), 1.0)))
    top_mask  = sky_mask[:scan_rows, :]
    sky_fraction = float(np.count_nonzero(top_mask)) / float(scan_rows * w)

    # ── horizon_y: bottommost row with ≥ row_threshold sky pixels ─────────────
    # row_fracs[r] = fraction of pixels in row r that are sky
    row_sky_counts = np.count_nonzero(sky_mask, axis=1).astype(np.float32)  # (H,)
    row_fracs      = row_sky_counts / float(w)
    sky_rows       = np.where(row_fracs >= row_threshold)[0]

    horizon_y = int(sky_rows.max()) if len(sky_rows) > 0 else -1

    return SkyResult(
        sky_fraction=sky_fraction,
        horizon_y=horizon_y,
        sky_mask=sky_mask,
    )
