import re
import  csv
import  os
from difflib import SequenceMatcher

from common import safeText

from PyQt6.QtWidgets import QWidget, QCheckBox, QComboBox, QLineEdit, \
    QTableWidgetItem
from PyQt6.QtCore import Qt, pyqtSignal

from Ui_tableCSV import Ui_tableCSV
from Ui_unicity import Ui_unicity

class TableCSV(QWidget, Ui_tableCSV):
    """
    Une table pour interagir avec le contenu d'un fichier CSV

    Paramètres du constructeur
    @parant un tabWidget conteneurt
    @parant mw la fenêtre principale
    """

    # signaux spéciaux
    foreignCells = pyqtSignal(int, bool, list) # on modifie un combo de type
    fcChange = pyqtSignal(int) # on modifie une case Vers... et/ou alias
    
    # les numéros de colonnes
    NOTABLE_COL = 0            # pour les boutons radio de champ notable
    FIRST_EXAMPLE_COL = 1      # pour les trois exemples de valeur
    TYPE_COL = 4               # pour le combo de choix de type
    UNICITY_COL = 5            # pour les boutons radio d'unicité
    VERS_COL = 6               # pour les classes des clés "foreign"
    ALIAS_COL = 7              # alias si plusieurs "foreign" existent

    def __init__(self, parent, mw):
        QWidget.__init__(self, parent)
        Ui_tableCSV.__init__(self)
        self.setupUi(self)
        # connexion des signaux spéciaux
        self.foreignCells.connect(self.manageForeignCells)
        self.fcChange.connect(self.updateData)

        # intialisation diverses
        self.mw = mw         # pointeur vers la fenêtre principale
        self.fieldnames = [] # liste des noms de champs (vertical headers)
        self.filename = ""   # chemin absolu du fichier CSV
        self.modele = ""     # nom du modèle découlant de celui du fichier
        return

    def getConf(self, fieldname, key):
        """
        Récupère une valeur de la configuration
        @param fieldame un nom de champ
        @param key une de ses propriétés
        @return la valeur correspondant à la clé key dans
          self.mw.config["data_CSV"], ce qui converne le tableau local
        """
        if self.filename not in self.mw.config["data_CSV"]:
            self.mw.config["data_CSV"][self.filename] = {}
        if fieldname not in self.mw.config["data_CSV"][self.filename]:
            self.mw.config["data_CSV"][self.filename][fieldname] = {}
        return self.mw.config["data_CSV"][self.filename][fieldname].get(
            key, None)
    
    def populate(self, filename):
        """
        Remplit en grande part le tableau à partir d'un nom de fichier,
        et renseigne par effet de bord les données de configuration
        @param filename chemin absolu du fichier CSV
        """
        self.filename = filename
        dr = csv.DictReader(
            open(filename, "r", encoding="iso-8859-15"), delimiter = ",")
        self.modele = fn2modele(filename)
        self.fieldnames = dr.fieldnames # mémorise les noms de champs localement
        # recopie ces propriétés en les ajoutant à self.tableWidget
        self.tableWidget.filename = self.filename
        self.tableWidget.modele = self.modele
        self.tableWidget.fieldnames = self.fieldnames
        self.tableWidget.setRowCount(len(dr.fieldnames))
        self.tableWidget.setVerticalHeaderLabels(dr.fieldnames)
        # première colonne de faible largeur
        self.tableWidget.setColumnWidth (self.NOTABLE_COL, 30)
        # mise en place des exemples de données lues dans le fichier CSV
        for i in range(3):
            try:
                dic = next(dr)
                for j, (key,val) in enumerate(dic.items()):
                    self.tableWidget.setItem(
                        j, i+self.FIRST_EXAMPLE_COL, QTableWidgetItem(val))
            except StopIteration:
                # s'il y a peu d'exemples, on laisse tomber
                break
        # mise en place de la case à cocher "notable", selon la
        # configuration, sinon cochée s'il y a déjà quelques exemples.
        for j, fn in enumerate(dr.fieldnames):
            notableWidget = QCheckBox("", self)
            isNotable = self.getConf(fn, "notable") == "true"
            checked = Qt.CheckState.Unchecked
            if isNotable:
                checked = Qt.CheckState.Checked
            elif self.getConf(fn, "notable") is None:
                # rien dans la config ...
                # faut-il cocher la case notable quand même ?
                # on regarde si on voit des données parmi les exemples
                notableVal = "false"
                for i in range(3):
                    cell = self.tableWidget.item(j, i+self.FIRST_EXAMPLE_COL)
                    if cell and cell.text():
                        checked = Qt.CheckState.Checked
                        notableVal = "true"
                        break
                self.mw.updateConfig.emit(
                    self.filename, fn, "notable", notableVal)
            notableWidget.setCheckState(checked)
            notableWidget.stateChanged.connect(lambda: self.mw.toDjango.emit())
            self.tableWidget.setCellWidget(j, self.NOTABLE_COL, notableWidget)
        # mise en place des combos pour le choix du type
        for j, fn in enumerate(dr.fieldnames):
            self.tableWidget.setCellWidget(
                j, self.TYPE_COL, TypeWidget(self, j, fn))
        # mise en place des boutons radio d'unicité
        for j, fieldname in enumerate(dr.fieldnames):
            self.tableWidget.setCellWidget(
                j, self.UNICITY_COL, UnicityWidget(self, fieldname))
        return

    def manageForeignCells(self, r, add = True, modeles = []):
        """
        Ajoute des cases pour les clés étrangères dans la ligne numéro r
        du tableau
        @param r Le numéro de ligne
        @param add Vrai [par défaut] quand on veut ajouter les cases
          dans le tableau. Faux quand on veut les supprimer
        @param modeles Une liste des modèles possibles à choisir, à préciser
          quand le paramètre add est vrai.
        """
        t = self.tableWidget
        if add:
            # mise en place du combo en colonne self.VERS_COL
            fieldname = self.fieldnames[r]
            # ajoute un combo de modèles
            modelW = ModelWidget(t, fieldname, modeles)
            modelW.currentIndexChanged.connect(lambda: self.fcChange.emit(r))
            t.setCellWidget(r, self.VERS_COL, modelW)
            cell = t.cellWidget(r, self.VERS_COL)
            foreign = self.getConf(fieldname, "foreign")
            if foreign:
                cell.setCurrentText(foreign)
                self.fcChange.emit(r) # ??? pourquoi ?
            else:
                # c'est un champ encore inconnu de la configuration
                # fait en sorte de deviner le meilleur modèle
                # étant donné le nom du champ
                fn = safeText(fieldname).lower()
                scores = {
                    m : SequenceMatcher(
                        None,
                        safeText(m).lower(), fn).find_longest_match().size for \
                    m in (cell.itemText(i) for i in range(cell.count()))
                }
                # s'il y a au moins 4 lettres en commun, on change de modèle
                best = max(scores.items(), key = lambda x: x[1])
                if best[1] > 3:
                    cell.setCurrentText(best[0])
                    self.mw.updateConfig.emit(
                        self.filename, fieldname, "foreign", best[0])
            # mise en place du lineEdit en colonne self.ALIAS_COL
            edit = QLineEdit("")
            edit.editingFinished.connect(lambda: self.fcChange.emit(r))
            edit.setPlaceholderText("alias ?")
            alias = self.getConf(fieldname, "alias")
            if alias:
                edit.setText(alias)
                self.fcChange.emit(r) # ??? pourquoi ?
            t.setCellWidget(r, self.ALIAS_COL, edit)
        else: # add est faux, on supprimes les cases
            t.removeCellWidget(r, self.VERS_COL)
            t.removeCellWidget(r, self.ALIAS_COL)
        return

    def updateData(self, r):
        """
        Mise à jour de la portion de configuration pointée par
        self.data pour prendre en compte le contenu des colonnes
        self.VERS_COL et self.ALIAS_COL. Cette fonction de rappel
        est connectée au signal self.fcChange.

        @param r la ligne du tableau où un changement a eu lieu
        """
        fieldname = self.fieldnames[r]
        c = self.tableWidget.cellWidget(r, self.VERS_COL)
        a = self.tableWidget.cellWidget(r, self.ALIAS_COL)
        if c:
            self.mw.updateConfig.emit(
                self.filename, fieldname, "foreign", c.currentText())
        if a:
            self.mw.updateConfig.emit(
                self.filename, fieldname, "alias", a.text())
        return

class TypeWidget(QComboBox):
    """
    Un widget pour choisir le type de champ. Les types possibles sont
    propres à la classe MaFenetre

    paramètres du constructeur:
    @param parent le widget parent
    @param row la ligne de tableau dans le widget parent
    @param fieldname le nom du champ pour la ligne courante ; il y a un
      traitement spécial quand la configuration est indéfinie et que
      le nom de champ commence par un préfixe spécial comme "Clé" ou "Id_"
    """

    prefixes_speciaux = {
        "cle" : "foreign",
        "id_"  : "int",
    }
    
    def __init__(self, parent, row, fieldname):
        QComboBox.__init__(self, parent)
        self.row = row
        self.mw = parent.mw
        self.parent = parent
        self.fieldname = fieldname
        self.addItems(self.mw.types.keys())
        parent.mw.registeredCombos[fieldname] = self
        atype = self.parent.getConf(fieldname, "type")
        if atype:
            self.setCurrentText(atype)
        else:
            # si le nom de champ commence par un préfixe spécial ...
            for prefixe, preference in self.prefixes_speciaux.items():
                if fieldname.lower().startswith(prefixe):
                    self.setCurrentText(preference)
        self.currentIndexChanged.connect(self.changed)
        return

    def changed(self):
        """
        fonction de rappel pour le changement de l'index courant
        """
        atype = self.currentText()
        if atype == "foreign":
            self.parent.foreignCells.emit(self.row, True, self.mw.modeles)
        else:
            self.parent.foreignCells.emit(self.row, False, [])
        self.mw.toDjango.emit()
        return
    

class UnicityWidget(QWidget,Ui_unicity):
    """
    Un widget avec des bouton radio pour clé primaire et unique.

    Paramètres du constructeur:
    @param parent le widget parent
    @param fieldname un nom de champ ; s'il commence par "Id_", le bouton
      de clé primaire sera coché
    """
    def __init__(self, parent, fieldname):
        QWidget.__init__(self, parent)
        Ui_unicity.__init__(self)
        self.setupUi(self)
        unicity = parent.getConf(fieldname, "unicity")
        if unicity == "primary_key":
            self.primButton.setChecked(True)
        elif unicity == "unique":
            self.uniqueButton.setChecked(True)
        else:
            # pas encore de config pour "unicity" ?
            # on regarde quand même si le nom de champ évoque une clé primaire
            if fieldname.lower().startswith("id_"):
                self.primButton.setChecked(True)
                parent.mw.updateConfig.emit(
                    parent.filename, fieldname, "unicity", "primary_key")
            else:
                self.radioButton.setChecked(True)
        # on connecte les trois boutons radio à la modification du code
        for button in (self.primButton, self.uniqueButton, self.radioButton):
            button.toggled.connect(lambda: parent.mw.toDjango.emit())
        return

    def __str__(self):
        if self.primButton.isChecked():
            return "primary_key"
        if self.uniqueButton.isChecked():
            return "unique"
        return "none"

    @property
    def isPrimaryKey(self):
        """
        @return Vrai si le bouton Clé primaire est coché
        """
        return self.primButton.isChecked()
    
    @property
    def isUnique(self):
        """
        @return Vrai si le bouton Unique est coché
        """
        return self.uniqueButton.isChecked()

    @property
    def djangoParam(self):
        """
        @return une mention pour un champ de modèle
        """
        if self.isPrimaryKey:
            return ", primary_key = True"
        elif self.isUnique:
            return ", unique = True"
        return ""
    
class ModelWidget(QComboBox):
    """
    Un widget pour choisir le modèle cible quand un champ est de type foreign

    paramètres du constructeur:
    @param parent le widget parent
    @param fieldname le nom du champ pour la ligne courante ; il y a un
      traitement spécial quand la configuration est indéfinie et que
      le nom de champ commence par un préfixe spécial comme "Clé" ou "Id"
    @param modeles une liste des modèles possibles
    """

    def __init__(self, parent, fieldname, modeles):
        QComboBox.__init__(self, parent)
        self.addItems(["Autre_Modele"] + list(modeles))
        return
    
def fn2modele(filename):
    """
    Déduit un nom de modèle du nom d'un fichier
    """
    return re.match(r".*/(.*)\.csv", filename).group(1)

def modele2fn(dir, modele):
    """
    Déduit un nom de fichier d'un nom de modèle
    """
    return os.path.join(dir, modele + ".csv")

