import math
import time

from encoderwheel import CENTRE, NUM_LEDS, EncoderWheel

print("""
Display a circular stop-watch on the Encoder Wheel's LED ring.

Press the centre button to start the stopwatch, then again to pause and resume.

Press Ctrl+C to stop the program.
""")

# Constants
BRIGHTNESS = 1.0                    # The brightness of the LEDs when the stopwatch is running
UPDATES = 50                        # How many times the LEDs will be updated per second
MINUTE_UPDATES = UPDATES * 60       # How many times the LEDs will be updated per minute
HOUR_UPDATES = MINUTE_UPDATES * 60  # How many times the LEDs will be updated per hour
UPDATE_RATE = 1 / UPDATES

IDLE_PULSE_MIN = 0.2                # The brightness (between 0.0 and 1.0) that the idle pulse will go down to
IDLE_PULSE_MAX = 0.5                # The brightness (between 0.0 and 1.0) that the idle pulse will go up to
IDLE_PULSE_TIME = 2                 # The time (in seconds) to perform a complete idle pulse
UPDATES_PER_PULSE = IDLE_PULSE_TIME * UPDATES

IDLE, COUNTING, PAUSED = range(3)   # The state constants used for program flow

# Create a new EncoderWheel
wheel = EncoderWheel()

# Variables
state = IDLE
idle_update = 0
second_update = 0
minute_update = 0
hour_update = 0
last_centre_pressed = False


# Simple function to clamp a value between a minimum and maximum
def clamp(n, smallest, largest):
    return max(smallest, min(n, largest))


# Sleep until a specific time in the future. Use this instead of time.sleep() to correct for
# inconsistent timings when dealing with complex operations or external communication
def sleep_until(end_time):
    time_to_sleep = end_time - time.monotonic()
    if time_to_sleep > 0.0:
        time.sleep(time_to_sleep)


# Record the current time
current_time = time.monotonic()

# Run the update loop forever
while True:

    # Read whether or not the wheen centre has been pressed
    centre_pressed = wheel.pressed(CENTRE)
    if centre_pressed and centre_pressed != last_centre_pressed:
        if state == IDLE:           # If we're currently idle, switch to counting
            second_update = 0
            minute_update = 0
            hour_update = 0
            state = COUNTING

        elif state == COUNTING:     # If we're counting, switch to paused
            state = PAUSED

        elif state == PAUSED:       # If we're paused, switch back to counting
            state = COUNTING
    last_centre_pressed = centre_pressed

    # If we're idle, perform a pulsing animation to show the stopwatch is ready to go
    if state == IDLE:
        percent_along = min(idle_update / UPDATES_PER_PULSE, 1.0)
        brightness = ((math.cos(percent_along * math.pi * 2) + 1.0) / 2.0) * ((IDLE_PULSE_MAX - IDLE_PULSE_MIN)) + IDLE_PULSE_MIN
        # Update all the LEDs
        for i in range(NUM_LEDS):
            wheel.set_hsv(i, 0.0, 0.0, brightness)
        wheel.show()

        # Advance to the next update, wrapping around to zero if at the end
        idle_update += 1
        if idle_update >= UPDATES_PER_PULSE:
            idle_update = 0

    # If we're counting, perform the stopwatch animation
    elif state == COUNTING:
        # Calculate how many LED channels should light, as a proportion of a second, minute, and hour
        r_to_light = min(second_update / UPDATES, 1.0) * 24
        g_to_light = min(minute_update / MINUTE_UPDATES, 1.0) * 24
        b_to_light = min(hour_update / HOUR_UPDATES, 1.0) * 24

        # Set each LED, such that ones below the current time are fully lit, ones after
        # are off, and the one at the transition is at a percentage of the brightness
        for i in range(NUM_LEDS):
            r = clamp(r_to_light - i, 0.0, 1.0) * BRIGHTNESS * 255
            g = clamp(g_to_light - i, 0.0, 1.0) * BRIGHTNESS * 255
            b = clamp(b_to_light - i, 0.0, 1.0) * BRIGHTNESS * 255
            wheel.set_rgb(i, r, g, b)
        wheel.show()

        # Advance the second updates count, wrapping around to zero if at the end
        second_update += 1
        if second_update >= UPDATES:
            second_update = 0

        # Advance the minute updates count, wrapping around to zero if at the end
        minute_update += 1
        if minute_update >= MINUTE_UPDATES:
            minute_update = 0

        # Advance the hour updates count, wrapping around to zero if at the end
        hour_update += 1
        if hour_update >= HOUR_UPDATES:
            hour_update = 0

    # Sleep until the next update, accounting for how long the above operations took to perform
    current_time += UPDATE_RATE
    sleep_until(current_time)
