"""Single color strip stream — solid color with optional animation."""

import colorsys
import math
import threading
import time

import numpy as np

from ledgrab.storage.bindable import bcolor
from ledgrab.utils import get_logger
from ledgrab.utils.frame_limiter import FrameLimiter
from ledgrab.utils.timer import high_resolution_timer

from .base import ColorStripStream

logger = get_logger(__name__)


class SingleColorStripStream(ColorStripStream):
    """Color strip stream that returns a constant single-color array.

    When animation is enabled a 30 fps background thread updates _colors with
    the animated result.  Parameters can be hot-updated via update_source().
    """

    def __init__(self, source):
        """
        Args:
            source: SingleColorStripSource config
        """
        self._colors_lock = threading.Lock()
        self._running = False
        self._thread: threading.Thread | None = None
        self._fps = 30
        self._frame_time = 1.0 / 30
        self._clock = None  # optional SyncClockRuntime
        self._update_from_source(source)

    def _update_from_source(self, source) -> None:
        self._source_color = bcolor(source.color, [255, 255, 255])
        _lc = getattr(source, "led_count", 0)
        self._auto_size = not _lc
        led_count = _lc if _lc and _lc > 0 else 1
        self._led_count = led_count
        self._animation = source.animation  # dict or None; read atomically by _animate_loop
        self._rebuild_colors()

    def _rebuild_colors(self) -> None:
        colors = np.tile(
            np.array(self._source_color, dtype=np.uint8),
            (self._led_count, 1),
        )
        with self._colors_lock:
            self._colors = colors

    def configure(self, device_led_count: int) -> None:
        """Set LED count from the target device (called by WledTargetProcessor on start).

        Only takes effect when led_count was 0 (auto-size).  Silently ignored
        when an explicit led_count was configured on the source.
        """
        if self._auto_size and device_led_count > 0 and device_led_count != self._led_count:
            self._led_count = device_led_count
            self._rebuild_colors()
            logger.debug(f"SingleColorStripStream auto-sized to {device_led_count} LEDs")

    @property
    def target_fps(self) -> int:
        return self._fps

    @property
    def is_animated(self) -> bool:
        anim = self._animation
        if anim and anim.get("enabled"):
            return True
        return self._is_color_bound()

    def _is_color_bound(self) -> bool:
        """True when the `color` property is driven by a ValueStream."""
        vs = self._value_streams
        return bool(vs and vs.get("color"))

    @property
    def led_count(self) -> int:
        return self._led_count

    def set_capture_fps(self, fps: int) -> None:
        """Update animation loop rate. Thread-safe (read atomically by the loop)."""
        fps = max(1, min(90, fps))
        self._fps = fps
        self._frame_time = 1.0 / fps

    def start(self) -> None:
        if self._running:
            return
        self._running = True
        self._thread = threading.Thread(
            target=self._animate_loop,
            name="css-single-animate",
            daemon=True,
        )
        self._thread.start()
        logger.info(f"SingleColorStripStream started (leds={self._led_count})")

    def stop(self) -> None:
        self._running = False
        if self._thread:
            self._thread.join(timeout=5.0)
            if self._thread.is_alive():
                logger.warning("SingleColorStripStream animate thread did not terminate within 5s")
            self._thread = None
        logger.info("SingleColorStripStream stopped")

    def get_latest_colors(self) -> np.ndarray | None:
        with self._colors_lock:
            return self._colors

    def update_source(self, source) -> None:
        from ledgrab.storage.color_strip_source import SingleColorStripSource

        if isinstance(source, SingleColorStripSource):
            prev_led_count = self._led_count if self._auto_size else None
            self._update_from_source(source)
            # If we were auto-sized, preserve the runtime LED count across updates
            if prev_led_count and self._auto_size:
                self._led_count = prev_led_count
                self._rebuild_colors()
            logger.info("SingleColorStripStream params updated in-place")

    def set_clock(self, clock) -> None:
        """Set or clear the sync clock runtime. Thread-safe (read atomically by loop)."""
        self._clock = clock

    def _animate_loop(self) -> None:
        """Background thread: compute animated colors at target fps when animation is active.

        Uses double-buffered output arrays (buf_a / buf_b) to avoid per-frame
        numpy allocations while preserving the identity check used by the
        processing loop (``colors is prev_colors``).
        """
        # Double-buffer pool — re-allocated only when LED count changes
        _pool_n = 0
        _buf_a = _buf_b = None
        _use_a = True

        limiter = FrameLimiter(self._fps)

        try:
            with high_resolution_timer():
                while self._running:
                    limiter.begin()
                    wall_start = time.perf_counter()
                    frame_time = self._frame_time
                    try:
                        anim = self._animation
                        if anim and anim.get("enabled"):
                            clock = self._clock
                            if clock:
                                if not clock.is_running:
                                    time.sleep(0.1)
                                    continue
                                speed = clock.speed
                                t = clock.get_time()
                            else:
                                speed = 1.0
                                t = wall_start
                            atype = anim.get("type", "breathing")
                            n = self._led_count

                            if n != _pool_n:
                                _pool_n = n
                                _buf_a = np.empty((n, 3), dtype=np.uint8)
                                _buf_b = np.empty((n, 3), dtype=np.uint8)

                            buf = _buf_a if _use_a else _buf_b
                            _use_a = not _use_a
                            colors = None

                            if atype == "breathing":
                                factor = 0.5 * (1 + math.sin(2 * math.pi * speed * t * 0.5))
                                r, g, b = self.resolve_color("color", self._source_color)
                                buf[:] = (
                                    min(255, int(r * factor)),
                                    min(255, int(g * factor)),
                                    min(255, int(b * factor)),
                                )
                                colors = buf

                            elif atype == "strobe":
                                # Square wave: on for half the period, off for the other half.
                                # speed=1.0 → 2 flashes/sec (one full on/off cycle per 0.5s)
                                if math.sin(2 * math.pi * speed * t * 2.0) >= 0:
                                    buf[:] = self.resolve_color("color", self._source_color)
                                else:
                                    buf[:] = 0
                                colors = buf

                            elif atype == "sparkle":
                                # Random LEDs flash white while the rest stay the base color
                                buf[:] = self.resolve_color("color", self._source_color)
                                density = min(0.5, 0.1 * speed)
                                mask = np.random.random(n) < density
                                buf[mask] = (255, 255, 255)
                                colors = buf

                            elif atype == "pulse":
                                # Sharp attack, slow exponential decay — heartbeat-like
                                # speed=1.0 → ~1 pulse per second
                                phase = (speed * t * 1.0) % 1.0
                                if phase < 0.1:
                                    factor = phase / 0.1
                                else:
                                    factor = math.exp(-5.0 * (phase - 0.1))
                                r, g, b = self.resolve_color("color", self._source_color)
                                buf[:] = (
                                    min(255, int(r * factor)),
                                    min(255, int(g * factor)),
                                    min(255, int(b * factor)),
                                )
                                colors = buf

                            elif atype == "candle":
                                # Random brightness fluctuations simulating a candle flame
                                base_factor = 0.75
                                flicker = 0.25 * math.sin(2 * math.pi * speed * t * 3.7)
                                flicker += 0.15 * math.sin(2 * math.pi * speed * t * 7.3)
                                flicker += 0.10 * (np.random.random() - 0.5)
                                factor = max(0.2, min(1.0, base_factor + flicker))
                                r, g, b = self.resolve_color("color", self._source_color)
                                buf[:] = (
                                    min(255, int(r * factor)),
                                    min(255, int(g * factor)),
                                    min(255, int(b * factor)),
                                )
                                colors = buf

                            elif atype == "rainbow_fade":
                                # Shift hue continuously from the base color
                                r, g, b = self.resolve_color("color", self._source_color)
                                h, s, v = colorsys.rgb_to_hsv(r / 255.0, g / 255.0, b / 255.0)
                                # speed=1.0 → one full hue rotation every ~10s
                                h_shift = (speed * t * 0.1) % 1.0
                                new_h = (h + h_shift) % 1.0
                                nr, ng, nb = colorsys.hsv_to_rgb(new_h, max(s, 0.5), max(v, 0.3))
                                buf[:] = (int(nr * 255), int(ng * 255), int(nb * 255))
                                colors = buf

                            if colors is not None:
                                with self._colors_lock:
                                    self._colors = colors
                        elif self._is_color_bound():
                            # No animation, but color is driven by a ValueStream —
                            # poll and forward live color updates so the bound
                            # source is honoured (otherwise LEDs stay stuck on
                            # the static fallback).
                            n = self._led_count
                            if n != _pool_n:
                                _pool_n = n
                                _buf_a = np.empty((n, 3), dtype=np.uint8)
                                _buf_b = np.empty((n, 3), dtype=np.uint8)
                            buf = _buf_a if _use_a else _buf_b
                            _use_a = not _use_a
                            buf[:] = self.resolve_color("color", self._source_color)
                            with self._colors_lock:
                                self._colors = buf
                    except Exception as e:
                        logger.error(f"SingleColorStripStream animation error: {e}")

                    if (anim and anim.get("enabled")) or self._is_color_bound():
                        sleep_target = frame_time
                    else:
                        sleep_target = 0.25
                    limiter.wait(sleep_target)
        except Exception as e:
            logger.error(f"Fatal SingleColorStripStream loop error: {e}", exc_info=True)
        finally:
            self._running = False
