"""Noise gate postprocessing filter."""

from typing import Any, Dict, List, Optional

import numpy as np

from wled_controller.core.filters.base import FilterOptionDef, PostprocessingFilter
from wled_controller.core.filters.image_pool import ImagePool
from wled_controller.core.filters.registry import FilterRegistry


@FilterRegistry.register
class NoiseGateFilter(PostprocessingFilter):
    """Suppresses small per-pixel color changes below threshold.

    Prevents shimmer / flicker on static or near-static content by keeping
    the previous pixel value when the max per-channel delta is below the
    configured threshold.
    """

    filter_id = "noise_gate"
    filter_name = "Noise Gate"

    def __init__(self, options: Dict[str, Any]):
        super().__init__(options)
        self._prev_frame: Optional[np.ndarray] = None
        # Pre-allocated scratch buffers (avoid per-frame allocation)
        self._i16_cur: Optional[np.ndarray] = None
        self._i16_prev: Optional[np.ndarray] = None
        self._buf_shape: Optional[tuple] = None

    @classmethod
    def get_options_schema(cls) -> List[FilterOptionDef]:
        return [
            FilterOptionDef(
                key="threshold",
                label="Threshold",
                option_type="int",
                default=10,
                min_value=1,
                max_value=50,
                step=1,
            ),
        ]

    def process_image(self, image: np.ndarray, image_pool: ImagePool) -> Optional[np.ndarray]:
        threshold = self.options["threshold"]
        h, w, c = image.shape
        shape = (h, w, c)

        if self._prev_frame is None or self._prev_frame.shape != shape:
            self._prev_frame = image.copy()
            self._i16_cur = np.empty(shape, dtype=np.int16)
            self._i16_prev = np.empty(shape, dtype=np.int16)
            self._buf_shape = shape
            return None

        # Reallocate scratch if shape changed
        if self._buf_shape != shape:
            self._i16_cur = np.empty(shape, dtype=np.int16)
            self._i16_prev = np.empty(shape, dtype=np.int16)
            self._buf_shape = shape

        # Compute per-channel absolute delta using int16 to avoid uint8 underflow
        np.copyto(self._i16_cur, image, casting="unsafe")
        np.copyto(self._i16_prev, self._prev_frame, casting="unsafe")
        self._i16_cur -= self._i16_prev
        np.abs(self._i16_cur, out=self._i16_cur)

        # Per-pixel: max channel delta
        max_delta = self._i16_cur.max(axis=2)  # shape (h, w)

        # Boolean mask: True where pixel is stable (keep previous)
        mask = max_delta < threshold
        mask_3d = mask[:, :, np.newaxis]  # broadcast to (h, w, 1)

        # Apply: keep previous where stable, otherwise take new
        np.where(mask_3d, self._prev_frame, image, out=image)

        # Update stored frame
        np.copyto(self._prev_frame, image)
        return None
