"""RGB LED controller for the Pi Multi-FX Pedal.

Controls WS2812B (NeoPixel) or APA102 (DotStar) LEDs for:
- Per-footswitch status LEDs
- Bypass indicator (red/green)
- Preset navigation animations
- Tap tempo flashing
- Configurable brightness
"""

from __future__ import annotations

import logging
import math
import time
from dataclasses import dataclass, field
from enum import Enum
from typing import Callable, Optional

logger = logging.getLogger(__name__)


class LEDDriver(Enum):
    """Which LED driver to use."""
    NEOPIXEL = "neopixel"      # WS2812B — adafruit-circuitpython-neopixel
    DOTSTAR = "dotstar"        # APA102 — adafruit-circuitpython-dotstar
    MOCK = "mock"              # No hardware — log only


class LEDPattern(Enum):
    """Built-in LED animation patterns."""
    SOLID = "solid"
    PULSE = "pulse"            # Gentle breathe
    BLINK = "blink"            # On/off square wave
    TAP_TEMPO = "tap_tempo"    # Flash at detected BPM
    SCAN = "scan"              # Chase across strip
    PRESET_UP = "preset_up"    # Sweep up
    PRESET_DOWN = "preset_down"  # Sweep down


@dataclass
class LEDConfig:
    """Per-LED configuration."""
    index: int
    default_color: tuple[int, int, int] = (0, 0, 0)  # RGB
    default_brightness: float = 0.5
    label: str = ""


@dataclass
class LEDAnimation:
    """Active animation state."""
    pattern: LEDPattern
    color: tuple[int, int, int]
    speed_ms: int = 500          # Cycle time in ms
    brightness: float = 0.5
    start_time: float = 0.0
    repeats: int = 0             # 0 = infinite


def _import_driver(driver: LEDDriver):
    """Import the correct LED driver library."""
    if driver == LEDDriver.NEOPIXEL:
        try:
            import board
            import neopixel
            return neopixel
        except (ImportError, NotImplementedError):
            logger.warning("NeoPixel not available — falling back to mock")
            return None
    elif driver == LEDDriver.DOTSTAR:
        try:
            import board
            import adafruit_dotstar as dotstar
            return dotstar
        except (ImportError, NotImplementedError):
            logger.warning("DotStar not available — falling back to mock")
            return None
    return None


class LEDController:
    """RGB LED controller with animation support.

    Handles per-LED animations for preset navigation, bypass status,
    tap tempo, and tuner mode.
    """

    def __init__(
        self,
        num_leds: int,
        driver: LEDDriver = LEDDriver.NEOPIXEL,
        pin: str = "D18",
        brightness: float = 0.5,
        led_configs: Optional[list[LEDConfig]] = None,
    ):
        self._num_leds = num_leds
        self._driver_type = driver
        self._pin = pin
        self._global_brightness = brightness
        self._led_configs = led_configs or [
            LEDConfig(i, default_brightness=brightness) for i in range(num_leds)
        ]

        # Physical LED strip handle
        self._strip = None
        self._initialized = False

        # Current pixel colors (RGB)
        self._pixels: list[tuple[int, int, int]] = [(0, 0, 0)] * num_leds

        # Active animations per LED
        self._animations: dict[int, LEDAnimation] = {}

        # Callbacks
        self._animation_callbacks: list[Callable] = []

    def initialize(self) -> bool:
        """Initialize the LED strip hardware.

        Returns False in dev mode (no hardware).
        """
        try:
            driver_mod = _import_driver(self._driver_type)
            if driver_mod is None or self._driver_type == LEDDriver.MOCK:
                self._initialized = False
                logger.info("LED driver in MOCK mode — no hardware")
                return False

            if self._driver_type == LEDDriver.NEOPIXEL:
                import board
                self._strip = driver_mod.NeoPixel(
                    getattr(board, self._pin),
                    self._num_leds,
                    brightness=self._global_brightness,
                    auto_write=False,
                )
            elif self._driver_type == LEDDriver.DOTSTAR:
                import board
                self._strip = driver_mod.DotStar(
                    board.SCK, board.MOSI,
                    self._num_leds,
                    brightness=self._global_brightness,
                    auto_write=False,
                )

            self._initialized = True
            logger.info(
                "LED strip initialized: %d LEDs, %s @ %s",
                self._num_leds, self._driver_type.value, self._pin,
            )
            return True

        except Exception as e:
            logger.info("No LED strip detected — running in mock mode: %s", e)
            self._initialized = False
            return False

    # --- Pixel control ---

    def set_pixel(
        self,
        index: int,
        color: tuple[int, int, int],
        brightness: Optional[float] = None,
    ) -> None:
        """Set a single LED to a color (0-255 per channel)."""
        if not 0 <= index < self._num_leds:
            logger.warning("LED index %d out of range (0-%d)", index, self._num_leds - 1)
            return

        bri = brightness if brightness is not None else self._global_brightness
        r, g, b = self._clamp_color(color)
        # Apply brightness scaling
        self._pixels[index] = (int(r * bri), int(g * bri), int(b * bri))
        self._write_pixel(index)

    def set_all(
        self,
        color: tuple[int, int, int],
        brightness: Optional[float] = None,
    ) -> None:
        """Set all LEDs to the same color."""
        bri = brightness if brightness is not None else self._global_brightness
        r, g, b = self._clamp_color(color)
        scaled = (int(r * bri), int(g * bri), int(b * bri))
        self._pixels = [scaled] * self._num_leds
        self._write_all()

    def set_bypass_led(self, index: int, bypassed: bool) -> None:
        """Set a bypass indicator LED.

        Red = bypassed, Green = active.
        """
        if bypassed:
            self.set_pixel(index, (255, 0, 0))
        else:
            self.set_pixel(index, (0, 255, 0))

    def preset_animate(self, direction: str = "up") -> None:
        """Animate preset change (sweep).

        direction: "up" → sweep left-to-right, "down" → right-to-left.
        """
        self._animate_scan(
            color=(0, 64, 255),  # Blue
            reverse=(direction == "down"),
            speed_ms=120,
        )

    def tap_tempo_blip(self) -> None:
        """Quick white flash indicating a tap tempo hit."""
        old = self._pixels[:]
        # Flash all LEDs white briefly
        self.set_all((255, 255, 255), brightness=0.8)
        time.sleep(0.03)
        # Restore — but don't block, schedule async
        for i in range(self._num_leds):
            self._pixels[i] = old[i]
        self._write_all()

    def tap_tempo_animate(self, bpm: float) -> None:
        """Start a tempo-synchronized flash on all LEDs.

        Animates in sync with the detected BPM.
        The caller still drives the actual flash via tap_tempo_blip;
        this sets a subtle pulse background at the tempo.
        """
        period_ms = int(60000 / max(bpm, 20))  # ms per beat
        self._animate_all(
            pattern=LEDPattern.TAP_TEMPO,
            color=(255, 255, 255),
            speed_ms=period_ms,
            brightness=0.15,  # Subtle
        )

    def clear_all(self) -> None:
        """Turn off all LEDs."""
        self._pixels = [(0, 0, 0)] * self._num_leds
        self._animations.clear()
        self._write_all()

    def set_brightness(self, brightness: float) -> None:
        """Set global brightness (0.0 - 1.0)."""
        self._global_brightness = max(0.0, min(1.0, brightness))
        if self._strip is not None:
            self._strip.brightness = self._global_brightness
        logger.info("Global brightness set to %.2f", self._global_brightness)

    # --- Animation engine ---

    def _animate_all(
        self,
        pattern: LEDPattern,
        color: tuple[int, int, int],
        speed_ms: int,
        brightness: float,
    ) -> None:
        """Start an animation on all LEDs."""
        return None  # Full animation tick runs on _animation_tick

    def _animate_scan(
        self,
        color: tuple[int, int, int],
        reverse: bool = False,
        speed_ms: int = 120,
    ) -> None:
        """Run a scan animation in a separate thread (non-blocking)."""
        import threading

        def _scan():
            r, g, b = color
            indices = range(self._num_leds)
            if reverse:
                indices = reversed(indices)
            for i in indices:
                self.set_all((0, 0, 0))
                self.set_pixel(i, (r, g, b))
                self._write_all()
                time.sleep(speed_ms / 1000)
            self.set_all((0, 0, 0))
            self._write_all()

        t = threading.Thread(target=_scan, daemon=True, name="led-scan")
        t.start()

    # --- Internal helpers ---

    def _clamp_color(self, color: tuple[int, int, int]) -> tuple[int, int, int]:
        """Clamp 0-255 per channel."""
        return (
            max(0, min(255, color[0])),
            max(0, min(255, color[1])),
            max(0, min(255, color[2])),
        )

    def _write_pixel(self, index: int) -> None:
        """Write a single pixel to the hardware strip."""
        if self._strip is not None and self._initialized:
            self._strip[index] = self._pixels[index]
            self._strip.show()
        else:
            logger.debug("LED[%d] → RGB(%d,%d,%d)", index, *self._pixels[index])

    def _write_all(self) -> None:
        """Write all pixels to the hardware strip."""
        if self._strip is not None and self._initialized:
            for i in range(self._num_leds):
                self._strip[i] = self._pixels[i]
            self._strip.show()
        else:
            for i, c in enumerate(self._pixels):
                if any(v > 0 for v in c):
                    logger.debug("LED[%d] → RGB(%d,%d,%d)", i, *c)

    def __enter__(self):
        self.initialize()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb) -> None:
        self.clear_all()