from machine import Pin, PWM, SPI, SoftI2C
import neopixel
import time
import _thread
#import cst816
#import gc9a01

# Configuration
BUTTON_PIN = 14  # Button pin, make sure it's suitable for input
LED_PIN = 33  # LED strip pin, ensure this pin supports digital output
LED_COUNT = 16  # Number of LEDs in the strip
BUZZER_PIN = 1  # Buzzer pin, adjust if necessary and ensure it's suitable for output
PLAYERS_CLK_PIN = 23 # Players clockwise rotation encoder pin
PLAYERS_DT_PIN = 22 # Players counterclockwise rotation encoder pin
SPEED_CLK_PIN = 19 # Speed clockwise rotation encoder pin
SPEED_DT_PIN = 18 # Speed counterclockwise rotation encoder pin

class PatchedI2C(SoftI2C):
    def try_lock(self):
        return True

    def unlock(self):
        pass

    def writeto_then_readfrom(self, address, write_buffer, read_buffer, out_start=0, out_end=None, in_start=0, in_end=None):
        if out_end is None:
            out_end = len(write_buffer)
        if in_end is None:
            in_end = len(read_buffer)
        self.writeto(address, write_buffer[out_start:out_end])
        read_data = self.readfrom(address, in_end - in_start)
        read_buffer[in_start:in_end] = read_data

    def read(self, nbytes):
        return self.readfrom(self.addr, nbytes, stop=True)
# State definitions
ANIMATION, IDLE, SHOW_COLOR, RUNNING, PAUSED, FINISHED, PULSING = range(7)
state = SHOW_COLOR  # Initial state

previous_millis = 0  # For timing
interval = 1000  # Interval for actions, in milliseconds
speeds = [625, 1250, 2500, 12500] # 15s, 30s, 60s, 300s
current_speed = 1 # Current intervals index, startup with 30s
current_player = 0  # Current player (if applicable)
led_index = LED_COUNT - 1  # For iterating LEDs, start from the last LED for anti-clockwise countdown
player_colors = [(255, 0, 255), (255, 255, 0), (0, 255, 255), (192, 0, 0), (255, 255, 255), (0, 255, 0)]  # Player colors
pulse_count = 0  # For pulsing effect
pulse_is_on = False
last_pulse_time = 0
amount_of_players = 6
min_players = 1
brightness = 1

button_pressed = False
button_press_start_time = 0
skip_player_pressed = False
animation_running = False
music_running = False
breathing_running = False
players_clk_value = 1 # 1 = not changed, 0 = changed
speed_clk_value = 1 # 1 = not changed, 0 = changed

# Initialize components
np = neopixel.NeoPixel(Pin(LED_PIN), LED_COUNT)
button = Pin(BUTTON_PIN, Pin.IN, Pin.PULL_UP)
buzzer = PWM(Pin(BUZZER_PIN))
players_clk = Pin(PLAYERS_CLK_PIN, Pin.IN, Pin.PULL_UP)
players_dt = Pin(PLAYERS_DT_PIN, Pin.IN, Pin.PULL_UP)
speed_clk = Pin(SPEED_CLK_PIN, Pin.IN, Pin.PULL_UP)
speed_dt = Pin(SPEED_DT_PIN, Pin.IN, Pin.PULL_UP)

# Initialize I2C for touch sensor
#i2c = PatchedI2C(scl=Pin(7), sda=Pin(6))
#touch_sensor = cst816.CST816(i2c)

# Initialize SPI for the display
#spi = SPI(2, baudrate=60000000, sck=Pin(10), mosi=Pin(11))
#display = gc9a01.GC9A01(
#    spi,
#    cs=Pin(9),
#    dc=Pin(8),
#    reset=Pin(14),
#    width=240,
#    height=240
#)

notes = {
    'E3': 82 * 4,  # E2 4 times higher
    'B3': 123 * 4, # B2 4 times higher
    'D4': 147 * 4, # D3 4 times higher
    'E4': 165 * 4, # E3 4 times higher
    'G4': 196 * 4, # G3 4 times higher
    'A4': 220 * 4, # A3 4 times higher
}

melody = [
    ('E3', 200), ('B3', 200), ('D4', 200), ('E4', 200),
    ('G4', 400),
    ('E3', 200), ('B3', 200), ('D4', 200), ('E4', 200),
    ('A4', 400),
]

# Function definitions for game logic, sound, LED control, etc.

def set_all_leds_color(color):
    adjusted_color = (int(color[0] * brightness), int(color[1] * brightness), int(color[2] * brightness))
    for i in range(LED_COUNT):
        np[i] = adjusted_color
    np.write()

def show_current_player_color():
    global state, led_index
    set_all_leds_color(player_colors[current_player])
    led_index = LED_COUNT - 1
    state = SHOW_COLOR

def start_timer():
    global led_index, previous_millis, state
    set_all_leds_color((0, 255, 0)) # Set all LEDs to green for the countdown
    led_index = LED_COUNT - 1
    previous_millis = time.ticks_ms()
    state = RUNNING

def pause_timer():
    global state
    state = PAUSED

def resume_timer():
    global state, previous_millis
    state = RUNNING
    previous_millis = time.ticks_ms() # Reset timer to maintain correct timing

def check_touch():
    global state, current_player, button_press_start_time, skip_player_pressed

    touch_detected = touch_sensor.get_touch()
    if touch_detected:
        if button_press_start_time == 0:
            button_press_start_time = time.ticks_ms()  # Record the time when touch starts
            skip_player_pressed = False

        # Check if the touch has been held for more than 1.5 seconds
        if not skip_player_pressed and time.ticks_ms() - button_press_start_time >= 1500:
            skip_player_pressed = True
            next_player()
            #set_display_color(player_colors[current_player])

    else:
        if button_press_start_time > 0 and not skip_player_pressed:
            touch_duration = time.ticks_ms() - button_press_start_time
            # Short touch detected, mimic the button functionality for Start/Pause/Resume
            if touch_duration >= 50:
                if state == SHOW_COLOR:
                    start_timer()
                elif state == RUNNING:
                    pause_timer()
                elif state == PAUSED:
                    resume_timer()
                elif state == IDLE:
                    show_current_player_color()

        # Reset the touch timer after processing the touch
        button_press_start_time = 0

def beep():
    play_tone('E4', 100)

def beepOn():
    buzzer.freq(660)  # Set frequency for the note
    buzzer.duty(512)  # 50% duty cycle to generate sound
    time.sleep(0.05)

def beepOff():
    buzzer.duty(0)

def play_tone(note, duration):
    if note in notes:
        buzzer.freq(notes[note])  # Set frequency for the note
        buzzer.duty(512)  # 50% duty cycle to generate sound
    else:
        buzzer.duty(0)  # No sound for rests
    time.sleep_ms(duration)
    buzzer.duty(0)  # Turn off between notes

def play_melody():
    start_time = time.ticks_ms()
    #while time.ticks_diff(time.ticks_ms(), start_time) < 7000:  # Loop for 7 seconds
    while animation_running: 
        for note, duration in melody:
            if animation_running:
                play_tone(note, duration)
                # Adjust the timing based on the song's rhythm
                time.sleep_ms(30)

def music_thread():
    global music_running
    music_running = True
    # Play the boot-up sound
    play_melody()

    music_running = False

def breathing_effect():
    global breathing_running
    breathing_running = True
    width, height = 240, 240  # Assuming a square display for simplicity
    center_x, center_y = width // 2, height // 2
    max_radius = min(center_x, center_y)
    selected_colors = [(255, 0, 255), (255, 255, 0), (0, 255, 255), (255, 255, 255)]  # The colors used in the animation
    for color in selected_colors:
        for t in range(0, 100, 5):  # Gradual intensity change for breathing effect
            intensity = abs(100 - t * 2) / 100.0  # Create a breathing effect
            adjusted_color = tuple(int(c * intensity) for c in color)
            color_16_bit = ((adjusted_color[0] & 0xF8) << 8) | ((adjusted_color[1] & 0xFC) << 3) | (adjusted_color[2] >> 3)

            radius = int(max_radius * (t / 100.0))
            display.circle(center_x, center_y, radius, color_16_bit)  # Draw a filled circle from center
            
            time.sleep(0.05)
    breathing_running = False

def start_animation():
    global music_running, animation_running, breathing_running
    animation_running = True
    
    thread2 = _thread.start_new_thread(music_thread, ())
    #thread3 = _thread.start_new_thread(breathing_effect, ())

    selected_colors = [(255, 0, 255), (255, 255, 0), (0, 255, 255), (255, 255, 255)]  # The colors used in the animation

    for color in selected_colors:      
        # Light up LEDs one by one
        for j in range(LED_COUNT):
            adjusted_color = (int(color[0] * brightness), int(color[1] * brightness), int(color[2] * brightness))
            np[j] = adjusted_color
            np.write()
            time.sleep(0.03)  # Delay between lighting up each LED
        
        time.sleep(0.3)  # Pause when all LEDs are lit
        
        # Turn off LEDs one by one
        for j in range(LED_COUNT):
            np[j] = (0, 0, 0)  # Turn off the LED at position j
            np.write()
            time.sleep(0.03)  # Delay between turning off each LED

    animation_running = False
    while music_running or breathing_running:
        time.sleep(0.1)  # Wait until startup music is finished
    
    show_current_player_color()  # Initialize the first player's color after the animation

def update_timer():
    global led_index, state, pulse_count, previous_millis
    if led_index > 0:
        np[led_index] = (0, 0, 0)  # Turn off the current LED
        led_index -= 1  # Move to the next LED for the next update
        previous_millis = time.ticks_ms()  # Reset the timer for the next update
        for i in range(led_index + 1):
            if led_index > LED_COUNT / 2:
                np[i] = (int(0 * brightness), int(255 * brightness), int(0 * brightness))  # Green at 5% brightness
            elif led_index > LED_COUNT / 4:
                np[i] = (int(255 * brightness), int(140 * brightness), int(0 * brightness))  # Orange at 5% brightness
            else:
                np[i] = (int(255 * brightness), int(0 * brightness), int(0 * brightness))  # Red at 5% brightness
        np.write()
        if (led_index < 3):
            beep()
    else:
        state = PULSING
        previous_millis = time.ticks_ms()

def next_player():
    global current_player
    current_player = (current_player + 1) % amount_of_players  # Cycle to the next player
    show_current_player_color()  # Initialize the next player turn
    #set_display_color(player_colors[current_player])  # Update the display to show the current player color


def pulse_last_led():
    global pulse_count, state, pulse_is_on, last_pulse_time
    if time.ticks_ms() - last_pulse_time > 100 and pulse_count < 20: # Faster pulse rate for the last LED, limit to 20 pulses
        pulse_is_on = not pulse_is_on
        # Adjust color for 5% brightness
        adjusted_color = (int(255 * brightness), int(0 * brightness), int(0 * brightness))
        np[0] = adjusted_color if pulse_is_on else (0, 0, 0) # Toggle the last LED
        np.write()
        last_pulse_time = time.ticks_ms()
        pulse_count += 1
        beepOn()
        if pulse_count == 20:
            np[0] = (0, 0, 0) # Ensure the last LED is off after pulsing
            np.write()
            beepOff()
            pulse_count = 0 # Reset pulse count for new cycle
            pulse_is_on = False
            last_pulse_time = 0
            state = FINISHED # Mark the cycle as finished

def increase_speed():
    global current_speed, interval
    total_speeds = len(speeds) - 1
    if current_speed < total_speeds:
        current_speed += 1
        interval = speeds[current_speed]
        current_player = 0
        if state != IDLE and state != ANIMATION:
            show_current_player_color()

def decrease_speed():
    global current_speed, interval
    total_speeds = len(speeds) - 1
    if current_speed > 0:
        current_speed -= 1
        interval = speeds[current_speed]
        current_player = 0
        if state != IDLE and state != ANIMATION:
            show_current_player_color()

def update_speed():
    global interval, speed_clk_value

    new_speed_clk_value = speed_clk.value()
    if (speed_clk_value != new_speed_clk_value):
        speed_clk_value = new_speed_clk_value

        speed_dt_value = speed_dt.value()
        if not speed_clk_value and speed_dt_value: #Clockwise rotation
            time.sleep(0.5)
            increase_speed()
        elif not speed_clk_value and not speed_dt_value: #Counterclockwise rotation
            time.sleep(0.5)
            decrease_speed()

def increase_players():
    global current_player, amount_of_players
    max_players = len(player_colors)
    if amount_of_players < max_players:
        amount_of_players += 1
        current_player = 0
        if state != IDLE and state != ANIMATION:
            show_current_player_color()

def decrease_players():
    global current_player, amount_of_players
    if min_players < amount_of_players:
        amount_of_players -= 1
        current_player = 0
        if state != IDLE and state != ANIMATION:
            show_current_player_color()

def update_number_of_players():
    global players_clk_value
    
    new_players_clk_value = players_clk.value()
    if (players_clk_value != new_players_clk_value):
        players_clk_value = new_players_clk_value

        dt_value = players_dt.value()
        if not players_clk_value and dt_value: #Clockwise rotation
            time.sleep(0.5)
            increase_players()
        elif not players_clk_value and not dt_value: #Counterclockwise rotation
            time.sleep(0.5)
            decrease_players()

def set_display_color(rgb):
    # Unpack the RGB tuple into individual components
    r, g, b = rgb

    # Convert the RGB values to a 16-bit format for the display
    color_16_bit = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)

    # Set the display color using the 16-bit color value
    # Assuming 'display' is an instance of your display driver
    display.fill(color_16_bit)

def check_touch():
    global state, current_player, button_press_start_time, skip_player_pressed

    touch_detected = touch_sensor.get_touch()
    if touch_detected:
        if button_press_start_time == 0:
            button_press_start_time = time.ticks_ms()  # Record the time when touch starts
            skip_player_pressed = False

        # Check if the touch has been held for more than 1.5 seconds
        if not skip_player_pressed and time.ticks_ms() - button_press_start_time >= 1500:
            skip_player_pressed = True
            next_player()
            #set_display_color(player_colors[current_player])

    else:
        if button_press_start_time > 0 and not skip_player_pressed:
            touch_duration = time.ticks_ms() - button_press_start_time
            # Short touch detected, mimic the button functionality for Start/Pause/Resume
            if touch_duration >= 50:
                if state == SHOW_COLOR:
                    start_timer()
                elif state == RUNNING:
                    pause_timer()
                elif state == PAUSED:
                    resume_timer()
                elif state == IDLE:
                    show_current_player_color()

        # Reset the touch timer after processing the touch
        button_press_start_time = 0

def check_button():
    global button_pressed, button_press_start_time, skip_player_pressed
    current_button_state = not button.value()
    if current_button_state and not button_pressed: # Detect button pressed
        time.sleep(0.05) # Debounce
        button_press_start_time = time.ticks_ms()
        button_pressed = True
        skip_player_pressed = False
    elif not current_button_state and button_pressed: # Detect button released
        time.sleep(0.05) # Debounce
        button_pressed = False
        if not skip_player_pressed:
            if state == SHOW_COLOR:
                start_timer()
            elif state == RUNNING:
                pause_timer()
            elif state == PAUSED:
                resume_timer()
            elif state == IDLE:
                show_current_player_color()
    elif current_button_state and button_pressed and not skip_player_pressed: # Button is still being pressed
            time.sleep(0.05) # Debounce
            if time.ticks_ms() - button_press_start_time >= 1500:  # Button held for 2 seconds
                skip_player_pressed = True  # Mark that we've triggered a skip
                next_player()  # Skip to the next player

def setup_display():
    global display, backlight
    spi = SPI(2, baudrate=60000000, polarity=0, phase=0, sck=Pin(10), mosi=Pin(11))
    reset_pin = Pin(14, Pin.OUT)
    cs_pin = Pin(9, Pin.OUT)
    dc_pin = Pin(8, Pin.OUT)
    backlight_pin = Pin(2, Pin.OUT)

    reset_pin.value(0)
    time.sleep(0.1)
    reset_pin.value(1)
    time.sleep(0.1)

    display = gc9a01.GC9A01(
        spi,
        reset=reset_pin,
        cs=cs_pin,
        dc=dc_pin,
        backlight=backlight_pin,
        width=240,
        height=240
    )
    
    display.init()
    display.fill(gc9a01.BLACK)
    backlight_pin.value(1)

def setup():
    update_number_of_players()
    update_speed()
    #setup_display()
    start_animation()
    #set_display_color(player_colors[current_player])

def loop():
    #check_touch()
    check_button()
    update_number_of_players()
    update_speed()
    if state == RUNNING and time.ticks_ms() - previous_millis >= interval:
        update_timer()
    if state == PULSING:
        pulse_last_led()
    if state == FINISHED:
        next_player()

def main():
    setup()
    while True:
        loop()

if __name__ == "__main__":
    main()