"""Candlelight LED stream — realistic per-LED candle flickering.

Implements CandlelightColorStripStream which produces warm, organic
flickering across all LEDs using layered sine waves and value noise.
Each "candle" is an independent flicker source that illuminates
nearby LEDs with smooth falloff.

Features:
  - Wind simulation: correlated brightness drops across all candles
  - Candle type presets: taper / votive / bonfire
  - Wax drip effect: localized brightness dips that recover over ~0.5 s
"""

import math
import threading
import time
from typing import List, Optional

import numpy as np

from wled_controller.core.processing.color_strip_stream import ColorStripStream
from wled_controller.utils import get_logger
from wled_controller.utils.timer import high_resolution_timer

logger = get_logger(__name__)

# ── Simple hash-based noise ──────────────────────────────────────────

_PERM = np.arange(256, dtype=np.int32)
_rng = np.random.RandomState(seed=17)
_rng.shuffle(_PERM)
_PERM = np.concatenate([_PERM, _PERM])  # 512 entries for wrap-free indexing


def _noise1d(x: np.ndarray) -> np.ndarray:
    """Fast 1-D value noise (vectorized). Returns float32 in [0, 1]."""
    xi = x.astype(np.int32) & 255
    xf = x - np.floor(x)
    # Smoothstep
    u = xf * xf * (3.0 - 2.0 * xf)
    a = _PERM[xi].astype(np.float32) / 255.0
    b = _PERM[xi + 1].astype(np.float32) / 255.0
    return a + u * (b - a)


# ── Candle type preset multipliers ──────────────────────────────────
# (flicker_amplitude_mul, speed_mul, sigma_mul, warm_bonus)
_CANDLE_PRESETS: dict = {
    "default": (1.0, 1.0, 1.0, 0.0),
    "taper":   (0.5, 1.3, 0.8, 0.0),    # tall, steady
    "votive":  (1.5, 1.0, 0.7, 0.0),    # small, flickery
    "bonfire": (2.0, 1.0, 1.5, 0.12),   # chaotic, warmer shift
}

_VALID_CANDLE_TYPES = frozenset(_CANDLE_PRESETS)


class CandlelightColorStripStream(ColorStripStream):
    """Color strip stream simulating realistic candle flickering.

    Each LED flickers independently with warm tones.  Multiple
    "candle sources" are distributed along the strip, each generating
    its own flicker pattern with smooth spatial falloff.
    """

    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
        self._led_count = 1
        self._auto_size = True
        # Scratch arrays
        self._s_bright: Optional[np.ndarray] = None
        self._s_noise: Optional[np.ndarray] = None
        self._s_x: Optional[np.ndarray] = None
        self._s_drip: Optional[np.ndarray] = None
        self._pool_n = 0
        # Wax drip events: [pos, brightness, phase(0=dim,1=recover)]
        self._drip_events: List[List[float]] = []
        self._drip_rng = np.random.RandomState(seed=42)
        self._last_drip_t = 0.0
        self._update_from_source(source)

    def _update_from_source(self, source) -> None:
        raw_color = getattr(source, "color", None)
        self._color = raw_color if isinstance(raw_color, list) and len(raw_color) == 3 else [255, 147, 41]
        self._intensity = float(getattr(source, "intensity", 1.0))
        self._num_candles = max(1, int(getattr(source, "num_candles", 3)))
        self._speed = float(getattr(source, "speed", 1.0))
        self._wind_strength = float(getattr(source, "wind_strength", 0.0))
        raw_type = getattr(source, "candle_type", "default")
        self._candle_type = raw_type if raw_type in _VALID_CANDLE_TYPES else "default"
        _lc = getattr(source, "led_count", 0)
        self._auto_size = not _lc
        self._led_count = _lc if _lc and _lc > 0 else 1
        with self._colors_lock:
            self._colors: Optional[np.ndarray] = None

    def configure(self, device_led_count: int) -> None:
        if self._auto_size and device_led_count > 0:
            new_count = max(self._led_count, device_led_count)
            if new_count != self._led_count:
                self._led_count = new_count

    @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:
        self._fps = max(1, min(90, fps))
        self._frame_time = 1.0 / self._fps

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

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

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

    def update_source(self, source) -> None:
        from wled_controller.storage.color_strip_source import CandlelightColorStripSource
        if isinstance(source, CandlelightColorStripSource):
            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
            logger.info("CandlelightColorStripStream params updated in-place")

    def set_clock(self, clock) -> None:
        self._clock = clock

    # ── Animation loop ──────────────────────────────────────────────

    def _animate_loop(self) -> None:
        _pool_n = 0
        _buf_a = _buf_b = None
        _use_a = True

        try:
            with high_resolution_timer():
                while self._running:
                    wall_start = time.perf_counter()
                    frame_time = self._frame_time
                    try:
                        clock = self._clock
                        if clock:
                            if not clock.is_running:
                                time.sleep(0.1)
                                continue
                            t = clock.get_time()
                            speed = clock.speed * self._speed
                        else:
                            t = wall_start
                            speed = self._speed

                        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)
                            self._s_bright = np.empty(n, dtype=np.float32)
                            self._s_noise = np.empty(n, dtype=np.float32)
                            self._s_x = np.arange(n, dtype=np.float32)
                            self._s_drip = np.ones(n, dtype=np.float32)

                        buf = _buf_a if _use_a else _buf_b
                        _use_a = not _use_a

                        self._update_drip_events(n, wall_start, frame_time)
                        self._render_candlelight(buf, n, t, speed, wall_start)

                        with self._colors_lock:
                            self._colors = buf
                    except Exception as e:
                        logger.error(f"CandlelightColorStripStream animation error: {e}")

                    elapsed = time.perf_counter() - wall_start
                    time.sleep(max(frame_time - elapsed, 0.001))
        except Exception as e:
            logger.error(f"Fatal CandlelightColorStripStream loop error: {e}", exc_info=True)
        finally:
            self._running = False

    # ── Drip management ─────────────────────────────────────────────

    def _update_drip_events(self, n: int, wall_t: float, dt: float) -> None:
        """Spawn new wax drip events and advance existing ones."""
        intensity = self._intensity
        spawn_interval = max(0.3, 1.0 / max(intensity, 0.01))
        if wall_t - self._last_drip_t >= spawn_interval and len(self._drip_events) < 5:
            self._last_drip_t = wall_t
            pos = float(self._drip_rng.randint(0, max(n, 1)))
            self._drip_events.append([pos, 1.0, 0])

        surviving = []
        for drip in self._drip_events:
            pos, bright, phase = drip
            if phase == 0:
                bright -= dt / 0.2 * 0.7
                if bright <= 0.3:
                    bright = 0.3
                    drip[2] = 1
            else:
                bright += dt / 0.5 * 0.7
                if bright >= 1.0:
                    continue  # drip complete
            drip[1] = bright
            surviving.append(drip)
        self._drip_events = surviving

        # Build per-LED drip factor
        drip_arr = self._s_drip
        drip_arr[:n] = 1.0
        x = self._s_x[:n]
        for drip in self._drip_events:
            pos, bright, _phase = drip
            dist = x - pos
            falloff = np.exp(-0.5 * dist * dist / 4.0)
            drip_arr[:n] *= 1.0 - (1.0 - bright) * falloff

    # ── Render ──────────────────────────────────────────────────────

    def _render_candlelight(self, buf: np.ndarray, n: int, t: float, speed: float, wall_t: float) -> None:
        """Render candle flickering into buf (n, 3) uint8."""
        amp_mul, spd_mul, sigma_mul, warm_bonus = _CANDLE_PRESETS[self._candle_type]

        eff_speed = speed * 0.35 * spd_mul
        intensity = self._intensity
        num_candles = self._num_candles
        base_r, base_g, base_b = self._color[0], self._color[1], self._color[2]

        # Wind modulation
        wind_strength = self._wind_strength
        if wind_strength > 0.0:
            wind_raw = (
                0.6 * math.sin(2.0 * math.pi * 0.15 * wall_t)
                + 0.4 * math.sin(2.0 * math.pi * 0.27 * wall_t + 1.1)
            )
            wind_mod = max(0.0, wind_raw)
        else:
            wind_mod = 0.0

        bright = self._s_bright
        bright[:n] = 0.0

        if num_candles == 1:
            positions = [n / 2.0]
        else:
            positions = [i * (n - 1) / (num_candles - 1) for i in range(num_candles)]

        x = self._s_x[:n]

        for ci, pos in enumerate(positions):
            offset = ci * 137.5
            flicker = (
                0.40 * math.sin(2.0 * math.pi * eff_speed * t * 3.7 + offset)
                + 0.25 * math.sin(2.0 * math.pi * eff_speed * t * 7.3 + offset * 0.7)
                + 0.15 * math.sin(2.0 * math.pi * eff_speed * t * 13.1 + offset * 1.3)
                + 0.10 * math.sin(2.0 * math.pi * eff_speed * t * 1.9 + offset * 0.3)
            )

            candle_brightness = 0.65 + 0.35 * flicker * intensity * amp_mul

            if wind_strength > 0.0:
                candle_brightness *= (1.0 - wind_strength * wind_mod * 0.4)

            candle_brightness = max(0.1, candle_brightness)

            sigma = max(n / (num_candles * 2.0), 2.0) * sigma_mul
            dist = x - pos
            falloff = np.exp(-0.5 * (dist * dist) / (sigma * sigma))

            bright[:n] += candle_brightness * falloff

        # Per-LED noise
        noise_x = x * 0.3 + t * eff_speed * 5.0
        noise = _noise1d(noise_x)
        bright[:n] *= (0.85 + 0.30 * noise)

        # Wax drip factor
        bright[:n] *= self._s_drip[:n]

        np.clip(bright[:n], 0.0, 1.0, out=bright[:n])

        # Colour mapping: dimmer = warmer
        warm_shift = (1.0 - bright[:n]) * (0.3 + warm_bonus)
        r = bright[:n] * base_r
        g = bright[:n] * base_g * (1.0 - warm_shift * 0.5)
        b = bright[:n] * base_b * (1.0 - warm_shift)

        buf[:, 0] = np.clip(r, 0, 255).astype(np.uint8)
        buf[:, 1] = np.clip(g, 0, 255).astype(np.uint8)
        buf[:, 2] = np.clip(b, 0, 255).astype(np.uint8)
