#name=leds_spectre_zero.py
import machine
import time
import uos
from machine import Pin, PWM
import sh1106
import neopixel

# -----------------------------------------------------------------------------
# 1. CONFIGURATION MATÉRIELLE (RP2040 / PICO ZERO)
# - R sur GP8 (Slice 4)
# - G sur GP9 (Slice 4)
# - B sur GP12 (Slice 6)
# - Modulation sur GP11 (Slice 5)
# - LED témoin externe sur GP29
# -----------------------------------------------------------------------------

# OLED 1.3" SH1106 en I2C matériel (SCL sur GP5, SDA sur GP4, 100kHz stable)[cite: 1]
i2c = machine.I2C(0, scl=Pin(5), sda=Pin(4), freq=100000)
oled = sh1106.SH1106_I2C(128, 64, i2c, res=None, addr=0x3C, rotate=180)

# Écran de démarrage : Affichage "Gestion RGB" pendant 2 secondes[cite: 1]
oled.fill(0)
oled.text("Gestion RGB", 24, 28, 1)
oled.show()
time.sleep(2)

# ENCODEUR ROTATIF (CLK=GP14, DT=GP15, SW=GP2)[cite: 1]
pin_clk = Pin(14, Pin.IN, Pin.PULL_UP)
pin_dt = Pin(15, Pin.IN, Pin.PULL_UP)
pin_sw = Pin(2, Pin.IN, Pin.PULL_UP)

# PWM LEDS RGB (R sur GP8, G sur GP9, B sur GP12 - GP16 réservé NeoPixel)[cite: 1]
pwm_r = PWM(Pin(8))
pwm_g = PWM(Pin(9))
pwm_b = PWM(Pin(12))

for p in (pwm_r, pwm_g, pwm_b):
    p.freq(1000)  # Fréquence PWM de base des LEDs (1kHz stable)[cite: 1]

# BROCHE DE MODULATION SUR GP11 (Indépendant, Slice 5)[cite: 1]
pwm_mod = PWM(Pin(11))
pwm_mod.duty_u16(32768)  # Duty cycle à 50% par défaut[cite: 1]

# LED NEOPIXEL WS2812 SUR GP16[cite: 1]
np = neopixel.NeoPixel(Pin(16), 1)

# LED TÉMOIN EXTERNE SUR GP29[cite: 1]
pin_temoin = Pin(29, Pin.OUT)
pin_temoin.value(0)

# Dictionnaire élargi (couleurs pures + couleurs intermédiaires)[cite: 1]
COLOR_NAMES = {
    "rouge": (255, 0, 0),
    "orange": (255, 128, 0),
    "ambre": (255, 191, 0),
    "jaune": (255, 255, 0),
    "lime": (128, 255, 0),
    "vert": (0, 255, 0),
    "turquoise": (64, 224, 208),
    "cyan": (0, 255, 255),
    "bleu": (0, 0, 255),
    "violet": (128, 0, 255),
    "magenta": (255, 0, 255),
    "rose": (255, 105, 180),
    "blanc": (255, 255, 255),
    "noir": (0, 0, 0)
}

# -----------------------------------------------------------------------------
# 2. GESTION DES MESSAGES SUR L'OLED (Max 16 caractères par ligne)[cite: 1]
# -----------------------------------------------------------------------------
def show_message(title, msg, duration_sec=2.0):
    """Affiche un message ou une erreur sur l'écran OLED."""
    oled.fill(0)
    oled.text(f"[{title}]", 0, 0, 1)
    
    oled.text(msg[:16], 0, 20, 1)
    if len(msg) > 16:
        oled.text(msg[16:32], 0, 35, 1)
        
    oled.text("Appuyer...", 0, 52, 1)
    oled.show()
    
    start = time.time()
    while time.time() - start < duration_sec:
        if pin_sw.value() == 0:
            time.sleep_ms(50)
            while pin_sw.value() == 0:
                time.sleep_ms(10)
            break
        time.sleep_ms(10)

# -----------------------------------------------------------------------------
# 3. PARAMÈTRES ET EEPROM (MÉMOIRE FLASH INTERNE)[cite: 1]
# -----------------------------------------------------------------------------
PARAM_FILE = "eeprom_config.dat"

config = {
    "r": 0,             # Bleu pur par défaut[cite: 1]
    "g": 0,             # Bleu pur par défaut[cite: 1]
    "b": 255,           # Bleu pur par défaut[cite: 1]
    "duration": 5,
    "freq_mod": 10.0,   # Fréquence de modulation initiale[cite: 1]
    "file_name": "config.txt",
    "intensity": 100.0  # Intensité de 0 à 100.0 %[cite: 1]
}

leds_state = False  # État courant des LEDs (Allumées/Éteintes)[cite: 1]

# Table de correction Gamma (2.8) pour un rendu visuel fidèle[cite: 1]
GAMMA_TABLE = [int(((i / 255.0) ** 2.8) * 65535) for i in range(256)]

def save_to_eeprom():
    """Sauvegarde les paramètres courants et l'état des LEDs dans la mémoire Flash."""
    try:
        state_val = 1 if leds_state else 0
        with open(PARAM_FILE, "w") as f:
            f.write(f"{config['r']},{config['g']},{config['b']},{config['duration']},{config['freq_mod']},{config['file_name']},{config['intensity']},{state_val}\n")
        show_message("EEPROM", "Sauvegarde OK!", 1.5)
    except Exception as e:
        show_message("Erreur EEP", str(e)[:16], 2.0)

def load_from_eeprom():
    global config, leds_state
    try:
        with open(PARAM_FILE, "r") as f:
            data = f.readline().strip().split(",")
            if len(data) >= 8:
                config["r"] = int(data[0])
                config["g"] = int(data[1])
                config["b"] = int(data[2])
                config["duration"] = int(data[3])
                config["freq_mod"] = max(0.0, float(data[4]))  # Accepte 0.0 (Off)[cite: 1]
                config["file_name"] = data[5]
                config["intensity"] = float(data[6])
                leds_state = True if int(data[7]) == 1 else False
    except:
        pass

load_from_eeprom()

# Gestion de la fréquence PWM initiale de modulation[cite: 1]
def apply_freq(freq):
    """Met à jour la fréquence ou positionne à 1 si 0 (laisse passer le PWM des LEDs)."""
    if freq > 0:
        safe_freq = max(10, int(freq))
        pwm_mod.freq(safe_freq)
        pwm_mod.duty_u16(32768)  # Signal carré à 50%[cite: 1]
    else:
        # Fréquence à 0 : signal de sortie à 1 (100% duty cycle)[cite: 1]
        pwm_mod.duty_u16(65535)

if config["freq_mod"] > 0:
    apply_freq(config["freq_mod"])
else:
    apply_freq(0)

# -----------------------------------------------------------------------------
# 4. GESTION DE L'ENCODEUR ROTATIF[cite: 1]
# -----------------------------------------------------------------------------
encoder_ticks = 0
last_clk = pin_clk.value()

def encoder_isr(pin):
    global encoder_ticks, last_clk
    clk_val = pin_clk.value()
    if clk_val != last_clk and clk_val == 0:
        if pin_dt.value() != clk_val:
            encoder_ticks += 1
        else:
            encoder_ticks -= 1
    last_clk = clk_val

pin_clk.irq(trigger=Pin.IRQ_FALLING | Pin.IRQ_RISING, handler=encoder_isr)

def get_button_click():
    if pin_sw.value() == 0:
        time.sleep_ms(50)
        if pin_sw.value() == 0:
            while pin_sw.value() == 0:
                time.sleep_ms(10)
            return True
    return False

# -----------------------------------------------------------------------------
# 5. FONCTIONS COULEURS, SPECTRE & NEOPIXEL[cite: 1]
# -----------------------------------------------------------------------------
current_r, current_g, current_b = 0, 0, 0
last_np_color = (-1, -1, -1)

def wheel(pos):
    """Génère les couleurs du spectre solaire (0 à 255).[cite: 1]"""
    pos = pos % 256
    if pos < 85:
        return (pos * 3, 255 - pos * 3, 0)
    elif pos < 170:
        pos -= 85
        return (255 - pos * 3, 0, pos * 3)
    else:
        pos -= 170
        return (0, pos * 3, 255 - pos * 3)

def get_hue_from_rgb(r, g, b):
    """Retrouve la position sur la roue chromatique (hue) la plus proche des valeurs RGB.[cite: 1]"""
    best_hue = 0
    min_diff = 999999
    for h in range(256):
        wr, wg, wb = wheel(h)
        diff = abs(wr - r) + abs(wg - g) + abs(wb - b)
        if diff < min_diff:
            min_diff = diff
            best_hue = h
    return best_hue

def apply_rgb(r, g, b, intensity=100.0):
    global current_r, current_g, current_b, last_np_color
    factor = max(0.0, min(100.0, intensity)) / 100.0
    scaled_r = int(r * factor)
    scaled_g = int(g * factor)
    scaled_b = int(b * factor)
    
    current_r, current_g, current_b = scaled_r, scaled_g, scaled_b
    
    pwm_r.duty_u16(GAMMA_TABLE[scaled_r])
    pwm_g.duty_u16(GAMMA_TABLE[scaled_g])
    pwm_b.duty_u16(GAMMA_TABLE[scaled_b])
    
    new_color = (scaled_r, scaled_g, scaled_b)
    if new_color != last_np_color:
        try:
            np[0] = new_color
            np.write()
            last_np_color = new_color
        except:
            pass

def fade_to_color(target_r, target_g, target_b, target_intensity, duration_ms=1000):
    """Fondu progressif vers la couleur et l'intensité cibles.[cite: 1]"""
    global current_r, current_g, current_b
    steps = 50
    delay = duration_ms // steps
    
    start_r, start_g, start_b = current_r, current_g, current_b
    factor = target_intensity / 100.0
    final_target_r = int(target_r * factor)
    final_target_g = int(target_g * factor)
    final_target_b = int(target_b * factor)
    
    for i in range(1, steps + 1):
        if get_button_click():
            return False
            
        r = int(start_r + (final_target_r - start_r) * i / steps)
        g = int(start_g + (final_target_g - start_g) * i / steps)
        b = int(start_b + (final_target_b - start_b) * i / steps)
        
        pwm_r.duty_u16(GAMMA_TABLE[max(0, min(255, r))])
        pwm_g.duty_u16(GAMMA_TABLE[max(0, min(255, g))])
        pwm_b.duty_u16(GAMMA_TABLE[max(0, min(255, b))])
        
        try:
            np[0] = (max(0, min(255, r)), max(0, min(255, g)), max(0, min(255, b)))
            np.write()
        except:
            pass
            
        time.sleep_ms(delay)
        
    apply_rgb(target_r, target_g, target_b, target_intensity)
    return True

def toggle_leds_manual():
    """Allumage / Extinction manuelle des LEDs via le menu.[cite: 1]"""
    global leds_state
    leds_state = not leds_state
    if leds_state:
        pin_temoin.value(1)
        apply_rgb(config["r"], config["g"], config["b"], config["intensity"])
        apply_freq(config["freq_mod"])
        show_message("LEDs", "Allumees", 1.0)
    else:
        pin_temoin.value(0)
        apply_rgb(0, 0, 0, 0)
        apply_freq(0)
        show_message("LEDs", "Eteintes", 1.0)

def run_txt_program():
    """Activation et lecture robuste du fichier .TXT avec décompte et affichage de 0 à la fin de chaque étape.[cite: 1]"""
    file_path = config["file_name"]
    try:
        with open(file_path, "r") as f:
            pin_temoin.value(1)
            for line in f:
                line = line.strip()
                if not line or line.startswith("#"):
                    continue
                
                parts = [p.strip() for p in line.split(",") if p.strip() != ""]
                if not parts:
                    continue
                
                token = parts[0].lower()
                target_r, target_g, target_b = 0, 0, 0
                dur = 5
                freq = config["freq_mod"]
                file_intensity = config["intensity"]
                
                if token in COLOR_NAMES:
                    target_r, target_g, target_b = COLOR_NAMES[token]
                    if len(parts) > 1:
                        try: dur = int(parts[1])
                        except: pass
                    if len(parts) > 2:
                        try: freq = float(parts[2])
                        except: pass
                    if len(parts) > 3:
                        try: file_intensity = float(parts[3])
                        except: pass
                else:
                    if len(parts) >= 3:
                        try:
                            target_r = max(0, min(255, int(parts[0])))
                            target_g = max(0, min(255, int(parts[1])))
                            target_b = max(0, min(255, int(parts[2])))
                            if len(parts) > 3: dur = max(0, int(parts[3]))
                            if len(parts) > 4: freq = float(parts[4])
                            if len(parts) > 5: file_intensity = float(parts[5])
                        except:
                            continue
                    else:
                        continue
                
                apply_freq(freq)
                if freq >= 0:
                    config["freq_mod"] = freq

                # Transition en fondu vers la couleur cible[cite: 1]
                if not fade_to_color(target_r, target_g, target_b, file_intensity, 1000):
                    apply_rgb(0, 0, 0, 0)
                    apply_freq(0)
                    pin_temoin.value(0)
                    return
                
                # Boucle de décompte seconde par seconde[cite: 1]
                start_step = time.time()
                last_remaining = -1
                
                while dur == 0 or (time.time() - start_step) < dur:
                    if get_button_click():
                        apply_rgb(0, 0, 0, 0)
                        apply_freq(0)
                        pin_temoin.value(0)
                        return
                    
                    now = time.time()
                    elapsed = int(now - start_step)
                    remaining = max(0, dur - elapsed) if dur > 0 else elapsed
                    
                    if remaining != last_remaining or last_remaining == -1:
                        last_remaining = remaining
                        
                        oled.fill(0)
                        oled.text("== EXECUTION ==", 10, 0, 1)
                        
                        if dur > 0:
                            oled.text(f"Temps : {remaining} s", 0, 13, 1)
                        else:
                            oled.text(f"Temps : {elapsed} s", 0, 13, 1)
                            
                        oled.text(f"RGB: {target_r},{target_g},{target_b}", 0, 26, 1)
                        
                        if freq <= 0:
                            freq_str = "Off"
                        else:
                            freq_str = f"{int(freq)}Hz"
                        oled.text(f"{file_intensity:.0f}% {freq_str}", 0, 39, 1)
                        oled.text("Clic: Arreter", 0, 52, 1)
                        oled.show()
                        
                    time.sleep_ms(50)
                
                # Forcer l'affichage à 0 s à la fin de l'étape (si durée définie)[cite: 1]
                if dur > 0:
                    oled.fill(0)
                    oled.text("== EXECUTION ==", 10, 0, 1)
                    oled.text("Temps : 0 s", 0, 13, 1)
                    oled.text(f"RGB: {target_r},{target_g},{target_b}", 0, 26, 1)
                    if freq <= 0:
                        freq_str = "Off"
                    else:
                        freq_str = f"{int(freq)}Hz"
                    oled.text(f"Int:{file_intensity:.0f}% F:{freq_str}", 0, 39, 1)
                    oled.text("Clic: Arreter", 0, 52, 1)
                    oled.show()
                    time.sleep_ms(100)  # Pause de 0.1 seconde[cite: 1]
                            
        fade_to_color(0, 0, 0, 0, 1000)
        apply_freq(0)
        pin_temoin.value(0)
        show_message("Prog Flash", "Fin execution", 1.5)
    except Exception as e:
        apply_rgb(0, 0, 0, 0)
        apply_freq(0)
        pin_temoin.value(0)
        show_message("Erreur Fichier", "Introuvable/Inval", 2.0)

# Application de l'état initial des LEDs chargé depuis l'EEPROM au démarrage[cite: 1]
if leds_state:
    pin_temoin.value(1)
    apply_rgb(config["r"], config["g"], config["b"], config["intensity"])
    apply_freq(config["freq_mod"])
else:
    pin_temoin.value(0)
    apply_rgb(0, 0, 0, 0)
    apply_freq(0)

# -----------------------------------------------------------------------------
# 6. INTERFACE MENU & SÉLECTION DU SPECTRE SOLAIRE[cite: 1]
# -----------------------------------------------------------------------------
def adjust_frequency():
    """Modifie et applique en temps réel la fréquence de modulation 
    avec des pas adaptatifs, gérant le 0 (Off) et l'appui long >2s pour mettre à Off."""
    global encoder_ticks, config
    freq = int(config["freq_mod"])
    encoder_ticks = 0
    
    apply_freq(freq)
    
    while True:
        ticks = encoder_ticks
        if ticks != 0:
            encoder_ticks = 0
            abs_ticks = abs(ticks)
            
            if abs_ticks >= 3:
                step = 1000   # 1 kHz[cite: 1]
            elif abs_ticks >= 2:
                step = 100    # 100 Hz[cite: 1]
            else:
                step = 1      # 1 Hz[cite: 1]
                
            if ticks > 0:
                if freq == 0:
                    freq = 10  # Repart à 10Hz si on augmente depuis Off
                else:
                    freq += step
            else:
                freq -= step
                if freq < 10:
                    freq = 0  # En dessous de 10Hz, passe à 0 (Off)[cite: 1]
                
            freq = max(0, min(200000, freq))
            apply_freq(freq)
            
        oled.fill(0)
        oled.text("< Freq Modulation >", 0, 2, 1)
        
        if freq == 0:
            freq_str = "Off"
        else:
            freq_str = f"{freq} Hz"
            
        oled.text(f"Freq: {freq_str}", 0, 18, 1)
        oled.text("Long:>2s (-> Off)", 0, 34, 1)
        oled.text("Clic: Valider", 0, 50, 1)
        oled.show()
        
        if pin_sw.value() == 0:
            time.sleep_ms(50)
            if pin_sw.value() == 0:
                start_time = time.ticks_ms()
                long_pressed = False
                
                # Détection d'un appui long > 2 secondes (2000 ms)
                while pin_sw.value() == 0:
                    if time.ticks_diff(time.ticks_ms(), start_time) > 2000:
                        long_pressed = True
                        break
                    time.sleep_ms(10)
                
                if long_pressed:
                    freq = 0  # Appui long > 2s met la fréquence à 0 (Off)
                    apply_freq(freq)
                    while pin_sw.value() == 0:
                        time.sleep_ms(10)
                    config["freq_mod"] = float(freq)
                    show_message("Frequence", "Off (0 Hz)", 1.2)
                    return
                else:
                    config["freq_mod"] = float(freq)
                    show_message("Frequence", f"{freq_str} OK", 1.2)
                    return
                    
        time.sleep_ms(20)

def adjust_value(title, current_val, min_val, max_val, step=1):
    global encoder_ticks
    encoder_ticks = 0
    val = current_val

    while True:
        if encoder_ticks != 0:
            val += encoder_ticks * step
            val = max(min_val, min(max_val, val))
            encoder_ticks = 0

        oled.fill(0)
        oled.text(f"< {title} >", 0, 10, 1)
        oled.text(f"Valeur: {val}", 10, 35, 1)
        oled.text("Clic: Valider", 0, 55, 1)
        oled.show()

        if get_button_click():
            return val
        time.sleep_ms(20)

def adjust_spectrum_and_intensity():
    """Parcours du spectre solaire à partir des valeurs actuelles et choix de l'intensité.[cite: 1]"""
    global encoder_ticks, config, leds_state
    
    hue = get_hue_from_rgb(config["r"], config["g"], config["b"])
    intensity = config["intensity"]
    focus = 0
    encoder_ticks = 0
    
    pin_temoin.value(1)

    while True:
        if encoder_ticks != 0:
            if focus == 0:
                hue = (hue + encoder_ticks * 4) % 256
            elif focus == 1:
                intensity = max(0.0, min(100.0, intensity + encoder_ticks * 5.0))
            elif focus == 2:
                focus = (focus + encoder_ticks) % 3
            encoder_ticks = 0

        raw_r, raw_g, raw_b = wheel(hue)
        apply_rgb(raw_r, raw_g, raw_b, intensity)

        oled.fill(0)
        oled.text("< Spectre Solaire >", 0, 0, 1)
        oled.text(f"TXT: {raw_r},{raw_g},{raw_b}", 0, 13, 1)
        
        h_sel = ">" if focus == 0 else " "
        i_sel = ">" if focus == 1 else " "
        
        oled.text(f"{h_sel} Hue: {hue}", 0, 27, 1)
        oled.text(f"{i_sel} Int: {intensity:.1f}%", 0, 39, 1)
        
        if focus == 2:
            oled.fill_rect(0, 52, 128, 12, 1)
            oled.text("> Valider", 4, 53, 0)
        else:
            oled.text("  Valider", 4, 53, 1)
            
        oled.show()

        if get_button_click():
            if focus < 2:
                focus += 1
            else:
                config["r"] = raw_r
                config["g"] = raw_g
                config["b"] = raw_b
                config["intensity"] = intensity
                leds_state = True
                apply_freq(config["freq_mod"])
                show_message("Spectre RGB", f"RGB:{raw_r},{raw_g},{raw_b}", 1.5)
                return
        time.sleep_ms(20)

def choose_txt_file():
    """Recherche dynamique des fichiers .txt présents dans la mémoire Flash.[cite: 1]"""
    try:
        all_files = uos.listdir()
        files = [f for f in all_files if f.endswith(".txt")]
    except:
        files = []
    
    if not files:
        show_message("Fichiers TXT", "Aucun trouve !", 2.0)
        return

    idx = 0
    global encoder_ticks
    encoder_ticks = 0
    
    while True:
        if encoder_ticks != 0:
            idx = (idx + encoder_ticks) % len(files)
            encoder_ticks = 0
            
        oled.fill(0)
        oled.text("< Fichier Flash >", 0, 5, 1)
        oled.text("Selectionnez :", 0, 20, 1)
        
        oled.fill_rect(0, 38, 128, 14, 1)
        oled.text("> " + files[idx], 4, 41, 0)
        oled.show()
        
        if get_button_click():
            config["file_name"] = files[idx]
            show_message("Fichier", "Selectionne!", 1.2)
            return
        time.sleep_ms(20)

def main_menu():
    global encoder_ticks, config
    
    menu_items = [
        "Choix fichier",
        "Spectre RGB",
        "Activer prog",
        "Freq modulation",
        "On / Off",
        "Sauver EEP"
    ]
    
    current_idx = 0
    encoder_ticks = 0

    while True:
        if encoder_ticks != 0:
            current_idx = (current_idx + encoder_ticks) % len(menu_items)
            encoder_ticks = 0

        oled.fill(0)
        
        for i in range(6):
            y_pos = i * 9
            text_line = menu_items[i]
            
            if i == current_idx:
                oled.fill_rect(0, y_pos, 128, 9, 1)
                oled.text("> " + text_line, 2, y_pos + 1, 0)
            else:
                oled.text("  " + text_line, 2, y_pos + 1, 1)

        oled.fill_rect(0, 55, 128, 9, 0)
        oled.hline(0, 54, 128, 1)
        
        f_val = int(config['freq_mod'])
        f_str = "Off" if f_val == 0 else f"{f_val}Hz"
        info_text = f"{f_str} {config['intensity']:.0f}%"
        oled.text(info_text[:16], 2, 55, 1)
        
        oled.show()

        if get_button_click():
            selected = menu_items[current_idx]
            
            if "Choix" in selected:
                choose_txt_file()
            elif "Spectre" in selected:
                adjust_spectrum_and_intensity()
            elif "Activer" in selected:
                run_txt_program()
            elif "Freq" in selected:
                adjust_frequency()
            elif "On" in selected:
                toggle_leds_manual()
            elif "Sauver" in selected:
                save_to_eeprom()

        time.sleep_ms(30)

if __name__ == "__main__":
    main_menu()