## Version en un seul fichier du script banbou ##


import subprocess


# Vérifier s'il existe une version plus récente de pip
try:
    result = subprocess.run(['pip', 'install', '--upgrade', 'pip', '--disable-pip-version-check'], capture_output=True, text=True)
    if result.returncode == 0:
        print("pip a été mis à jour avec succès.")
except subprocess.CalledProcessError:
    print("La mise à jour de pip a échoué.")

bibliotheques = ['os', 'getpass', 're', 'shutil', 'datetime', 'openpyxl']

for bibliotheque in bibliotheques:
    try:
        __import__(bibliotheque)
    except ImportError:
        # Installer la bibliothèque
        print(f"{bibliotheque} n'est pas installée. Installation en cours...")
        subprocess.check_call(['pip', 'install', bibliotheque])
        print(f"{bibliotheque} a été installée avec succès.")




import os, getpass
import re       # module RegEx
import shutil, datetime
import openpyxl as xls






### CONSTANTES ###


# chemin absolue en dur du modèle
prenom = getpass.getuser()
MODELE = f"C:\\Users\\{prenom}\\Desktop\\Banbou\\VISA_BANBOU.xlsx"


# nom du dossier créé pour l'opérateur
TRAVAIL = "Travail"


# séparateur d'un CSV
SEP = ";"


# regex, motifs à chercher dans une chaine. Explication : 
#       (MOTIF)+        matche 1 OU PLUS occurence du mot MOTIF
#       (?:)            le groupe ne sera pas capturé, du coup dans python le groupe ne créé pas de chaine vide si pas de match du groupe en question
#       |               OR
EXPRESSION = r"(?:fiche)+|(?:relev)+|(?:topo)+|(?:info)+|(?:ouvr)+|(?:\.doc)+|(?:\.dwg)+|(?:\.csv)+|(?:\.odt)+|(?:\.pdf)+|(?:\.doc)+"


# Masques Binaires représentants les controles de chaques catégorie traitées
# les bits sont allumés à chaque position où un test existe
FAIL_CSV = 7            # Explication : il existe pour le moment 3 tests pour les CSVs donc :   "0000 0000 0000 0111"
FAIL_DWG = 16           #               un seul test pour les DWGs                          :   "0000 0000 0001 0000"
FAIL_PDF = 256          #               un seul test pour les PDFs                          :   "0000 0001 0000 0000"
FAIL_INFO = 4096        #               un seul test pour les fiches infos                  :   "0001 0000 0000 0000"


# Valeurs constantes définie par IGN
PROJECTIONS = [
    {
        "nom" : "GPS",
        "E0" : 0,
        "N0" : 0,
        "offsetE0" : 180,
        "offsetN0" : 90,
        "EPSG" : 4326 # Correspond à la projection des GPS
    },
    {
        "nom" : "Lambert93",
        "E0" : 700000,
        "N0" : 6600000,
        "offsetE0" : 600000,
        "offsetN0" : 600000,
        "EPSG" : 2154 # Attention la plus recente est 9794
    },
    {
        "nom" : "CC42",
        "E0" : 1700000,
        "N0" : 1200000,
        "offsetE0" : 600000,
        "offsetN0" : 111000,
        "EPSG" : 3942
    },
    {
        "nom" : "CC43",
        "E0" : 1700000,
        "N0" : 2200000,
        "offsetE0" : 600000,
        "offsetN0" : 111000,
        "EPSG" : 3943
    },
    {
        "nom" : "CC44",
        "E0" : 1700000,
        "N0" : 3200000,
        "offsetE0" : 600000,
        "offsetN0" : 111000,
        "EPSG" : 3944
    },
    {
        "nom" : "CC45",
        "E0" : 1700000,
        "N0" : 4200000,
        "offsetE0" : 600000,
        "offsetN0" : 111000,
        "EPSG" : 3945
    },
    {
        "nom" : "CC46",
        "E0" : 1700000,
        "N0" : 5200000,
        "offsetE0" : 600000,
        "offsetN0" : 111000,
        "EPSG" : 3946
    },
    {
        "nom" : "CC47",
        "E0" : 1700000,
        "N0" : 6200000,
        "offsetE0" : 600000,
        "offsetN0" : 111000,
        "EPSG" : 3947
    },
    {
        "nom" : "CC48",
        "E0" : 1700000,
        "N0" : 7200000,
        "offsetE0" : 600000,
        "offsetN0" : 111000,
        "EPSG" : 3948
    },
    {
        "nom" : "CC49",
        "E0" : 1700000,
        "N0" : 8200000,
        "offsetE0" : 600000,
        "offsetN0" : 111000,
        "EPSG" : 3949
    },
    {
        "nom" : "CC50",
        "E0" : 1700000,
        "N0" : 9200000,
        "offsetE0" : 600000,
        "offsetN0" : 111000,
        "EPSG" : 3950
    }
    # TODO : ajouter les projections DOMTOMs
]







### CLASSES ET FONCTIONS


class _Fichier:


    def __init__(self,
                 #nom_original="Pas de nom original",
                 chemin="Pas de chemin",
                 extension="Pas d'extension",
                 nom= "Pas de nom",
                 implication="Non-conforme",
                 categorie="Pas de catégorie",
                 taille=0):
        #self.nom_original = nom_original    # - son nom original
        self.chemin = chemin                # - son chemin absolue (dossier+fichier+extension)
        self.extension = extension          # - son extension (ecrit en minuscule)
        self.nom = nom                      # - son nom formaté
        self.implication = implication      # - son implication dans le projet {"Necessaire", "Non-conforme", "A-ignorer"}
        self.categorie = categorie          # - sa categorie {"CSV", "DWG", "SHEMAS", "TOPO", "Non-definie"}
        self.taille = taille                # taille en octets


    def afficher(self):
        """
            Affiche dans la sortie standard les éléments du fichier
        """
        print("\nfichier.afficher()")
        print("nom :".ljust(16) + self.nom)
        #print("nom orig :".ljust(16) + self.nom_original)
        print("ext :".ljust(16) + self.extension)
        print("chemin :".ljust(16) + self.chemin)
        print("implication :".ljust(16) + self.implication)
        print("categorie :".ljust(16) + self.categorie)
        print("taille :".ljust(16) + str(self.taille) )


def lire(dossier, fichier):
    """
        Lit le nom du fichier et du dossier
        
        Construit un élement _Fichier ET met à jour TOUS ses attributs
        :param user_input: chemin absolue du dossier, nom du fichier
        :return: un element _Fichier 
    """
    print("\nfichier.lire()")
    # initialiser un _Fichier
    ce_Fichier = _Fichier()

    # son chemin absolue
    ce_Fichier.chemin = dossier + "\\" + fichier
    
    # déterminer son nom original et son extension
    # vérifie qu'il y est au moins un point dans le nom
    if "." in fichier :

        *nom_original, ce_Fichier.extension = fichier.rsplit(".", maxsplit=1)
        # reconstitue une chaine simple depuis la liste
        nom_original = "".join(nom_original)

        # lettrer l'extension en minuscule
        ce_Fichier.extension = ce_Fichier.extension.casefold()

        # formatter et écrire le nom
        ce_Fichier.nom = formater(nom_original)
        # déterminer son implication et sa categorie
        ce_Fichier.implication, ce_Fichier.categorie = impliquer(fichier)
        # calculer sa taille
        ce_Fichier.taille = os.path.getsize(ce_Fichier.chemin)
        print(f"implication : {ce_Fichier.implication}    {ce_Fichier.categorie}")
    else:
        print(f'"{fichier}" est un fichier sans extension --> Non-conforme.')
        ce_Fichier.nom = fichier
        ce_Fichier.implication = "Non-conforme"
        #TODO: voir si il manque pas des données à lire ici pour pas bloquer le reste du prog

    return ce_Fichier





class _Notification:
    def __init__(self,
                 categorie="Pas de catégorie",
                 texte="Pas de texte"):
        self.categorie = categorie  # vu comme une énumération de l'ensemble {"CSV", "DWG", "PDF", "FRONT"} seule ces valeurs sont donc possibles et traitées par le script
        self.texte = texte









def controler(id_point, point_x, point_y):
    """
        regarde si les coordonnées d'un point sont bien dans une des projections
        autorisées

        Contrainte :
        - Pour le moment, ne peut reconnaitre que des projections
        de points situés en France métropolitaine (Lambert93 et les 9 zones CC).
        - Ce sont toutes des projections coniques depuis le pole nord --> la longitude (axe Ouest Est) n'est pas déformé par ces projections et donc reste valide.
        Mais je vais aussi contraindre la longitude à la France métropolitaine, pour pas qu'un point situé en Russie par exemple apparaisse comme valide.
        NOTE : On ne controle pas l'élevation (coord Z), ces projections ne prennent pas en compte l'élevation.
        
        param user_input: un point de type (int, float, float) TODO: avoir si conversion nécessaire ou faire en chaine de carac
        return: Une chaine nommant la projection trouvé sinon "Mauvaise projection"
    """

    #print("\nprojection.controler()")

    # projection à retourner
    projection = "Mauvaise projection"

    # NOTE : Une partie des ensembles de coordonnées de CC47 et de Lambert93 s'intersectionne : 
    # Lorsqu'un point de coordonnées (X, Y) se situe dans la plage 1100000, 1300000 pour X et
    # la plage 3089000, 3311000 pour Y, alors ce point est à la fois valide en CC47 et en Lambert93
    # Avant de déterminer la projection, je vais donc vérifier si le point à controler n'est pas dans cette plage
    # Et avertir si necessaire
    if 6089000  <  point_y  <  6311000:
        if 1100000  <  point_x  <  1300000:
            print(f"AVERTISSEMENT : ID {ID_POINT} : SES COORDONNÉES PEUVENT ÊTRE INTERPRÉTER CORRECTEMENT COMME DU CC47 ET DU LAMBERT93.")
            print("DANS LA PLUPART DES CAS, VOUS DEVREZ PROJETER LE FICHIER CSV EN CC47.")


    # définie la projection conique Nord en regardant dans quelle intervalle la valeur se situe
    for P in PROJECTIONS:
        borne_basse = P["N0"] - P["offsetN0"]
        borne_haute = P["N0"] + P["offsetN0"]
        if  borne_basse  <  point_y  <  borne_haute :
            projection = P["nom"]

    #print(f"Proj. conique Nord trouvé : {projection}")
    print(f" --> {projection:10}")

    # Controle la longitude      
    longitude_correcte = False
    match projection:
        case "Lambert93":
            if 100000  <  point_x  < 1300000 :
                longitude_correcte = True
        case "CC42" | "CC43" | "CC44" | "CC45" | "CC46" | "CC47" | "CC48" | "CC49" | "CC50":
            if 1100000  <  point_x  < 2300000 :
                longitude_correcte = True
        case "GPS":
            if -180 < point_x < 180:
                longitude_correcte = True
        case _:
            pass
    
    if not longitude_correcte :
        projection = "Mauvaise projection"
        print(f"AVERTISSEMENT : LONGITUDE DU POINT ID {ID_POINT} PAS EN MÉTROPOLE.")
    
    return projection





## Analyse lexicale


def tokeniser(ligne): #Pas utilisé, je le laisse au cas ou.
    """
        Tokenise une ligne de texte.

        Lit en entrée une "ligne" d'un fichier texte, découpe la ligne en mots,
        chaque mot étant séparé par le séparateur "sep".
        Attendu : si un champ est vide alors la liste reçoit quand même un 
        item pour representer ce champ (une chaine vide "" qui peut servir 
        pour tester sa présence)
        Retourne une liste des mots trouvés, ou une liste vide si aucun mot.
    """
    sortie = []

    # enleve les espaces superflues avant et après la chaine
    tempo = ligne.strip()
    tempo = tempo.split(sep=SEP)

    #affichage test
    print("tokeniser")
    print(tempo)

    # enlever les mots vides
    for i in tempo:
        print
        if i not in '':
            sortie.append(i)

    return sortie




## Conversion, Formatage


def formater(chaine):
    """Formate selon nomenclature.

    Formate la chaine de caractères passée en paramètre :
    Enlève tous les accents français. Enlève la cédille du C.
    Remplace les espaces ' ', les traits d'union '-' et les ''' apostrophes
    par des tirets bas '_'.
    Ne traite pas pour le moment le AE et OE ligaturé.
    Si plusieurs '_' se suivent, les réduire à un seul.
    """
    #TODO : gérer les accents sur majuscules
    #TODO : amélioration ou exercice,  utiliser la méthode str.translate() et maketrans
    resultat = ""
    precedent = None
    for c in chaine:
        match c:
            case "à" | "â" | "ä":
                resultat+= "a"
            case "é" | "è" | "ê" | "ë":
                resultat+= "e"
            case "î" | "ï":
                resultat+= "i"
            case "ô" | "ö":
                resultat+= "o"
            case "ù" | "û" | "ü":
                resultat+= "u"
            case "ÿ":
                resultat+= "y"
            case "ç":
                resultat+= "c"
            case " " | "-" | "_" | "'":
                if('_' not in precedent ):
                    resultat+= "_"
                c = "_"
            case _:
                resultat+= c
        precedent = c
    return resultat




## Analyse semantique


def impliquer(chaine):
        """
            Définir l'implication d'une chaine (attendu : un nom de fichier avec son extension).

            Retourne un tuple de str (Necessité, Catégorie).

            La necessité peut prendre une des valeurs suivantes :
                {"Necessaire", "A-ignorer}.
            La catégorie peut prendre une des valeurs suivantes:
                {"CSV", "DWG", "SHEMAS", "TOPO", "Non-definie"}.
            Il est attendu que lorsqu'un fichier obtient une valeur de catégorie
            Alors il est aussi considéré "Necessaire".

            Les fichiers "Necessaire" seront les fichiers copiés dans le 
            répertoire "Travail".

            La catégorie est définie par une recherche de motif dans le nom du fichier.
            # TODO: voir si elle peut pas être définie en lisant les métadonnées des fichiers

            Les fichiers nécessaires sont les DWGs, les CSVs pour les datas.
            Pour les shémas et relevés de topo :
            - PDFs, DOCs, ODTs (Doc LibreOffice), 
        """

        print(f"\nimpliquer({chaine})")

        # mettre la chaine en minuscule
        chaine = chaine.casefold()

        # cherche la PREMIERE correspondance du motif (alors que findall() cherche toutes les correspondances)
        motif = re.search(EXPRESSION, chaine) # re.search() retourne un objet Match si trouvé sinon None

        if motif:
            # print("Il y a un match")
            motif = motif.group()   # Match.group() permet de retourner la valeur matché
        else:
            # print("Pas de match")
            pass
        
        match motif :
            # On va traiter les mots clés d'abord
            case "fiche" | "relev" | "topo" | "ouvr" | "info" | ".doc" | ".odt" :
                return ("Necessaire", "TOPO")
            case ".dwg" :
                return ("Necessaire", "DWG")
            case ".csv" :
                return ("Necessaire", "CSV")
            case ".pdf" :
                return ("Necessaire", "SHEMAS")
            case _ :
                return ("A-ignorer","Non-definie")




## Fonctions Input Utilisateur

def saisir_annee_topo():
    """
        demande à l'utilisateur de saisir l'année du relevé topographique.

        Demande à l'utilisateur l'année affichée dans le relevé topograhique
        du projet.
        Lit la valeur saisi dans l'entrée standard.
        
        Les fichiers n'étant pas encore créés dans le dossier "Travail" à
        ce moment du programme, l'utilisateur doit donc ouvrir la fiche
        du relevé topographique dans le dossier original.

        Pour ne pas être bloquant (cas ou l'année n'est pas trouvable), 
        la saisi de '0' permet de ne rien mettre.
        
        Retourne la valeur saisi.
    """
    print("\nVeuillez saisir l'année de la date du relevé topographique.")
    print("Pour cela ouvrez la fiche topo du dossier/sous-dossier original.")
    
    correct = False
    while(not correct):
        saisi = input("Saisir 4 chiffres : ")
        if saisi in "0":
            return ""

        correct = saisi.isdecimal() and len(saisi) == 4
    
    return saisi









class _Projet:


    def __init__(self,
                 nom="Pas de nom",
                 date="Pas de date",
                 racine="Pas de chemin",
                 fichiers=[],
                 nb_fichiers=0,
                 taille=0,
                 rapport="Pas de fichier",
                 nb_shemas=0,
                 nb_releves=0,
                 nb_csvs=0,
                 points=[],
                 nb_points=0,
                 nb_dwgs=0,
                 notifs=[],
                 controles=0):
        self.nom = nom                  # nom du projet
        self.date = date                # date du traitement
        self.racine = racine            # chemin racine du projet
        self.fichiers = fichiers        # liste de _Fichier
        self.nb_fichiers = nb_fichiers  # nb de fichiers dans "Travail"
        self.taille = taille            # taille des fichiers dans "Travail"
        self.rapport = rapport          # chemin vers le visa
        self.nb_shemas = nb_shemas      # nb de plans de la mise en place
        self.nb_releves = nb_releves    # nb de rapports de relevés topo
        self.nb_csvs = nb_csvs            # nb de fichiers CSV
        self.points = points            # liste des points du CSV
        self.nb_points = nb_points      # nb de points des CSVs     TODO: REDONDANT avec len(points)
        self.nb_dwgs = nb_dwgs          # nb de fichiers DWG
        self.notifs = notifs            # liste contenant les notifs du VISA
        self.controles = controles      # variable drapeau contenant toutes les validations de controles 
                                        # (voir Document pour plus d'info sur sa représentation)
    
    def notifier(self, categorie, texte):
        """
            Ajoute une notification au _Projet
        """
        print("\n_Projet.notifier()")
        print(f"Incident ajouté :{texte} ")
        incident = _Notification( categorie, texte)
        self.notifs.append(incident)


    def lister_fichiers(self, dossier):
        """
            Construit une liste avec les fichiers de 'dossier'

             - Parcours le dossier et ses sous-dossiers, ajoute TOUS les fichiers
               dans une liste de _Fichier.
             - Chaque _Fichier ajouté a ses attributs mis a jour.
             - Les nombres des différentes catégories de fichiers nécessaires
               au projet sont mis a jour.
            :param user_input: nom complet du dossier
            :return: liste d'élements de type _Fichier
        """
        print("\n_Projet.lister_fichiers()")
        self.fichiers = []

        print("Création liste de _Fichier...")

        for dossier_courant, list_sousdossiers, list_fichiers in os.walk(dossier):
            for  fichier_courant in list_fichiers:  
                ce_Fichier = lire(dossier_courant, fichier_courant)
                self.fichiers.append(ce_Fichier)
                print(f'"{fichier_courant}" ajouté.')

                # mettre à jour les nbs des catégories de fichiers necessaires
                if ce_Fichier.implication in "Necessaire":
                    ### sa categorie {"CSV", "DWG", "SHEMAS", "TOPO", "Non-definie"}
                    match ce_Fichier.categorie :
                        case "CSV":
                            self.nb_csvs += 1
                        case "DWG":
                            self.nb_dwgs += 1
                        case "SHEMAS":
                            self.nb_shemas += 1
                        case "TOPO":
                            self.nb_releves += 1
                        case _:
                            pass       
        
        print("Fin création liste.")


# TODO surcharger la fonction print native pour cet affichage
def afficher_liste(liste):
    """
        Affiche le nom des fichiers de la liste des _Fichiers
    """
    print("\nprojet.afficher_liste()")
    for courant in liste:
        print("\n" + courant.nom + "." + courant.extension)


def enraciner(projet):
        """
            récupère le repertoire de travail (working directory) courant
            met à jour l'attribut 'racine' d'un projet
        """
        print("\nprojet.enraciner()")
        projet.racine = os.getcwd()
        print("Racine : ".ljust(16), f"{projet.racine}")


def calculer_taille(projet):
    """
        calcule la taille des fichiers necessaires d'un liste
        d'élements _Fichier.
        Met à jour l'attribut 'taille' dans le projet
    """
    print("\nprojet.calculer_taille()")
    taille = 0.0
    for courant in projet.fichiers:
        if courant.implication in "Necessaire":
            taille += courant.taille
    projet.taille = taille
    # Affichage adapté à la bonne unité
    if taille < 1024 :
        unite = "octets"
    elif taille < 1024**2 :
        unite = "Ko"
        taille /= 1024
    else :
        unite = "Mo"
        taille /= 1024**2
    print(f"Taille totale : {taille:.2f} {unite}.")


def dater(projet):
    """
        recupère la date du jour
        met à jour l'attribut 'date' du projet
    """
    print("\nprojet.dater()")
    projet.date = datetime.datetime.today().strftime('%Y%m%d')
    print(f"Date : {projet.date}")


def nommer(projet):
    """
        Met à jour l'attribut 'nom' en composant un nom.

        Le nom est constitué du NOM du dossier racine ET de la DATE
        courante formatté.

    """
    print("\nprojet.nommer()")
    projet.nom = f"{os.path.basename(os.getcwd())}_{projet.date}"
    projet.nom = formater(projet.nom)
    print(f'Nom : "{projet.nom}"')


def preparer_dossier_travail(projet):
    """
        Créer un dossier "Travail" dans la racine du working directory et
        le peuple des fichiers nécessaires

        Préfixe et suffixe les fichiers le nécessitant (le CSV et le DWG)
    """
    print("\nprojet.preparer_dossier_travail()")
    #travail = "Travail"

    # création du dossier "Travail" et de ses sous-dossiers "
    _chemin = f"{projet.racine}\\{TRAVAIL}"
    #nomemclaturer les noms des sousdossiers
    sousdossier = f"{_chemin}\\{projet.nom}"

    # pour le moment tous les sous dossiers sont 
    dossierCSVs = _chemin
    dossierDWGs = _chemin
    dossierPDFs = _chemin
    dossierTOPOs = _chemin

    print(f'Chemin du dossier de préparation : "{_chemin}"')
    try:
        os.mkdir(_chemin)
        print (f'Dossier "{TRAVAIL}" créé.')
        os.mkdir(sousdossier) # dossier pour le projet ARCGIS

        if projet.nb_csvs > 1:
            dossierCSVs = f"{sousdossier}_CSV"
            os.mkdir(dossierCSV)
            print (f'Dossier "CSV" créé.')
        if projet.nb_dwgs > 1:
            dossierDWGs = f"{sousdossier}_DWG"
            os.mkdir(dossierDWGs)
            print (f'Dossier "DWG" créé.')
        if projet.nb_shemas > 1:
            dossierPDFs = f"{sousdossier}_PDF"
            os.mkdir(dossierPDFs)
            print (f'Dossier "PDF" créé.')
        if projet.nb_releves > 1:
            dossierTOPOs = f"{sousdossier}_TOPO"
            os.mkdir(dossierTOPOs)
            print (f'Dossier "TOPO" créé.')

    except FileExistsError as erreur:
        print(f'AVERTISSEMENT: LE DOSSIER "{TRAVAIL}" EXISTE DÉJA. SUPPRIMER LE, PUIS RELANCER LE SCRIPT SVP.')
        purger_et_finir_programme(projet)
    except OSError as erreur:
        print(f"FICHIER NON TROUVÉ. SUREMENT UN PB DE CHEMIN EN AMONT.")
        purger_et_finir_programme(projet)
    

    
    # peuplement du dossier Travail avec les fichiers necessaires
    print("Copie des fichiers nécessaires et nomenclature de leurs noms...")

    # demande l'annnée du revelé topo à l'utilisateur
    annee_topo = saisir_annee_topo()

    # nom du dossier projet
    dossier = f"{os.path.basename(os.getcwd())}"
    dossier = formater(dossier)
    print(f"dossier : {dossier}")

    # les suffixes ajoutés en cas de plusieurs fichiers dans la même catégorie
    letterCSV = "\0" if projet.nb_csvs <= 1 else "A"      # condition ternaire en python
    letterDWG = "\0" if projet.nb_dwgs <= 1 else "A"
    letterSHEMAS = "\0" if projet.nb_shemas <= 1 else "A"
    letterTOPO = "\0" if projet.nb_releves <= 1 else "A"

    for fichier in projet.fichiers:
        if fichier.implication in "Necessaire":
            source = fichier.chemin
            print(f"source : {source}")

            # fabrication du nom du fichier de destination
            dest = _chemin + "\\"

            # lors du peuplement préfixer et suffixer les noms des fichiers concernés comme suit : "Plan_nomfichierdwg.dwg" et "Point_nomfichiercsv_IN.csv"
            # Confirmation par Audrey qu'il n'y a nécessairement qu'un fichier CSV par projet --> Le script s'occupe de le renommer.
            # Mais parfois il peut y avoir plusieurs DWGs dont un seul est utile --> PASS -- Je laisse l'opérateur choisir lequel utiliser et le renommer manuellement.

            #{"CSV", "DWG", "SHEMAS", "TOPO", "Non-definie"}
            #ANCIENNE VERSION
            #-----------------------------------------------------------------
            # match fichier.categorie :
            #     case "CSV":
            #         dest = f"{dossierCSVs}\\Point_{fichier.nom}_IN"
            #     case "DWG":
            #         dest = f"{dossierDWGs}\\Plan_{fichier.nom}"
            #     case "SHEMAS":
            #         dest = f"{dossierPDFs}\\{fichier.nom}"
            #     case "TOPO":
            #         dest = f"{dossierTOPOs}\\{fichier.nom}"
            #     case _:
            #         pass # TODO: voir pour les autres cas, normalement il n'y en a pas pour le moment
            #-----------------------------------------------------------------
            
            #{"CSV", "DWG", "SHEMAS", "TOPO", "Non-definie"}
            match fichier.categorie :
                case "CSV":
                    if projet.nb_csvs <= 1 :
                        dest = f"{dossierCSVs}\\{dossier}_IN_Points_{annee_topo}"
                    else :
                        dest = f"{dossierCSVs}\\{dossier}_IN_Points_{annee_topo}_{letterCSV}"
                        letterCSV = chr(ord(letterCSV)+1) # on incremente la lettre
                case "DWG":
                    if projet.nb_dwgs <= 1 :
                        dest = f"{dossierDWGs}\\{dossier}_Plan_{annee_topo}"
                    else :
                        dest = f"{dossierDWGs}\\{dossier}_IN_Points_{annee_topo}_{letterDWG}"
                        letterDWG = chr(ord(letterDWG)+1)
                case "SHEMAS":
                    if projet.nb_shemas <= 1 :
                        dest = f"{dossierPDFs}\\{dossier}_Plan_{annee_topo}"
                    else :
                        dest = f"{dossierPDFs}\\{dossier}_IN_Points_{annee_topo}_{letterSHEMAS}"
                        letterSHEMAS = chr(ord(letterSHEMAS)+1)
                case "TOPO":
                    if projet.nb_releves <= 1 :
                        dest = f"{dossierTOPOs}\\{dossier}_Infos_{annee_topo}"
                    else :
                        dest = f"{dossierTOPOs}\\{dossier}_IN_Points_{annee_topo}_{letterTOPO}"
                        letterTOPO = chr(ord(letterTOPO)+1)
                case _:
                    pass # TODO: voir pour les autres cas, normalement il n'y en a pas pour le moment
                
                # renomme le fichier en conséquence
            fichier.nom = os.path.basename(dest)
            
            dest = dest + f".{fichier.extension}"
            print(f"dest : {dest}")

            if fichier.implication in "Necessaire":
                try:
                    shutil.copyfile( source , dest)
                    print("" + fichier.nom.ljust(40,".") + "copié")
                except shutil.SameFileError as err :
                    print(f"Le fichier existe déjà.")
        #TODO : 
        # Confirmation par Audrey qu'il n'y a necessairement qu'un fichier CSV par projet --> On peut le renommer.
        # Mais parfois il peut y avoir plusieurs DWGs dont un seul est utile --> PASS -- Je laisse l'opérateur choisir lequel utiliser et le renommer manuellement. EDIT : Nathalie prefere que tous les fichier DWG soient préfixé avec Plan_
        #TODO : verifier aussi la longueur des noms de fichiers. Notifier si nécessaire


def controler_longueur_noms(projet):
        """
        controle la longueur des noms des fichiers du projet.
        
        Notifie si necessaire les noms trop longs
        """
        #print("\nprojet.controler_longueur_noms()")
        for fichier in projet.fichiers:
            match fichier.extension:
                case "dwg":
                    if len(fichier.nom) > 38 :  # 40 - 2 si lettres de suffixe "_A", "_B", etc
                        projet.notifier("DWG", f'"{fichier.nom}.dwg" : Nom trop long.')
                case "csv" :
                    if len(fichier.nom) > 43 : 
                        projet.notifier("CSV", f'"{fichier.nom}.csv" : Nom trop long.')
                case "pdf" :
                    if len(fichier.nom) > 43 :
                        projet.notifier("PDF", f'"{fichier.nom}.pdf" : Nom trop long.')
                case _:
                    if len(fichier.nom) > 45 :
                        projet.notifier("FRONT", f'"{fichier.nom}.{fichier.extension}" : Nom trop long.')

def formater_vers_ArcGIS(projet, fichier_entree, fichier_sortie):
    """
        Lit et formatte un fichier CSV pour son importation dans ArcGIS.

        Lit la 1ère ligne et s'assure de la présence des titres de colonnes,
        formatte cette 1ère ligne.
        Supprime les lignes vides.
        Change le séparateur décimal ',' en '.' (necessaire car sinon je ne peux convertir les strings en float)
        Notifie si nécessaire les points de controles qui ne passent pas.
        Met à jour l'attribut nb_points
        Contraintes :
        - Le fichier CSV NÉCESSITE des ';' comme séparateur d'élements. (Normalement c'est toujours le cas de toutes façon)
        - Le fichier doit avoir exactement 5 colonnes. TODO : vois si plus de souplesse avec le catchage de paramètres restants
        param user_input: nom complet d'un fichier csv
        NOTE: Ne pas formatter le fichier original, mais celui qui est deja copié dans le dossier Travail
    """
    print("\nprojet.formatter_vers_ArcGIS()")

    # titres des colonnes correctement formattés.
    titres = "id_point;TYPE;X;Y;Z\n"

    sortie = [titres] # le fichier de sortie ( representé comme une liste de lignes ) contient un premiere ligne de titre


    df = open(fichier_entree, "r")
    ligne = df.readline()

    # analyser la premiere ligne et formatter cette ligne    
    # Si le premier mot est un nombre Alors il manque les titres, dans
    # ce cas insérer une ligne.
    mots = ligne.split(sep=";", maxsplit=1)
    print(f"Mots[0] = {mots[0]}")


    if mots[0].isnumeric():
        #insérer ligne
        sortie.append(ligne)
        projet.nb_points += 1

    # la tête de lecture du descripteur de fichier ne reset pas sa position
    # donc on peut continuer le parcours des lignes directement
    for ligne_courante in df :

        # lit et controle la ligne courante
        courant = lire_et_controler_ligne(projet, ligne_courante)
        # ajoute la ligne
        if courant not in "":
            sortie.append(f"{courant}\n")
        # compter ce point
            projet.nb_points += 1

    df.close()

    # je reouvre le descripteur en mode w only pour ecrire le fichier
    df = open(fichier_sortie, "w")
    # écriture
    for ligne in sortie:
        #print(f"{ligne}")
        df.write(ligne) # NOTE: write()
    
    df.close()

    print("Fin formatage du CSV.")
    

def lire_et_controler_ligne(projet, ligne):
    """
        Analyse une chaine de caractère (concrètement, une ligne du CSV)/

        remplace les séparateurs décimaux ',' par '.'
        Notifie (dans le projet lié) les points de controles qui ne passent pas.
        Met à jour l'attribut nb_points du projet lié
        Met à jour la variable de controles du projet lié
        Contraintes :
        - Le fichier CSV NÉCESSITE des ';' comme séparateur d'élements.
        :return: une chaine de carac formatté pour ArcGIS
    """
    #print("\nprojet.lire_et_controler_ligne()")
    #print(f'ENTREE : "{ligne}"')
    # enlever les espaces
    ligne = ligne.replace(" ", "")
    ligne = ligne.replace(",", ".")
    ligne = ligne.replace("\n", "")
    # si la ligne n'est pas vides alors
    # TODO : changer le match en  if ligne not in ["", ";;;;"]:
    match ligne:
        case ";;;;;;" | ";;;;;" | ";;;;" | ";;;" | ";;" | ";" | "" :
            ligne = ""
        case _:
            # controler la projection de ce point
            # NOTE: split() tokenise la ligne en items (des strings), si un champ est manquant, alors il tokenise une chaine vide( "" )
            # On peut donc tester cette chaine vide pour savoir si une champ est manquant.abs
            # Je vais dont controler la présence du champ ID, car il apparait que parfois il est manquant. Si tel est le cas,
            # Notifier et terminer le programme --> dossier FAIL
            #
            # NOTE: split() n'enleve pas le \n de la fin de ligne dans la chaine --> ligne = ligne.replace("\n", "") que j'ai rajouter au dessus
            try :
                id_point, type_, point_x, point_y, point_z, *autres = ligne.split(sep=";")
            except ValueError:
                print("LE FICHIER SOURCE CSV EST MAL FORMATÉ, IL DOIT MANQUER DES CHAMPS ET DE SÉPARATEURS DE CHAMPS.")
                purger_et_finir_programme(projet)


            #PRINT de controle
            print(f'ID:"{id_point}", TYPE:"{type_}", X:"{point_x}", Y:"{point_y}", Z:"{point_z}", *autres={autres}')

            # controler qu'il n'y est pas un champ vide pour id_point, TYPE, X et Y. (Z étant optionnel)
            # Si tel est le cas, on quitte FAIL
            if id_point in "":
                print('AVERTISSEMENT : COLONNE "ID_POINT" MANQUANTE.')
                purger_et_finir_programme(projet)
            if type_ in "":
                print('AVERTISSEMENT : COLONNE "TYPE" MANQUANTE.')
                purger_et_finir_programme(projet)
            if point_x in "":
                print('AVERTISSEMENT : COLONNE "X" MANQUANTE.')
                purger_et_finir_programme(projet)
            if point_y in "":
                print('AVERTISSEMENT : COLONNE "Y" MANQUANTE.')
                purger_et_finir_programme(projet)
            if point_z in "":
                print('AVERTISSEMENT : COLONNE "Z" MANQUANTE.')
                purger_et_finir_programme(projet)

            # controler que id_point est un entier
            # Si c'est un flottant alors ya un pb, on quitte FAIL
            try :
                if float(id_point)/int(id_point) != 1.0 :
                    print("AVERTISSEMENT : LA PREMIÈRE COLONNE EST UN FLOTTANT. ATTENDU UN ID_POINT DE VALEUR ENTIÈRE.")
                    print('IL DOIT MANQUER LA COLONNE "ID_POINT AINSI QUE LE SÉPARATEUR DE CHAMP ";".')
                    purger_et_finir_programme(projet)
            except ValueError :
                print("AVERTISSEMENT : LA COLONNE ID_POINT N'A PAS UN NOMBRE.")
                print(f'ATTENDU UN ENTIER. TROUVÉ ID_POINT DE TYPE "{type(id_point)}"')
                purger_et_finir_programme(projet)


            # A partir d'ici il est attendu que la tokenisation se soit bien déroulée
            try :
                projo = controler(id_point, float(point_x), float(point_y))
            except ValueError :
                print("AVERTISSEMENT : UNE COLLONNE (X OU Y) EST MAL CONVERTIE.")
                print(f'ATTENDU VALEURS DÉCIMALES. TROUVÉ X : DE TYPE "{type(point_x)}", Y DE TYPE "{type(point_y)}"')
                purger_et_finir_programme(projet)
            # notifier si pas bonne projection
            if projo in "Mauvaise projection":
                incident = _Notification("CSV", f"Point ID {id_point} : Mauvaise projection")
                projet.notifs.append(incident)
                projet.controles |= 2**2
    
    #print(f'SORTIE : "{ligne}"')

    return ligne









def remplir(modele, projet):
    """
        Complète le Visa du projet à partir d'un fichier modele.

        Controle le nb de fichiers de chaque catégorie et notifie.
        Parcours la liste de notifications du projet et ajoute chaque
        notification dans la feuille concerné du classeur
        A défaut, une notification de la liste qui n'a pas de catégorie
        avec sa propre feuille sera ajoutée à la première feuille (FRONT)

        Enregistre le Visa dans le repertoire de Travail
        Contraintes : 
         - le modele du VISA doit être NÉCESSAIREMENT dans le même
           dossier que le script. TODO : traité l'exception si fichier non trouvé
         - le modele du VISA NE doit PAS avoir eu de modification notable 
           dans son design ( emplacements des cellules utilisées pour 
           notifier, nom des feuilles, etc) 
    """

    print("\nvisa.remplir()")



    # creation d'un workbook à partir d'un modele
    classeur = xls.load_workbook(modele)
    

    ## definition personnalisé vers chaque feuille du classeur
    FRONT = classeur[classeur.sheetnames[0]]
    PDF = classeur[classeur.sheetnames[1]]
    CSV = classeur[classeur.sheetnames[2]]
    DWG = classeur[classeur.sheetnames[3]]


    # Nom dossier
    # Date de reception
    # Date Visa
    FRONT['C1'].value = f'Analyse : {projet.nom}\nDate de réception :\nDate visa : {projet.date}'

    # Taille du dossier
    taille = projet.taille
    # Affichage adapté à la bonne unité
    if taille < 1024 :
        unite = "octets"
    elif taille < 1024**2 :
        unite = "Ko"
        taille /= 1024
    else :
        unite = "Mo"
        taille /= 1024**2


    FRONT['C10'].value = f"Taille totale : {taille:.2f} {unite}."


    # TODO: factoriser les instructions ci dessous (pas sur...)



    ### FRONT DWG
    # Je ne peux pas controler automatiquement la projection du DWG (c'est un binaire illisible hors de AutoCAD)
    # J'ajoute donc automatiquement la mention comme dans le script précedent.
    FRONT['C3'].value = f"{projet.nb_dwgs} fichier(s) DWG présent(s).\nProjection de tous les fichiers DWG : RFG93-CC43 (EPSG:3943)."

    # Controle le nb de fichiers DWG
    if projet.nb_dwgs == 0:
        projet.controles |= 2**4      #  allume le bit 5ième bit
        FRONT['C3'].value = "Pas de fichier DWG présent."
    
    # Regarde si des fails ont été détectés sur les DWGs
    fail = projet.controles & FAIL_DWG    # operation logique avec le masque
    # notifie en consequence
    if fail :
        FRONT['B3'].value = "FAIL"
    else :
        FRONT['B3'].value = "OK"



    ### FRONT PDF
    FRONT['C4'].value = f"{projet.nb_shemas} fichier(s) PDF présent(s)."

    # Controle le nb de fichiers PDF
    if projet.nb_shemas == 0:
        projet.controles |= 2**8      #  allume le 9ième bit 
        FRONT['C4'] = "Pas de fichier PDF présent."
    
    # Regarde si des fails ont été détectés sur les PDFs
    fail = projet.controles & FAIL_PDF
    # notifie en consequence
    if fail :
        FRONT['B4'].value = "FAIL"
    else :
        FRONT['B4'].value = "OK"



    ### FRONT CSV
    FRONT['C5'].value = f"1 fichier(s) CSV présent(s).\n{projet.nb_points} point(s) en RFG93-CC43 (EPSG:3943)."

    # Controle le nb de fichiers CSV
    if projet.nb_csvs == 0:
        projet.controles |= 2**0  # allume le 1er bit
        FRONT['C5'].value = "Pas de fichier CSV présent."
    
    # Regarde si des fails ont été détectés sur les CSVs
    fail = projet.controles & FAIL_CSV
    # notifie en consequence
    if fail : 
        FRONT['B5'].value = "FAIL"
    else :
        FRONT['B5'].value = "OK"



    # FRONT FICHE INFO TOPOLOGIE
    FRONT['C6'].value = f"{projet.nb_releves} fiche(s) Info. présente(s)."

    # Controle le nb de fichiers Fiche Topologique
    if projet.nb_releves == 0:
        projet.controles |= 2**12  # allume le 13ième bit
        FRONT['C6'].value = "Pas de fiche d'Info. Topologie présente."
    
    # Regarde si des fails ont été détectés sur les Fiches Topo
    fail = projet.controles & FAIL_INFO
    # notifie en consequence
    if fail : 
        FRONT['B6'].value = "FAIL"
    else :
        FRONT['B6'].value = "OK"




    # Notifier dans les autres Feuilles

    # lignes en cours à remplir pour chaque feuille
    # initialisé aux lignes où on va commencer à notifier
    A = 2        # "B2"    # Pour la feuille PDF
    B = 2        # "B2"    # Pour la feuille CSV
    C = 2        # "B2"    # Pour la feuille DWG
    D = 12       # "C12"      # Pour la feuille FRONT

    cell = ""   # cellule courante qui va être notifier

    for notif in projet.notifs :
        match notif.categorie :
            case "PDF" :
                cell = PDF["B"+str(A)]
                A += 1
            case "CSV" :
                cell = CSV["B"+str(B)]
                B += 1
            case "DWG" :
                cell = DWG["B"+ str(C)]
                C += 1
            case _:
                cell = FRONT["C"+str(D)]
                D += 1
            
        cell.value = notif.texte


    # Ici le VISA doit être correctement rempli
    # on sauvegarde dans un fichier le classeur
    classeur.save(f"Travail\\{projet.nom}_VISA.xlsx")


def purger_et_finir_programme(projet):
    """
        Efface le dossier "Travail" si il existe puis termine le programme.

        Cette fonction est utilisée quand des expections sont levées et 
        qu'elles nécessitent la fermeture prématurée du programme.
    """
    print("\npurger_et_finir_programme()")
    print("\nFIN DE PROGRAMME.\n")
    # Ne fonctionne pas car des descripteurs de fichiers sont utilisés au moment de la demande
    ## shutil.rmtree(f"{projet.racine}\\{TRAVAIL}" , ignore_errors=True)
    quit()











## NOTE : Pour traiter le cas des sous-dossiers lors du dézippage
# il vaut mieux que l'opérateur copie le fichier banbou.py dans chaque 
# sous-dossiers puis l'execute.
## Donc Il est attendu que :
#  - le fichier script se situe DANS le dossier contenant que 
#    les fichiers originaux.
#  - le modele du visa soit dans un autre dossier BIEN SPÉCIFIÉ 
#    (je vais remettre un chemin absolue en dur "Desktop\Banbou"
#    car suivant l'opérateur, le dézippage crée des sous-sous-dossiers,
#    et serait problématique pour un chemin relatif)



# Création entité projet et mise à jour de ses attributs
ce_Projet = _Projet()

enraciner(ce_Projet)

dater(ce_Projet)

nommer(ce_Projet)
# controle de la longueur du nom du projet
if len(ce_Projet.nom) >=46 :
    print(f"\n{ce_Projet.nom} EST UN NOM DE DOSSIER TROP LONG (+ DE 46 CARACTÈRES). VEUILLEZ RACCOURCIR SON NOM.")
    purger_et_finir_programme(ce_Projet)

# explorer le dossier Trouvé et fabriquer la liste des fichiers nécessaires
ce_Projet.lister_fichiers(ce_Projet.racine)

# calcule la taille des fichiers nécessaires
calculer_taille(ce_Projet)

# Ici le projet doit avoir toutes les données nécessaires pour fabriquer
# et remplir les dossiers attendus
preparer_dossier_travail(ce_Projet)


# Règle nouvellement ajouté : il faut normalement UN SEUL fichier CSV, 
# l'opérateur ne devrait plus fusionner manuellement des fichiers CSV.
if ce_Projet.nb_csvs > 1:
    print("\nAVERTISSEMENT : IL Y A PLUSIEURS FICHIERS CSV.")
    print("VEUILLEZ NE GARDER QU'UN SEUL FICHIER CSV DANS LE DOSSIER ORIGINAL.")
    purger_et_finir_programme(ce_Projet)


# On formatte le/les fichier/s CSV pour ArcGIS
for fichier in ce_Projet.fichiers:
    if fichier.categorie in "CSV":
        entree = ce_Projet.racine + "\\Travail\\" + fichier.nom + ".csv"
        sortie = ce_Projet.racine + "\\Travail\\" + fichier.nom + ".csv"
        formater_vers_ArcGIS(ce_Projet, entree, sortie)


# On controle les noms des fichiers copiés
controler_longueur_noms(ce_Projet)


# On fabrique le VISA
#modele = ce_Projet.racine + "\\VISA_BANBOU.xlsx"
remplir(MODELE, ce_Projet)






#Fin de Programme Attendu
print("\nProgramme terminé correctement. ")