import time

import _thread
from machine import I2C, Pin, Timer, PWM

from timing import timeit

# Drivers for the RTC and the display!
# Credit: https://github.com/mcauser/micropython-tinyrtc-i2c
from ds1307 import DS1307

# Credit: https://github.com/T-622/RPI-PICO-I2C-LCD
from pico_i2c_lcd import I2cLcd


time.sleep(0.1) # Wait for USB to become ready


month_name = ["", "Jan", "Feb", "Mar", "Apr",
              "May", "Jun", "Jul","Aug", "Sept",
              "Oct", "Nov", "Dec"]


# Setup RTC clock
rtc_clock = I2C(0, scl=Pin(9), sda=Pin(8), freq=800000)
ds1307 = DS1307(addr=0x68, i2c=rtc_clock)

# Setup the display to print everything to!
display = I2C(1, scl=Pin(3), sda=Pin(2), freq=800000)
lcd = I2cLcd(display, 0x27, 2, 16)


# As far as I understand the pi should get accurate
# time from a time server, from which it will set it

# Convert to different times for testing lol
#pi_time = list(time.gmtime(time.time()))  # convert to list for editing
#pi_time[3] = 10
#pi_time[4] = 59
#pi_time[5] = 50
#ds1307.datetime = tuple(pi_time) # It expects a tuple type lol

# Setup the dimming system
photo_pin = Pin(16, Pin.IN)
dim_timer = Timer()

alr_on = False
def screen_light(setting: bool, *, timer: bool = False):
    global alr_on
    if setting:
        if not alr_on:
            alr_on = True
            lcd.backlight_on()

        if timer and photo_pin.value() == 1: # Dim on timer if meant to be dark only
            dim_timer.init(period=5000, mode=Timer.ONE_SHOT, callback=lambda t:screen_light(False))            
    else:
        alr_on = False
        lcd.backlight_off()


def handle_screen(pin):
    if pin.value() == 1:
        screen_light(False)
    elif pin.value() == 0:
        screen_light(True)

photo_pin.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=handle_screen)


# Setup buttons, buzzer and variables for the alarm
alarm_buzzer = PWM(Pin(20))
alarm_buzzer.freq(1000)
alarm_toggle = False # Determines whether to toggle the sound
alarm_status = False # Determines whether alarm is on

alarm = Timer()
clear_display = Timer()

# Buttons
alarm_btn = Pin(21, Pin.IN, Pin.PULL_UP)
hour_btn = Pin(26, Pin.IN, Pin.PULL_UP)
minute_btn = Pin(27, Pin.IN, Pin.PULL_UP)

# Last press time for debounce
alarm_last = time.ticks_ms()
hour_last = time.ticks_ms()
minute_last = time.ticks_ms()

button_pressed = False     # If alarm btn is being pressed
press_duration = 0         # How long the alarm btn was pressed for

alarm_config_mode = False  # Weather the alarm is in setup mode (I.e setting time)
alarm_time = None          # Stores the time the alarm should activate
halt_loop = False          # Pause loop when this is `True`


def close_menu(pin):
    global halt_loop
    lcd.clear()
    halt_loop = False
 
def button_irq_handler(pin):
    global button_pressed, press_duration, halt_loop
    global alarm_status, alarm_time, alarm_last, alarm_config_mode
    
    current_time = time.ticks_ms()
    
    # Debounce
    if time.ticks_diff(current_time, alarm_last) < 50:
        return

    if pin.value() == 0 and not button_pressed: # When pressed
        button_pressed = True
        press_duration = current_time
        alarm_last = current_time  # debounce
    elif pin.value() == 1 and button_pressed:   # When let go
        button_pressed = False
        press_duration = time.ticks_diff(current_time, press_duration)
        alarm_last = current_time  # debounce

        if alarm_status: # Options if the alarm is ON:
            if press_duration > 1000: # Shut off alarm
                alarm_status = False
                time_period = alarm_time["period"]
                alarm_time = None
                alarm.deinit()
                alarm_buzzer.duty_u16(0)

                halt_loop = True
                lcd.clear()
                screen_light(True, timer=True)
                lcd.putstr("Alarm off!")
                lcd.move_to(0,1)
                if time_period == "AM":
                    lcd.putstr("Good morning!")
                else:
                    lcd.putstr("Good afternoon!")

                clear_display.init(mode=Timer.ONE_SHOT, period=1500, callback=close_menu)

            else:
                alarm_time["minute"] += 5 # SNOOZE!

                if alarm_time["minute"] >= 60:
                    alarm_time["hour"] += 1
                    alarm_time["minute"] -= 60
                
                if alarm_time["hour"] > 12:
                    alarm_time["hour"] -= 12
                    alarm_time["period"] = "PM" if alarm_time["period"] == "AM" else "AM"
        
                elif alarm_time["hour"] == 12:
                    alarm_time["period"] = "PM" if alarm_time["period"] == "AM" else "AM"
                
                alarm_status = False
                alarm.deinit()
                alarm_buzzer.duty_u16(0)

                halt_loop = True
                lcd.clear()
                lcd.putstr("Snoozed 5 Min!")
                lcd.move_to(0,1)
                lcd.putstr("Zzzzz...")

                clear_display.init(mode=Timer.ONE_SHOT, period=1500, callback=close_menu)
                screen_light(True, timer=True)

        else: # options if alarm is OFF
            if press_duration > 1000 and alarm_time: # If hold & alarm set, clear alarm
                alarm_time = None
                halt_loop = True
                lcd.clear()
                lcd.putstr("Cleared alarm")

                clear_display.init(mode=Timer.ONE_SHOT, period=1500, callback=close_menu)
                screen_light(True, timer=True)

            elif press_duration < 1000: # Otherwise set or edit alarm, or leave menu
                screen_light(True)

                if not alarm_config_mode:
                    alarm_config_mode = True
                    halt_loop = True

                    dt_obj = ds1307.datetime

                    lcd.clear()
                    if not alarm_time:
                        lcd.putstr("Setup alarm:")
                        alarm_time = {
                            "hour": int(get_hour(dt_obj, get_period=False)),
                            "minute": int(dt_obj[4]),
                            "period": get_hour(dt_obj, get_period=True)
                        }
                    else:
                        lcd.putstr("Edit alarm:")
                    
                    lcd.move_to(0,1)

                    lcd.putstr(f"{alarm_time['hour']:02d}:{alarm_time['minute']:02d} {alarm_time['period']}")
                else:
                    if photo_pin.value() == 1:
                        screen_light(False)

                    alarm_config_mode = False
                    halt_loop = False
                    lcd.clear()

def hour_handler(pin):
    global hour_last
    global alarm_config_mode, alarm_time

    if time.ticks_diff(time.ticks_ms(), hour_last) < 200: # Debounce
        return
    if alarm_config_mode:
        hour_last = time.ticks_ms()
        if alarm_time["hour"] == 12:
            alarm_time["hour"] = 1
            alarm_time["period"] = "AM" if alarm_time["period"] == "PM" else "PM"
        else:
            alarm_time["hour"] += 1
        lcd.move_to(0,1)
        lcd.putstr(f"{alarm_time['hour']:02d}:{alarm_time['minute']:02d} {alarm_time['period']}")
    elif pin.value() == 0: # Brighten screen when not alarm mode just cause
        screen_light(True, timer=True)
    

def minute_handler(pin):
    global minute_last
    global alarm_config_mode, alarm_time

    if time.ticks_diff(time.ticks_ms(), minute_last) < 200: # Debounce
        return

    if alarm_config_mode:
        minute_last = time.ticks_ms()
        if alarm_time["minute"] == 59:
            alarm_time["minute"] = 0
        else:
            alarm_time["minute"] += 1
        lcd.move_to(0,1)
        lcd.putstr(f"{alarm_time['hour']:02d}:{alarm_time['minute']:02d} {alarm_time['period']}")
    elif pin.value() == 0: # Brighten screen when not alarm mode just cause
        screen_light(True, timer=True)


alarm_btn.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=button_irq_handler)
hour_btn.irq(trigger=Pin.IRQ_RISING, handler=hour_handler)
minute_btn.irq(trigger=Pin.IRQ_RISING, handler=minute_handler)


# Bugfix: Don't print text to screen when clock is halted
def screentext(string: str):
    if not halt_loop:
        lcd.putstr(string)


def toggle_alarm(timer):
    global alarm_toggle
    if alarm_toggle:
        alarm_buzzer.duty_u16(500)
    else:
        alarm_buzzer.duty_u16(0)
    alarm_toggle = not alarm_toggle


def only_ones_changed(prev_time: int, new_time: int):
    if not prev_time: # Handle nonetypes
        return False

    prev_tens_place = prev_time // 10
    prev_ones_place = prev_time % 10
    new_tens_place = new_time // 10
    new_ones_place = new_time % 10

    return prev_tens_place == new_tens_place and prev_ones_place != new_ones_place


# get time! THE TIME!
# formatted as str so I can directly display
# convert 24hr time => 12 hr time
def get_hour(dt_obj, get_period=False):
    hour = dt_obj[3]
    period = "AM"

    if hour > 12:
        hour -= 12
        period = "PM"
    elif hour == 0:
        hour = 12  # Account for midnight
    elif hour == 12:
        period = "PM"

    if get_period:
        return period
    else:
        return f"{hour:02d}"


def get_date(dt_obj):
    return f"{month_name[dt_obj[1]]} {dt_obj[2]}, {dt_obj[0]}"


dt_obj = ds1307.datetime

lcd.move_to(0,0)
screentext(get_date(dt_obj))

lcd.move_to(0,1)
screentext(f"{get_hour(dt_obj, get_period=False)}:{dt_obj[4]:02d}:{dt_obj[5]:02d} {get_hour(dt_obj, get_period=True)}")

previous_second = ds1307.second
while True:
    dt_obj = ds1307.datetime # Save dt object to avoid too many i2c calls
    if (dt_obj[3] == 0) and (dt_obj[4] == 0): # Update date if time is 12:00 am
        lcd.move_to(0,0)
        screentext(get_date(dt_obj))
    

    if dt_obj[4] == 0: # Update hour if minutes at 00
        lcd.move_to(0,1)
        screentext(get_hour(dt_obj, get_period=False))
        lcd.move_to(9,1)
        screentext(get_hour(dt_obj, get_period=True))


    if dt_obj[5] == 0: # Update minute if seconds at 00
        lcd.move_to(3,1)
        screentext(f"{dt_obj[4]:02d}")

    if previous_second != dt_obj[5]: # Only change needed digits
        if only_ones_changed(previous_second, dt_obj[5]):
            lcd.move_to(7,1)
            screentext(str(dt_obj[5] % 10))
        else:
            lcd.move_to(6,1)
            screentext(f"{dt_obj[5]:02d}")
        previous_second = ds1307.second

    loop_ran = False
    while halt_loop: # Halt loop when paused
        time.sleep(0.1)
        loop_ran = True

    if loop_ran:
        dt_obj = ds1307.datetime
        lcd.move_to(0,0)
        screentext(get_date(dt_obj))

        lcd.move_to(0,1)
        screentext(f"{get_hour(dt_obj, get_period=False)}:{dt_obj[4]:02d}:{dt_obj[5]:02d} {get_hour(dt_obj, get_period=True)}")

    if alarm_time is not None and not alarm_status:
        if f"{alarm_time['hour']:02d}" == get_hour(dt_obj):
            if alarm_time["minute"] == dt_obj[4]: 
                if alarm_time["period"] == get_hour(dt_obj, get_period=True):
                    alarm_status = True
                    screen_light(True)
                    alarm.init(mode=Timer.PERIODIC, period=1500, callback=toggle_alarm)
        
    time.sleep(0.2)
$abcdeabcde151015202530354045505560fghijfghij
GND5VSDASCLSQWRTCDS1307+