from time import monotonic, sleep
import board
import neopixel
import digitalio
import json

from adafruit_debouncer import Debouncer

from adafruit_led_animation.animation.pulse import Pulse
from adafruit_led_animation.animation.solid import Solid
from adafruit_led_animation.animation.blink import Blink
from adafruit_led_animation.color import GREEN, BLUE, RED, WHITE, BLACK
from adafruit_led_animation.sequence import AnimationSequence

from adafruit_ble import BLERadio
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
from adafruit_ble.services.nordic import UARTService

# Constants and Setup
DEVICE_NAME = "Pill Button"
LONG_PRESS_TIME = 1.0  # How long to press to register a long-press
SHORT_PRESS_WAIT = 0.5 # Maximum time between a short press and another press in order to make e.g. a double press.
BTN_PIN = board.A2     # Where you attached the non-ground wire running to the arcade button.
LED_BRIGHTNESS = 1     # 0-1. How bright do you want the LED?

# Suggested Script and Action Mapping
# These are sent on connection to the SGT to pre-populate the Action/Write scripts for quick save.
suggestions = {
    "script": [
        '0 sgtState;sgtColor;sgtTurnTime;sgtPlayerTime%0A'
    ],
    "scriptName": DEVICE_NAME + " Write",
    "defaultTriggers": ["includePlayers","includePause","includeAdmin","includeSimultaneousTurns","includeGameStart","includeGameEnd","includeSandTimerStart","includeSandTimerReset","includeSandTimerStop","includeSandTimerOutOfTime","runOnStateChange","runOnPlayerOrderChange","runOnPoll","runOnBluetoothConnect","runOnBluetoothDisconnect"],
    "actionMap": [
        ('Short Press 1', 'remoteActionPrimary'),
        ('Short Press 2', 'remoteActionToggleAdmin'),
        ('Long Press 0', 'remoteActionSecondary'),
        ('Long Press 1', 'remoteActionTogglePause'),
        ('Long Press', 'remoteActionUndo'),
        ('Connected', 'remoteActionPoll'),
    ],
    "actionMapName": DEVICE_NAME + " Actions",
}

# LED Setup
led = None        # This will be set to an animation object.
                  # To change, call one of the 'switchTo...' methods.
                  # Also call led.animate() as often as possible to animate the LED animation.

pixels = neopixel.NeoPixel(board.D5, 1, brightness=LED_BRIGHTNESS, auto_write=False)

################## END OF THINGS YOU SHOULD MESS WITH ################

# System Constants
STATE_PLAYING = 'pl'
STATE_SIM_TURN = 'si'
STATE_ADMIN = 'ad'
STATE_PAUSE = 'pa'
STATE_START = 'st'
STATE_FINISHED = 'en'
STATE_NOT_CONNECTED = 'nc'
STATE_RUNNING = 'ru'
STATE_NOT_RUNNING = 'nr'

TIMER_MODE_COUNT_UP = 'cu'
TIMER_MODE_COUNT_DOWN = 'cd'
TIMER_MODE_SAND_TIMER = 'st'
TIMER_MODE_NO_TIMER = 'nt'

SAND_COLOR_OUT_OF_TIME = (255, 0, 0)
SAND_COLOR_TIME_LEFT = (0, 255, 0)
SAND_COLOR_TIME_USED = (0, 0, 90)

class BetterAnimationSequence:
    # This is a terrible hack for combining differently timed animations.
    # There seem to be a PR for simplifying this.
    # https://github.com/adafruit/Adafruit_CircuitPython_LED_Animation/pull/67
    def __init__(self, *anim_and_time_tuples):
        self._anim_and_time_tuples = anim_and_time_tuples
        self._last_advance = monotonic()
        self._current_index = 0
        animations = list(map(lambda tuple: tuple[0], anim_and_time_tuples))
        self._animation_seq = AnimationSequence(*animations, auto_reset=True)
        self._time_limits = list(map(lambda tuple: tuple[1], anim_and_time_tuples))

    def animate(self):
        time_with_current_animation = monotonic() - self._last_advance
        time_limit_of_current_animation = self._time_limits[self._current_index]
        if time_with_current_animation > time_limit_of_current_animation:
            self._animation_seq.next()
            self._animation_seq.current_animation.reset()
            self._last_advance = monotonic()
            self._current_index = (self._current_index + 1) % len(self._time_limits)
        self._animation_seq.animate()

class SandtimerAnimation():
    out_of_time_animation_running = Blink(pixels, speed=0.2, color=SAND_COLOR_OUT_OF_TIME)
    out_of_time_animation_stopped = Solid(pixels, color=SAND_COLOR_OUT_OF_TIME)

    def animate(self):
        time_added_by_monotonic = 0 if sgt_state == STATE_NOT_RUNNING else monotonic() - sgt_update_ts
        turn_time = sgt_turn_time_sec + time_added_by_monotonic
        remaining_time = max(sgt_player_time_sec - turn_time, 0)

        if (remaining_time == 0):
            if sgt_state == STATE_RUNNING:
                SandtimerAnimation.out_of_time_animation_running.animate()
            else:
                SandtimerAnimation.out_of_time_animation_stopped.animate()
        else :
            SandtimerAnimation.out_of_time_animation = None
            fraction_into_current_pixel = remaining_time / sgt_player_time_sec
            color_current_pixel = (
                SAND_COLOR_TIME_LEFT[0]*fraction_into_current_pixel + SAND_COLOR_TIME_USED[0]*(1-fraction_into_current_pixel),
                SAND_COLOR_TIME_LEFT[1]*fraction_into_current_pixel + SAND_COLOR_TIME_USED[1]*(1-fraction_into_current_pixel),
                SAND_COLOR_TIME_LEFT[2]*fraction_into_current_pixel + SAND_COLOR_TIME_USED[2]*(1-fraction_into_current_pixel)
                )
            if sgt_state == STATE_NOT_RUNNING:
                pixels[0] = pulsate_color(color_current_pixel)
            else:
                pixels[0] = color_current_pixel
            pixels.show()

def set_button_led_to_solid(color):
    global led
    led = Solid(pixels, color=color)
    led.animate()
def set_button_led_to_blink(color, speed):
    global led
    led = Blink(pixels, speed=speed, color=color)
    led.animate()
def set_button_led_to_pulse(color, pulse_time):
    global led
    led = Pulse(pixels, speed=0.01, color=color, period=pulse_time)
    led.animate()

def set_button_led_to_periodic_pulse(color, pulse_time, pause_time):
    global led
    pause = Solid(pixels, color=BLACK)
    pulse = Pulse(pixels, speed=0.01, color=color, period=pulse_time)
    led = BetterAnimationSequence((pulse,pulse_time), (pause,pause_time))
    led.animate()

def switch_to_playing():
    set_button_led_to_pulse(sgt_color, 4)

def switch_to_simultaneous_turn():
    set_button_led_to_periodic_pulse(sgt_color, 1, 2)

def switch_to_sandtimer_running():
    global led
    led = SandtimerAnimation()
def switch_to_sandtimer_not_running():
    global led
    led = SandtimerAnimation()

def switch_to_admin_time():
    set_button_led_to_periodic_pulse(sgt_color, 1, 2)

def switch_to_paused():
    set_button_led_to_periodic_pulse(sgt_color, 1, 10)

def switch_to_start():
    set_button_led_to_periodic_pulse(sgt_color, 1, 2)

def switch_to_end():
    set_button_led_to_solid(BLACK)

def switchToTrying_to_connect():
    set_button_led_to_periodic_pulse(BLUE, 2, 2)

def switch_to_connecting():
    set_button_led_to_solid(BLUE)

def switch_to_error():
    set_button_led_to_blink(RED, 0.2)

# Bluetooth Setup
ble = BLERadio()
uart = UARTService()
advertisement = ProvideServicesAdvertisement(uart)

# Button Setup
btn_pin = digitalio.DigitalInOut(BTN_PIN)
btn_pin.direction = digitalio.Direction.INPUT
btn_pin.pull = digitalio.Pull.UP
btn = Debouncer(btn_pin)
btn.update()

# State Management
# We read in sgtColor and sgtState from the bluetooth. We also keep track of the current
# values so we know when and how they change, and if they require us to update the LED animation.
sgt_timer_mode = None           # Current timer-mode (cd/cu/st/nt for Count-Down/Up, SandTimer, No Timer)
sgt_state=STATE_NOT_CONNECTED   # The current state. SandTimer,
                                     # Sand, ru/nr/pa/en for running, not running, paused or end
                                     # Not Sand, st/en/pa/ad/pl for start, end, pause, admin or playing
sgt_color = None                # (not sand) The current or next-up player color

sgt_turn_time_sec=0             # Count-Up, time taken this turn or pause time or admin time
                                # Count-Down, same as above, but negative values during Delay Time
                                # Sand, time taken out of the sand timer

sgt_player_time_sec=0           # Count-Up, total time taken, or blank for admin/pause time
                                # Count-Down, remaining time bank, or blank for admin/pause time
                                # Sand, sand-timer reset size

sgt_update_ts=0                 # When did we last get an update from SGT?
sgt_incomplete_line_read=''     # If we've read data not ending in new line, store it here for now
sgt_last_read_line=''           # Last completed line read. Used to avoid duplicate lines.


""" Read from the UART TX channel and update the global state if new info came in. """
def read_state():
    global sgt_incomplete_line_read
    while uart.in_waiting > 0:
        text_to_process = sgt_incomplete_line_read + str(uart.read(uart.in_waiting), 'utf-8')
        lines = text_to_process.split("\n")
        if len(lines) == 0:
            return
        last_item = lines.pop()
        if last_item == '':
            do_this_line = lines.pop()
            if len(lines) > 0:
                print(f"SKIP: {lines}")
            sgt_incomplete_line_read = ''
            sleep(0.1)
            if uart.in_waiting:
                print(f"SKIP AFTER ALL: {do_this_line}")
            else:
                handle_state_update(do_this_line)
        else:
            if len(lines) > 0:
                print(f"SKIP: {lines}")
            print(f"Incomplete Line: {last_item}")
            sgt_incomplete_line_read = last_item

""" Read from the UART TX channel and update the global state if new info came in. """
def handle_state_update(state_line):
    if state_line == "GET SETUP":
        send(json.dumps(suggestions))
        send("Connected")
        return
    if (sgt_last_read_line == state_line):
        return
    print(f"Handle State Update: {state_line}")

    global sgt_timer_mode, sgt_state, sgt_color, sgt_turn_time_sec, sgt_update_ts, sgt_player_time_sec, sgt_total_play_time_sec, sgt_last_screen_update_ts
    sgt_last_screen_update_ts = 0
    try:
        newSgtState, color_hex, tmp_turn_time, tmp_player_time_sec = [item.strip() for item in state_line.split(";")]
        sgt_turn_time_sec = int(tmp_turn_time) if len(tmp_turn_time) > 0 else 0
        sgt_player_time_sec = int(tmp_player_time_sec) if len(tmp_player_time_sec) > 0 else 0
        newSgtColor = simplify_color((int(color_hex[0:2],16),int(color_hex[2:4],16),int(color_hex[4:6],16))) if len(color_hex) == 6 else (255,255,255)
        sgt_update_ts=monotonic()

        require_update = newSgtState != sgt_state or newSgtColor != sgt_color
        sgt_state = newSgtState
        sgt_color = newSgtColor

        if require_update:
            if sgt_state == STATE_PLAYING:
                switch_to_playing()
            elif sgt_state == STATE_SIM_TURN:
                switch_to_simultaneous_turn()
            elif sgt_state == STATE_ADMIN:
                switch_to_admin_time()
            elif sgt_state == STATE_PAUSE:
                switch_to_paused()
            elif sgt_state == STATE_FINISHED:
                switch_to_end()
            elif sgt_state == STATE_START:
                switch_to_start()
            elif sgt_state == STATE_RUNNING:
                switch_to_sandtimer_running()
            elif sgt_state == STATE_NOT_RUNNING:
                switch_to_sandtimer_not_running()
            else :
                switch_to_error()
    except Exception as e:
        result = repr(e)
        print(result)


# Colors that look good on screen may not look so good on a simple LED. Usuallly, they need to be 'simplified' to be bolder.
def simplify_color(color):
    newColor = (simplify_color_part(color[0]), simplify_color_part(color[1]), simplify_color_part(color[2]))
    return newColor

def simplify_color_part(colorPart):
    return (colorPart//86)*127

def pulsate_color(color):
    t = monotonic() % 2
    if t < 1:
        tb = t * t * (3.0 - 2.0 * t)
    else:
        tt = t - 1
        tb = 1 - (tt * tt * (3.0 - 2.0 * tt))
    tb = max(tb, 0.08)
    return (int(color[0]*tb), int(color[1]*tb), int(color[2]*tb))

""" Counts a number of short presses, terminated either by a long press or a timeout.
The number of short presses is sent across bluetooth including information if the final
termination was a timeout or a long press.
"""
def check_buttons():
    global pixels
    # Remember! TRUE = not pressed, FALSE = pressed, ROSE = released, FELL = pressed
    # Yes, it is counter-intuitive, but this is how people into electronics think of things.
    btn.update()
    if btn.fell:
        short_presses = 0
        long_pressed = False
        done_registering_short_presses = False
        while not done_registering_short_presses and not long_pressed:
            pixels.fill(WHITE)
            pixels.show()
            ts_pressed = monotonic()
            while not btn.value and not long_pressed:
                btn.update()
                if not btn.value and monotonic() - ts_pressed > LONG_PRESS_TIME:
                    long_pressed = True

            # We've either released the button, or long pressed.
            pixels.fill(BLACK)
            pixels.show()
            if not long_pressed:
                # short press! Increment short press counter, and wait for next short press to start
                short_presses = short_presses + 1
                ts_short_press_released = monotonic()
                while monotonic() - ts_short_press_released < SHORT_PRESS_WAIT and not btn.fell:
                    btn.update()

                # we either times out, or pressed the button again
                # If the button was not released (fell), then we timed out, and we are done_registering_short_presses
                done_registering_short_presses = btn.value

        # We are done! Write to the uart.
        if long_pressed:
            send(f'Long Press {short_presses}')
        else :
            send(f'Short Press {short_presses}')

def send(string):
    print(string)
    uart.write((string+"\n").encode("utf-8"))

ble.name = DEVICE_NAME
while True:
    sgt_state="nc"
    ble.start_advertising(advertisement)
    print("Waiting to connect")
    switchToTrying_to_connect()
    while not ble.connected:
        led.animate()
    ble.stop_advertising()
    print("Connecting")
    switch_to_connecting()
    ts = monotonic()
    while monotonic() - ts < 2:
        led.animate()
    send("Connected")
    while ble.connected:
        led.animate()
        read_state()
        check_buttons()
