import utime as time

from machine import I2C, Pin, Timer, PWM

# 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

# Debugger
from debug import base_logger
logger = base_logger(level="DEBUG")

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


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

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

lcd.custom_char(0, bytearray([
    0x04,
    0x0E,
    0x0E,
    0x0E,
    0x1F,
    0x00,
    0x04,
    0x00
])) # Alarm active icon

# Get the alarm adk/snooze button
alarm_button = Pin(19, Pin.IN, Pin.PULL_UP)

# Config & random variables
month_name = ["", "Jan", "Feb", "Mar", "Apr",
              "May", "Jun", "Jul","Aug", "Sept",
              "Oct", "Nov", "Dec"]

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

# 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

# Keypad config
rows = [9, 8, 7, 6]
cols = [5, 4, 3, 2]

row_pins = [Pin(pin_num, Pin.OUT) for pin_num in rows]
col_pins = [Pin(pin_num, Pin.IN, Pin.PULL_DOWN) for pin_num in cols]

keypad = [
    ['1', '2', '3', 'A'],
    ['4', '5', '6', 'B'],
    ['7', '8', '9', 'C'],
    ['*', '0', '#', 'D']
]


# Setup buttons, buzzer and variables for the alarm
alarm_buzzer = PWM(Pin(18))
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`
menu_stage = "hr_tens"

def keypad_handler(key_pressed):
    """
    Key mappings:
    A : Enable + edit / disable alarm config mode
    B : Clear alarm if set
    C : Enable display light
    D : ** Unused **

    1-9 : Set alarm time
    * : Change between AM/PM in alarm config mode
    # : Confirm alarm choice (basically enter key)
    """
    global button_pressed, halt_loop
    global alarm_status, alarm_time, alarm_config_mode, menu_stage
    
    if key_pressed == "A": # If keypad input = 'A'
        clear_display.deinit() # BUGFIX: Prevent menu from closing if buttons spammed
        if alarm_status: # Don't do anything if alarm on
            logger.warning("Ignoring A press as alarm is active")
            return

        if not alarm_config_mode: # If we're not in alarm setup mode, enter that mode
            screen_light(True)
            lcd.blink_cursor_on() # Blinky thingy look nice

            alarm_config_mode = True
            halt_loop = True
            lcd.clear()

            if not alarm_time:
                lcd.putstr("Setup alarm:")
                alarm_time = {
                    "hour": "__",
                    "minute": "__",
                    "period": get_hour(dt_obj, get_period=True)
                }
            else:
                lcd.putstr("Edit alarm:")
        
            # Add time text
            lcd.move_to(0,1)
            lcd.putstr(f"{z_pad(alarm_time['hour'])}:{z_pad(alarm_time['minute'])} {alarm_time['period']}")
            lcd.move_to(0,1)
            lcd.putstr("") # Temp not so working bugfix: Above ^ doesn't work sometimes
            logger.info("Alarm Setup Mode - ON")
            menu_stage = "hr_tens"
            

    elif key_pressed == "B": # Clear alarm when set
        if alarm_time:
            alarm_time = None
            halt_loop = True
            lcd.clear()
            if alarm_config_mode:
                alarm_config_mode = False
                lcd.putstr("Cancelled alarm")
            else:
                lcd.putstr("Cleared alarm")

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

    elif key_pressed == "C": # Enable display
        screen_light(True, timer=True)
    
    elif key_pressed == "D": # ** Unused **
        ...

    elif key_pressed == "*": # Toggle AM/PM
        if not alarm_config_mode:
            logger.warning("Ignoring * press as we are not in alarm config mode")
            return

        lcd.move_to(6,1)
        if alarm_time["period"] == "AM":
            lcd.putstr("PM")
        elif alarm_time["period"] == "PM":
            lcd.putstr("AM")
        alarm_time["period"] = "PM" if alarm_time["period"] == "AM" else "AM"

    elif key_pressed == "#": # Enter key
        if not alarm_config_mode:
            logger.warning("Ignoring # press as we are not in alarm config mode")
            return

        if photo_pin.value() == 1: # If display on, turn it off
            screen_light(False)

        lcd.blink_cursor_off() 
        lcd.hide_cursor() # BUGFIX: Cursor keeps appearing idk why
        alarm_config_mode = False
        logger.info("Alarm Setup Mode - OFF")

        if "_" in str(alarm_time["hour"]) or "_" in str(alarm_time["minute"]) or alarm_time["hour"] == 0:
            logger.warning("Alarm input is invalid and will be ignored")
            alarm_time = None # Invalidate alarm
            lcd.clear()
            lcd.putstr("Alarm invalid and not set")
            clear_display.init(mode=Timer.ONE_SHOT, period=1500, callback=close_menu)
        else:
            close_menu()

    elif key_pressed in [str(i) for i in range(10)]: # If keypad input = number
        if not alarm_config_mode:
            logger.warning("Ignoring num press as we are not in alarm config mode")
            return

        if menu_stage == "hr_tens": # If hour not setup, setup
            if key_pressed not in [str(i) for i in range(2)]: # Can't have a number > 1 in the start of an hr (I.e 20pm)
                return
            lcd.move_to(0,1) # BUGFIX TO THE BUGFIX: Ugh
            lcd.putstr(key_pressed)
            if alarm_time["hour"] == "__": # If time is blank
                alarm_time["hour"] = key_pressed + "_"
            else:
                alarm_time["hour"] = key_pressed + (str(alarm_time["hour"]) + "0")[1]
                logger.debug(f"alarm hr string {str(alarm_time['hour'])}")
            menu_stage = "hr_ones"

        elif menu_stage == "hr_ones": # See if hour is partially filled
            if alarm_time["hour"][:-1] == "1" and key_pressed not in [str(i) for i in range(3)]: # Can't have a number > 12 (I.e. 15pm)
                return
            elif alarm_time["hour"][:-1] == "0" and key_pressed == "0": # Can't have 00 as a time
                return
            lcd.move_to(1,1)
            lcd.putstr(key_pressed)
            alarm_time["hour"] = int(alarm_time["hour"][:-1] + key_pressed) # Finish hr, convert to int
            menu_stage = "min_tens"
            lcd.move_to(3,1) # Move cursor so it looks nice

        elif menu_stage == "min_tens": # If minute not setup, setup
            if key_pressed not in [str(i) for i in range(6)]: # Can't have a number > 5 in the start of an hr (I.e 69min)
                return
            lcd.move_to(3,1)
            lcd.putstr(key_pressed)
            if alarm_time["minute"] == "__": # If time is blank
                alarm_time["minute"] = str(key_pressed) + "_"
            else:
                alarm_time["minute"] = key_pressed + (str(alarm_time["minute"]) + "0")[1]
            menu_stage = "min_ones"

        elif menu_stage == "min_ones": # See if minute is partially filled
            # Note to self: :D no data validation needed here :D
            lcd.move_to(4,1)
            lcd.putstr(key_pressed)
            alarm_time["minute"] = int(alarm_time["minute"][:-1] + str(key_pressed)) # Finish hr, convert to int
            menu_stage = "hr_tens"
            lcd.move_to(0,1) # Move cursor so it looks nice

        logger.debug(alarm_time)


def keypad_irq_handler(col_pin):
    """
    Run the irq handler, and ensure the setup + cleanup function is always run
    """
    if not col_pin.value(): # If pin is off
        return

    for col in col_pins: # disable interrupts so prevent repeat calls
        col.irq(handler=None)

    for row_index, row_pin in enumerate(row_pins):
        row_pin.low()
        if not col_pin.value(): # If column turns off with the row, we found it!
            key_pressed = keypad[row_index][col_pins.index(col_pin)]
            logger.info(f"'{key_pressed}' was pressed")

        row_pin.high()

    keypad_handler(key_pressed) # Run the core code and handler

    logger.debug("Interrupts renabled")
    for col in col_pins: # Renable interrupt :D
        col.irq(trigger=Pin.IRQ_RISING, handler=keypad_irq_handler)


# Set up row pins and power them
for row in row_pins:
    row.high()
    logger.debug(f"set {row} -> {row.value()}")

# Set up column pins and interrupts
for col in col_pins:
    logger.debug(f"set {col} -> IRQ")
    col.irq(trigger=Pin.IRQ_RISING, handler=keypad_irq_handler)


def button_irq_handler(pin):
    global button_pressed, press_duration, halt_loop
    global alarm_status, alarm_time, alarm_last
    
    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
        logger.info("Alarm button pressed")
        button_pressed = False
        press_duration = time.ticks_diff(current_time, press_duration)
        alarm_last = current_time  # debounce

        if not alarm_status:
            return
        
        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)

alarm_button.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=button_irq_handler)


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)


def close_menu(pin=None): # We don't need pin btw
    global halt_loop, alarm_config_mode
    lcd.clear()
    halt_loop = False
    if alarm_config_mode: #BUGFIX: Prevent a bug from occuring if the menu is spammed
        alarm_config_mode = False


def z_pad(number): # Zero pad a number
    num_str = str(number)
    logger.debug(f"Zero padding {number}")
    if '_' in num_str:
        return num_str
    else:
        return f"{int(num_str):02d}"


# Bugfix: Don't print text to screen when clock is halted
def screentext(string: str):
    if not halt_loop:
        logger.debug(f"Printing '{string}' to LCD")
        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 and dt_obj[5] == 0: # Update hour if minutes & seconds 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: # Add notification symbol
            lcd.move_to(15,0)
            screentext(chr(0))

    # Logic to check if the alarm should go off, like the most important bit lol
    if alarm_time is not None and not alarm_status:
        logger.debug(alarm_time)
        if "_" in str(alarm_time["hour"]) or "_" in str(alarm_time["minute"]): # BUGFIX: Capture and fix a bug which occurs when menus are spammed
            alarm_time = None

        elif 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+