"""
LED_controller.py

Controls a 16-LED WS2812/NeoPixel ring.

Connections:
    GPIO13 -> LED data input
    5V     -> LED power
    GND    -> LED ground

Animations run continuously until their stop_event is set.
This allows main.py to switch animations immediately.
"""

import math
import random
import time
from threading import Event

import board
import neopixel


# ============================================================
# LED SETTINGS
# ============================================================

NUM_LEDS = 16
LED_PIN = board.D13
LED_BRIGHTNESS = 0.3

FRAME_DELAY = 0.03


# ============================================================
# HARDWARE SETUP
# ============================================================

pixels = neopixel.NeoPixel(
    LED_PIN,
    NUM_LEDS,
    brightness=LED_BRIGHTNESS,
    auto_write=False
)


# ============================================================
# BASIC LED FUNCTIONS
# ============================================================

def off():
    """
    Turn all LEDs off.
    """

    pixels.fill((0, 0, 0))
    pixels.show()


def set_all(color):
    """
    Set every LED to one color.

    Args:
        color:
            RGB tuple such as (255, 0, 0).
    """

    pixels.fill(color)
    pixels.show()


def set_pixel(index, color):
    """
    Set one LED to a specific color.

    Args:
        index:
            LED index from 0 through NUM_LEDS - 1.

        color:
            RGB tuple such as (255, 0, 0).
    """

    if not 0 <= index < NUM_LEDS:
        raise IndexError(
            f"LED index must be between 0 and {NUM_LEDS - 1}"
        )

    pixels[index] = color
    pixels.show()


def clamp_color(value):
    """
    Clamp one RGB channel to the valid range of 0 through 255.
    """

    return max(0, min(255, int(value)))


def should_stop(stop_event):
    """
    Return True when an animation should stop.
    """

    return stop_event is not None and stop_event.is_set()


def animation_sleep(stop_event, duration):
    """
    Sleep while still allowing an animation to stop quickly.

    Returns:
        True if the stop event was triggered.
        False if the full delay completed.
    """

    if stop_event is None:
        time.sleep(duration)
        return False

    return stop_event.wait(duration)


# ============================================================
# COLOR HELPERS
# ============================================================

def wheel(position):
    """
    Convert a value from 0 through 255 into an RGB rainbow color.
    """

    position = int(position) % 256

    if position < 85:
        return (
            position * 3,
            255 - position * 3,
            0
        )

    if position < 170:
        position -= 85

        return (
            255 - position * 3,
            0,
            position * 3
        )

    position -= 170

    return (
        0,
        position * 3,
        255 - position * 3
    )


def fade_color(color, amount):
    """
    Fill the ring with a scaled version of a color.

    Args:
        color:
            RGB tuple.

        amount:
            Brightness multiplier from 0.0 through 1.0.
    """

    amount = max(0.0, min(1.0, float(amount)))

    pixels.fill(
        (
            clamp_color(color[0] * amount),
            clamp_color(color[1] * amount),
            clamp_color(color[2] * amount)
        )
    )

    pixels.show()


# ============================================================
# ANIMATIONS
# ============================================================

def ocean_wave(stop_event=None, speed=0.08):
    """
    Moving blue ocean-wave animation.
    """

    while not should_stop(stop_event):
        current_time = time.monotonic()

        for index in range(NUM_LEDS):
            wave = (
                math.sin(index * 0.6 + current_time * 1.5) + 1
            ) / 2

            pixels[index] = (
                0,
                clamp_color(70 * wave),
                clamp_color(255 * wave)
            )

        pixels.show()

        if animation_sleep(stop_event, speed):
            break


def aurora(stop_event=None, speed=0.05):
    """
    Flowing green, teal, and purple aurora animation.
    """

    while not should_stop(stop_event):
        current_time = time.monotonic()

        for index in range(NUM_LEDS):
            green_wave = (
                math.sin(index * 0.4 + current_time * 0.6) + 1
            ) / 2

            purple_wave = (
                math.sin(index * 0.3 - current_time * 0.45 + 2.0) + 1
            ) / 2

            red = 70 * purple_wave
            green = 255 * green_wave
            blue = 120 + 100 * purple_wave

            pixels[index] = (
                clamp_color(red),
                clamp_color(green),
                clamp_color(blue)
            )

        pixels.show()

        if animation_sleep(stop_event, speed):
            break


def peppermint(stop_event=None, speed=0.03):
    """
    Moving red-and-green peppermint animation with white sparkles.
    """

    while not should_stop(stop_event):
        current_time = time.monotonic()

        for index in range(NUM_LEDS):
            phase = math.sin(
                index * 0.8 + current_time * 1.2
            )

            if phase > 0:
                color = (255, 40, 40)
            else:
                color = (40, 255, 40)

            pixels[index] = color

        if random.random() < 0.30:
            sparkle_index = random.randrange(NUM_LEDS)
            pixels[sparkle_index] = (255, 255, 255)

        pixels.show()

        if animation_sleep(stop_event, speed):
            break


def lava(stop_event=None, speed=0.03):
    """
    Moving red, orange, and yellow molten-lava animation.
    """

    while not should_stop(stop_event):
        current_time = time.monotonic()

        for index in range(NUM_LEDS):
            blob_1 = (
                math.sin(index * 0.55 + current_time * 0.9) + 1
            ) / 2

            blob_2 = (
                math.sin(index * 1.05 - current_time * 0.45) + 1
            ) / 2

            blob_3 = (
                math.sin(
                    index * 0.25
                    + current_time * 0.2
                    + 2.0
                ) + 1
            ) / 2

            glow = (
                blob_1 * 0.50
                + blob_2 * 0.35
                + blob_3 * 0.15
            )

            red = 120 + 135 * glow
            green = 20 + 120 * glow * glow
            blue = 25 * (1 - glow)

            if glow > 0.90:
                red += 30
                green += 40

            pixels[index] = (
                clamp_color(red),
                clamp_color(green),
                clamp_color(blue)
            )

        pixels.show()

        if animation_sleep(stop_event, speed):
            break


def candle(stop_event=None):
    """
    Warm flickering candle-light animation.
    """

    while not should_stop(stop_event):
        brightness = random.randint(120, 255)

        red = brightness
        green = brightness * random.uniform(0.35, 0.50)
        blue = random.randint(0, 8)

        pixels.fill(
            (
                clamp_color(red),
                clamp_color(green),
                clamp_color(blue)
            )
        )

        # Add slight differences around the ring so it does not
        # look like one completely flat color.
        for _ in range(random.randint(1, 3)):
            index = random.randrange(NUM_LEDS)
            variation = random.uniform(0.70, 1.0)

            pixels[index] = (
                clamp_color(red * variation),
                clamp_color(green * variation),
                clamp_color(blue)
            )

        pixels.show()

        delay = random.uniform(0.03, 0.12)

        if animation_sleep(stop_event, delay):
            break


def nebula(stop_event=None, speed=0.03):
    """
    Purple, blue, and pink nebula with longer accent streaks.
    """

    streaks = []
    next_streak_time = 0

    while not should_stop(stop_event):
        current_time = time.monotonic()

        # Main purple, blue, and pink nebula.
        for index in range(NUM_LEDS):
            purple = max(
                0,
                math.sin(index * 0.45 + current_time * 0.35)
            )

            blue = max(
                0,
                math.sin(
                    index * 0.45
                    + current_time * 0.35
                    + 2.1
                )
            )

            pink = max(
                0,
                math.sin(
                    index * 0.45
                    + current_time * 0.35
                    + 4.2
                )
            )

            red = 150 * purple + 220 * pink
            green = 40 * blue
            blue_channel = (
                220 * purple
                + 255 * blue
                + 120 * pink
            )

            pixels[index] = (
                clamp_color(red),
                clamp_color(green),
                clamp_color(blue_channel)
            )

        # Create accent streaks that remain visible for multiple
        # frames instead of disappearing immediately.
        if current_time >= next_streak_time:
            streaks.append(
                {
                    "start": random.randrange(NUM_LEDS),
                    "length": random.randint(4, 8),
                    "color": random.choice(
                        [
                            (255, 220, 40),
                            (255, 100, 30),
                            (255, 40, 100)
                        ]
                    ),
                    "created": current_time,
                    "lifetime": random.uniform(0.5, 1.2)
                }
            )

            next_streak_time = (
                current_time + random.uniform(0.20, 0.65)
            )

        active_streaks = []

        for streak in streaks:
            age = current_time - streak["created"]
            lifetime = streak["lifetime"]

            if age >= lifetime:
                continue

            active_streaks.append(streak)

            life_brightness = 1 - (age / lifetime)

            for offset in range(streak["length"]):
                position_brightness = (
                    1 - offset / streak["length"]
                )

                brightness = (
                    life_brightness * position_brightness
                )

                index = (
                    streak["start"] + offset
                ) % NUM_LEDS

                accent = streak["color"]

                pixels[index] = (
                    clamp_color(accent[0] * brightness),
                    clamp_color(accent[1] * brightness),
                    clamp_color(accent[2] * brightness)
                )

        streaks = active_streaks

        if random.random() < 0.08:
            pixels[random.randrange(NUM_LEDS)] = (
                255,
                255,
                255
            )

        pixels.show()

        if animation_sleep(stop_event, speed):
            break


def gradient_spin(stop_event=None, speed=1.4):
    """
    Blue and purple thunderstorm animation with lightning flashes.

    This mode is displayed as 'Thunderstorm' in the LCD menu.
    """

    lightning_until = 0

    while not should_stop(stop_event):
        current_time = time.monotonic()

        for index in range(NUM_LEDS):
            wave = (
                math.sin(
                    index * 0.45
                    + current_time * speed
                ) + 1
            ) / 2

            secondary_wave = (
                math.sin(
                    index * 0.8
                    - current_time * 0.7
                ) + 1
            ) / 2

            red = 40 + 90 * wave
            green = 10 + 35 * secondary_wave
            blue = 130 + 125 * wave

            pixels[index] = (
                clamp_color(red),
                clamp_color(green),
                clamp_color(blue)
            )

        # Occasionally begin a short lightning flash.
        if (
            current_time >= lightning_until
            and random.random() < 0.025
        ):
            lightning_until = current_time + random.uniform(
                0.04,
                0.12
            )

        if current_time < lightning_until:
            flash_start = random.randrange(NUM_LEDS)
            flash_length = random.randint(2, 6)

            for offset in range(flash_length):
                index = (
                    flash_start + offset
                ) % NUM_LEDS

                pixels[index] = (
                    255,
                    255,
                    255
                )

        pixels.show()

        if animation_sleep(stop_event, FRAME_DELAY):
            break


def rainbow_cycle(stop_event=None, speed=0.03):
    """
    Continuously rotating rainbow animation.
    """

    offset = 0

    while not should_stop(stop_event):
        for index in range(NUM_LEDS):
            hue = (
                index * 256 // NUM_LEDS + offset
            ) % 256

            pixels[index] = wheel(hue)

        pixels.show()

        offset = (offset + 1) % 256

        if animation_sleep(stop_event, speed):
            break


def breathe(
    stop_event=None,
    color=(0, 0, 255),
    speed=1.5
):
    """
    Smooth breathing animation.

    Args:
        color:
            Main RGB color.

        speed:
            Approximate breathing-wave speed.
    """

    while not should_stop(stop_event):
        current_time = time.monotonic()

        brightness = (
            math.sin(current_time * speed) + 1
        ) / 2

        # Keep a small minimum brightness.
        brightness = 0.04 + brightness * 0.96

        fade_color(color, brightness)

        if animation_sleep(stop_event, FRAME_DELAY):
            break


def chase(
    stop_event=None,
    color=(255, 0, 0),
    speed=0.08
):
    """
    Moving colored light with a fading tail.
    """

    position = 0

    while not should_stop(stop_event):
        pixels.fill((0, 0, 0))

        tail_length = 5

        for tail_index in range(tail_length):
            led_index = (
                position - tail_index
            ) % NUM_LEDS

            brightness = 1 - (
                tail_index / tail_length
            )

            pixels[led_index] = (
                clamp_color(color[0] * brightness),
                clamp_color(color[1] * brightness),
                clamp_color(color[2] * brightness)
            )

        pixels.show()

        position = (position + 1) % NUM_LEDS

        if animation_sleep(stop_event, speed):
            break


def sparkle(stop_event=None, speed=0.08):
    """
    Random white and blue sparkling animation.
    """

    brightness_levels = [0.0] * NUM_LEDS

    while not should_stop(stop_event):
        # Slowly fade existing sparkles.
        for index in range(NUM_LEDS):
            brightness_levels[index] *= 0.65

        # Create new sparkles.
        for _ in range(random.randint(1, 3)):
            index = random.randrange(NUM_LEDS)
            brightness_levels[index] = random.uniform(
                0.65,
                1.0
            )

        for index, brightness in enumerate(
            brightness_levels
        ):
            pixels[index] = (
                clamp_color(180 * brightness),
                clamp_color(220 * brightness),
                clamp_color(255 * brightness)
            )

        pixels.show()

        if animation_sleep(stop_event, speed):
            break


# ============================================================
# AVAILABLE MODES
# ============================================================

MODE_FUNCTIONS = {
    "Ocean": ocean_wave,
    "Aurora": aurora,
    "Peppermint": peppermint,
    "Lava": lava,
    "Candle": candle,
    "Nebula": nebula,
    "Thunderstorm": gradient_spin,
    "Rainbow": rainbow_cycle,
    "Breathe": breathe,
    "Chase": chase,
    "Sparkle": sparkle
}

MODE_NAMES = list(MODE_FUNCTIONS.keys())


# ============================================================
# MODE CONTROL
# ============================================================

def run_mode(mode_name, stop_event=None):
    """
    Run one animation until stop_event is set.

    Args:
        mode_name:
            Name from MODE_FUNCTIONS.

        stop_event:
            threading.Event used by main.py to stop the animation.

    Example:
        stop_event = Event()
        run_mode("Nebula", stop_event)
    """

    if mode_name not in MODE_FUNCTIONS:
        available = ", ".join(MODE_NAMES)

        raise ValueError(
            f"Unknown LED mode '{mode_name}'. "
            f"Available modes: {available}"
        )

    if stop_event is None:
        stop_event = Event()

    animation = MODE_FUNCTIONS[mode_name]

    print(f"Starting LED mode: {mode_name}")

    try:
        animation(stop_event)

    finally:
        off()
        print(f"Stopped LED mode: {mode_name}")


def cleanup():
    """
    Turn off the LEDs and release the NeoPixel resource.
    """

    try:
        off()
    except Exception:
        pass

    try:
        pixels.deinit()
    except Exception:
        pass


# ============================================================
# STANDALONE TEST
# ============================================================

if __name__ == "__main__":
    print("LED controller test starting...")
    print("Running Nebula. Press Ctrl+C to stop.")

    test_stop_event = Event()

    try:
        run_mode(
            mode_name="Nebula",
            stop_event=test_stop_event
        )

    except KeyboardInterrupt:
        print("\nLED test stopped")
        test_stop_event.set()

    finally:
        cleanup()