# festival-of-lights.py
#
# Show a planet name + image on a HUB75 panel when a capacitive pad is touched.
# Uses a Raspberry Pi Pico / Pico 2 with a HUB75 matrix and an MPR121
# capacitive touch breakout.

# ---- WIRING POSITIONS ----

# --- HUB75 ---
# R1 - GP2
# G1 - GP3
# B1 - GP6
# GND1 - Rail Ground
# R2 - GP7
# G2 - GP8
# B2 - GP9
# GND2 - EMPTY

# A - GP10
# B - GP14
# C - GP15
# D - GP20

# CLK - GP11
# LAT - GP12
# OE - GP13
#GND3 - Rail Ground

# --- STEMMAQT (MPR121 Capacative Touch Sensor, VL53L1X TOF Sensor) ---
# Power/Red - 3.3v Rail
# GND/Black - Rail Ground
# Blue - GP4
# Yellow - GP5

# --- Neopixel light strip ---
# Power - 3.3v Rail
# Ground - Rail Ground
# Data - GP21

# --- Speaker ---
# Power - 3.3V Rail
# Ground - Rail Ground
# Data - GP22

import board
import displayio
import time
import gc
import rgbmatrix
import framebufferio
from adafruit_bitmap_font import bitmap_font
from adafruit_display_text.label import Label
import adafruit_mpr121
import busio, sdcardio, storage, os, digitalio
import adafruit_vl53l1x, neopixel
import pwmio
from audiopwmio import PWMAudioOut as AudioOut
import random



# ----- Setup I2C + capacitive touch (MPR121) -----
displayio.release_displays()

i2c = board.STEMMA_I2C()
touchpad = adafruit_mpr121.MPR121(i2c)  # touchpad[0]..touchpad[11]

# ---- Setup distance sensor and side lights ----
num_lights = 30
board_distance_cm = 60
touch_pad = adafruit_mpr121.MPR121(board.STEMMA_I2C())
strip = neopixel.NeoPixel(board.GP21, num_lights, brightness = 0.5)

distance_sensor = adafruit_vl53l1x.VL53L1X(i2c)
distance_sensor.distance_mode = 1
distance_sensor.timing_budget = 100
distance_sensor.start_ranging()

distance_ratio = num_lights / board_distance_cm

WHITE = (255, 255, 255)
BLACK = (0, 0, 0)

# ---- Setup and test the SD card ----
SD_CS = board.GP17

# SPI0 setup for SD card
spi0 = busio.SPI(board.GP18, board.GP19, board.GP16)
sdcard = sdcardio.SDCard(spi0, SD_CS)
vfs = storage.VfsFat(sdcard)

try:
    storage.mount(vfs, "/sd")
    print("😎 sd card mounted")
except ValueError:
    print("❌ no SD card")


# ---- Setup speaker and Tone Creation ----
tone = None
audio = None

tone_octive_diff = 232
tone_ratio = tone_octive_diff / board_distance_cm

def start_tone_mode():
    """Create PWMOut on GP22 for distance-based tone."""
    global tone, audio
    # If audio was being used, turn it off
    if audio is not None:
        audio.deinit()
        audio = None
    # (Re)create the PWM tone object if needed
    if tone is None:
        tone = pwmio.PWMOut(board.GP22, variable_frequency=True)
        tone.duty_cycle = 0  # start silent


# -----Could not figure out the MP3, some of them would work, others wouldn't
# and did not have time to troubleshoot-----

# def start_audio_mode():
#     """Create PWMAudioOut on GP22 for MP3 playback."""
#     global tone, audio
#     # If tone was being used, turn it off
#     if tone is not None:
#         tone.deinit()
#         tone = None
#     # (Re)create the audio object if needed
#     if audio is None:
#         audio = AudioOut(board.GP22)

# ---- Setup MP3 decoder ----
# from audiomp3 import MP3Decoder
#
# path = "/sd/planets_songs/"
#
#
# # set up the mp3 decoder
# filename = "sun.mp3" # change to valid file in your path
# mp3_file = open(path + filename, "rb")
# decoder = MP3Decoder(mp3_file)
#
# MP3_BITRATE_KBPS = 128
#
#
# # Call this function passing in a String for the full filename with extension.
# def play_mp3(filename, start_seconds):
#         """Play a 10-second segment from an MP3, starting at start_seconds."""
#         segment_len = 10  # seconds
#
#         # Convert seconds -> byte offset (approx, assumes constant bitrate)
#         # bytes_per_second = (bitrate_kbps * 1000 bits/s) / 8
#         bytes_per_second = MP3_BITRATE_KBPS * 1000 // 8
#         start_byte = int(start_seconds * bytes_per_second)
#
#         # Switch GP22 into audio mode (deinit tone, init AudioOut)
#         start_audio_mode()
#
#         # Open file and seek to the desired start position
#         f = open(path + filename, "rb")
#         f.seek(start_byte)
#         decoder.file = f
#
#         # Start playback
#         audio.play(decoder)
#
#         # Let it play for up to 10 seconds (or until file ends)
#         t0 = time.monotonic()
#         while audio.playing and (time.monotonic() - t0 < segment_len):
#             strip.fill(PLANET_COLORS[pad_index])
#             show_planet(pad_index)
#
#         audio.stop()
#         f.close()
#
#         # Go back to tone mode on GP22 when done
#         start_tone_mode()


# ----- Setup HUB75 matrix for Pico -----
matrix = rgbmatrix.RGBMatrix(
    width=64,    # Change if your panel is a different size
    height=32,
    bit_depth=6,
    rgb_pins=[
        board.GP2,   # R1
        board.GP3,   # G1
        board.GP6,   # B1
        board.GP7,   # R2
        board.GP8,   # G2
        board.GP9,   # B2
    ],
    addr_pins=[
        board.GP10,  # A
        board.GP14,  # B
        board.GP15,  # C
        board.GP20,  # D
    ],
    clock_pin=board.GP11,
    latch_pin=board.GP12,
    output_enable_pin=board.GP13,
    tile=1,
    serpentine=False,
    doublebuffer=True,
)

display = framebufferio.FramebufferDisplay(matrix)
WIDTH = display.width
HEIGHT = display.height

# Root group that everything gets added to
main_group = displayio.Group()
display.root_group = main_group

# ----- Fonts -----
# Make sure these font files are in a /fonts folder on CIRCUITPY
font_large = bitmap_font.load_font("/fonts/helvB12.bdf")

# ----- Colors -----
WHITE     = 0xFFFFFF
GOLDENROD = 0xDAA520

# ----- Planet configuration -----
# Each entry corresponds to a *touch pad index* on the MPR121.
# Pad 0 -> Neptune, Pad 1 -> Uranus, Pad 2 -> Saturn, etc.
PLANETS = [
    {
        "name": "Neptune",   # touch pad 0
        "image": "/sd/planet_images/neptune.bmp",  # <-- put your Neptune image filename here
        "color": WHITE,
    },
    {
        "name": "Uranus",    # touch pad 1
        "image": "/sd/planet_images/uranus.bmp",   # <-- Uranus image
        "color": GOLDENROD,
    },
    {
        "name": "Saturn",    # touch pad 2
        "image": "/sd/planet_images/saturn.bmp",   # <-- Saturn image
        "color": GOLDENROD,
    },
    {
        "name": "Jupiter",   # touch pad 3
        "image": "/sd/planet_images/jupiter.bmp",  # <-- Jupiter image
        "color": GOLDENROD,
    },
    {
        "name": "Mars",      # touch pad 4
        "image": "/sd/planet_images/mars.bmp",     # <-- Mars image
        "color": GOLDENROD,
    },
    {
        "name": "Earth",     # touch pad 5
        "image": "/sd/planet_images/earth.bmp",    # <-- Earth image
        "color": GOLDENROD,
    },
    {
        "name": "Venus",     # touch pad 6
        "image": "/sd/planet_images/venus.bmp",    # <-- Venus image
        "color": GOLDENROD,
    },
    {
        "name": "Mercury",   # touch pad 7
        "image": "/sd/planet_images/mercury.bmp",  # <-- Mercury image
        "color": GOLDENROD,
    },
    {
        "name": "Sun",   # touch pad 0
        "image": "/sd/planet_images/sun.bmp",  # <-- Sun image
        "color": GOLDENROD,
    },
    # Pads 8–11 are currently unused, but you could add more entries here
]

PLANET_COLORS = [
    (0, 0, 255), #Neptune, Blue
    (0, 255, 180), #Uranus, Teal
    (255, 140, 0), #Saturn, Orange
    (200, 120, 40), #Jupiter, Brown/Tan
    (255, 0, 0), #Mars, Red
    (0, 200, 80), #Earth, Green
    (240, 210, 80), #Venus, Gold
    (120, 120, 140), #Mercury, Gray
    (255, 220, 0), #Sun, Yellow
]


def play_win_tone():

    # If you have start_tone_mode() in your code, make sure we’re in tone mode:
    try:
        start_tone_mode()
    except NameError:
        pass  # ignore if you’re not using the audio/tone switching helpers

    # (frequency in Hz, duration in seconds)
    melody = [
        (784, 0.15),  # G5
        (880, 0.15),  # A5
        (988, 0.20),  # B5
        (0,   0.08),  # short pause
        (988, 0.15),  # B5
        (1175, 0.15), # D6
        (1319, 0.30), # E6
    ]

    for freq, dur in melody:
        if freq == 0:
            # rest (silence)
            tone.duty_cycle = 0
            time.sleep(dur)
        else:
            tone.frequency = freq
            tone.duty_cycle = 300  # or a bit higher if you want it louder
            time.sleep(dur)

    # turn sound off at the end
    tone.duty_cycle = 0

def play_lose_tone():

    # If you have start_tone_mode() in your code, make sure we’re in tone mode:
    try:
        start_tone_mode()
    except NameError:
        pass  # ignore if you’re not using the audio/tone switching helpers

    # (frequency in Hz, duration in seconds)
    melody = [
        (1319, 0.30),
        (1175, 0.15),
        (988, 0.15),
        (0, 0.08),
        (988, 0.20),
        (880, 0.15),
        (784, 0.15)
    ]

    for freq, dur in melody:
        if freq == 0:
            # rest (silence)
            tone.duty_cycle = 0
            time.sleep(dur)
        else:
            tone.frequency = freq
            tone.duty_cycle = 300  # or a bit higher if you want it louder
            time.sleep(dur)

    # turn sound off at the end
    tone.duty_cycle = 0


# ----- Helper: build a Group with image + centered text -----
def create_planet_group(name, image_path, color):
    """Return a displayio.Group containing the planet image + label.

    Layout:
      [ planet image ][ planet name text ]

    * Image is on the left side.
    * Text is on the right side, vertically centered on the 32-pixel-tall panel.
    """
    group = displayio.Group()

    image_width = 0

    # Try to load the planet image (if you gave a filename)
    if image_path:
        try:
            bitmap = displayio.OnDiskBitmap(image_path)
            tilegrid = displayio.TileGrid(
                bitmap,
                pixel_shader=bitmap.pixel_shader,
                x=0,
                y=0,
            )
            group.append(tilegrid)
            image_width = tilegrid.width
        except Exception as e:
            # If the image can't be loaded, we just skip it and still show text.
            print("Could not load image for", name, ":", e)
            image_width = 0

    # Create the label for the planet name
    label = Label(font_large, text=name, color=color)

    # bounding_box -> (x_offset, y_offset, width, height)
    _, _, text_w, text_h = label.bounding_box

    # Text starts just to the right of the image
    text_x = image_width + 2  # "off to the right side" of the icon

    # Vertically center the text on the 32-pixel panel
    text_y = (HEIGHT // 2) - (text_h // 2)

    label.x = text_x
    label.y = text_y

    group.append(label)

    return group

# ----- Helper: show a planet for 5 seconds and then clear the display -----
def show_planet(planet_index):
    # Guard against presses on pads > number of defined planets
    if planet_index < 0 or planet_index >= len(PLANETS):
        return

    planet = PLANETS[planet_index]

    print("Pad", planet_index, "->", planet["name"])

    gc.collect()

    planet_group = create_planet_group(
        planet["name"],
        planet["image"],
        planet["color"],
    )

    # Clear anything currently on screen
    while len(main_group):
        main_group.pop()

    main_group.append(planet_group)

    # Keep the planet on screen for 5 seconds
    start = time.monotonic()
    while time.monotonic() - start < 5:
        # Simple delay loop; during this time the display stays as-is.
        pass

    # After 5 seconds, clear the screen again
    while len(main_group):
        main_group.pop()

    gc.collect()
    time.sleep(0.2)  # small debounce so the same touch doesn't retrigger immediately




# ----- Main loop -----
print("*** Running HUB75 + capacitive touch planets demo ***")
print("Touch a pad on the MPR121 to show a planet.")

#set
strip.fill(BLACK)
time.sleep(0.5)
start_tone_mode()

while True:
    # We only care about as many pads as we have planets
    for pad_index in range(len(PLANETS)):
        #set the light amount to current distance
        if distance_sensor.data_ready:
            distance = distance_sensor.distance
            print(distance)
            if distance == None:
                pass
            else:
                lights = int((distance * distance_ratio) / 2)
                strip.fill(BLACK)
                strip[0:lights] = [WHITE] * lights
                strip[num_lights - lights: num_lights] = [WHITE] * lights

                tone.duty_cycle = 300
                freq = int((distance * tone_ratio) + 262)
                tone.frequency = freq

            # this is run when the "ship lands" on a planet/step
        if touchpad[pad_index].value:
            if pad_index == 5:
                play_win_tone()
            else:
                play_lose_tone()
            strip.fill(PLANET_COLORS[pad_index])
            tone.duty_cycle = 0
            show_planet(pad_index)
            strip.fill(BLACK)
            start_tone_mode()