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

##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 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
#
##############################################################################

"""Fonctions de calcul des moyennes d'UE
"""
import numpy as np
import pandas as pd

from app import db
from app import models
from app.models import UniteEns, Module, ModuleImpl, ModuleUECoef
from app.comp import moy_mod
from app.models.formsemestre import FormSemestre
from app.scodoc import sco_codes_parcours


def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.DataFrame:
    """Charge les coefs des modules de la formation pour le semestre indiqué.

    Ces coefs lient les modules à chaque UE.

    Résultat: (module_coefs_df, ues, modules)
        DataFrame rows = UEs, columns = modules, value = coef.

    Considère toutes les UE (sauf sport) et modules du semestre.
    Les coefs non définis (pas en base) sont mis à zéro.

    Si semestre_idx None, prend toutes les UE de la formation.
    """
    ues = (
        UniteEns.query.filter_by(formation_id=formation_id)
        .filter(UniteEns.type != sco_codes_parcours.UE_SPORT)
        .order_by(UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme)
    )
    modules = Module.query.filter_by(formation_id=formation_id).order_by(
        Module.semestre_id, Module.module_type.desc(), Module.numero, Module.code
    )
    if semestre_idx is not None:
        ues = ues.filter_by(semestre_idx=semestre_idx)
        modules = modules.filter_by(semestre_id=semestre_idx)
    ues = ues.all()
    modules = modules.all()
    ue_ids = [ue.id for ue in ues]
    module_ids = [module.id for module in modules]
    module_coefs_df = pd.DataFrame(columns=module_ids, index=ue_ids, dtype=float)
    query = (
        db.session.query(ModuleUECoef)
        .filter(UniteEns.formation_id == formation_id)
        .filter(ModuleUECoef.ue_id == UniteEns.id)
    )
    if semestre_idx is not None:
        query = query.filter(UniteEns.semestre_idx == semestre_idx)

    for mod_coef in query:
        module_coefs_df[mod_coef.module_id][mod_coef.ue_id] = mod_coef.coef

    module_coefs_df.fillna(value=0, inplace=True)

    return module_coefs_df, ues, modules


def df_load_modimpl_coefs(
    formsemestre: models.FormSemestre, ues=None, modimpls=None
) -> pd.DataFrame:
    """Charge les coefs des modules du formsemestre indiqué.

    Comme df_load_module_coefs mais prend seulement les UE
    et modules du formsemestre.
    Si ues et modimpls sont None, prend tous ceux du formsemestre.
    Résultat: (module_coefs_df, ues, modules)
        DataFrame rows = UEs, columns = modimpl, value = coef.
    """
    if ues is None:
        ues = formsemestre.query_ues().all()
    ue_ids = [x.id for x in ues]
    if modimpls is None:
        modimpls = formsemestre.modimpls.all()
    modimpl_ids = [x.id for x in modimpls]
    mod2impl = {m.module.id: m.id for m in modimpls}
    modimpl_coefs_df = pd.DataFrame(columns=modimpl_ids, index=ue_ids, dtype=float)
    mod_coefs = (
        db.session.query(ModuleUECoef)
        .filter(ModuleUECoef.module_id == ModuleImpl.module_id)
        .filter(ModuleImpl.formsemestre_id == formsemestre.id)
    )

    for mod_coef in mod_coefs:
        modimpl_coefs_df[mod2impl[mod_coef.module_id]][mod_coef.ue_id] = mod_coef.coef
    modimpl_coefs_df.fillna(value=0, inplace=True)
    return modimpl_coefs_df, ues, modimpls


def notes_sem_assemble_cube(modimpls_notes: list[pd.DataFrame]) -> np.ndarray:
    """Réuni les notes moyennes des modules du semestre en un "cube"

    modimpls_notes : liste des moyennes de module
                     (DataFrames rendus par compute_module_moy, (etud x UE))
    Resultat: ndarray (etud x module x UE)
    """
    modimpls_notes_arr = [df.values for df in modimpls_notes]
    modimpls_notes = np.stack(modimpls_notes_arr)
    # passe de (mod x etud x ue) à (etud x mod x UE)
    return modimpls_notes.swapaxes(0, 1)


def notes_sem_load_cube(formsemestre):
    """Calcule le cube des notes du semestre
    (charge toutes les notes, calcule les moyenne des modules
    et assemble le cube)
    Resultat:
        sem_cube : ndarray (etuds x modimpls x UEs)
        modimpls_evals_poids dict { modimpl.id : evals_poids }
        modimpls_evals_notes dict { modimpl.id : evals_notes }
        modimpls_evaluations dict { modimpl.id : liste des évaluations }
        modimpls_evaluations_complete: {modimpl_id : liste de booleens (complete/non)}
    """
    modimpls_evals_poids = {}
    modimpls_evals_notes = {}
    modimpls_evaluations = {}
    modimpls_evaluations_complete = {}
    modimpls_notes = []
    for modimpl in formsemestre.modimpls:
        evals_notes, evaluations, evaluations_completes = moy_mod.df_load_modimpl_notes(
            modimpl.id
        )
        evals_poids, ues = moy_mod.df_load_evaluations_poids(modimpl.id)
        etuds_moy_module = moy_mod.compute_module_moy(
            evals_notes, evals_poids, evaluations, evaluations_completes
        )
        modimpls_evals_poids[modimpl.id] = evals_poids
        modimpls_evals_notes[modimpl.id] = evals_notes
        modimpls_evaluations[modimpl.id] = evaluations
        modimpls_evaluations_complete[modimpl.id] = evaluations_completes
        modimpls_notes.append(etuds_moy_module)
    return (
        notes_sem_assemble_cube(modimpls_notes),
        modimpls_evals_poids,
        modimpls_evals_notes,
        modimpls_evaluations,
        modimpls_evaluations_complete,
    )


def compute_ue_moys(
    sem_cube: np.array,
    etuds: list,
    modimpls: list,
    ues: list,
    modimpl_inscr_df: pd.DataFrame,
    modimpl_coefs_df: pd.DataFrame,
) -> pd.DataFrame:
    """Calcul de la moyenne d'UE
    La moyenne d'UE est un nombre (note/20), ou NI ou NA ou ERR
    NI non inscrit à (au moins un) module de cette UE
    NA pas de notes disponibles
    ERR erreur dans une formule utilisateur. [XXX pas encore gérées ici]

    sem_cube: notes moyennes aux modules
                ndarray (etuds x modimpls x UEs)
                (floats avec des NaN)
    etuds : lites des étudiants (dim. 0 du cube)
    modimpls : liste des modules à considérer (dim. 1 du cube)
    ues : liste des UE (dim. 2 du cube)
    module_inscr_df: matrice d'inscription du semestre (etud x modimpl)
    module_coefs_df: matrice coefficients (UE x modimpl)

    Resultat: DataFrame columns UE, rows etudid
    """
    nb_etuds, nb_modules, nb_ues = sem_cube.shape
    assert len(etuds) == nb_etuds
    assert len(modimpls) == nb_modules
    assert len(ues) == nb_ues
    assert modimpl_inscr_df.shape[0] == nb_etuds
    assert modimpl_inscr_df.shape[1] == nb_modules
    assert modimpl_coefs_df.shape[0] == nb_ues
    assert modimpl_coefs_df.shape[1] == nb_modules
    modimpl_inscr = modimpl_inscr_df.values
    modimpl_coefs = modimpl_coefs_df.values
    if nb_etuds == 0:
        return pd.DataFrame(
            index=modimpl_inscr_df.index, columns=modimpl_coefs_df.index
        )
    # Duplique les inscriptions sur les UEs:
    modimpl_inscr_stacked = np.stack([modimpl_inscr] * nb_ues, axis=2)
    # Enlève les NaN du numérateur:
    # si on veut prendre en compte les modules avec notes neutralisées ?
    sem_cube_no_nan = np.nan_to_num(sem_cube, nan=0.0)

    # Ne prend pas en compte les notes des étudiants non inscrits au module:
    # Annule les notes:
    sem_cube_inscrits = np.where(modimpl_inscr_stacked, sem_cube_no_nan, 0.0)
    # Annule les coefs des modules où l'étudiant n'est pas inscrit:
    modimpl_coefs_etuds = np.where(
        modimpl_inscr_stacked, np.stack([modimpl_coefs.T] * nb_etuds), 0.0
    )
    # Annule les coefs des modules NaN
    modimpl_coefs_etuds_no_nan = np.where(np.isnan(sem_cube), 0.0, modimpl_coefs_etuds)
    #
    # Version vectorisée
    #
    etud_moy_ue = np.sum(
        modimpl_coefs_etuds_no_nan * sem_cube_inscrits, axis=1
    ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
    return pd.DataFrame(
        etud_moy_ue, index=modimpl_inscr_df.index, columns=modimpl_coefs_df.index
    )