# -*- 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.models import FormSemestre
from app.pe import pe_affichage
import pandas as pd
import numpy as np
from app.pe.rcss import pe_rcs, pe_rcsemx
import app.pe.moys.pe_sxtag as pe_sxtag
import app.pe.pe_comp as pe_comp
from app.pe.moys import pe_tabletags, pe_moytag
from app.scodoc.sco_utils import ModuleType


class RCSemXTag(pe_tabletags.TableTag):
    def __init__(
        self,
        rcsemx: pe_rcsemx.RCSemX,
        sxstags: dict[(str, int) : pe_sxtag.SxTag],
        semXs_suivis: dict[int, dict],
    ):
        """Calcule les moyennes par tag (orientées compétences)
        d'un regroupement de SxTag
        (RCRCF), pour extraire les classements par tag pour un
        groupe d'étudiants donnés. Le groupe d'étudiants est formé par ceux ayant tous
        participé au même semestre terminal.

        Args:
            rcsemx: Le RCSemX (identifié par un nom et l'id de son semestre terminal)
            sxstags: Les données sur les SemX taggués
            semXs_suivis: Les données indiquant quels SXTags sont à prendre en compte
                          pour chaque étudiant
        """
        pe_tabletags.TableTag.__init__(self)

        self.rcs_id: tuple(str, int) = rcsemx.rcs_id
        """Identifiant du RCSemXTag (identique au RCSemX sur lequel il s'appuie)"""

        self.rcsemx: pe_rcsemx.RCSemX = rcsemx
        """Le regroupement RCSemX associé au RCSemXTag"""

        self.semXs_suivis = semXs_suivis
        """Les semXs suivis par les étudiants"""

        self.nom = self.get_repr()
        """Représentation textuelle du RSCtag"""

        # Les données du semestre final
        self.formsemestre_final: FormSemestre = rcsemx.formsemestre_final
        """Le semestre final"""
        self.fid_final: int = rcsemx.formsemestre_final.formsemestre_id
        """Le fid du semestre final"""

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

        # Les données aggrégés (RCRCF + SxTags)
        self.semXs_aggreges: dict[(str, int) : pe_rcsemx.RCSemX] = rcsemx.semXs_aggreges
        """Les SemX aggrégés"""
        self.sxstags_aggreges = {}
        """Les SxTag associés aux SemX aggrégés"""
        try:
            for rcf_id in self.semXs_aggreges:
                self.sxstags_aggreges[rcf_id] = sxstags[rcf_id]
        except:
            raise ValueError("Semestres SxTag manquants")
        self.sxtags_connus = sxstags  # Tous les sxstags connus

        # Les étudiants (etuds, états civils & etudis)
        sems_dans_aggregat = rcsemx.aggregat
        sxtag_final = self.sxstags_aggreges[(sems_dans_aggregat[-1], self.rcs_id[1])]
        self.etuds = sxtag_final.etuds
        """Les étudiants (extraits du semestre final)"""
        self.add_etuds(self.etuds)
        self.etudids_sorted = sorted(self.etudids)
        """Les étudids triés"""

        # Les compétences (extraites de tous les Sxtags)
        self.acronymes_ues_to_competences = self._do_acronymes_to_competences()
        """L'association acronyme d'UEs -> compétence (extraites des SxTag aggrégés)"""

        self.competences_sorted = sorted(
            set(self.acronymes_ues_to_competences.values())
        )
        """Compétences (triées par nom, extraites des SxTag aggrégés)"""
        aff = pe_affichage.repr_comp_et_ues(self.acronymes_ues_to_competences)
        pe_affichage.pe_print(f"--> Compétences : {', '.join(self.competences_sorted)}")

        # Les tags
        self.tags_sorted = self._do_taglist()
        """Tags extraits de tous les SxTag aggrégés"""
        aff_tag = ["👜" + tag for tag in self.tags_sorted]
        pe_affichage.pe_print(f"--> Tags : {', '.join(aff_tag)}")

        # Les moyennes
        self.moyennes_tags: dict[str, pe_moytag.MoyennesTag] = {}

        """Synthétise les moyennes/classements par tag (qu'ils soient personnalisé ou de compétences)"""
        for tag in self.tags_sorted:
            pe_affichage.pe_print(f"--> Moyennes du tag 👜{tag}")

            # Traitement des inscriptions aux semX(tags)
            # ******************************************
            # Cube d'inscription (etudids_sorted x compétences_sorted x sxstags)
            # indiquant quel sxtag est valide pour chaque étudiant
            inscr_df, inscr_cube = self.compute_inscriptions_comps_cube(tag)

            # Traitement des notes
            # ********************
            # Cube de notes  (etudids_sorted x compétences_sorted x sxstags)
            notes_df, notes_cube = self.compute_notes_comps_cube(tag)
            # Calcule les moyennes sous forme d'un dataframe en les "aggrégant"
            # compétence par compétence
            moys_competences = self.compute_notes_competences(notes_cube, inscr_cube)

            # Traitement des coeffs pour la moyenne générale
            # ***********************************************
            # Df des coeffs sur tous les SxTags aggrégés
            coeffs_df, coeffs_cube = self.compute_coeffs_comps_cube(tag)

            # Synthèse des coefficients à prendre en compte pour la moyenne générale
            matrice_coeffs_moy_gen = self.compute_coeffs_competences(
                coeffs_cube, inscr_cube, notes_cube
            )

            # Affichage des coeffs
            aff = pe_affichage.repr_profil_coeffs(
                matrice_coeffs_moy_gen, with_index=True
            )
            pe_affichage.pe_print(f"     > Moyenne calculée avec pour coeffs : {aff}")

            # Mémorise les moyennes et les coeff associés
            self.moyennes_tags[tag] = pe_moytag.MoyennesTag(
                tag,
                pe_moytag.CODE_MOY_COMPETENCES,
                moys_competences,
                matrice_coeffs_moy_gen,
            )

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

    def get_repr(self, verbose=True) -> str:
        """Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle
        est basée)"""
        if verbose:
            return f"{self.__class__.__name__} basé sur " + self.rcsemx.get_repr(
                verbose=verbose
            )
        else:
            return f"{self.__class__.__name__} {self.rcs_id}"

    def compute_notes_comps_cube(self, tag):
        """Pour un tag donné, construit le cube de notes (etudid x competences x SxTag)
        nécessaire au calcul des moyennes,
        en remplaçant les données d'UE (obtenus du SxTag) par les compétences

        Args:
            tag: Le tag visé
        """
        #         etudids_sorted: list[int],
        #         competences_sorted: list[str],
        #         sxstags: dict[(str, int) : pe_sxtag.SxTag],
        notes_dfs = {}

        for sxtag_id, sxtag in self.sxstags_aggreges.items():
            # Partant d'un dataframe vierge
            notes_df = pd.DataFrame(
                np.nan, index=self.etudids_sorted, columns=self.competences_sorted
            )
            # Charge les notes du semestre tag (copie car changement de nom de colonnes à venir)
            if tag in sxtag.moyennes_tags:  # si le tag est présent dans le semestre
                moys_tag = sxtag.moyennes_tags[tag]

                notes = moys_tag.matrice_notes_gen.copy()  # dataframe etudids x ues

                # Traduction des acronymes d'UE en compétences
                acronymes_ues_columns = notes.columns
                acronymes_to_comps = [
                    self.acronymes_ues_to_competences[acro]
                    for acro in acronymes_ues_columns
                ]
                notes.columns = acronymes_to_comps

                # Les étudiants et les compétences communes
                (
                    etudids_communs,
                    comp_communes,
                ) = pe_comp.find_index_and_columns_communs(notes_df, notes)

                # Recopie des notes et des coeffs
                notes_df.loc[etudids_communs, comp_communes] = notes.loc[
                    etudids_communs, comp_communes
                ]

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

            # Stocke les dfs
            notes_dfs[sxtag_id] = notes_df

        """Réunit les notes sous forme d'un cube etudids x competences x semestres"""
        sxtag_x_etudids_x_comps = [
            notes_dfs[sxtag_id] for sxtag_id in self.sxstags_aggreges
        ]
        notes_etudids_x_comps_x_sxtag = np.stack(sxtag_x_etudids_x_comps, axis=-1)

        return notes_dfs, notes_etudids_x_comps_x_sxtag

    def compute_coeffs_comps_cube(self, tag):
        """Pour un tag donné, construit
        le cube de coeffs (etudid x competences x SxTag) (traduisant les inscriptions
        des étudiants aux UEs en fonction de leur parcours)
        qui s'applique aux différents SxTag
        en remplaçant les données d'UE (obtenus du SxTag) par les compétences

        Args:
            tag: Le tag visé
        """
        #         etudids_sorted: list[int],
        #         competences_sorted: list[str],
        #         sxstags: dict[(str, int) : pe_sxtag.SxTag],

        coeffs_dfs = {}

        for sxtag_id, sxtag in self.sxstags_aggreges.items():
            # Partant d'un dataframe vierge
            coeffs_df = pd.DataFrame(
                np.nan, index=self.etudids_sorted, columns=self.competences_sorted
            )
            if tag in sxtag.moyennes_tags:
                moys_tag = sxtag.moyennes_tags[tag]

                # Charge les notes et les coeffs du semestre tag
                coeffs = moys_tag.matrice_coeffs_moy_gen.copy()  # les coeffs

                # Traduction des acronymes d'UE en compétences
                acronymes_ues_columns = coeffs.columns
                acronymes_to_comps = [
                    self.acronymes_ues_to_competences[acro]
                    for acro in acronymes_ues_columns
                ]
                coeffs.columns = acronymes_to_comps

                # Les étudiants et les compétences communes
                etudids_communs, comp_communes = pe_comp.find_index_and_columns_communs(
                    coeffs_df, coeffs
                )

                # Recopie des notes et des coeffs
                coeffs_df.loc[etudids_communs, comp_communes] = coeffs.loc[
                    etudids_communs, comp_communes
                ]

            # Stocke les dfs
            coeffs_dfs[sxtag_id] = coeffs_df

        """Réunit les coeffs sous forme d'un cube etudids x competences x semestres"""
        sxtag_x_etudids_x_comps = [
            coeffs_dfs[sxtag_id] for sxtag_id in self.sxstags_aggreges
        ]
        coeffs_etudids_x_comps_x_sxtag = np.stack(sxtag_x_etudids_x_comps, axis=-1)

        return coeffs_dfs, coeffs_etudids_x_comps_x_sxtag

    def compute_inscriptions_comps_cube(
        self,
        tag,
    ):
        """Pour un tag donné, construit
        le cube etudid x competences x SxTag traduisant quels sxtags est à prendre
        en compte pour chaque étudiant.
        Contient des 0 et des 1 pour indiquer la prise en compte.

        Args:
            tag: Le tag visé
        """
        #         etudids_sorted: list[int],
        #         competences_sorted: list[str],
        #         sxstags: dict[(str, int) : pe_sxtag.SxTag],
        # Initialisation
        inscriptions_dfs = {}

        for sxtag_id, sxtag in self.sxstags_aggreges.items():
            # Partant d'un dataframe vierge
            inscription_df = pd.DataFrame(
                0, index=self.etudids_sorted, columns=self.competences_sorted
            )

            # Les étudiants dont les résultats au sxtag ont été calculés
            etudids_sxtag = sxtag.etudids_sorted

            # Les étudiants communs
            etudids_communs = sorted(set(self.etudids_sorted) & set(etudids_sxtag))

            # Acte l'inscription
            inscription_df.loc[etudids_communs, :] = 1

            # Stocke les dfs
            inscriptions_dfs[sxtag_id] = inscription_df

        """Réunit les inscriptions sous forme d'un cube etudids x competences x semestres"""
        sxtag_x_etudids_x_comps = [
            inscriptions_dfs[sxtag_id] for sxtag_id in self.sxstags_aggreges
        ]
        inscriptions_etudids_x_comps_x_sxtag = np.stack(
            sxtag_x_etudids_x_comps, axis=-1
        )

        return inscriptions_dfs, inscriptions_etudids_x_comps_x_sxtag

    def _do_taglist(self) -> list[str]:
        """Synthétise les tags à partir des Sxtags aggrégés.

        Returns:
            Liste de tags triés par ordre alphabétique
        """
        tags = []
        for frmsem_id in self.sxstags_aggreges:
            tags.extend(self.sxstags_aggreges[frmsem_id].tags_sorted)
        return sorted(set(tags))

    def _do_acronymes_to_competences(self) -> dict[str:str]:
        """Synthétise l'association complète {acronyme_ue: competences}
        extraite de toutes les données/associations des SxTags
        aggrégés.

        Returns:
            Un dictionnaire {'acronyme_ue' : 'compétences'}
        """
        dict_competences = {}
        for sxtag_id, sxtag in self.sxstags_aggreges.items():
            dict_competences |= sxtag.acronymes_ues_to_competences
        return dict_competences

    def compute_notes_competences(self, set_cube: np.array, inscriptions: np.array):
        """Calcule la moyenne par compétences (à un tag donné) sur plusieurs semestres (partant du set_cube).

        La moyenne est un nombre (note/20), ou NaN si pas de notes disponibles

        *Remarque* : Adaptation de moy_ue.compute_ue_moys_apc au cas des moyennes de tag
        par aggrégat de plusieurs semestres.

        Args:
            set_cube: notes moyennes aux compétences ndarray
                     (etuds x UEs|compétences x sxtags), des floats avec des NaN
            inscriptions: inscrptions aux compétences ndarray
                     (etuds x UEs|compétences x sxtags), des 0 et des 1
        Returns:
            Un DataFrame avec pour columns les moyennes par tags,
            et pour rows les etudid
        """
        #             etudids_sorted: liste des étudiants (dim. 0 du cube)
        #             competences_sorted: list (dim. 1 du cube)
        nb_etuds, nb_comps, nb_semestres = set_cube.shape
        # assert nb_etuds == len(etudids_sorted)
        # assert nb_comps == len(competences_sorted)

        # Applique le masque d'inscriptions
        set_cube_significatif = set_cube * inscriptions

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

        # Enlève les NaN du cube de notes pour les entrées manquantes
        set_cube_no_nan = np.nan_to_num(set_cube_significatif, nan=0.0)

        # Les moyennes par tag
        with np.errstate(invalid="ignore"):  # ignore les 0/0 (-> NaN)
            etud_moy_tag = np.sum(set_cube_no_nan, axis=2) / np.sum(mask, axis=2)

        # Le dataFrame des notes moyennes
        etud_moy_tag_df = pd.DataFrame(
            etud_moy_tag,
            index=self.etudids_sorted,  # les etudids
            columns=self.competences_sorted,  # les competences
        )
        etud_moy_tag_df.fillna(np.nan)

        return etud_moy_tag_df

    def compute_coeffs_competences(
        self,
        coeff_cube: np.array,
        inscriptions: np.array,
        set_cube: np.array,
    ):
        """Calcule les coeffs à utiliser pour la moyenne générale (toutes compétences
        confondues), en fonction des inscriptions.

        Args:
            coeffs_cube: coeffs impliqués dans la moyenne générale (semestres par semestres)
            inscriptions: inscriptions aux UES|Compétences ndarray
                     (etuds x UEs|compétences x sxtags), des 0 ou des 1
            set_cube: les notes


        Returns:
            Un DataFrame de coefficients (etudids_sorted x compétences_sorted)
        """
        #             etudids_sorted: liste des étudiants (dim. 0 du cube)
        #             competences_sorted: list (dim. 1 du cube)
        nb_etuds, nb_comps, nb_semestres = inscriptions.shape
        # assert nb_etuds == len(etudids_sorted)
        # assert nb_comps == len(competences_sorted)

        # Applique le masque des inscriptions aux coeffs et aux notes
        coeffs_significatifs = coeff_cube * inscriptions

        # Enlève les NaN du cube de notes pour les entrées manquantes
        coeffs_cube_no_nan = np.nan_to_num(coeffs_significatifs, nan=0.0)

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

        # Retire les coefficients associés à des données sans notes
        coeffs_cube_no_nan = coeffs_cube_no_nan * mask

        # Somme les coefficients (correspondant à des notes)
        coeff_tag = np.sum(coeffs_cube_no_nan, axis=2)

        # Le dataFrame des coeffs
        coeffs_df = pd.DataFrame(
            coeff_tag, index=self.etudids_sorted, columns=self.competences_sorted
        )
        # Remet à Nan les coeffs à 0
        coeffs_df = coeffs_df.fillna(np.nan)

        return coeffs_df
