# --------------------------------------------------------------------
# NadHAT MK2 + Raspberry Pi Pico
# Gestion SMS : Prénom / Couleur / Luminosité / Reset + Bouton
# Version : 0.5.1a
# Auteur  : François Mocq (Framboise314)
# Licence : MIT
# --------------------------------------------------------------------

import time
import ujson as json

from machine import UART, Pin, I2C, RTC
import neopixel
import ssd1306

from sim76xx.core import SIM76XX, Notifications
from sim76xx.sms import SMS

from couleurs import COULEURS  # /lib/couleurs.py

# --------------------------------------------------------------------
# CONFIGURATION MATERIELLE ET PARAMETRES
# --------------------------------------------------------------------

PIN_UART_TX = 4          # GP4 TX Pico -> RX modem
PIN_UART_RX = 5          # GP5 RX Pico <- TX modem
PIN_PWRKEY  = 26         # GP26 -> PWRKEY du modem

PIN_I2C_SDA = 0
PIN_I2C_SCL = 1
I2C_FREQ    = 400000
OLED_LARGEUR = 128
OLED_HAUTEUR = 64

PIN_WS2812  = 8
NB_LEDS     = 16         # Anneau 16 LEDs

PIN_BOUTON  = 10         # Entrée bouton poussoir (pull-up interne)

FICHIER_CONFIG = "config.json"

NUM_NADHAT   = "07xxxx3993"   # Numéro de la carte NadHAT
NUM_ADMIN    = "06xxxx4501"   # Numéro administrateur
ADMIN_SUFFIX = "4501"         # 4 derniers chiffres de l’admin

DUREE_TIMEOUT_SECONDES = 5 * 60  # 5 minutes d’inactivité
LIEN_SITE = "framboise314.fr"    # Lien ajouté dans les SMS visiteurs

# --------------------------------------------------------------------
# VARIABLES GLOBALES
# --------------------------------------------------------------------

rtc = RTC()
uart_modem = None
modem = None
sms_modem = None
ecran = None
anneau = None

texte_ligne2 = ""
texte_ligne3 = ""
texte_ligne4 = ""

dernier_num4 = "----"
dernier_num_complet = None
temps_derniere_activite = 0

sms_total = 0
sms_jour  = 0
jour_courant = 0
luminosite = 255
mode_attente_prenom = True

# Animation comète
pos_attractif = 0
hue_attractif = 0
dernier_tick_attractif = 0
couleur_animation = None

# Bouton poussoir
bouton = None
bouton_etat_precedent = 1
bouton_dernier_ms = 0

# --------------------------------------------------------------------
# OUTILS TEXTE
# --------------------------------------------------------------------

ACCENTS_SOURCE = "àáâäãåçéèêëîïìíôöòóùúûüÿñÀÁÂÄÃÅÇÉÈÊËÎÏÌÍÔÖÒÓÙÚÛÜŸÑ"
ACCENTS_CIBLE  = "aaaaaaceeeeiiiioooouuuuynAAAAAACEEEEIIIIOOOOUUUUYN"


def supprimer_accents(texte):
    """Remplace les lettres accentuées par leurs équivalents simples."""
    resultat = ""
    for c in texte:
        idx = ACCENTS_SOURCE.find(c)
        resultat += ACCENTS_CIBLE[idx] if idx >= 0 else c
    return resultat


def normaliser_texte(texte):
    """Nettoie un texte pour comparaison : minuscules, sans accents ni espaces."""
    if not isinstance(texte, str):
        return ""
    t = texte.strip().lower()
    t = supprimer_accents(t).replace(" ", "")
    return t


def nettoyer_pour_oled(texte):
    """Supprime les accents pour un affichage lisible sur l’écran OLED."""
    if not isinstance(texte, str):
        return ""
    return supprimer_accents(texte)


def nettoyer_pour_sms(texte):
    """Supprime les accents avant envoi SMS (évite les caractères non supportés)."""
    if not isinstance(texte, str):
        texte = str(texte)
    return supprimer_accents(texte)

# --------------------------------------------------------------------
# CONFIGURATION JSON
# --------------------------------------------------------------------

def charger_config():
    global sms_total, sms_jour, jour_courant, luminosite
    try:
        with open(FICHIER_CONFIG, "r") as f:
            cfg = json.load(f)
    except:
        cfg = {}
    sms_total = int(cfg.get("sms_total", 0))
    sms_jour = int(cfg.get("sms_jour", 0))
    jour_courant = int(cfg.get("jour_courant", 0))
    luminosite = int(cfg.get("luminosite", 255))


def sauver_config():
    cfg = {
        "sms_total": sms_total,
        "sms_jour": sms_jour,
        "jour_courant": jour_courant,
        "luminosite": luminosite,
    }
    try:
        with open(FICHIER_CONFIG, "w") as f:
            json.dump(cfg, f)
    except Exception as e:
        print("Erreur sauvegarde config:", e)


def mettre_a_jour_journee():
    """Remet le compteur journalier à zéro si la date a changé."""
    global sms_jour, jour_courant
    an, mo, jo, jsem, h, m, s, ss = rtc.datetime()
    if jour_courant != jo:
        jour_courant = jo
        sms_jour = 0
        sauver_config()

# --------------------------------------------------------------------
# OUTILS DATES / HEURES
# --------------------------------------------------------------------

def texte_date_heure_sms():
    """Retourne une date/heure abrégée pour les SMS."""
    an, mo, jo, jsem, h, m, s, ss = rtc.datetime()
    return "{:02d}:{:02d}, le {:02d}/{:02d}/{:02d}".format(h, m, jo, mo, an % 100)

# --------------------------------------------------------------------
# OLED
# --------------------------------------------------------------------

def init_oled():
    global ecran
    i2c = I2C(0, sda=Pin(PIN_I2C_SDA), scl=Pin(PIN_I2C_SCL), freq=I2C_FREQ)
    ecran = ssd1306.SSD1306_I2C(OLED_LARGEUR, OLED_HAUTEUR, i2c)
    ecran.fill(0)
    ecran.show()


def rafraichir_affichage():
    """Met à jour l’écran OLED avec la date/heure et les lignes de texte."""
    if ecran is None:
        return
    ecran.fill(0)
    an, mo, jo, jsem, h, m, s, ss = rtc.datetime()
    ecran.text("{:02d}/{:02d} {:02d}:{:02d}:{:02d}".format(jo, mo, h, m, s), 0, 0)
    if texte_ligne2:
        ecran.text(texte_ligne2, 0, 16)
    if texte_ligne3:
        ecran.text(texte_ligne3, 0, 32)
    if texte_ligne4:
        ecran.text(texte_ligne4, 0, 48)
    ecran.show()


def afficher_invite_prenom():
    """Affiche le message d’invite pour l’envoi du prénom."""
    global texte_ligne2, texte_ligne3, texte_ligne4
    texte_ligne2 = "Envoyez votre"
    texte_ligne3 = "prenom par SMS"
    texte_ligne4 = "au {}".format(NUM_NADHAT)
    rafraichir_affichage()

# --------------------------------------------------------------------
# WS2812 + ANIMATION COMÈTE
# --------------------------------------------------------------------

def init_anneau():
    global anneau
    anneau = neopixel.NeoPixel(Pin(PIN_WS2812), NB_LEDS)
    eteindre_anneau()


def eteindre_anneau():
    if anneau:
        for i in range(NB_LEDS):
            anneau[i] = (0, 0, 0)
        anneau.write()


def arc_en_ciel(position):
    """Renvoie une couleur (r,g,b) du spectre pour position 0–255."""
    if position < 85:
        return (255 - position * 3, position * 3, 0)
    elif position < 170:
        position -= 85
        return (0, 255 - position * 3, position * 3)
    position -= 170
    return (position * 3, 0, 255 - position * 3)


profil_comete = [1.0, 0.6, 0.3, 0.15, 0.05]


def scalaire_couleur(couleur, facteur):
    r, g, b = couleur
    return int(r * facteur), int(g * facteur), int(b * facteur)


def update_effet_attractif():
    """Met à jour l’effet comète (rotation permanente)."""
    global pos_attractif, hue_attractif, dernier_tick_attractif, couleur_animation
    if not anneau:
        return
    maintenant = time.ticks_ms()
    if time.ticks_diff(maintenant, dernier_tick_attractif) < 80:
        return
    dernier_tick_attractif = maintenant
    couleur_tete = couleur_animation or arc_en_ciel(hue_attractif)
    hue_attractif = (hue_attractif + 3) & 255
    for i in range(NB_LEDS):
        anneau[i] = (0, 0, 0)
    for offset, facteur in enumerate(profil_comete):
        idx_led = (pos_attractif - offset) % NB_LEDS
        r, g, b = scalaire_couleur(couleur_tete, facteur)
        fact = luminosite / 255.0
        anneau[idx_led] = (int(r * fact), int(g * fact), int(b * fact))
    anneau.write()
    pos_attractif = (pos_attractif + 1) % NB_LEDS

# --------------------------------------------------------------------
# BOUTON POUSSOIR
# --------------------------------------------------------------------

def init_bouton():
    """Initialise le bouton poussoir sur GP10 avec pull-up interne."""
    global bouton, bouton_etat_precedent, bouton_dernier_ms
    bouton = Pin(PIN_BOUTON, Pin.IN, Pin.PULL_UP)
    bouton_etat_precedent = bouton.value()
    bouton_dernier_ms = time.ticks_ms()


def get_numero_cible_bouton():
    """Retourne le numéro destinataire selon le mode actif."""
    if (not mode_attente_prenom) and (dernier_num_complet is not None):
        return dernier_num_complet
    return NUM_ADMIN


def envoyer_sms_bouton():
    """Envoie un SMS lors d’un appui sur le bouton."""
    numero = get_numero_cible_bouton()
    texte = "Le bouton poussoir a ete actionne !"
    envoyer_sms(numero, texte)


def surveiller_bouton():
    """Détecte les appuis sur le bouton (front descendant, anti-rebond)."""
    global bouton_etat_precedent, bouton_dernier_ms
    if not bouton:
        return
    etat = bouton.value()
    if bouton_etat_precedent == 1 and etat == 0:
        maintenant = time.ticks_ms()
        if time.ticks_diff(maintenant, bouton_dernier_ms) > 300:
            bouton_dernier_ms = maintenant
            envoyer_sms_bouton()
    bouton_etat_precedent = etat

# --------------------------------------------------------------------
# MODEM ET HEURE RESEAU
# --------------------------------------------------------------------

def init_modem():
    global uart_modem, modem, sms_modem
    pwr = Pin(PIN_PWRKEY, Pin.OUT, value=False)
    uart_modem = UART(1, tx=Pin(PIN_UART_TX), rx=Pin(PIN_UART_RX),
                      baudrate=115200, timeout=500)
    modem = SIM76XX(uart=uart_modem, pwr_pin=pwr, uart_training=True)
    modem.power_up()
    while not modem.is_registered:
        time.sleep(1)
    sms_modem = SMS(modem)


def synchroniser_heure_depuis_reseau():
    """Récupère l'heure du réseau via AT+CCLK? et met à jour la RTC."""
    global uart_modem
    try:
        while uart_modem.any():
            uart_modem.read()
        uart_modem.write(b'AT+CCLK?\r')
        ligne_cclk = None
        buffer = b""
        t0 = time.ticks_ms()
        while time.ticks_diff(time.ticks_ms(), t0) < 5000:
            if uart_modem.any():
                data = uart_modem.read()
                if data:
                    buffer += data
                    while b"\r\n" in buffer:
                        ligne, buffer = buffer.split(b"\r\n", 1)
                        if b"+CCLK:" in ligne:
                            ligne_cclk = ligne.decode("utf-8")
                            break
            if ligne_cclk:
                break
            time.sleep_ms(50)
        if not ligne_cclk:
            print("AT+CCLK? : aucune reponse exploitable")
            return
        debut = ligne_cclk.find('"')
        fin = ligne_cclk.rfind('"')
        contenu = ligne_cclk[debut + 1:fin]
        date_txt, time_txt = contenu.split(",")
        yy, mm, dd = date_txt.split("/")
        hh, mi, ss = time_txt[:8].split(":")
        rtc.datetime((2000 + int(yy), int(mm), int(dd), 0,
                      int(hh), int(mi), int(ss), 0))
        print("RTC synchronisee :", rtc.datetime())
    except Exception as e:
        print("Erreur synchro RTC :", e)

# --------------------------------------------------------------------
# SMS
# --------------------------------------------------------------------

def envoyer_sms(numero, message):
    message_nettoye = nettoyer_pour_sms(message)
    print("Envoi SMS ->", numero, ":", message_nettoye)
    try:
        sms_modem.send(numero, message_nettoye)
    except Exception as e:
        print("Erreur envoi SMS:", e)

# --------------------------------------------------------------------
# BOUCLE PRINCIPALE
# --------------------------------------------------------------------

def boucle_principale():
    global mode_attente_prenom, couleur_animation, temps_derniere_activite, dernier_num_complet
    dernier_rafraich = time.ticks_ms()
    afficher_invite_prenom()
    while True:
        update_effet_attractif()
        surveiller_bouton()
        maintenant_ms = time.ticks_ms()
        if time.ticks_diff(maintenant_ms, dernier_rafraich) >= 500:
            rafraichir_affichage()
            dernier_rafraich = maintenant_ms
        time.sleep_ms(50)

# --------------------------------------------------------------------
# MAIN
# --------------------------------------------------------------------

def main():
    charger_config()
    init_oled()
    init_anneau()
    init_bouton()
    init_modem()
    synchroniser_heure_depuis_reseau()
    mettre_a_jour_journee()
    envoyer_sms(NUM_ADMIN, "Module NadHAT demarre. Il est {}.".format(texte_date_heure_sms()))
    boucle_principale()


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        eteindre_anneau()
