##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet.  All rights reserved.
# See LICENSE
##############################################################################

"""Classes spécifiques de calcul du bonus sport, culture ou autres activités

Les classes de Bonus fournissent deux méthodes:
    - get_bonus_ues()
    - get_bonus_moy_gen()


"""
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 bonus_sport
from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc.sco_utils import ModuleType


def get_bonus_sport_class_from_name(dept_id):
    """La classe de bonus sport pour le département indiqué.
    Note: en ScoDoc 9, le bonus sport est défini gloabelement et
    ne dépend donc pas du département.
    Résultat: une sous-classe de BonusSport
    """
    raise NotImplementedError()


class BonusSport:
    """Calcul du bonus sport.

    Arguments:
        - sem_modimpl_moys :
            notes moyennes aux modules (tous les étuds x tous les modimpls)
            floats avec des NaN.
            En classique: sem_matrix, ndarray (etuds x modimpls)
            En APC: sem_cube, ndarray (etuds x modimpls x UEs)
        - ues: les ues du semestre (incluant le bonus sport)
        - modimpl_inscr_df: matrice d'inscription aux modules du semestre (etud x modimpl)
        - modimpl_coefs: les coefs des modules
            En classique: 1d ndarray de float (modimpl)
            En APC: 2d ndarray de float, (modimpl x UE) <= attention à transposer
    """

    # Si vrai, en APC, si le bonus UE est None, reporte le bonus moy gen:
    apc_apply_bonus_mg_to_ues = True
    # Attributs virtuels:
    seuil_moy_gen = None
    proportion_point = None
    bonus_moy_gen_limit = None

    name = "virtual"

    def __init__(
        self,
        formsemestre: FormSemestre,
        sem_modimpl_moys: np.array,
        ues: list,
        modimpl_inscr_df: pd.DataFrame,
        modimpl_coefs: np.array,
    ):
        self.formsemestre = formsemestre
        self.ues = ues
        self.etuds_idx = modimpl_inscr_df.index  # les étudiants inscrits au semestre
        self.bonus_ues: pd.DataFrame = None  # virtual
        self.bonus_moy_gen: pd.Series = None  # virtual
        # Restreint aux modules standards des UE de type "sport":
        modimpl_mask = np.array(
            [
                (m.module.module_type == ModuleType.STANDARD)
                and (m.module.ue.type == UE_SPORT)
                for m in formsemestre.modimpls_sorted
            ]
        )
        self.modimpls_spo = [
            modimpl
            for i, modimpl in enumerate(formsemestre.modimpls_sorted)
            if modimpl_mask[i]
        ]
        "liste des modimpls sport"

        # Les moyennes des modules "sport": (une par UE en APC)
        sem_modimpl_moys_spo = sem_modimpl_moys[:, modimpl_mask]
        # Les inscriptions aux modules sport:
        modimpl_inscr_spo = modimpl_inscr_df.values[:, modimpl_mask]
        # Les coefficients des modules sport (en apc: nb_mod_sport x nb_ue)
        modimpl_coefs_spo = modimpl_coefs[modimpl_mask]
        # sem_modimpl_moys_spo est (nb_etuds, nb_mod_sport)
        #                       ou (nb_etuds, nb_mod_sport, nb_ues)
        nb_etuds, nb_mod_sport = sem_modimpl_moys_spo.shape[:2]
        nb_ues = len(ues)
        # Enlève les NaN du numérateur:
        sem_modimpl_moys_no_nan = np.nan_to_num(sem_modimpl_moys_spo, nan=0.0)

        # Annule les coefs des modules où l'étudiant n'est pas inscrit:
        if formsemestre.formation.is_apc():
            # BUT
            nb_ues_no_bonus = sem_modimpl_moys.shape[2]
            # Duplique les inscriptions sur les UEs non bonus:
            modimpl_inscr_spo_stacked = np.stack(
                [modimpl_inscr_spo] * nb_ues_no_bonus, axis=2
            )
            # Ne prend pas en compte les notes des étudiants non inscrits au module:
            # Annule les notes:
            sem_modimpl_moys_inscrits = np.where(
                modimpl_inscr_spo_stacked, sem_modimpl_moys_no_nan, 0.0
            )
            # Annule les coefs des modules où l'étudiant n'est pas inscrit:
            modimpl_coefs_etuds = np.where(
                modimpl_inscr_spo_stacked,
                np.stack([modimpl_coefs_spo.T] * nb_etuds),
                0.0,
            )
        else:
            # Formations classiques
            # Ne prend pas en compte les notes des étudiants non inscrits au module:
            # Annule les notes:
            sem_modimpl_moys_inscrits = np.where(
                modimpl_inscr_spo, sem_modimpl_moys_no_nan, 0.0
            )
            modimpl_coefs_spo = modimpl_coefs_spo.T
            modimpl_coefs_etuds = np.where(
                modimpl_inscr_spo, np.stack([modimpl_coefs_spo] * nb_etuds), 0.0
            )
        # Annule les coefs des modules NaN (nb_etuds x nb_mod_sport)
        modimpl_coefs_etuds_no_nan = np.where(
            np.isnan(sem_modimpl_moys_spo), 0.0, modimpl_coefs_etuds
        )
        #
        self.compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)

    def compute_bonus(
        self,
        sem_modimpl_moys_inscrits: np.ndarray,
        modimpl_coefs_etuds_no_nan: np.ndarray,
    ):
        """Calcul des bonus: méthode virtuelle à écraser.
        Arguments:
            - sem_modimpl_moys_inscrits:
                    ndarray (nb_etuds, mod_sport) ou en APC (nb_etuds, mods_sport, nb_ue)
                les notes aux modules sports auxquel l'étudiant est inscrit, 0 sinon. Pas de nans.
            - modimpl_coefs_etuds_no_nan:
                les coefficients: float ndarray

        Résultat: None
        """
        raise NotImplementedError("méthode virtuelle")

    def get_bonus_ues(self) -> pd.Series:
        """Les bonus à appliquer aux UE
        Résultat: DataFrame de float, index etudid, columns: ue.id
        """
        if (
            self.formsemestre.formation.is_apc()
            and self.apc_apply_bonus_mg_to_ues
            and self.bonus_ues is None
        ):
            # reporte uniformément le bonus moyenne générale sur les UEs
            # (assure la compatibilité de la plupart des anciens bonus avec le BUT)
            # ues = self.formsemestre.query_ues(with_sport=False)
            ues_idx = [ue.id for ue in self.formsemestre.query_ues(with_sport=False)]
            bonus_moy_gen = self.get_bonus_moy_gen()
            bonus_ues = np.stack([bonus_moy_gen.values] * len(ues_idx), axis=1)
            return pd.DataFrame(bonus_ues, index=self.etuds_idx, columns=ues_idx)

        return self.bonus_ues

    def get_bonus_moy_gen(self):
        """Le bonus à appliquer à la moyenne générale.
        Résultat: Series de float, index etudid
        """
        return self.bonus_moy_gen


class BonusSportSimples(BonusSport):
    """Les bonus sport simples calcule un bonus à partir des notes moyennes de modules
    de l'UE sport, et ce bonus est soit appliqué sur la moyenne générale (formations classiques),
    soit réparti sur les UE (formations APC).

    Le bonus est par défaut calculé comme:
        Les points au-dessus du seuil (par défaut) 10 sur 20 obtenus dans chacun des
        modules optionnels sont cumulés et une fraction de ces points cumulés s'ajoute
         à la moyenne générale du semestre déjà obtenue par l'étudiant.
    """

    seuil_moy_gen = 10.0  # seuls les points au dessus du seuil sont comptés
    proportion_point = 0.05  # multiplie les points au dessus du seuil

    def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
        """calcul du bonus"""
        bonus_moy_gen_arr = np.sum(
            np.where(
                sem_modimpl_moys_inscrits > self.seuil_moy_gen,
                (sem_modimpl_moys_inscrits - self.seuil_moy_gen)
                * self.proportion_point,
                0.0,
            ),
            axis=1,
        )
        # en APC, applati la moyenne gen. XXX pourrait être fait en amont
        if len(bonus_moy_gen_arr.shape) > 1:
            bonus_moy_gen_arr = bonus_moy_gen_arr.sum(axis=1)
        # Bonus moyenne générale, et 0 sur les UE
        self.bonus_moy_gen = pd.Series(
            bonus_moy_gen_arr, index=self.etuds_idx, dtype=float
        )
        if self.bonus_moy_gen_limit is not None:
            # Seuil: bonus (sur moy. gen.) limité à bonus_moy_gen_limit points
            self.bonus_moy_gen = self.bonus_moy_gen.clip(upper=self.bonus_moy_gen_limit)

        # Laisse bonus_ues à None, en APC le bonus moy. gen. sera réparti sur les UEs.


# bonus_ue = np.stack([modimpl_coefs_spo.T] * nb_ues)


class BonusIUTV(BonusSportSimples):
    """Calcul bonus modules optionels (sport, culture), règle IUT Villetaneuse

    Les étudiants de l'IUT peuvent suivre des enseignements optionnels
    de l'Université Paris 13 (sports, musique, deuxième langue,
    culture, etc) non rattachés à une unité d'enseignement. Les points
    au-dessus de 10 sur 20 obtenus dans chacune des matières
    optionnelles sont cumulés et 5% de ces points cumulés s'ajoutent à
    la moyenne générale du semestre déjà obtenue par l'étudiant.
    """

    name = "bonus_iutv"
    pass  # oui, c'ets le bonus par défaut


class BonusDirect(BonusSportSimples):
    """Bonus direct: les points sont directement ajoutés à la moyenne générale.
    Les coefficients sont ignorés: tous les points de bonus sont sommés.
    (rappel: la note est ramenée sur 20 avant application).
    """

    name = "bonus_direct"
    seuil_moy_gen = 0.0  # seuls le spoints au dessus du seuil sont comptés
    proportion_point = 1.0


class BonusIUTStDenis(BonusIUTV):
    """Semblable à bonus_iutv mais sans coefficients et total limité à 0.5 points."""

    name = "bonus_iut_stdenis"
    bonus_moy_gen_limit = 0.5


class BonusColmar(BonusSportSimples):
    """Calcul bonus modules optionels (sport, culture), règle IUT Colmar.

    Les étudiants de l'IUT peuvent suivre des enseignements optionnels
    de l'U.H.A.  (sports, musique, deuxième langue, culture, etc) non
    rattachés à une unité d'enseignement. Les points au-dessus de 10
    sur 20 obtenus dans chacune des matières optionnelles sont cumulés
    dans la limite de 10 points. 5% de ces points cumulés s'ajoutent à
    la moyenne générale du semestre déjà obtenue par l'étudiant.
    """

    # note: cela revient à dire que l'on ajoute 5% des points au dessus de 10,
    # et qu'on limite à 5% de 10, soit 0.5 points
    # ce bonus est donc strictement identique à celui de St Denis (BonusIUTStDenis)
    name = "bonus_colmar"
    bonus_moy_gen_limit = 0.5


class BonusVilleAvray:
    """Calcul bonus modules optionels (sport, culture), règle IUT Ville d'Avray

    Les étudiants de l'IUT peuvent suivre des enseignements optionnels
    de l'Université Paris 10 (C2I) non rattachés à une unité d'enseignement.
    Si la note est >= 10 et < 12, bonus de 0.1 point
    Si la note est >= 12 et < 16, bonus de 0.2 point
    Si la note est >= 16, bonus de 0.3 point
    Ce bonus s'ajoute à la moyenne générale du semestre déjà obtenue par
    l'étudiant.
    """

    name = "bonus_iutva"

    def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
        """calcul du bonus"""
        # Calcule moyenne pondérée des notes de sport:
        bonus_moy_gen_arr = np.sum(
            sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1
        ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
        bonus_moy_gen_arr[bonus_moy_gen_arr >= 10.0] = 0.1
        bonus_moy_gen_arr[bonus_moy_gen_arr >= 12.0] = 0.2
        bonus_moy_gen_arr[bonus_moy_gen_arr >= 16.0] = 0.3

        # Bonus moyenne générale, et 0 sur les UE
        self.bonus_moy_gen = pd.Series(
            bonus_moy_gen_arr, index=self.etuds_idx, dtype=float
        )
        if self.bonus_moy_gen_limit is not None:
            # Seuil: bonus (sur moy. gen.) limité à bonus_moy_gen_limit points
            self.bonus_moy_gen = self.bonus_moy_gen.clip(upper=self.bonus_moy_gen_limit)

        # Laisse bonus_ues à None, en APC le bonus moy. gen. sera réparti sur les UEs.


def get_bonus_class_dict(start=BonusSport, d=None):
    """Dictionnaire des classes de bonus
    (liste les sous-classes de BonusSport ayant un nom)
    Resultat: { name : class }
    """
    if d is None:
        d = {}
    if start.name != "virtual":
        d[start.name] = start
    for subclass in start.__subclasses__():
        get_bonus_class_dict(subclass, d=d)
    return d
