# Imports:
# import network
# import socket
from time import sleep, time
import picozero
#from picozero import pico_temp_sensor, pico_led, Button, InputDevice, OutputDevice, PWMOutputDevice
from picozero import pico_temp_sensor, pico_led, Button, PWMOutputDevice, Speaker, RGBLED
import machine
import utime
from machine import I2C, Pin, Timer
from I2C_LCD import I2CLcd
import math
import hashable_timer
from hashable_timer import HashableTimer
import random


# Variables:

upButton = Button(2)
downButton = Button(3)
setButton = Button(4)

#hapticFeedback = PWMOutputDevice(15)
speaker = Speaker(6)
rgb = RGBLED(red = 28, green = 27, blue = 26)

i2c = I2C(1, sda=Pin(14), scl=Pin(15), freq=400000)
devices = i2c.scan()
if devices != []:
    lcd = I2CLcd(i2c, devices[0], 2, 16)
else:
    print("No address found")

#Relevant to numerous areas:
setModes = ["clock", "timer", "reminder"] #["clock", "timer", "reminder"] #["sleep", "default", "timer", "reminder", "calendarEvent", "study", "personalGoal"]
setModeFunction = {
    "sleep" : lambda: sleepMode(),
    "clock" : lambda: clockMode(),
    "timer" : lambda: timerMode(),
    "reminder" : lambda: reminderMode(),
    #"calendarEvent" : lambda: calendarMode(),
    #"study" : lambda: studyMode(),
    #"personalGoal" : lambda: personalGoalMode()
    }
selectFunction = {
    "timer" : lambda: selectTimer(),
    "reminder" : lambda: selectReminder(),
    "clock" : lambda: startClock(),
    "settings" : lambda: changeSettings()
}
modeIndex = 0
currMode = setModes[modeIndex]
selecting = False


#Relevant to specific categories:
#clock variables:
clockSecondsElapsed = 0
startTime = 0
#timer variables:
activeTimers = []
#timerIDs = {}
#Stored in seconds
timerLengths = {}
timersElapsed = {}
currTimeIndex = 0
displayingTimer = False
#reminder variables:
activeReminders = []
reminderOptions = ["Do Laundry", "Take medicine", "Feed cat", "Call parents", "Do homework", "Pick up sister", "Get groceries"]
#reminderOptions = ["Revisit question", "Revisit section", "Answer question", "Check question"]
currRemindIndex = 0


#user preference stuff:
timeInterval = 2.0
#start the time interval reminder once the user chooses the length
alarmType = "rgb" #"speaker" #other option is "rgb"
alarmSpecification = (1, 0, 0) #"d3" #alternately, a color in (r, g, b) format (values of 1 or 0)


# Functions:
def changeMode(value):
    global currMode, modeIndex, setModes, lcd
    lcd.display_on()
    modeIndex = setModes.index(currMode)
    modeIndex += value
    modeIndex = modeIndex % (len(setModes))
    currMode = setModes[modeIndex]

    #note: sleep mode does not work this way. it is not in the list setModes

    #the following code produces a vibration to signal which setting they're selecting
    """for i in range(modeIndex):
        hapticFeedback.value = 0.5
        sleep(0.5)
        hapticFeedback.value = 0
        sleep(0.5)"""
    #instead just display which mode is being selected
    print(currMode)
    setModeFunction[currMode]()
    
def modeButtons():
    global currMode
    while (True):
        if upButton.is_pressed:
            changeMode(1)
        elif downButton.is_pressed:
            changeMode(-1)
        elif setButton.is_pressed:
            selectFunction[currMode]()

#mode functions:

def sleepMode():
    global selecting, lcd
    selecting = False
    #screen off, minimize power usage
    lcd.display_off()

    while (not selecting):
        if (upButton.is_pressed or downButton.is_pressed or setButton.is_pressed):
            #when they press a button, turn display on and enter clock mode
            """lcd.display_on()
            currMode = "clock"
            setModeFunction(currMode)"""
            #might change it to be that they enter last used mode
            changeMode(0)

        selecting = True



def clockMode():
    global clockSecondsElapsed
    #display clock w/ current time
    if clockSecondsElapsed == 0:
        lcd.clear()
        lcd.move_to(0,0)
        lcd.putstr("Begin Timing?")
    else:
        clockIncrementTimer = HashableTimer(mode=Timer.PERIODIC, period = 1000, callback = displayClock)
    
    
def timerMode():
    global currTimeIndex, displayingTimer, selecting
    selecting = False
    #display current timer (soonest ending)

    currTimeIndex = 0
    if len(activeTimers) >= 1:
        currentTimer = activeTimers[currTimeIndex]
        displayingTimer = True
        displayTimer(currentTimer)
    else:
        lcd.clear()
        lcd.move_to(0,0)
        lcd.putstr("No active timers :(")
    #select button lets you rotate through if multiple timers are set

    #last option on the list of timers is to set new time

    
def reminderMode():
    global currRemindIndex, activeReminders
    #display either the most urgent reminder or the one pinned by the user (consider using queues). Alternatively, display the reminders randomly
    #like timers, select button lets you rotate through the various reminders (THESE WILL BE SORTED)
    #last option sets new reminder
    if len(activeReminders) >= 1:
        currentReminder = activeReminders[currRemindIndex]
        currRemindIndex = random.randint(0, len(activeReminders)-1)
        currentReminder = activeReminders[currRemindIndex]
        displayReminder(currentReminder)
    else:
        lcd.clear()
        lcd.move_to(0,0)
        lcd.putstr("No active reminders :(")
    
#REMINDER: MAKE FUNCTIONS FOR THE OTHER MODES


# Clock Functions:
def startClock():
    global clockSecondsElapsed, startTime, timeInterval
    startTime =  utime.time()
    clockSecondsElapsed = 1
    #clock = Timer(mode=Timer.PERIODIC, period = 1000, callback = incrementClock)
    if (not (timeInterval == 0)):
        timeIntervalReminder = Timer(mode=Timer.PERIODIC, period = (int)(timeInterval * 60000), callback = indicateTimeInterval)
        #use a function that wraps around and returns the endTimer function
    clockMode()

def incrementClock(t):
    global clockSecondsElapsed
    clockSecondsElapsed += 1

def indicateTimeInterval(t):
    global alarmType, alarmSpecification
    if alarmType == "speaker":
        speaker.play("alarmSpecification", 0.35)
    elif alarmType == "rgb":
        rgb.pulse(n = 1)

def displayClock(t):
    global currMode, clockSecondsElapsed, startTime
    if currMode == "clock":
        clockSecondsElapsed = utime.time() - startTime
        hoursElapsed = math.floor(clockSecondsElapsed / 3600)
        minutesElapsed = math.floor((clockSecondsElapsed % 3600) / 60)
        secondsElapsed = math.floor(clockSecondsElapsed % 60)
        clockTimeString = "{:02}:{:02}:{:02}".format(hoursElapsed, minutesElapsed, secondsElapsed)

        lcd.clear()
        lcd.move_to(0,0)
        lcd.putstr(clockTimeString)
    else:
        return


# Timer Functions:

#in the future, allow users to customize the intervals they want for setting timers
def setTimer():
    exitSelection = Timer(mode=Timer.ONE_SHOT, period = 5000, callback = lambda t: timerMode())
    selecting = True
    lcd.clear()
    lcd.move_to(0, 0)
    lcd.putstr("Setting Timer:")
    lcd.move_to(0, 1)
    
    timerLength = 0.5 #CHANGE THIS BACK TO 0.5
    lcd.putstr(str(timerLength) + " Minutes")
    while selecting:
        #if (buttonUp.is_pressed and buttonDown.is_pressed):
            #do whatever happens then
        if (upButton.is_pressed): #elif (upButton.is_pressed):
            exitSelection.deinit()
            exitSelection = Timer(mode=Timer.ONE_SHOT, period = 5000, callback = lambda t: timerMode())
            if timerLength <= 5:
                timerLength+=0.5
            elif timerLength <= 60:
                timerLength += 5
            else:
                timerLength += 15
            lcd.clear()
            lcd.move_to(0, 0)
            lcd.putstr("Setting Timer...")
            lcd.move_to(0, 1)
            lcd.putstr(str(timerLength) + " Minutes")
            
        #change this for decreasing
        elif (downButton.is_pressed):
            exitSelection.deinit()
            exitSelection = Timer(mode=Timer.ONE_SHOT, period = 5000, callback = lambda t: timerMode())
            if timerLength > 0.5:
                if timerLength <= 5:
                    timerLength-=0.5
                elif timerLength <= 60:
                    timerLength -= 5
                else:
                    timerLength -= 15
                lcd.clear()
                lcd.move_to(0, 0)
                lcd.putstr("Setting Timer...")
                lcd.move_to(0, 1)
                lcd.putstr(str(timerLength) + " Minutes")
            
        #timer class: https://docs.micropython.org/en/latest/library/machine.Timer.html
        
        if (setButton.is_pressed):
            exitSelection.deinit()
            sortTimer(HashableTimer(mode=Timer.ONE_SHOT, period = int((timerLength * 60000)), callback = lambda t: endTimer(t, timerLength*60)), timerLength)
            selecting = False
            
    timerMode()

def sortTimer(newTimer, timerLength):
    global timerLengths, timersElapsed
    timerLengths.update({newTimer:(timerLength * 60)})
    timersElapsed.update({newTimer:0})
    #This runs a function every second that increases the variable tracking seconds elapsed
    Timer(mode=Timer.PERIODIC, period = 1000, callback = lambda t: updateTimerTimeElapsed(newTimer))
    if len(activeTimers) >= 1:
        foundSpot = False
        lowBound = 0
        highBound = len(activeTimers)
        while not foundSpot:
            currIndex = int((highBound-lowBound)/2) + lowBound
            if timersElapsed[activeTimers[currIndex]] >= timersElapsed[newTimer]:
                activeTimers.insert(currIndex, newTimer)
                newTimer.secondsElapsed = 0
                foundSpot = True
            elif timersElapsed[thisTimer] < activeTimers[currIndex]:
                highBound = currIndex - 1
            elif timersElapsed[thisTimer] > activeTimers[currIndex]:
                lowBound = currIndex + 1
    else:
        activeTimers.append(newTimer)

def updateTimerTimeElapsed(thisTimer):
    global timersElapsed
    timersElapsed[thisTimer] += 1

def endTimer(thisTimer, timerLength):
    global timerLengths, displayingTimer
    displayingTimer = False
    lcd.clear()
    lcd.move_to(0, 0)
    lcd.putstr(str((timerLength / 60.0)) + " Minute Timer Has Ended")
    
    #CODE VIBRATION MOTOR ALARM
    runningAlarm = True
    """for i in range (5): # ultimately make this a while loop that ends when the user turns off the timer. while(runningAlarm)
        for i in range(5):
            hapticFeedback.value = 0.5
            sleep(0.25)
            hapticFeedback.value = 0
            sleep(0.25)
        sleep(0.5)"""
    
    #find a way to check for this simultaneous with the alarm buzzing. Look into threading.
    selecting = True
    while (selecting):
        if (upButton.is_pressed):
            #snooze timer
            lcd.clear()
            lcd.move_to(0,0)
            lcd.putstr("Snoozing Timer")
            sortTimer(HashableTimer(mode=Timer.ONE_SHOT, period = 300000, callback = lambda t: endTimer(t, 300)), 300)
            
        elif (downButton.is_pressed):
            #repeat timer
            lcd.clear()
            lcd.move_to(0,0)
            lcd.putstr("Repeating Timer")
            sortTimer(HashableTimer(mode=Timer.ONE_SHOT, period = timerLength * 1000, callback = lambda t: endTimer(t, timerLength)), timerLength / 60.0)
            
        elif (setButton.is_pressed):
            #stop/turn off timer
            lcd.clear()
            lcd.move_to(0,0)
            lcd.putstr("Stopping Timer")
            selecting = False
            runningAlarm = False
    
    timerMode()

def displayTimer(thisTimer):
    global timerLengths, displayingTimer
    #add a way to pause the timer while it is displayed
    #length in minutes
    timerLength = timerLengths[thisTimer]

    lcd.clear()
    
    timeLeftString = str(timerLength / 60.0) + " minute timer: "
    lcd.putstr(timeLeftString)
    
    #consider finding a way to format this to be 00:00
    displayingTimer = True
    updateTimerDisplay(thisTimer, HashableTimer(mode=Timer.ONE_SHOT, period = 1000, callback = lambda t: print()))
    
    #could try using a periodic timer to continuously update the screen
    HashableTimer(mode=Timer.PERIODIC, period = 1000, callback = lambda t: updateTimerDisplay(thisTimer, t))
    #note: this will update it every second, but the seconds will start when the function does so it may not align with the actual time

def updateTimerDisplay(thisTimer, t):
    global timerLengths, currTimeIndex, displayingTimer, currMode
    if (not (activeTimers[currTimeIndex] == thisTimer)) or (not displayingTimer) or (not (currMode == "timer")):
        t.deinit()
        return

    #change what is displayed to be equal to the timer's current time elapsed
    #total time remaining in seconds
    timerLength = timerLengths[thisTimer]

    lcd.clear()
    lcd.move_to(0, 0)
    lcd.putstr(str(timerLength / 60.0) + " minute timer: ")
    lcd.move_to(0,1)

    timerRemainingSeconds = (timerLength) - (timersElapsed[thisTimer])
    #hours remaining
    hoursRemaining = math.floor(timerRemainingSeconds / 3600)
    #minutes remaining after the hours
    minutesRemaining = math.floor((timerRemainingSeconds % 3600) / 60)
    #seconds remaining after the minutes
    secondsRemaining = timerRemainingSeconds % 60
    
    #consider finding a way to format this to be 00:00
    if hoursRemaining > 0:
        #timeLeftString = hoursRemaining + ":" + minutesRemaining + ":" + secondsRemaining
        timeLeftString = "{:02}:{:02}:{:02} remaining".format(hoursRemaining, minutesRemaining, secondsRemaining)
    else:
        timeLeftString = "{:02}:{:02} remaining".format(minutesRemaining, secondsRemaining)
    lcd.move_to(0, 1)
    lcd.putstr(timeLeftString)

def selectTimer():
    global currTimeIndex, displayingTimer
    exitSelection = Timer(mode=Timer.ONE_SHOT, period = 5000, callback = lambda t: timerMode())
    while (True):
        if len(activeTimers) >= 1:
            if upButton.is_pressed:
                exitSelection.deinit()
                exitSelection = Timer(mode=Timer.ONE_SHOT, period = 5000, callback = lambda t: timerMode())
                displayingTimer = True
                currTimeIndex += 1
                currTimeIndex = currTimeIndex % len(activeTimers)
                currentTimer = activeTimers[currTimeIndex]
                displayTimer(currentTimer)
            elif downButton.is_pressed:
                exitSelection.deinit()
                exitSelection = Timer(mode=Timer.ONE_SHOT, period = 5000, callback = lambda t: timerMode())
                displayingTimer = True
                currTimeIndex -= 1
                currTimeIndex = currTimeIndex % len(activeTimers)
                currentTimer = activeTimers[currTimeIndex]
                displayTimer(currentTimer)
            elif setButton.is_pressed:
                exitSelection.deinit()
                displayingTimer = False
                setTimer()
        else:
            selecting = True
            lcd.clear()
            lcd.move_to(0, 0)
            lcd.putstr("Set new timer?")
            exitSelection = Timer(mode=Timer.ONE_SHOT, period = 5000, callback = lambda t: timerMode())
            while (selecting):
                if setButton.is_pressed:
                    exitSelection.deinit()
                    setTimer()

    
#Reminder Functions:

def setReminder():
    global activeReminders, reminderOptions

    lcd.clear()
    lcd.move_to(0, 0)
    lcd.putstr("Setting Reminder:")
    lcd.move_to(0, 1)
    
    currentIndex = 0
    currReminderOption = reminderOptions[currentIndex]
    
    lcd.putstr(currReminderOption)

    selecting = True
    while selecting:
        if upButton.is_pressed:
            currentIndex += 1
            currReminderOption = reminderOptions[currentIndex]
            lcd.clear()
            lcd.move_to(0, 0)
            lcd.putstr("Setting Reminder:")
            lcd.move_to(0, 1)
            lcd.putstr(currReminderOption)
        elif downButton.is_pressed:
            currentIndex -= 1
            currReminderOption = reminderOptions[currentIndex]
            lcd.clear()
            lcd.move_to(0, 0)
            lcd.putstr("Setting Reminder:")
            lcd.move_to(0, 1)
            lcd.putstr(currReminderOption)
        elif setButton.is_pressed:
            lcd.clear()
            lcd.move_to(0,0)
            lcd.putstr("Reminder set!")
            lcd.move_to(0,1)
            lcd.putstr(currReminderOption)
            activeReminders.append(currReminderOption)
            selecting = False
    
    reminderMode()



def displayReminder(thisReminder):
    lcd.clear()
    lcd.move_to(0, 0)
    lcd.putstr("Remember to " + thisReminder)


def selectReminder():
    global currRemindIndex, activeReminders
    exitSelection = Timer(mode=Timer.ONE_SHOT, period = 5000, callback = lambda t: reminderMode())
    while (True):
        if len(activeReminders) >= 1:
            if upButton.is_pressed:
                exitSelection.deinit()
                exitSelection = Timer(mode=Timer.ONE_SHOT, period = 5000, callback = lambda t: reminderMode())
                currRemindIndex += 1
                currRemindIndex = currRemindIndex % len(activeReminders)
                currentReminder = activeReminders[currRemindIndex]
                displayReminder(currentReminder)
            elif downButton.is_pressed:
                exitSelection.deinit()
                exitSelection = Timer(mode=Timer.ONE_SHOT, period = 5000, callback = lambda t: reminderMode())
                currRemindIndex -= 1
                currRemindIndex = currRemindIndex % len(activeReminders)
                currentReminder = activeReminders[currRemindIndex]
                displayReminder(currentReminder)
            elif setButton.is_pressed:
                exitSelection.deinit()
                setReminder()
        else:
            selecting = True
            lcd.clear()
            lcd.move_to(0, 0)
            lcd.putstr("Set new Reminder?")
            exitSelection = Timer(mode=Timer.ONE_SHOT, period = 5000, callback = lambda t: reminderMode())
            while (selecting):
                if setButton.is_pressed:
                    exitSelection.deinit()
                    setReminder()


# Main Code:
"""setModeFunction = {
    "sleep" : lambda: sleepMode(),
    #"clock" : lambda: clockMode(),
    "timer" : lambda: timerMode(),
    #"reminder" : lambda: reminderMode(),
    #"calendarEvent" : lambda: calendarMode(),
    #"study" : lambda: studyMode(),
    #"personalGoal" : lambda: personalGoalMode()
}"""

sleepMode()

while (True):
    modeButtons()
    print("back")



# TO DO:
# Add new Timer
# Add new Reminder
# Add new Calendar Event
# Set Study Mode
# Update Personal Goals
$abcdeabcde151015202530fghijfghij
BOOTSELLED1239USBRaspberryPiPico©2020RP2-8020/21P64M15.00TTT