"""Frame interpolation postprocessing filter."""

import time
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 FrameInterpolationFilter(PostprocessingFilter):
    """Blends consecutive frames to fill idle loop ticks with smooth output.

    When ProcessedLiveStream runs at 2× source capture rate, idle ticks
    (where no new source frame has arrived) receive an interpolated blend of
    the two most recently captured frames instead of a duplicate.

    Algorithm (backward blend):
    - When frame B arrives at T_b: start blending A → B over the interval
      (T_b - T_a), the duration between the two most recent source frames.
    - At time T: alpha = clamp((T - T_b) / (T_b - T_a), 0, 1)
    - output = (1 - alpha)*A + alpha*B

    New vs idle frames are detected by comparing a cheap 64-byte signature of
    the filter input. Since preceding filters are deterministic and the source
    image is unchanged between idle ticks, the signature is stable.

    Introduces at most one capture-interval of lag (≤33 ms at 30 fps) —
    imperceptible for ambient lighting use cases.
    """

    filter_id = "frame_interpolation"
    filter_name = "Frame Interpolation"
    supports_idle_frames = True

    def __init__(self, options: Dict[str, Any]):
        super().__init__(options)
        self._frame_a: Optional[np.ndarray] = None  # frame N-1
        self._frame_b: Optional[np.ndarray] = None  # frame N (latest source)
        self._time_a: float = 0.0
        self._time_b: float = 0.0
        self._sig_b: Optional[bytes] = None  # 64-byte signature of frame_b input
        # Pre-allocated uint16 scratch buffers for blending
        self._u16_a: Optional[np.ndarray] = None
        self._u16_b: Optional[np.ndarray] = None
        self._blend_shape: Optional[tuple] = None

    @classmethod
    def get_options_schema(cls) -> List[FilterOptionDef]:
        return []

    def process_image(self, image: np.ndarray, image_pool: ImagePool) -> Optional[np.ndarray]:
        """Return interpolated blend on idle ticks; update state on new source frames.

        Returns:
            None  — image passes through unchanged (no blend needed).
            ndarray — blended output acquired from image_pool.
        """
        return self._blend(image, lambda shape: image_pool.acquire(*shape))

    def process_strip(self, strip: np.ndarray) -> Optional[np.ndarray]:
        """Frame interpolation for 1D LED strips — allocates directly."""
        return self._blend(strip, lambda shape: np.empty(shape, dtype=np.uint8))

    def _blend(self, data: np.ndarray, alloc_fn) -> Optional[np.ndarray]:
        """Shared blend logic for both images and strips."""
        now = time.perf_counter()

        # Detect new vs idle frame via cheap 64-byte signature
        sig = bytes(data.ravel()[:64])
        if sig != self._sig_b:
            # New source frame — shift A ← B, B ← current
            self._frame_a = self._frame_b
            self._time_a = self._time_b
            self._frame_b = data.copy()
            self._time_b = now
            self._sig_b = sig

        # Need at least two frames and a positive interval to interpolate
        if self._frame_a is None or self._time_b <= self._time_a:
            return None  # pass through unchanged

        interval = self._time_b - self._time_a
        alpha = min(1.0, (now - self._time_b) / interval)

        if alpha >= 1.0:
            return None  # fully transitioned to B, no blend needed

        # Blend: output = (1 - alpha)*A + alpha*B  (integer fast path)
        alpha_i = int(alpha * 256)
        shape = data.shape

        # Resize scratch buffers on shape change
        if self._blend_shape != shape:
            self._u16_a = np.empty(shape, dtype=np.uint16)
            self._u16_b = np.empty(shape, dtype=np.uint16)
            self._blend_shape = shape

        out = alloc_fn(shape)
        np.copyto(self._u16_a, self._frame_a, casting='unsafe')
        np.copyto(self._u16_b, data, casting='unsafe')
        self._u16_a *= (256 - alpha_i)
        self._u16_b *= alpha_i
        self._u16_a += self._u16_b
        self._u16_a >>= 8
        np.copyto(out, self._u16_a, casting='unsafe')
        return out
