# graphique_regulation_tor.py
# Visualisation et enregistrement d'une regulation de niveau TOR.
# Courbes affichees : consigne (mm), niveau (mm), commande pompe/relais (0 ou 100 %).
#
# Protocole serie recommande cote Arduino, une ligne par mesure :
#   t_s;consigne_mm;niveau_mm;commande_pct
# Exemple :
#   12.50;100.0;93.4;100
#
# Le programme accepte aussi l'ancien protocole en 4 lignes successives :
#   t_s
#   consigne_mm
#   niveau_mm
#   commande_pct
#
# Debut d'experience : Arduino envoie DEBUT quand la regulation commence.
# Fin d'experience : envoyer -1 ou FIN sur une ligne.

from __future__ import annotations

import argparse
import csv
import re
import sys
from datetime import datetime
from pathlib import Path

import matplotlib.pyplot as plt
import serial
from serial.tools import list_ports


DEFAULT_BAUDRATE = 115200
DEFAULT_WINDOW_S = 60.0
DEFAULT_YMAX_MM = 200.0


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Affiche et enregistre une regulation de niveau TOR depuis le port serie Arduino."
    )
    parser.add_argument("--port", help="Port serie, par exemple COM7. Si absent, detection automatique.")
    parser.add_argument("--baud", type=int, default=DEFAULT_BAUDRATE, help="Debit serie. Defaut : 115200.")
    parser.add_argument("--window", type=float, default=DEFAULT_WINDOW_S, help="Largeur de la fenetre temporelle en secondes.")
    parser.add_argument("--ymax", type=float, default=DEFAULT_YMAX_MM, help="Maximum de l'axe niveau en mm.")
    parser.add_argument("--outfile", help="Nom du fichier de sortie CSV. Defaut : horodatage automatique.")
    parser.add_argument("--no-wait-debut", action="store_true", help="Compatibilite ancien protocole : ne pas attendre le mot-cle DEBUT.")
    return parser.parse_args()


def choisir_port(port_demande: str | None) -> str:
    if port_demande:
        return port_demande

    ports = list(list_ports.comports())
    if not ports:
        raise RuntimeError("Aucun port serie detecte. Brancher l'Arduino puis relancer le programme.")

    mots_cles = ("arduino", "nano", "ch340", "wch", "usb serial", "usb-sERIAL")
    candidats = []
    for port in ports:
        description = f"{port.description} {port.manufacturer or ''} {port.hwid}".lower()
        if any(mot.lower() in description for mot in mots_cles):
            candidats.append(port)

    if len(candidats) == 1:
        return candidats[0].device

    if len(ports) == 1:
        return ports[0].device

    print("Plusieurs ports serie sont disponibles :")
    for indice, port in enumerate(ports, start=1):
        print(f"  {indice}. {port.device} - {port.description}")

    while True:
        choix = input("Numero du port Arduino a utiliser : ").strip()
        if choix.isdigit():
            indice = int(choix)
            if 1 <= indice <= len(ports):
                return ports[indice - 1].device
        print("Choix non valide.")


def convertir_nombre(texte: str) -> float:
    return float(texte.strip().replace(",", "."))


def extraire_nombres(ligne: str) -> list[float]:
    # Accepte les separateurs ; tabulation, espace, ou virgule si elle separe des champs.
    # Les virgules decimales sont acceptees dans le cas d'un seul nombre par ligne.
    ligne = ligne.strip()
    if ";" in ligne or "\t" in ligne:
        morceaux = re.split(r"[;\t]+", ligne)
        return [convertir_nombre(m) for m in morceaux if m.strip()]

    morceaux = ligne.split()
    if len(morceaux) > 1:
        return [convertir_nombre(m) for m in morceaux]

    return [convertir_nombre(ligne)]



def attendre_debut(port_serie: serial.Serial) -> bool:
    """Attend le mot-cle DEBUT envoye par l'Arduino avant de tracer les donnees."""

    print("En attente du mot-cle DEBUT envoye par l'Arduino...")
    print("Si l'Arduino attend, appuyer brievement sur le bouton de la carte pour demarrer.")

    while True:
        ligne = port_serie.readline().decode("utf-8", errors="replace").strip()
        if not ligne:
            continue

        ligne_maj = ligne.upper()
        if ligne_maj == "DEBUT":
            print("DEBUT recu : acquisition et graphique demarrent maintenant.")
            return True

        if ligne_maj == "FIN" or ligne == "-1":
            print("Fin recue avant le debut de l'acquisition.")
            return False

        print(f"Arduino : {ligne}")

def lire_mesure(port_serie: serial.Serial) -> tuple[float, float, float, float] | None:
    """Retourne (t, consigne, niveau, commande) ou None si l'experience est finie."""

    while True:
        ligne = port_serie.readline().decode("utf-8", errors="replace").strip()
        if not ligne:
            continue

        if ligne.upper() == "FIN" or ligne == "-1":
            return None

        try:
            valeurs = extraire_nombres(ligne)
        except ValueError:
            print(f"Ligne ignoree : {ligne}")
            continue

        if len(valeurs) >= 4:
            return valeurs[0], valeurs[1], valeurs[2], valeurs[3]

        if len(valeurs) == 1:
            # Mode compatible avec l'ancien programme : t puis 3 autres lignes.
            t = valeurs[0]
            if t == -1:
                return None

            autres = []
            while len(autres) < 3:
                ligne2 = port_serie.readline().decode("utf-8", errors="replace").strip()
                if not ligne2:
                    continue
                if ligne2.upper() == "FIN" or ligne2 == "-1":
                    return None
                try:
                    valeurs2 = extraire_nombres(ligne2)
                except ValueError:
                    print(f"Ligne ignoree : {ligne2}")
                    continue
                autres.extend(valeurs2)

            return t, autres[0], autres[1], autres[2]


def fichier_sortie(nom: str | None) -> Path:
    if nom:
        return Path(nom)
    horodatage = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
    return Path(__file__).with_name(f"regulation_tor_{horodatage}.csv")


def format_fr(nombre: float) -> str:
    return f"{nombre:.3f}".replace(".", ",")


def sauvegarder_csv(chemin: Path, temps: list[float], consigne: list[float], niveau: list[float], commande: list[float]) -> None:
    with chemin.open("w", newline="", encoding="utf-8") as fichier:
        writer = csv.writer(fichier, delimiter=";")
        writer.writerow(["t_s", "consigne_mm", "niveau_mm", "commande_pct"])
        for ligne in zip(temps, consigne, niveau, commande):
            writer.writerow([format_fr(valeur) for valeur in ligne])


def main() -> int:
    args = parse_args()
    chemin_csv = fichier_sortie(args.outfile)

    try:
        port = choisir_port(args.port)
    except RuntimeError as erreur:
        print(erreur)
        return 1

    print(f"Ouverture du port {port} a {args.baud} bauds...")
    print("Pour arreter proprement : fermer la fenetre du graphique ou interrompre le programme.")

    temps: list[float] = []
    consigne: list[float] = []
    niveau: list[float] = []
    commande: list[float] = []

    plt.ion()
    fig, ax_niveau = plt.subplots(figsize=(9, 5))
    ax_commande = ax_niveau.twinx()

    ligne_consigne, = ax_niveau.plot([], [], "g-", label="consigne W (mm)")
    ligne_niveau, = ax_niveau.plot([], [], "r-", label="niveau h (mm)")
    ligne_commande, = ax_commande.step([], [], "b-", where="post", label="pompe / relais (%)")

    ax_niveau.set_xlabel("t (s)")
    ax_niveau.set_ylabel("niveau (mm)")
    ax_commande.set_ylabel("commande pompe (%)")
    ax_niveau.set_ylim(0, args.ymax)
    ax_commande.set_ylim(-5, 105)
    ax_niveau.grid(True)

    lignes = [ligne_consigne, ligne_niveau, ligne_commande]
    etiquettes = [ligne.get_label() for ligne in lignes]
    ax_niveau.legend(lignes, etiquettes, loc="upper left")
    ax_niveau.set_title("Regulation de niveau TOR")

    texte_info = ax_niveau.text(
        0.02,
        0.95,
        "commande : 0 % = pompe arretee ; 100 % = pompe active",
        transform=ax_niveau.transAxes,
        va="top",
    )

    try:
        with serial.Serial(port=port, baudrate=args.baud, timeout=1) as port_serie:
            if not args.no_wait_debut:
                if not attendre_debut(port_serie):
                    return 0

            while plt.fignum_exists(fig.number):
                mesure = lire_mesure(port_serie)
                if mesure is None:
                    break

                t, w, h, y = mesure
                temps.append(t)
                consigne.append(w)
                niveau.append(h)
                commande.append(y)

                t_min = max(0.0, t - args.window)
                t_max = max(args.window, t + 5.0)

                ligne_consigne.set_data(temps, consigne)
                ligne_niveau.set_data(temps, niveau)
                ligne_commande.set_data(temps, commande)

                ax_niveau.set_xlim(t_min, t_max)
                texte_info.set_text(f"niveau = {h:.1f} mm ; consigne = {w:.1f} mm ; pompe = {y:.0f} %")

                fig.canvas.draw_idle()
                plt.pause(0.001)

    except serial.SerialException as erreur:
        print(f"Erreur serie : {erreur}")
        return 1
    except KeyboardInterrupt:
        print("Arret demande par l'utilisateur.")
    finally:
        if temps:
            sauvegarder_csv(chemin_csv, temps, consigne, niveau, commande)
            print(f"Donnees enregistrees dans : {chemin_csv}")
        else:
            print("Aucune donnee recue, aucun fichier cree.")
        plt.ioff()
        plt.close("all")

    return 0


if __name__ == "__main__":
    sys.exit(main())