"""Base ColorStripStream ABC and shared noise utility."""

from abc import ABC, abstractmethod
from typing import Optional

import numpy as np

from ledgrab.core.capture.calibration import CalibrationConfig
from ledgrab.utils import get_logger

logger = get_logger(__name__)


class _SimpleNoise1D:
    """Minimal 1-D value noise for gradient perturbation (avoids circular import)."""

    def __init__(self, seed: int = 99):
        rng = np.random.RandomState(seed)
        self._table = rng.random(512).astype(np.float32)

    def noise(self, x: np.ndarray) -> np.ndarray:
        size = len(self._table)
        xi = np.floor(x).astype(np.int64)
        frac = x - np.floor(x)
        t = frac * frac * (3.0 - 2.0 * frac)
        a = self._table[xi % size]
        b = self._table[(xi + 1) % size]
        return a + t * (b - a)


# Module-level noise for gradient perturbation
_gradient_noise = _SimpleNoise1D(seed=99)


class ColorStripStream(ABC):
    """Abstract base: a runtime source of LED color arrays.

    Produces a continuous stream of np.ndarray (led_count, 3) uint8 values.
    Consumers call get_latest_colors() (non-blocking) to read the most recent
    computed frame.
    """

    @property
    @abstractmethod
    def target_fps(self) -> int:
        """Target processing rate."""

    @property
    def actual_fps(self) -> Optional[float]:
        """Measured rate of *new* frames the stream is delivering, or ``None``.

        Only streams backed by an external capture (screen, audio device, API
        push) implement this — the value answers "is the upstream actually
        keeping up?". Synthetic streams (gradient/static/cycle/effect/...)
        always tick at their `target_fps` by construction, so reporting an
        actual rate would just duplicate `target_fps` without diagnostic
        value; they keep the default ``None``.
        """
        return None

    @property
    @abstractmethod
    def led_count(self) -> int:
        """Number of LEDs this stream produces colors for."""

    @property
    def is_animated(self) -> bool:
        """Whether this stream is actively producing new frames.

        Used by the processor to adjust polling rate: animated streams are
        polled at 5 ms (SKIP_REPOLL) to stay in sync with the animation
        thread, while static streams are polled at frame_time rate.
        """
        return True

    @property
    def display_index(self) -> Optional[int]:
        """Display index of the underlying capture, or None."""
        return None

    @property
    def calibration(self) -> Optional[CalibrationConfig]:
        """Calibration config, or None if not applicable."""
        return None

    @abstractmethod
    def start(self) -> None:
        """Start producing colors."""

    @abstractmethod
    def stop(self) -> None:
        """Stop producing colors and release resources."""

    @abstractmethod
    def get_latest_colors(self) -> Optional[np.ndarray]:
        """Get the most recent LED color array (led_count, 3) uint8, or None."""

    def get_last_timing(self) -> dict:
        """Return per-stage timing from the last processed frame (ms)."""
        return {}

    def update_source(self, source) -> None:
        """Hot-update processing parameters. No-op by default."""

    # ── BindableFloat value stream resolution ──

    _value_streams: dict = None  # property_name → ValueStream

    def set_value_stream(self, prop: str, stream) -> None:
        """Inject a ValueStream for a bindable property."""
        if self._value_streams is None:
            self._value_streams = {}
        self._value_streams[prop] = stream

    def remove_value_stream(self, prop: str) -> None:
        """Remove a ValueStream for a bindable property."""
        if self._value_streams:
            self._value_streams.pop(prop, None)

    def resolve(self, prop: str, static: float) -> float:
        """Resolve a bindable property: ValueStream value if bound, else static."""
        if self._value_streams:
            vs = self._value_streams.get(prop)
            if vs is not None:
                try:
                    return vs.get_value()
                except Exception:
                    pass
        return static

    def resolve_color(self, prop: str, static: list) -> list:
        """Resolve a bindable color: ValueStream color if bound, else static [R,G,B]."""
        if self._value_streams:
            vs = self._value_streams.get(prop)
            if vs is not None:
                try:
                    c = vs.get_color()
                    return [c[0], c[1], c[2]]
                except Exception:
                    pass
        return static
