# NeoPixel animations with constant total brightness.
# Made for Circuit Playground Bluefruit
# Follow this guide to setup the board - https://learn.adafruit.com/adafruit-circuit-playground-bluefruit
#
# Can be controlled from Bluefruit LE Connect application - https://learn.adafruit.com/bluefruit-le-connect/ios-setup
# In the app:
# 1. Connect to CIRCUITPY device
# 2. In menu Controller => Control Pad:
#    - Button 1 - set animation type "loop classic"
#    - Button 2 - set animation type "loop chase" (it's set by default)
#    - Button 3 - set animation type "scanner"
#    - Button 4 - set animation type "fire"
#    - Button Up - increase animation speed
#    - Button Down - slow down animation speed
#    - Button Right - increase speed of changing colors (default is 1 - loop takes about 2 minutes, max 10 - loop takes 1 sec)
#    - Button Left - slow down speed of changing colors
# 3. In menu Controller => Color Picker:
#    - Select a color and send it - will set the current color (speed of changing colors automatically set to 0)

import board
import neopixel
import time
import math
from adafruit_bluefruit_connect.packet import Packet
from adafruit_bluefruit_connect.color_packet import ColorPacket
from adafruit_bluefruit_connect.button_packet import ButtonPacket

# Other packets are not used, we import them to avoid interruption caused by "unrecognized packet" if the phone sends them.
from adafruit_bluefruit_connect.accelerometer_packet import AccelerometerPacket
from adafruit_bluefruit_connect.gyro_packet import GyroPacket
from adafruit_bluefruit_connect.location_packet import LocationPacket
from adafruit_bluefruit_connect.magnetometer_packet import MagnetometerPacket
from adafruit_bluefruit_connect.quaternion_packet import QuaternionPacket

from adafruit_ble import BLERadio
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
from adafruit_ble.services.nordic import UARTService

ble = BLERadio()
uart_service = UARTService()
advertisement = ProvideServicesAdvertisement(uart_service)

num_pixels = 10
pixels = neopixel.NeoPixel(board.NEOPIXEL, num_pixels, brightness=1, auto_write=False)

num_frames = 6000
min_num_frames = num_frames // 10
max_num_frames = num_frames * 4
frames_per_second = 60
frame_duration = 1 / frames_per_second
current_frame_index = 0

color_change_progress = 0
color_change_step = 1 / (120 * frames_per_second)
color_change_speed = 1

selected_color = (255, 41, 0)
# selected_color = (255, 24, 52)
# selected_color = (255, 41, 0)
current_color = selected_color

activation_values = []
for i in range(num_pixels):
    activation_values.append(0)

ANIMATION_LOOP_CLASSIC = 1
ANIMATION_LOOP_CHASE = 2
ANIMATION_FIRE = 3
ANIMATION_SCANNER = 4

# =============
# Helper functions.
# =============
def my_colorwheel(pos):
    # Input value 0 to 255, output is tuple = (red, green, blue).
    if pos < 0 or pos > 255:
        r = g = b = 0
    elif pos < 70:
        r = int(pos * 3)
        g = int(255 - pos * 3)
        b = 0
    elif pos < 100:
        # This part adds Green and Blue, because it's too dark when Red is alone.
        if pos < 85:
            r = int(pos * 3)
            pos -= 70
            g = int(45 - pos * 2)
            b = 0
        else:
            pos -= 85
            r = int(255 - pos * 3)
            g = int(15 - pos)
            b = int(pos * 3)
    elif pos < 170:
        pos -= 85
        r = int(255 - pos * 3)
        g = 0
        b = int(pos * 3)
    else:
        pos -= 170
        r = 0
        g = int(pos * 3)
        b = int(255 - pos * 3)
    return (r, g, b)

def apply_activation_values(values):
    for i in range(num_pixels):
        current_pixel = (0, 0, 0)
        activation = values[i]
        r = sum_component(current_pixel[0], current_color[0] * activation)
        g = sum_component(current_pixel[1], current_color[1] * activation)
        b = sum_component(current_pixel[2], current_color[2] * activation)
        new_pixel = (r, g, b)
        pixels[i] = new_pixel

def create_animation(type):
    if type == ANIMATION_LOOP_CLASSIC:
        order = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
        computer = AnimationComputer(order, num_pixels, activation=ActivationQuadratic(), activation_length=5.2, brightness=1.2, progress_multiplier=100)
        return AnimationMixer([computer])
    elif type == ANIMATION_LOOP_CHASE:
        order1 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
        computer1 = AnimationComputer(order1, num_pixels, activation=ActivationQuadratic(), activation_length=6, brightness=0.6, progress_multiplier=62)
        order2 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
        computer2 = AnimationComputer(order2, num_pixels, activation=ActivationLinear(), activation_length=3.2, brightness=0.59, progress_multiplier=100)
        return AnimationMixer([computer1, computer2])
    elif type == ANIMATION_SCANNER:
        mult = 100
        loop1 = [0, 1, 2, 3, 4, 3, 2, 1]
        tail1 = AnimationComputer(loop1, num_pixels, activation=ActivationQuadratic(), activation_length=6, brightness=0.3, progress_multiplier=mult)
        head1 = AnimationComputer(loop1, num_pixels, activation=ActivationConstant(), activation_length=1.2, brightness=0.6, advance=4.8, progress_multiplier=mult)

        loop2 = [5, 6, 7, 8, 9, 8, 7, 6]
        tail2 = AnimationComputer(loop2, num_pixels, activation=ActivationQuadratic(), activation_length=6, brightness=0.3, progress_multiplier=mult)
        head2 = AnimationComputer(loop2, num_pixels, activation=ActivationConstant(), activation_length=1.2, brightness=0.6, advance=4.8, progress_multiplier=mult)

        return AnimationMixer([tail1, head1, tail2, head2])
    elif type == ANIMATION_FIRE:
        # Fire with constant total brightness.
        loop1 = [0, 9, 1, 8, 2, 7, 3, 6, 4, 5]
        flames1 = AnimationComputer(loop1, num_pixels, activation=ActivationQuadratic(), activation_length=5, brightness=0.55, progress_multiplier=120)
        loop2 = [3, 2, 4, 1, 5, 0, 6, 9, 7, 8]
        flames2 = AnimationComputer(loop2, num_pixels, activation=ActivationQuadratic(), activation_length=7, brightness=0.6, progress_multiplier=78)
        return AnimationMixer([flames1, flames2])

        # Fire with jumpy total brightness.
#         loop1 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
#         quick_wave = AnimationComputer(loop1, num_pixels, activation=ActivationCosinusKxPlusB(k=6*math.pi, b=math.pi, middle=0.2), activation_length=10, brightness=0.5, progress_multiplier=100)
#         loop2 = [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
#         slow_wave = AnimationComputer(loop2, num_pixels, activation=ActivationQuadratic(), activation_length=5, brightness=0.6, progress_multiplier=78)
#         return AnimationMixer([quick_wave, slow_wave])
    else:
        return None

def create_indexes_all_pixels(num):
    array = []
    for i in range(num):
        array.append(i)
    return array

def sum_component(a, b):
    result = a + b
    if result < 0:
#         print("overflow minus", result)
        result = 0
    elif result > 255:
        print("overflow plus", result)
        result = 255
    return int(result)

def relative_position_in_range(value, my_range):
    if value < my_range[0]:
        return 0
    if value > my_range[1]:
        return 1
    length = my_range[1] - my_range[0]
    return (value - my_range[0]) / length

class ActivationBase(object):
    def function(self, x):
        return 0
    def antiderivative_function(self, x):
        return 0

class ActivationConstant(ActivationBase):
    def function(self, x):
        return 1
    def antiderivative_function(self, x):
        return x

class ActivationLinear(ActivationBase):
    def function(self, x):
        return x
    def antiderivative_function(self, x):
        return x * x / 2

class ActivationQuadratic(ActivationBase):
    def function(self, x):
        return x * x
    def antiderivative_function(self, x):
        return x * x * x / 3

class ActivationCosinusKxPlusB(ActivationBase):
    def __init__(self, k = 1, b = 0, middle=0.5):
        self.k = k
        self.b = b
        self.middle = middle
    def function(self, x):
        return self.middle + 0.5 * math.cos(self.k * x + self.b)
    def antiderivative_function(self, x):
        return self.middle * x + math.sin(self.k * x + self.b) / (2 * self.k)

class AnimationComputer(object):
    def __init__(self, activation_indexes, num_output_values, activation = ActivationConstant(), brightness = 1, activation_length = 3.5, advance = 0, progress_multiplier=1):
        self.activation_indexes = activation_indexes
        self.activation = activation
        self.brightness = brightness
        self.activation_length = activation_length
        self.activation_offset = advance
        self.num_output_values = num_output_values
        self.progress_multiplier = progress_multiplier

        num_activation_indexes = len(self.activation_indexes)
        self.relative_activation_length = self.activation_length / num_activation_indexes
        self.relative_activation_offset = self.activation_offset / num_activation_indexes
        self.limit_step = 1 / activation_length

        self.antiderivative_function_at_0 = activation.antiderivative_function(0)
        self.antiderivative_function_at_1 = activation.antiderivative_function(1)

    def compute_activation_values(self, progress, result_values):
        num_activation_indexes = len(self.activation_indexes)
        relative_start = self.progress_multiplier * progress + self.relative_activation_offset
        relative_activation_range = (relative_start, relative_start + self.relative_activation_length)
        first_activation_index = int(relative_start * num_activation_indexes)

        relative_pixel_length = 1 / num_activation_indexes
        relative_first_pixel_end = (first_activation_index + 1) / num_activation_indexes
        first_pixel_high_limit = relative_position_in_range(relative_first_pixel_end, relative_activation_range)

        antiderivative_function_values = [self.antiderivative_function_at_0]
        current_high_limit = first_pixel_high_limit
        while current_high_limit < 1:
            antiderivative_function_values.append(self.activation.antiderivative_function(current_high_limit))
            current_high_limit += self.limit_step
        antiderivative_function_values.append(self.antiderivative_function_at_1)

        for i in range(len(antiderivative_function_values) - 1):
            filled_square = antiderivative_function_values[i + 1] - antiderivative_function_values[i]
            activation = self.brightness * filled_square * self.activation_length

            destination_index = self.activation_indexes[(first_activation_index + i) % num_activation_indexes]
            new_value = result_values[destination_index] + activation
            result_values[destination_index] = new_value

class AnimationMixer(object):
    def __init__(self, animators):
        self.animators = animators

    def compute_activation_values(self, progress, cumulative_values):
        for animator in self.animators:
            animator.compute_activation_values(progress, cumulative_values)

def move_to_next_frame():
    global current_frame_index
    current_frame_index += 1
    if current_frame_index >= num_frames:
        current_frame_index = 0

def process_current_frame():
    global activation_values
    global current_color
    global color_change_progress

    if color_change_speed > 0:
        color_index = int(255 * color_change_progress)
        current_color = my_colorwheel(color_index)
        color_change_progress += color_change_speed * color_change_speed * color_change_step
        if color_change_progress > 1:
            color_change_progress -= 1

    if num_frames < min_num_frames:
        pixels.fill(current_color)
    else:
        for i in range(num_pixels):
            activation_values[i] = 0

        progress = current_frame_index / num_frames
        mixer.compute_activation_values(progress, activation_values)
        apply_activation_values(activation_values)
    pixels.show()
    time.sleep(frame_duration)
    move_to_next_frame()

# =============
# Main program.
# =============

mixer = create_animation(ANIMATION_LOOP_CHASE)
while True:
    # Advertise when not connected.
    ble.start_advertising(advertisement)
    while not ble.connected:
        process_current_frame()
    ble.stop_advertising()

    while ble.connected:
        if uart_service.in_waiting:
            packet = Packet.from_stream(uart_service)
            if isinstance(packet, ColorPacket):
                print("New color: ", packet.color)
                current_color = packet.color
                color_change_speed = 0
            elif isinstance(packet, ButtonPacket):
                if packet.pressed:
                    current_progress = current_frame_index / num_frames
                    percent = 0.25
                    delta = int(percent * num_frames)
                    if packet.button == ButtonPacket.UP:
                        print("Button UP pressed, increasing speed")
                        if num_frames - delta >= min_num_frames:
                            num_frames -= delta
                            current_frame_index = int(current_frame_index * (1 - percent))
                    elif packet.button == ButtonPacket.DOWN:
                        print("Button DOWN pressed, decreasing speed")
                        if num_frames + delta <= max_num_frames:
                            num_frames += delta
                            current_frame_index = int(current_frame_index * (1 + percent))
                    elif packet.button == ButtonPacket.LEFT:
                        if color_change_speed > 0:
                            color_change_speed -= 1
                    elif packet.button == ButtonPacket.RIGHT:
                        if color_change_speed < 10:
                            color_change_speed += 1
                    elif packet.button == ButtonPacket.BUTTON_1:
                        mixer = create_animation(ANIMATION_LOOP_CLASSIC)
                    elif packet.button == ButtonPacket.BUTTON_2:
                        mixer = create_animation(ANIMATION_LOOP_CHASE)
                    elif packet.button == ButtonPacket.BUTTON_3:
                        mixer = create_animation(ANIMATION_SCANNER)
                    elif packet.button == ButtonPacket.BUTTON_4:
                        mixer = create_animation(ANIMATION_FIRE)
            else:
                print("Unexpected packet: ", packet)
        process_current_frame()
