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

##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2024 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.pe import pe_affichage, pe_comp
import app.pe.moys.pe_ressemtag as pe_ressemtag
import pandas as pd
import numpy as np

from app.pe.moys import pe_moytag, pe_tabletags
import app.pe.rcss.pe_trajectoires as pe_trajectoires
from app.scodoc.sco_utils import ModuleType


class SxTag(pe_tabletags.TableTag):
    def __init__(
        self,
        sxtag_id: (str, int),
        semx: pe_trajectoires.SemX,
        ressembuttags: dict[int, pe_ressemtag.ResSemBUTTag],
    ):
        """Calcule les moyennes/classements par tag d'un semestre de type 'Sx'
        (par ex. 'S1', 'S2', ...) représentés par acronyme d'UE.

        Il représente :

        * pour les étudiants *non redoublants* : moyennes/classements
          du semestre suivi
        * pour les étudiants *redoublants* : une fusion des moyennes/classements
          dans les (2) 'Sx' qu'il a suivi, en exploitant les informations de capitalisation :
          meilleure moyenne entre l'UE capitalisée et l'UE refaite (la notion de meilleure
          s'appliquant à la moyenne d'UE)

        Un SxTag (regroupant potentiellement plusieurs semestres) est identifié
        par un tuple ``(Sx, fid)`` où :

        * ``x`` est le rang (semestre_id) du semestre
        * ``fid`` le formsemestre_id du semestre final (le plus récent) du regroupement.

        Les **tags**, les **UE** et les inscriptions aux UEs (pour les étudiants)
        considérés sont uniquement ceux du semestre final.

        Args:
            sxtag_id: L'identifiant de SxTag
            ressembuttags: Un dictionnaire de la forme `{fid: ResSemBUTTag(fid)}` donnant
                           les semestres à regrouper et les résultats/moyennes par tag des
                           semestres
        """
        pe_tabletags.TableTag.__init__(self)

        assert sxtag_id and len(sxtag_id) == 2 and sxtag_id[1] in ressembuttags

        self.sxtag_id: (str, int) = sxtag_id
        """Identifiant du SxTag de la forme (nom_Sx, fid_semestre_final)"""
        assert (
            len(self.sxtag_id) == 2
            and isinstance(self.sxtag_id[0], str)
            and isinstance(self.sxtag_id[1], int)
        ), "Format de l'identifiant du SxTag non respecté"

        self.agregat = sxtag_id[0]
        """Nom de l'aggrégat du RCS"""

        self.semx = semx
        """Le SemX sur lequel il s'appuie"""
        assert semx.rcs_id == sxtag_id, "Problème de correspondance SxTag/SemX"

        # Les resultats des semestres taggués à prendre en compte dans le SemX
        self.ressembuttags = {
            fid: ressembuttags[fid] for fid in semx.semestres_aggreges
        }
        """Les ResSemBUTTags à regrouper dans le SxTag"""

        # Les données du semestre final
        self.fid_final = sxtag_id[1]
        self.ressembuttag_final = ressembuttags[self.fid_final]
        """Le ResSemBUTTag final"""

        # Ajoute les etudids et les états civils
        self.etuds = self.ressembuttag_final.etuds
        """Les étudiants (extraits du ReSemBUTTag final)"""
        self.add_etuds(self.etuds)
        self.etudids_sorted = sorted(self.etudids)
        """Les etudids triés"""

        # Affichage
        pe_affichage.pe_print(f"*** {self.get_repr(verbose=True)}")

        # Les tags
        self.tags_sorted = self.ressembuttag_final.tags_sorted
        """Tags (extraits du ReSemBUTTag final)"""
        aff_tag = pe_affichage.repr_tags(self.tags_sorted)
        pe_affichage.pe_print(f"--> Tags : {aff_tag}")

        # Les UE données par leur acronyme
        self.acronymes_sorted = self.ressembuttag_final.acronymes_sorted
        """Les acronymes des UEs (extraits du ResSemBUTTag final)"""

        # L'association UE-compétences extraites du dernier semestre
        self.acronymes_ues_to_competences = (
            self.ressembuttag_final.acronymes_ues_to_competences
        )
        """L'association acronyme d'UEs -> compétence"""
        self.competences_sorted = sorted(self.acronymes_ues_to_competences.values())
        """Les compétences triées par nom"""

        aff = pe_affichage.repr_asso_ue_comp(self.acronymes_ues_to_competences)
        pe_affichage.pe_print(f"--> UEs/Compétences : {aff}")

        # Les coeffs pour la moyenne générale (traduisant également l'inscription
        # des étudiants aux UEs) (etudids_sorted x acronymes_ues_sorted)
        self.matrice_coeffs_moy_gen = self.ressembuttag_final.matrice_coeffs_moy_gen
        """La matrice des coeffs pour la moyenne générale"""
        aff = pe_affichage.repr_profil_coeffs(self.matrice_coeffs_moy_gen)
        pe_affichage.pe_print(
            f"--> Moyenne générale calculée avec pour coeffs d'UEs : {aff}"
        )

        # Masque des inscriptions et des capitalisations
        self.masque_df = None
        """Le DataFrame traduisant les capitalisations des différents semestres"""
        self.masque_df, masque_cube = compute_masques_capitalisation_cube(
            self.etudids_sorted,
            self.acronymes_sorted,
            self.ressembuttags,
            self.fid_final,
        )
        pe_affichage.aff_capitalisations(
            self.etuds,
            self.ressembuttags,
            self.fid_final,
            self.acronymes_sorted,
            self.masque_df,
        )

        # Les moyennes par tag
        self.moyennes_tags: dict[str, pd.DataFrame] = {}
        """Moyennes aux UEs (identifiées par leur acronyme) des différents tags"""

        if self.tags_sorted:
            pe_affichage.pe_print("--> Calcul des moyennes par tags :")

        for tag in self.tags_sorted:
            pe_affichage.pe_print(f"     > MoyTag 👜{tag}")

            # Masque des inscriptions aux UEs (extraits de la matrice de coefficients)
            inscr_mask: np.array = ~np.isnan(self.matrice_coeffs_moy_gen.to_numpy())

            # Moyennes (tous modules confondus)
            if not self.has_notes_tag(tag):
                pe_affichage.pe_print(
                    f"        --> Semestre (final) actuellement sans notes"
                )
                matrice_moys_ues = pd.DataFrame(
                    np.nan, index=self.etudids_sorted, columns=self.acronymes_sorted
                )
            else:
                # Moyennes tous modules confondus
                ### Cube de note etudids x UEs tous modules confondus
                notes_df_gen, notes_cube_gen = self.compute_notes_ues_cube(tag)

                # DataFrame des moyennes (tous modules confondus)
                matrice_moys_ues = self.compute_notes_ues(
                    notes_cube_gen, masque_cube, inscr_mask
                )

            # Mémorise les infos pour la moyenne au tag
            self.moyennes_tags[tag] = pe_moytag.MoyennesTag(
                tag,
                pe_moytag.CODE_MOY_UE,
                matrice_moys_ues,
                self.matrice_coeffs_moy_gen,
            )

            # Affichage de debug
            aff = pe_affichage.repr_profil_coeffs(
                self.matrice_coeffs_moy_gen, with_index=True
            )
            pe_affichage.pe_print(f"     > Moyenne générale calculée avec : {aff}")

    def has_notes_tag(self, tag):
        """Détermine si le SxTag, pour un tag donné, est en cours d'évaluation.
        Si oui, n'a pas (encore) de notes dans le resformsemestre final.

        Args:
            tag: Le tag visé

        Returns:
            True si a des notes, False sinon
        """
        moy_tag_dernier_sem = self.ressembuttag_final.moyennes_tags[tag]
        return moy_tag_dernier_sem.has_notes()

    def __eq__(self, other):
        """Egalité de 2 SxTag sur la base de leur identifiant"""
        return self.sxtag_id == other.sxtag_id

    def get_repr(self, verbose=False) -> str:
        """Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle
        est basée)"""
        if verbose:
            return f"SXTag basé sur {self.semx.get_repr()}"
        else:
            # affichage = [str(fid) for fid in self.ressembuttags]
            return f"SXTag {self.agregat}#{self.fid_final}"

    def compute_notes_ues_cube(self, tag) -> (pd.DataFrame, np.array):
        """Construit le cube de notes des UEs (etudid x accronyme_ue x semestre_aggregé)
        nécessaire au calcul des moyennes du tag pour le RCS Sx.
        (Renvoie également le dataframe associé pour debug).

        Args:
            tag: Le tag considéré (personalisé ou "but")
        """
        # Index du cube (etudids -> dim 0, ues -> dim 1, semestres -> dim2)
        # etudids_sorted = etudids_sorted
        # acronymes_ues = sorted([ue.acronyme for ue in selMf.ues.values()])
        semestres_id = list(self.ressembuttags.keys())

        dfs = {}

        for frmsem_id in semestres_id:
            # Partant d'un dataframe vierge
            df = pd.DataFrame(
                np.nan, index=self.etudids_sorted, columns=self.acronymes_sorted
            )

            # Charge les notes du semestre tag
            sem_tag = self.ressembuttags[frmsem_id]
            moys_tag = sem_tag.moyennes_tags[tag]
            notes = moys_tag.matrice_notes_gen  # dataframe etudids x ues

            # les étudiants et les acronymes communs
            etudids_communs, acronymes_communs = pe_comp.find_index_and_columns_communs(
                df, notes
            )

            # Recopie
            df.loc[etudids_communs, acronymes_communs] = notes.loc[
                etudids_communs, acronymes_communs
            ]

            # Supprime tout ce qui n'est pas numérique
            for col in df.columns:
                df[col] = pd.to_numeric(df[col], errors="coerce")

            # Stocke le df
            dfs[frmsem_id] = df

        """Réunit les notes sous forme d'un cube etudids x ues x semestres"""
        semestres_x_etudids_x_ues = [dfs[fid].values for fid in dfs]
        etudids_x_ues_x_semestres = np.stack(semestres_x_etudids_x_ues, axis=-1)
        return dfs, etudids_x_ues_x_semestres

    def compute_notes_ues(
        self,
        set_cube: np.array,
        masque_cube: np.array,
        inscr_mask: np.array,
    ) -> pd.DataFrame:
        """Calcule la moyenne par UEs à un tag donné en prenant la note maximum (UE
        par UE) obtenue par un étudiant à un semestre.

        Args:
            set_cube: notes moyennes aux modules ndarray
                     (semestre_ids x etudids x UEs), des floats avec des NaN
            masque_cube: masque indiquant si la note doit être prise en compte ndarray
                         (semestre_ids x etudids x UEs), des 1.0 ou des 0.0
            inscr_mask: masque etudids x UE traduisant les inscriptions des
                                   étudiants aux UE (du semestre terminal)
        Returns:
            Un DataFrame avec pour columns les moyennes par ues,
            et pour rows les etudid
        """
        # etudids_sorted: liste des étudiants (dim. 0 du cube) trié par etudid
        # acronymes_sorted: liste des acronymes des ues (dim. 1 du cube) trié par acronyme
        nb_etuds, nb_ues, nb_semestres = set_cube.shape
        nb_etuds_mask, nb_ues_mask = inscr_mask.shape
        # assert nb_etuds == len(self.etudids_sorted)
        # assert nb_ues == len(self.acronymes_sorted)
        # assert nb_etuds == nb_etuds_mask
        # assert nb_ues == nb_ues_mask

        # Entrées à garder dans le cube en fonction du masque d'inscription aux UEs du parcours
        inscr_mask_3D = np.stack([inscr_mask] * nb_semestres, axis=-1)
        set_cube = set_cube * inscr_mask_3D

        # Entrées à garder en fonction des UEs capitalisées ou non
        set_cube = set_cube * masque_cube

        # Quelles entrées du cube contiennent des notes ?
        mask = ~np.isnan(set_cube)

        # Enlève les NaN du cube pour les entrées manquantes : NaN -> -1.0
        set_cube_no_nan = np.nan_to_num(set_cube, nan=-1.0)

        # Les moyennes par ues
        # TODO: Pour l'instant un max sans prise en compte des UE capitalisées
        etud_moy = np.max(set_cube_no_nan, axis=2)

        # Fix les max non calculé -1 -> NaN
        etud_moy[etud_moy < 0] = np.NaN

        # Le dataFrame
        etud_moy_tag_df = pd.DataFrame(
            etud_moy,
            index=self.etudids_sorted,  # les etudids
            columns=self.acronymes_sorted,  # les acronymes d'UEs
        )

        etud_moy_tag_df = etud_moy_tag_df.fillna(np.nan)

        return etud_moy_tag_df


def compute_masques_capitalisation_cube(
    etudids_sorted: list[int],
    acronymes_sorted: list[str],
    ressembuttags: dict[int, pe_ressemtag.ResSemBUTTag],
    formsemestre_id_final: int,
) -> (pd.DataFrame, np.array):
    """Construit le cube traduisant les masques des UEs à prendre en compte dans le calcul
    des moyennes, en utilisant le dataFrame de capitalisations de chaque ResSemBUTTag

    Ces masques contiennent : 1 si la note doit être prise en compte, 0 sinon

    Le masque des UEs à prendre en compte correspondant au semestre final (identifié par
    son formsemestre_id_final) est systématiquement à 1 (puisque les résultats
    de ce semestre doivent systématiquement
    être pris en compte notamment pour les étudiants non redoublant).

    Args:
        etudids_sorted: La liste des etudids triés par ordre croissant (dim 0)
        acronymes_sorted: La liste des acronymes de UEs triés par acronyme croissant (dim 1)
        ressembuttags: Le dictionnaire des résultats de semestres BUT (tous tags confondus)
        formsemestre_id_final: L'identifiant du formsemestre_id_final
    """
    # Index du cube (etudids -> dim 0, ues -> dim 1, semestres -> dim2)
    # etudids_sorted = etudids_sorted
    # acronymes_ues = sorted([ue.acronyme for ue in selMf.ues.values()])
    semestres_id = list(ressembuttags.keys())

    dfs = {}

    for frmsem_id in semestres_id:
        # Partant d'un dataframe contenant des 1.0
        if frmsem_id == formsemestre_id_final:
            df = pd.DataFrame(1.0, index=etudids_sorted, columns=acronymes_sorted)
        else:  # semestres redoublés
            df = pd.DataFrame(0.0, index=etudids_sorted, columns=acronymes_sorted)

            # Traitement des capitalisations : remplace les infos de capitalisations par les coeff 1 ou 0
            capitalisations = ressembuttags[frmsem_id].capitalisations
            capitalisations = capitalisations.replace(True, 1.0).replace(False, 0.0)

            # Met à 0 les coeffs des UEs non capitalisées pour les étudiants
            # inscrits dans les 2 semestres: 1.0*False => 0.0
            etudids_communs, acronymes_communs = pe_comp.find_index_and_columns_communs(
                df, capitalisations
            )

            df.loc[etudids_communs, acronymes_communs] = capitalisations.loc[
                etudids_communs, acronymes_communs
            ]

        # Stocke le df
        dfs[frmsem_id] = df

    """Réunit les notes sous forme d'un cube etudids x ues x semestres"""
    semestres_x_etudids_x_ues = [dfs[fid].values for fid in dfs]
    etudids_x_ues_x_semestres = np.stack(semestres_x_etudids_x_ues, axis=-1)
    return dfs, etudids_x_ues_x_semestres
