"""Color cycle stream — smoothly cycles through user-defined colors."""

import threading
import time
from typing import Optional

import numpy as np

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 ColorCycleColorStripStream(ColorStripStream):
    """Color strip stream that smoothly cycles through a user-defined color list.

    All LEDs receive the same solid color at any moment, continuously interpolating
    between the configured colors in a loop.

    LED count auto-sizes from the connected device when led_count == 0 in
    the source config; configure(device_led_count) is called by
    WledTargetProcessor on start.
    """

    def __init__(self, source):
        self._colors_lock = threading.Lock()
        self._running = False
        self._thread: Optional[threading.Thread] = 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:
        raw = source.colors if isinstance(source.colors, list) else []
        default = [
            [255, 0, 0],
            [255, 255, 0],
            [0, 255, 0],
            [0, 255, 255],
            [0, 0, 255],
            [255, 0, 255],
        ]
        self._color_list = [c for c in raw if isinstance(c, list) and len(c) == 3] or default
        _lc = getattr(source, "led_count", 0)
        self._auto_size = not _lc
        self._led_count = _lc if _lc > 0 else 1
        self._rebuild_colors()

    def _rebuild_colors(self) -> None:
        pixel = np.array(self._color_list[0], dtype=np.uint8)
        colors = np.tile(pixel, (self._led_count, 1))
        with self._colors_lock:
            self._colors = colors

    def configure(self, device_led_count: int) -> None:
        """Size to device LED count when led_count was 0 (auto-size)."""
        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"ColorCycleColorStripStream auto-sized to {device_led_count} LEDs")

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

    @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-color-cycle",
            daemon=True,
        )
        self._thread.start()
        logger.info(
            f"ColorCycleColorStripStream started (leds={self._led_count}, colors={len(self._color_list)})"
        )

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

    def get_latest_colors(self) -> Optional[np.ndarray]:
        with self._colors_lock:
            return self._colors

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

        if isinstance(source, ColorCycleColorStripSource):
            prev_led_count = self._led_count if self._auto_size else None
            self._update_from_source(source)
            if prev_led_count and self._auto_size:
                self._led_count = prev_led_count
                self._rebuild_colors()
            logger.info("ColorCycleColorStripStream 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: interpolate between colors at target fps.

        Uses double-buffered output arrays to avoid per-frame allocations.
        """
        _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:
                        color_list = self._color_list
                        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
                        n = self._led_count
                        num = len(color_list)
                        if num >= 2:
                            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

                            # 0.05 factor → one full cycle every 20s at speed=1.0
                            cycle_pos = (speed * t * 0.05) % 1.0
                            seg = cycle_pos * num
                            idx = int(seg) % num
                            t_i = seg - int(seg)
                            c1 = color_list[idx]
                            c2 = color_list[(idx + 1) % num]
                            buf[:] = (
                                min(255, int(c1[0] + (c2[0] - c1[0]) * t_i)),
                                min(255, int(c1[1] + (c2[1] - c1[1]) * t_i)),
                                min(255, int(c1[2] + (c2[2] - c1[2]) * t_i)),
                            )
                            with self._colors_lock:
                                self._colors = buf
                    except Exception as e:
                        logger.error(f"ColorCycleColorStripStream animation error: {e}")
                    limiter.wait(frame_time)
        except Exception as e:
            logger.error(f"Fatal ColorCycleColorStripStream loop error: {e}", exc_info=True)
        finally:
            self._running = False
