import utime
from machine import Pin, I2C
import ssd1306

#
# =======================
#  Pin Definitions
# =======================
#
ENCODER_CLK = 2  # GP2
ENCODER_DT  = 3  # GP3
ENCODER_SW  = 4  # GP4

I2C_SDA_PIN = 0  # GP0
I2C_SCL_PIN = 1  # GP1

#
# =======================
#  I2C + Display Setup
# =======================
#
i2c = I2C(0, sda=Pin(I2C_SDA_PIN), scl=Pin(I2C_SCL_PIN), freq=400_000)
display = ssd1306.SSD1306_I2C(128, 64, i2c)

#
# =======================
#  Rotary Encoder Pins
# =======================
#
clk_pin = Pin(ENCODER_CLK, Pin.IN, Pin.PULL_UP)
dt_pin  = Pin(ENCODER_DT,  Pin.IN, Pin.PULL_UP)
sw_pin  = Pin(ENCODER_SW,  Pin.IN, Pin.PULL_UP)

#
# =======================
#  Screen (Mode) States
# =======================
#
GRINDER          = 0
STOVE            = 1
STOVE_TIMER      = 2
EXTRACTION_TIMER = 3
STATS_SCREEN     = 4

mode = GRINDER

#
# =======================
#  User Settings
# =======================
#
grinder_setting = 0
stove_setting   = 0

#
# =======================
#  Timers (Stove/Extraction)
# =======================
#
stove_timer_running      = False
stove_start_time_ms      = 0
stove_duration_s         = 0.0  # final result in seconds

extraction_timer_running = False
extraction_start_time_ms = 0
extraction_duration_s    = 0.0  # final result in seconds

#
# For detecting rotary encoder falling edge
#
prev_clk_value = clk_pin.value()

#
# For button debounce
#
last_button_press_ms = 0
DEBOUNCE_MS = 300

#
# =======================
#  Helper Functions
# =======================
#
def clamp(value, min_val=0, max_val=100):
    """Clamp an integer 'value' to the range [min_val, max_val]."""
    return max(min_val, min(value, max_val))

def stoveAdvice(duration_s):
    """
    Provide advice based on the stove timer result.
    Target is between 3 and 6 minutes (180–360 seconds).
    """
    if duration_s < 180:
        return "Reduce heat"
    elif duration_s > 360:
        return "Increase heat"
    else:
        return "Dope"

def extractionAdvice(duration_s):
    """
    Provide advice based on the extraction timer result.
    Target is between 27 and 33 seconds.
    """
    if duration_s < 27:
        return "Grind finer"
    elif duration_s > 33:
        return "Grind coarser"
    else:
        return "Nailed"

def setMode(new_mode):
    """Perform any setup when entering a new mode, then update the display."""
    global mode
    global stove_timer_running, stove_start_time_ms
    global extraction_timer_running, extraction_start_time_ms

    mode = new_mode

    # If we enter STOVE_TIMER or EXTRACTION_TIMER, start automatically
    if mode == STOVE_TIMER:
        stove_timer_running = True
        stove_start_time_ms = utime.ticks_ms()
    elif mode == EXTRACTION_TIMER:
        extraction_timer_running = True
        extraction_start_time_ms = utime.ticks_ms()

    updateDisplay()

def nextMode():
    """Go to the next screen in the sequence, wrapping after STATS_SCREEN."""
    new_mode = (mode + 1) % 5  # 5 total screens
    setMode(new_mode)

def updateValue(delta):
    """
    If we're on the GRINDER or STOVE screen, adjust the value by 'delta',
    clamped between 0 and 100.
    """
    global grinder_setting, stove_setting

    if mode == GRINDER:
        grinder_setting = clamp(grinder_setting + delta)
    elif mode == STOVE:
        stove_setting = clamp(stove_setting + delta)

def updateDisplay():
    """Render text to the SSD1306 display according to the current mode."""
    display.fill(0)

    if mode == GRINDER:
        display.text("GRINDER:", 0, 0)
        display.text(str(grinder_setting), 0, 16)
        # Show last extraction advice
        adv = extractionAdvice(extraction_duration_s)
        display.text(adv, 0, 32)

    elif mode == STOVE:
        display.text("STOVE:", 0, 0)
        display.text(str(stove_setting), 0, 16)
        # Show last stove advice
        adv = stoveAdvice(stove_duration_s)
        display.text(adv, 0, 32)

    elif mode == STOVE_TIMER:
        display.text("STOVE TIMER", 0, 0)
        if stove_timer_running:
            elapsed_ms = utime.ticks_ms() - stove_start_time_ms
            elapsed_s  = elapsed_ms / 1000.0
            display.text("{:.1f}s".format(elapsed_s), 0, 16)

    elif mode == EXTRACTION_TIMER:
        display.text("EXTRACTION", 0, 0)
        if extraction_timer_running:
            elapsed_ms = utime.ticks_ms() - extraction_start_time_ms
            elapsed_s  = elapsed_ms / 1000.0
            display.text("{:.1f}s".format(elapsed_s), 0, 16)

    elif mode == STATS_SCREEN:
        # 4 lines: stove time, stove advice, extraction time, extraction advice
        stove_line = "Stove: {:.1f}s".format(stove_duration_s)
        display.text(stove_line, 0, 0)

        stove_adv  = stoveAdvice(stove_duration_s)
        display.text(stove_adv, 0, 16)

        extr_line  = "Extr.: {:.1f}s".format(extraction_duration_s)
        display.text(extr_line, 0, 32)

        extr_adv   = extractionAdvice(extraction_duration_s)
        display.text(extr_adv, 0, 48)

    display.show()

#
# =======================
#  Initial Display
# =======================
#
updateDisplay()

#
# =======================
#  Main Loop
# =======================
#
while True:
    # 1) Check button press (active-low)
    if sw_pin.value() == 0:
        now_ms = utime.ticks_ms()
        if (now_ms - last_button_press_ms) > DEBOUNCE_MS:
            last_button_press_ms = now_ms

            if mode == GRINDER or mode == STOVE:
                nextMode()

            elif mode == STOVE_TIMER:
                # Stop stove timer, record final duration, then next
                stove_timer_running = False
                elapsed_ms = utime.ticks_ms() - stove_start_time_ms
                stove_duration_s = elapsed_ms / 1000.0
                nextMode()

            elif mode == EXTRACTION_TIMER:
                # Stop extraction timer, record final duration, then next
                extraction_timer_running = False
                elapsed_ms = utime.ticks_ms() - extraction_start_time_ms
                extraction_duration_s = elapsed_ms / 1000.0
                nextMode()

            elif mode == STATS_SCREEN:
                nextMode()  # Back to Grinder

            updateDisplay()

    # 2) Check rotary encoder rotation (CLK falling edge)
    current_clk_value = clk_pin.value()
    if (current_clk_value != prev_clk_value) and (current_clk_value == 0):
        # Only adjust a value if on GRINDER or STOVE
        if mode in (GRINDER, STOVE):
            if dt_pin.value() == 1:
                # dt=HIGH => clockwise => +1
                updateValue(+1)
            else:
                # dt=LOW => counterclockwise => -1
                updateValue(-1)
            updateDisplay()

    prev_clk_value = current_clk_value

    # 3) If a timer is running, update the display so the user sees real-time progress
    if mode == STOVE_TIMER and stove_timer_running:
        updateDisplay()
    elif mode == EXTRACTION_TIMER and extraction_timer_running:
        updateDisplay()

    # 4) Small delay to avoid missing fast encoder steps
    utime.sleep_ms(2)
BOOTSELLED1239USBRaspberryPiPico©2020RP2-8020/21P64M15.00TTT