# -*- mode: python -*-
# -*- coding: utf-8 -*-

##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2023 Emmanuel Viennet.  All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
#   Emmanuel Viennet      emmanuel.viennet@viennet.net
#
##############################################################################

##############################################################################
#  Module "Avis de poursuite d'étude"
#  conçu et développé par Cléo Baras (IUT de Grenoble)
##############################################################################

"""
Created on Fri Sep  9 09:15:05 2016

@author: barasc
"""

from app import log
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre
from app.models.moduleimpls import ModuleImpl
from app.pe import pe_tagtable

from app.scodoc import codes_cursus
from app.scodoc import sco_tag_module
from app.scodoc import sco_utils as scu


class SemestreTag(pe_tagtable.TableTag):
    """Un SemestreTag représente un tableau de notes (basé sur notesTable)
    modélisant les résultats des étudiants sous forme de moyennes par tag.

    Attributs récupérés via des NotesTables :
    - nt: le tableau de notes du semestre considéré
    - nt.inscrlist: étudiants inscrits à ce semestre, par ordre alphabétique (avec demissions)
    - nt.identdict: { etudid : ident }
    - liste des moduleimpl { ... 'module_id', ...}

    Attributs supplémentaires :
    - inscrlist/identdict: étudiants inscrits hors démissionnaires ou défaillants
    - _tagdict : Dictionnaire résumant les tags et les modules du semestre auxquels ils sont liés


    Attributs hérités de TableTag :
    - nom :
    - resultats: {tag: { etudid: (note_moy, somme_coff), ...} , ...}
    - rang
    - statistiques

    Redéfinition :
    - get_etudids() : les etudids des étudiants non défaillants ni démissionnaires
    """

    DEBUG = True

    # -----------------------------------------------------------------------------
    # Fonctions d'initialisation
    # -----------------------------------------------------------------------------
    def __init__(self, notetable, sem):  # Initialisation sur la base d'une notetable
        """Instantiation d'un objet SemestreTag à partir d'un tableau de note
        et des informations sur le semestre pour le dater
        """
        pe_tagtable.TableTag.__init__(
            self,
            nom="S%d %s %s-%s"
            % (
                sem["semestre_id"],
                "ENEPS"
                if "ENEPS" in sem["titre"]
                else "UFA"
                if "UFA" in sem["titre"]
                else "FI",
                sem["annee_debut"],
                sem["annee_fin"],
            ),
        )

        # Les attributs spécifiques
        self.nt = notetable

        # Les attributs hérités : la liste des étudiants
        self.inscrlist = [
            etud
            for etud in self.nt.inscrlist
            if self.nt.get_etud_etat(etud["etudid"]) == scu.INSCRIT
        ]
        self.identdict = {
            etudid: ident
            for (etudid, ident) in self.nt.identdict.items()
            if etudid in self.get_etudids()
        }  # Liste des étudiants non démissionnaires et non défaillants

        # Les modules pris en compte dans le calcul des moyennes par tag => ceux des UE standards
        self.modimpls = [
            modimpl
            for modimpl in self.nt.formsemestre.modimpls_sorted
            if modimpl.module.ue.type == codes_cursus.UE_STANDARD
        ]  # la liste des modules (objet modimpl)
        self.somme_coeffs = sum(
            [
                modimpl.module.coefficient
                for modimpl in self.modimpls
                if modimpl.module.coefficient is not None
            ]
        )

    # -----------------------------------------------------------------------------
    def comp_data_semtag(self):
        """Calcule tous les données numériques associées au semtag"""
        # Attributs relatifs aux tag pour les modules pris en compte
        self.tagdict = (
            self.do_tagdict()
        )  # Dictionnaire résumant les tags et les données (normalisées) des modules du semestre auxquels ils sont liés

        # Calcul des moyennes de chaque étudiant puis ajoute la moyenne au sens "DUT"
        for tag in self.tagdict:
            self.add_moyennesTag(tag, self.comp_MoyennesTag(tag, force=True))
        self.add_moyennesTag("dut", self.get_moyennes_DUT())
        self.taglist = sorted(
            list(self.tagdict.keys()) + ["dut"]
        )  # actualise la liste des tags

    # -----------------------------------------------------------------------------
    def get_etudids(self):
        """Renvoie la liste des etud_id des étudiants inscrits au semestre"""
        return [etud["etudid"] for etud in self.inscrlist]

    # -----------------------------------------------------------------------------
    def do_tagdict(self):
        """Parcourt les modimpl du semestre (instance des modules d'un programme) et synthétise leurs données sous la
        forme d'un dictionnaire reliant les tags saisis dans le programme aux
        données des modules qui les concernent, à savoir les modimpl_id, les module_id, le code du module, le coeff,
        la pondération fournie avec le tag (par défaut 1 si non indiquée).
        { tagname1 : { modimpl_id1 : { 'module_id' : ..., 'coeff' : ..., 'coeff_norm' : ..., 'ponderation' : ..., 'module_code' : ..., 'ue_xxx' : ...},
                       modimpl_id2 : ....
                     },
          tagname2 : ...
        }
        Renvoie le dictionnaire ainsi construit.

        Rq: choix fait de repérer les modules par rapport à leur modimpl_id (valable uniquement pour un semestre), car
        correspond à la majorité des calculs de moyennes pour les étudiants
        (seuls ceux qui ont capitalisé des ue auront un régime de calcul différent).
        """
        tagdict = {}

        for modimpl in self.modimpls:
            modimpl_id = modimpl.id
            # liste des tags pour le modimpl concerné:
            tags = sco_tag_module.module_tag_list(modimpl.module.id)

            for (
                tag
            ) in tags:  # tag de la forme "mathématiques", "théorie", "pe:0", "maths:2"
                [tagname, ponderation] = sco_tag_module.split_tagname_coeff(
                    tag
                )  # extrait un tagname et un éventuel coefficient de pondération (par defaut: 1)
                # tagname = tagname
                if tagname not in tagdict:  # Ajout d'une clé pour le tag
                    tagdict[tagname] = {}

                # Ajout du modimpl au tagname considéré
                tagdict[tagname][modimpl_id] = {
                    "module_id": modimpl.module.id,  # les données sur le module
                    "coeff": modimpl.module.coefficient,  # le coeff du module dans le semestre
                    "ponderation": ponderation,  # la pondération demandée pour le tag sur le module
                    "module_code": modimpl.module.code,  # le code qui doit se retrouver à l'identique dans des ue capitalisee
                    "ue_id": modimpl.module.ue.id,  # les données sur l'ue
                    "ue_code": modimpl.module.ue.ue_code,
                    "ue_acronyme": modimpl.module.ue.acronyme,
                }
        return tagdict

    # -----------------------------------------------------------------------------
    def comp_MoyennesTag(self, tag, force=False) -> list:
        """Calcule et renvoie les "moyennes" de tous les étudiants du SemTag
        (non défaillants) à un tag donné, en prenant en compte
        tous les modimpl_id concerné par le tag, leur coeff et leur pondération.
        Force ou non le calcul de la moyenne lorsque des notes sont manquantes.

        Renvoie les informations sous la forme d'une liste
            [ (moy, somme_coeff_normalise, etudid), ...]
        """
        lesMoyennes = []
        for etudid in self.get_etudids():
            (
                notes,
                coeffs_norm,
                ponderations,
            ) = self.get_listesNotesEtCoeffsTagEtudiant(
                tag, etudid
            )  # les notes associées au tag
            coeffs = comp_coeff_pond(
                coeffs_norm, ponderations
            )  # les coeff pondérés par les tags
            (moyenne, somme_coeffs) = pe_tagtable.moyenne_ponderee_terme_a_terme(
                notes, coeffs, force=force
            )
            lesMoyennes += [
                (moyenne, somme_coeffs, etudid)
            ]  # Un tuple (pour classement résumant les données)
        return lesMoyennes

    # -----------------------------------------------------------------------------
    def get_moyennes_DUT(self):
        """Lit les moyennes DUT du semestre pour tous les étudiants
        et les renvoie au même format que comp_MoyennesTag"""
        return [
            (self.nt.etud_moy_gen[etudid], 1.0, etudid) for etudid in self.get_etudids()
        ]

    # -----------------------------------------------------------------------------
    def get_noteEtCoeff_modimpl(self, modimpl_id, etudid, profondeur=2):
        """Renvoie un couple donnant la note et le coeff normalisé d'un étudiant à un module d'id modimpl_id.
        La note et le coeff sont extraits :
        1) soit des données du semestre en normalisant le coefficient par rapport à la somme des coefficients des modules du semestre,
        2) soit des données des UE précédemment capitalisées, en recherchant un module de même CODE que le modimpl_id proposé,
        le coefficient normalisé l'étant alors par rapport au total des coefficients du semestre auquel appartient l'ue capitalisée
        """
        (note, coeff_norm) = (None, None)

        modimpl = get_moduleimpl(modimpl_id)  # Le module considéré
        if modimpl == None or profondeur < 0:
            return (None, None)

        # Y-a-t-il eu capitalisation d'UE ?
        ue_capitalisees = self.get_ue_capitalisees(
            etudid
        )  # les ue capitalisées des étudiants
        ue_capitalisees_id = {
            ue_cap["ue_id"] for ue_cap in ue_capitalisees
        }  # les id des ue capitalisées

        # Si le module ne fait pas partie des UE capitalisées
        if modimpl.module.ue.id not in ue_capitalisees_id:
            note = self.nt.get_etud_mod_moy(modimpl_id, etudid)  # lecture de la note
            coeff = modimpl.module.coefficient or 0.0  # le coeff (! non compatible BUT)
            coeff_norm = (
                coeff / self.somme_coeffs if self.somme_coeffs != 0 else 0
            )  # le coeff normalisé

        # Si le module fait partie d'une UE capitalisée
        elif len(ue_capitalisees) > 0:
            moy_ue_actuelle = get_moy_ue_from_nt(
                self.nt, etudid, modimpl_id
            )  # la moyenne actuelle
            # A quel semestre correspond l'ue capitalisée et quelles sont ses notes ?
            fids_prec = [
                ue_cap["formsemestre_id"]
                for ue_cap in ue_capitalisees
                if ue_cap["ue_code"] == modimpl.module.ue.ue_code
            ]  # and ue['semestre_id'] == semestre_id]
            if len(fids_prec) > 0:
                # => le formsemestre_id du semestre dont vient la capitalisation
                fid_prec = fids_prec[0]
                # Lecture des notes de ce semestre
                # le tableau de note du semestre considéré:
                formsemestre_prec = FormSemestre.get_formsemestre(fid_prec)
                nt_prec: NotesTableCompat = res_sem.load_formsemestre_results(
                    formsemestre_prec
                )

                # Y-a-t-il un module équivalent c'est à dire correspondant au même code (le module_id n'étant pas significatif en cas de changement de PPN)

                modimpl_prec = [
                    modi
                    for modi in nt_prec.formsemestre.modimpls_sorted
                    if modi.module.code == modimpl.module.code
                ]
                if len(modimpl_prec) > 0:  # si une correspondance est trouvée
                    modprec_id = modimpl_prec[0].id
                    moy_ue_capitalisee = get_moy_ue_from_nt(nt_prec, etudid, modprec_id)
                    if (
                        moy_ue_capitalisee is None
                    ) or moy_ue_actuelle >= moy_ue_capitalisee:  # on prend la meilleure ue
                        note = self.nt.get_etud_mod_moy(
                            modimpl_id, etudid
                        )  # lecture de la note
                        coeff = modimpl.module.coefficient  # le coeff
                        # nota: self.somme_coeffs peut être None
                        coeff_norm = (
                            coeff / self.somme_coeffs if self.somme_coeffs else 0
                        )  # le coeff normalisé
                    else:
                        semtag_prec = SemestreTag(nt_prec, nt_prec.sem)
                        (note, coeff_norm) = semtag_prec.get_noteEtCoeff_modimpl(
                            modprec_id, etudid, profondeur=profondeur - 1
                        )  # lecture de la note via le semtag associé au modimpl capitalisé

                # Sinon - pas de notes à prendre en compte
        return (note, coeff_norm)

    # -----------------------------------------------------------------------------
    def get_ue_capitalisees(self, etudid) -> list[dict]:
        """Renvoie la liste des capitalisation effectivement capitalisées par un étudiant"""
        if etudid in self.nt.validations.ue_capitalisees.index:
            return self.nt.validations.ue_capitalisees.loc[[etudid]].to_dict("records")
        return []

    # -----------------------------------------------------------------------------
    def get_listesNotesEtCoeffsTagEtudiant(self, tag, etudid):
        """Renvoie un triplet (notes, coeffs_norm, ponderations) où notes, coeff_norm et ponderation désignent trois listes
        donnant -pour un tag donné- les note, coeff et ponderation de chaque modimpl à prendre en compte dans
        le calcul de la moyenne du tag.
        Les notes et coeff_norm sont extraits grâce à SemestreTag.get_noteEtCoeff_modimpl (donc dans semestre courant ou UE capitalisée).
        Les pondérations sont celles déclarées avec le tag (cf. _tagdict)."""

        notes = []
        coeffs_norm = []
        ponderations = []
        for moduleimpl_id, modimpl in self.tagdict[
            tag
        ].items():  # pour chaque module du semestre relatif au tag
            (note, coeff_norm) = self.get_noteEtCoeff_modimpl(moduleimpl_id, etudid)
            if note != None:
                notes.append(note)
                coeffs_norm.append(coeff_norm)
                ponderations.append(modimpl["ponderation"])
        return (notes, coeffs_norm, ponderations)

    # -----------------------------------------------------------------------------
    # Fonctions d'affichage (et d'export csv) des données du semestre en mode debug
    # -----------------------------------------------------------------------------
    def str_detail_resultat_d_un_tag(self, tag, etudid=None, delim=";"):
        """Renvoie une chaine de caractère décrivant les résultats d'étudiants à un tag :
        rappelle les notes obtenues dans les modules à prendre en compte, les moyennes et les rangs calculés.
        Si etudid=None, tous les étudiants inscrits dans le semestre sont pris en compte. Sinon seuls les étudiants indiqués sont affichés.
        """
        # Entete
        chaine = delim.join(["%15s" % "nom", "etudid"]) + delim
        taglist = self.get_all_tags()
        if tag in taglist:
            for mod in self.tagdict[tag].values():
                chaine += mod["module_code"] + delim
                chaine += ("%1.1f" % mod["ponderation"]) + delim
                chaine += "coeff" + delim
            chaine += delim.join(
                ["moyenne", "rang", "nbinscrit", "somme_coeff", "somme_coeff"]
            )  # ligne 1
        chaine += "\n"

        # Différents cas de boucles sur les étudiants (de 1 à plusieurs)
        if etudid == None:
            lesEtuds = self.get_etudids()
        elif isinstance(etudid, str) and etudid in self.get_etudids():
            lesEtuds = [etudid]
        elif isinstance(etudid, list):
            lesEtuds = [eid for eid in self.get_etudids() if eid in etudid]
        else:
            lesEtuds = []

        for etudid in lesEtuds:
            descr = (
                "%15s" % self.nt.get_nom_short(etudid)[:15]
                + delim
                + str(etudid)
                + delim
            )
            if tag in taglist:
                for modimpl_id in self.tagdict[tag]:
                    (note, coeff) = self.get_noteEtCoeff_modimpl(modimpl_id, etudid)
                    descr += (
                        (
                            "%2.2f" % note
                            if note != None and isinstance(note, float)
                            else str(note)
                        )
                        + delim
                        + (
                            "%1.5f" % coeff
                            if coeff != None and isinstance(coeff, float)
                            else str(coeff)
                        )
                        + delim
                        + (
                            "%1.5f" % (coeff * self.somme_coeffs)
                            if coeff != None and isinstance(coeff, float)
                            else "???"  # str(coeff * self._sum_coeff_semestre) # voir avec Cléo
                        )
                        + delim
                    )
                moy = self.get_moy_from_resultats(tag, etudid)
                rang = self.get_rang_from_resultats(tag, etudid)
                coeff = self.get_coeff_from_resultats(tag, etudid)
                tot = (
                    coeff * self.somme_coeffs
                    if coeff != None
                    and self.somme_coeffs != None
                    and isinstance(coeff, float)
                    else None
                )
                descr += (
                    pe_tagtable.TableTag.str_moytag(
                        moy, rang, len(self.get_etudids()), delim=delim
                    )
                    + delim
                )
                descr += (
                    (
                        "%1.5f" % coeff
                        if coeff != None and isinstance(coeff, float)
                        else str(coeff)
                    )
                    + delim
                    + (
                        "%.2f" % (tot)
                        if tot != None
                        else str(coeff) + "*" + str(self.somme_coeffs)
                    )
                )
            chaine += descr
            chaine += "\n"
        return chaine

    def str_tagsModulesEtCoeffs(self):
        """Renvoie une chaine affichant la liste des tags associés au semestre, les modules qui les concernent et les coeffs de pondération.
        Plus concrêtement permet d'afficher le contenu de self._tagdict"""
        chaine = "Semestre %s d'id %d" % (self.nom, id(self)) + "\n"
        chaine += " -> somme de coeffs: " + str(self.somme_coeffs) + "\n"
        taglist = self.get_all_tags()
        for tag in taglist:
            chaine += " > " + tag + ": "
            for modid, mod in self.tagdict[tag].items():
                chaine += (
                    mod["module_code"]
                    + " ("
                    + str(mod["coeff"])
                    + "*"
                    + str(mod["ponderation"])
                    + ") "
                    + str(modid)
                    + ", "
                )
            chaine += "\n"
        return chaine


# ************************************************************************
# Fonctions diverses
# ************************************************************************


# *********************************************
def comp_coeff_pond(coeffs, ponderations):
    """
    Applique une ponderation (indiquée dans la liste ponderations) à une liste de coefficients :
    ex: coeff = [2, 3, 1, None], ponderation = [1, 2, 0, 1] => [2*1, 3*2, 1*0, None]
    Les coeff peuvent éventuellement être None auquel cas None est conservé ;
    Les pondérations sont des floattants
    """
    if (
        coeffs == None
        or ponderations == None
        or not isinstance(coeffs, list)
        or not isinstance(ponderations, list)
        or len(coeffs) != len(ponderations)
    ):
        raise ValueError("Erreur de paramètres dans comp_coeff_pond")
    return [
        (None if coeffs[i] == None else coeffs[i] * ponderations[i])
        for i in range(len(coeffs))
    ]


# -----------------------------------------------------------------------------
def get_moduleimpl(modimpl_id) -> dict:
    """Renvoie l'objet modimpl dont l'id est modimpl_id"""
    modimpl = ModuleImpl.query.get(modimpl_id)
    if modimpl:
        return modimpl
    if SemestreTag.DEBUG:
        log(
            "SemestreTag.get_moduleimpl( %s ) : le modimpl recherche n'existe pas"
            % (modimpl_id)
        )
    return None


# **********************************************
def get_moy_ue_from_nt(nt, etudid, modimpl_id) -> float:
    """Renvoie la moyenne de l'UE d'un etudid dans laquelle se trouve
    le module de modimpl_id
    """
    # ré-écrit
    modimpl = get_moduleimpl(modimpl_id)  # le module
    ue_status = nt.get_etud_ue_status(etudid, modimpl.module.ue.id)
    if ue_status is None:
        return None
    return ue_status["moy"]
