##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2023 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 datetime
import numpy as np
import pandas as pd

from flask import g

from app.scodoc.codes_cursus import UE_SPORT
from app.scodoc.codes_cursus import CursusDUT, CursusDUTMono
from app.scodoc.sco_utils import ModuleType


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 non bonus)
        - 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
        - etud_moy_gen: Series, index etudid, valeur float (moyenne générale avant bonus)
        - etud_moy_ue: DataFrame columns UE (sans sport), rows etudid (moyennes avant bonus)

        etud_moy_gen et etud_moy_ue ne sont PAS modifiés (mais utilisés par certains bonus non additifs).
    """

    # En classique, active un bonus sur les UEs: (dans ce cas bonus_moy_gen est ajusté pour le prendre en compte)
    classic_use_bonus_ues = False

    # Attributs virtuels:
    seuil_moy_gen = None
    proportion_point = None
    bonus_max = None

    name = "virtual"

    def __init__(
        self,
        formsemestre: "FormSemestre",
        sem_modimpl_moys: np.array,
        ues: list,
        modimpl_inscr_df: pd.DataFrame,
        modimpl_coefs: np.array,
        etud_moy_gen,
        etud_moy_ue,
    ):
        self.formsemestre = formsemestre
        self.ues = ues
        self.etud_moy_gen = etud_moy_gen
        self.etud_moy_ue = etud_moy_ue
        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 (pour formations non apc slt)
        # 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
            ]
        )
        if len(modimpl_mask) == 0:
            modimpl_mask = np.s_[:]  # il n'y a rien, on prend tout donc rien
        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)
        # donc (nb_etuds, nb_mod_sport, nb_ues_non_bonus)
        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) (toutes ues)
        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_non_bonus)
        nb_etuds, nb_mod_sport = sem_modimpl_moys_spo.shape[:2]
        if nb_etuds == 0 or nb_mod_sport == 0:
            return  # no bonus at all
        # 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: (nb_etuds, nb_mod_bonus, nb_ues_non_bonus)
            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] * 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
            if nb_etuds == 0:
                modimpl_coefs_etuds = modimpl_inscr_spo  # vide
            else:
                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_non_bonus)
                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.DataFrame:
        """Les bonus à appliquer aux UE
        Résultat: DataFrame de float, index etudid, columns: ue.id
        """
        if self.classic_use_bonus_ues or self.formsemestre.formation.is_apc():
            return self.bonus_ues
        return None

    def get_bonus_moy_gen(self):
        """Le bonus à appliquer à la moyenne générale.
        Résultat: Series de float, index etudid
        """
        if self.formsemestre.formation.is_apc():
            return None  # garde-fou
        return self.bonus_moy_gen


class BonusSportAdditif(BonusSport):
    """Bonus sport simples calcule un bonus à partir des notes moyennes de modules
    de l'UE sport, et ce bonus est soit ajouté à la moyenne générale (formations classiques),
    soit ajouté à chaque 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 bonus au dessus du seuil sont pris en compte
    # les points au dessus du seuil sont comptés (defaut: seuil_moy_gen):
    seuil_comptage = None
    proportion_point = 0.05  # multiplie les points au dessus du seuil
    bonux_max = 20.0  # le bonus ne peut dépasser 20 points
    bonus_min = 0.0  # et ne peut pas être négatif

    def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
        """calcul du bonus
        sem_modimpl_moys_inscrits: les notes de sport
            En APC: ndarray (nb_etuds, nb_mod_sport, nb_ues_non_bonus)
            En classic: ndarray (nb_etuds, nb_mod_sport)
        modimpl_coefs_etuds_no_nan: même shape, les coefs.
        """
        if 0 in sem_modimpl_moys_inscrits.shape:
            # pas d'étudiants ou pas d'UE ou pas de module...
            return
        seuil_comptage = (
            self.seuil_moy_gen if self.seuil_comptage is None else self.seuil_comptage
        )
        bonus_moy_arr = np.sum(
            np.where(
                (sem_modimpl_moys_inscrits >= self.seuil_moy_gen)
                & (modimpl_coefs_etuds_no_nan > 0),
                (sem_modimpl_moys_inscrits - seuil_comptage) * self.proportion_point,
                0.0,
            ),
            axis=1,
        )
        # Seuil: bonus dans [min, max] (défaut [0,20])
        bonus_max = self.bonus_max or 20.0
        np.clip(bonus_moy_arr, self.bonus_min, bonus_max, out=bonus_moy_arr)

        self.bonus_additif(bonus_moy_arr)

    def bonus_additif(self, bonus_moy_arr: np.array):
        "Set bonus_ues et bonus_moy_gen"
        # en APC, bonus_moy_arr est (nb_etuds, nb_ues_non_bonus)
        if self.formsemestre.formation.is_apc():
            # Bonus sur les UE et None sur moyenne générale
            ues_idx = [ue.id for ue in self.formsemestre.query_ues(with_sport=False)]
            self.bonus_ues = pd.DataFrame(
                bonus_moy_arr, index=self.etuds_idx, columns=ues_idx, dtype=float
            )
        elif self.classic_use_bonus_ues:
            # Formations classiques apppliquant le bonus sur les UEs
            # ici bonus_moy_arr = ndarray 1d nb_etuds
            ues_idx = [ue.id for ue in self.formsemestre.query_ues(with_sport=False)]
            self.bonus_ues = pd.DataFrame(
                np.stack([bonus_moy_arr] * len(ues_idx)).T,
                index=self.etuds_idx,
                columns=ues_idx,
                dtype=float,
            )
        else:
            # Bonus sur la moyenne générale seulement
            self.bonus_moy_gen = pd.Series(
                bonus_moy_arr, index=self.etuds_idx, dtype=float
            )


class BonusSportMultiplicatif(BonusSport):
    """Bonus sport qui multiplie les moyennes d'UE par un facteur"""

    seuil_moy_gen = 10.0  # seuls les points au dessus du seuil sont comptés
    amplitude = 0.005  # multiplie les points au dessus du seuil
    # En classique, les bonus multiplicatifs agissent par défaut sur les UE:
    classic_use_bonus_ues = True
    # Facteur multiplicatif max: (bonus = moy_ue*factor)
    factor_max = 1000.0  # infini

    # C'est un bonus "multiplicatif": on l'exprime en additif,
    # sur chaque moyenne d'UE m_0
    # Augmenter de 5% correspond à multiplier par a=1.05
    # m_1 = a . m_0
    # m_1 = m_0 + bonus
    # bonus = m_0 (a - 1)
    def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
        """calcul du bonus"""
        if 0 in sem_modimpl_moys_inscrits.shape:
            # pas d'étudiants ou pas d'UE ou pas de module...
            return
        # Calcule moyenne pondérée des notes de sport:
        notes = np.sum(
            sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1
        ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
        notes = np.nan_to_num(notes, copy=False)
        factor = (notes - self.seuil_moy_gen) * self.amplitude  # 5% si note=20
        factor[factor <= 0] = 0.0  # note < seuil_moy_gen, pas de bonus
        # note < seuil_moy_gen, pas de bonus: pas de facteur négatif, ni
        factor.clip(0.0, self.factor_max, out=factor)

        # Ne s'applique qu'aux moyennes d'UE
        if len(factor.shape) == 1:  # classic
            factor = factor[:, np.newaxis]
        bonus = self.etud_moy_ue * factor
        if self.bonus_max is not None:
            # Seuil: bonus limité à bonus_max points
            bonus.clip(upper=self.bonus_max, inplace=True)

        self.bonus_ues = bonus  # DataFrame

        # Les bonus multiplicatifs ne s'appliquent pas à la moyenne générale
        self.bonus_moy_gen = None


class BonusDirect(BonusSportAdditif):
    """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"
    displayed_name = 'Bonus "direct"'
    seuil_moy_gen = 0.0  # tous les points sont comptés
    proportion_point = 1.0


class BonusAisneStQuentin(BonusSportAdditif):
    """Calcul bonus modules optionels (sport, culture), règle IUT Aisne St Quentin

    <p>Les étudiants de l'IUT peuvent suivre des enseignements optionnels
    de l'Université de St Quentin non rattachés à une unité d'enseignement.
    </p>
    <ul>
    <li>Si la note est >= 10 et < 12.1, bonus de 0.1 point</li>
    <li>Si la note est >= 12.1 et < 14.1, bonus de 0.2 point</li>
    <li>Si la note est >= 14.1 et < 16.1, bonus de 0.3 point</li>
    <li>Si la note est >= 16.1 et < 18.1, bonus de 0.4 point</li>
    <li>Si la note est >= 18.1, bonus de 0.5 point</li>
    </ul>
    <p>
    Ce bonus s'ajoute à la moyenne générale du semestre déjà obtenue par
    l'étudiant (en BUT, s'ajoute à la moyenne de chaque UE).
    </p>
    """

    name = "bonus_iutstq"
    displayed_name = "IUT de Saint-Quentin"

    def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
        """calcul du bonus"""
        if 0 in sem_modimpl_moys_inscrits.shape:
            # pas d'étudiants ou pas d'UE ou pas de module...
            return
        # Calcule moyenne pondérée des notes de sport:
        with np.errstate(invalid="ignore"):  # ignore les 0/0 (-> NaN)
            bonus_moy_arr = np.sum(
                sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1
            ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
        np.nan_to_num(bonus_moy_arr, nan=0.0, copy=False)

        bonus_moy_arr[bonus_moy_arr < 10.0] = 0.0
        bonus_moy_arr[bonus_moy_arr >= 18.1] = 0.5
        bonus_moy_arr[bonus_moy_arr >= 16.1] = 0.4
        bonus_moy_arr[bonus_moy_arr >= 14.1] = 0.3
        bonus_moy_arr[bonus_moy_arr >= 12.1] = 0.2
        bonus_moy_arr[bonus_moy_arr >= 10] = 0.1

        self.bonus_additif(bonus_moy_arr)


class BonusAmiens(BonusSportAdditif):
    """Bonus IUT Amiens pour les modules optionnels (sport, culture, ...)

    <p><b>À partir d'août 2022:</b></p>
    <p>
    Deux activités optionnelles sont possibles chaque semestre, et peuvent donner lieu à une bonification de 0,1 chacune sur la moyenne de chaque UE.
    </p><p>
    La note saisie peut valoir 0 (pas de bonus), 1 (bonus de 0,1 points) ou 2 (bonus de 0,2 points).
    </p>

    <p><b>Avant juillet 2022:</b></p>
    <p>Toute note non nulle, peu importe sa valeur, entraine un bonus de 0,1 point
    sur toutes les moyennes d'UE.
    </p>
    """

    name = "bonus_amiens"
    displayed_name = "IUT d'Amiens"
    classic_use_bonus_ues = True  # s'applique aux UEs en DUT et LP
    seuil_moy_gen = 0.0  # tous les points sont comptés

    def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
        """calcul du bonus, avec réglage différent suivant la date"""

        if self.formsemestre.date_debut > datetime.date(2022, 8, 1):
            self.proportion_point = 0.1
            self.bonus_max = 0.2
        else:  # anciens semestres
            self.proportion_point = 1e10
            self.bonus_max = 0.1

        super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)


# Finalement ils n'en veulent pas.
# class BonusAnnecy(BonusSport):
#     """Calcul bonus modules optionnels (sport), règle IUT d'Annecy.

#     Il peut y avoir plusieurs modules de bonus.
#     Prend pour chaque étudiant la meilleure de ses notes bonus et
#     ajoute à chaque UE :<br>
#     0.05 point si >=10,<br>
#     0.1 point si >=12,<br>
#     0.15 point si >=14,<br>
#     0.2 point si >=16,<br>
#     0.25 point si >=18.
#     """

#     name = "bonus_iut_annecy"
#     displayed_name = "IUT d'Annecy"

#     def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
#         """calcul du bonus"""
#         # if math.prod(sem_modimpl_moys_inscrits.shape) == 0:
#         #    return  # no etuds or no mod sport
#         # Prend la note de chaque modimpl, sans considération d'UE
#         if len(sem_modimpl_moys_inscrits.shape) > 2:  # apc
#             sem_modimpl_moys_inscrits = sem_modimpl_moys_inscrits[:, :, 0]
#         # ici sem_modimpl_moys_inscrits est nb_etuds x nb_mods_bonus, en APC et en classic
#         note_bonus_max = np.max(sem_modimpl_moys_inscrits, axis=1)  # 1d, nb_etuds
#         bonus = np.zeros(note_bonus_max.shape)
#         bonus[note_bonus_max >= 10.0] = 0.05
#         bonus[note_bonus_max >= 12.0] = 0.10
#         bonus[note_bonus_max >= 14.0] = 0.15
#         bonus[note_bonus_max >= 16.0] = 0.20
#         bonus[note_bonus_max >= 18.0] = 0.25

#         # Bonus moyenne générale et sur les UE
#         self.bonus_moy_gen = pd.Series(bonus, index=self.etuds_idx, dtype=float)
#         ues_idx = [ue.id for ue in self.formsemestre.query_ues(with_sport=False)]
#         nb_ues_no_bonus = len(ues_idx)
#         self.bonus_ues = pd.DataFrame(
#             np.stack([bonus] * nb_ues_no_bonus, axis=1),
#             columns=ues_idx,
#             index=self.etuds_idx,
#             dtype=float,
#         )


class BonusBesanconVesoul(BonusSportAdditif):
    """Bonus IUT Besançon - Vesoul pour les UE libres

    <p>Toute note non nulle, peu importe sa valeur, entraine un bonus de 0,2 point
    sur toutes les moyennes d'UE.
    </p>
    """

    name = "bonus_besancon_vesoul"
    displayed_name = "IUT de Besançon - Vesoul"
    classic_use_bonus_ues = True  # s'applique aux UEs en DUT et LP
    seuil_moy_gen = 0.0  # tous les points sont comptés
    proportion_point = 1e10  # infini
    bonus_max = 0.2


class BonusBethune(BonusSportMultiplicatif):
    """
    Calcul bonus modules optionnels (sport, culture), règle IUT de Béthune.
    <p>
    <b>Pour le BUT :</b>
    La note de sport est sur 20, et on calcule une bonification (en %)
    qui va s'appliquer à <b>la moyenne de chaque UE</b> du semestre en appliquant
    la formule : bonification (en %) = max(note-10, 0)*(1/<b>500</b>).
    </p><p>
    <em>La bonification ne s'applique que si la note est supérieure à 10.</em>
    </p><p>
    (Une note de 10 donne donc 0% de bonif,
     1 point au dessus de 10 augmente la moyenne des UE de 0.2%)
    </p>
    <p>
    <b>Pour le DUT/LP :</b>
    La note de sport est sur 20, et on calcule une bonification (en %)
    qui va s'appliquer à <b>la moyenne générale</b> du semestre en appliquant
    la formule : bonification (en %) = max(note-10, 0)*(1/<b>200</b>).
    </p><p>
    <em>La bonification ne s'applique que si la note est supérieure à 10.</em>
    </p><p>
    (Une note de 10 donne donc 0% de bonif,
     1 point au dessus de 10 augmente la moyenne des UE de 0.5%)
    </p>
    """

    name = "bonus_iutbethune"
    displayed_name = "IUT de Béthune"
    seuil_moy_gen = 10.0  # points comptés au dessus de 10.

    def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
        """calcul du bonus"""
        if self.formsemestre.formation.is_apc():
            self.amplitude = 0.002
        else:
            self.amplitude = 0.005

        return super().compute_bonus(
            sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan
        )


class BonusBezier(BonusSportAdditif):
    """Calcul bonus modules optionnels (sport, culture), règle IUT de Bézier.

    Les étudiants de l'IUT peuvent suivre des enseignements optionnels
    sport , 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 3% de ces points cumulés s'ajoutent à
    la moyenne générale du semestre déjà obtenue par l'étudiant, dans
    la limite de 0,3 points.
    """

    # 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_iutbeziers"
    displayed_name = "IUT de Bézier"
    bonus_max = 0.3
    seuil_moy_gen = 10.0  # tous les points sont comptés
    proportion_point = 0.03


class BonusBlagnac(BonusSportAdditif):
    """Calcul bonus modules optionnels (sport, culture), règle IUT de Blagnac.

    Le bonus est égal à 5% des points au dessus de 10 à appliquer sur toutes
    les UE du semestre, applicable dans toutes les formations (DUT, BUT, ...).
    """

    name = "bonus_iutblagnac"
    displayed_name = "IUT de Blagnac"
    proportion_point = 0.05
    classic_use_bonus_ues = True  # toujours sur les UE


class BonusBordeaux1(BonusSportMultiplicatif):
    """Calcul bonus modules optionnels (sport, culture), règle IUT Bordeaux 1,
    sur moyenne générale et UEs.
    <p>
    Les étudiants de l'IUT peuvent suivre des enseignements optionnels
    de l'Université Bordeaux 1 (sport, théâtre) non rattachés à une unité d'enseignement.
    </p><p>
    Chaque point au-dessus de 10 sur 20 obtenus dans cet enseignement correspond à un %
    qui augmente la moyenne de chaque UE et la moyenne générale.<br>
    Formule : pourcentage = (points au dessus de 10) / 2
    </p><p>
    Par exemple : sport 13/20 : chaque UE sera multipliée par 1+0,015, ainsi que la moyenne générale.
    </p>
    """

    name = "bonus_iutBordeaux1"
    displayed_name = "IUT de Bordeaux"
    classic_use_bonus_ues = True  # s'applique aux UEs en DUT et LP
    seuil_moy_gen = 10.0
    amplitude = 0.005


# Exactement le même que Bordeaux:
class BonusBrest(BonusSportMultiplicatif):
    """Calcul bonus modules optionnels (sport, culture), règle IUT de Brest,
    sur moyenne générale et UEs.
    <p>
    Les étudiants de l'IUT peuvent suivre des enseignements optionnels
    de l'Université (sport, théâtre) non rattachés à une unité d'enseignement.
    </p><p>
    Chaque point au-dessus de 10 sur 20 obtenus dans cet enseignement correspond à un %
    qui augmente la moyenne de chaque UE et la moyenne générale.<br>
    Formule : pourcentage = (points au dessus de 10) / 2
    </p><p>
    Par exemple : sport 13/20 : chaque UE sera multipliée par 1+0,015, ainsi que la moyenne générale.
    </p>
    """

    name = "bonus_iut_brest"
    displayed_name = "IUT de Brest"
    classic_use_bonus_ues = True  # s'applique aux UEs en DUT et LP
    seuil_moy_gen = 10.0
    amplitude = 0.005


class BonusCachan1(BonusSportAdditif):
    """Calcul bonus optionnels (sport, culture), règle IUT de Cachan 1.

    <ul>
    <li> DUT/LP : la meilleure note d'option, si elle est supérieure à 10,
    bonifie les moyennes d'UE (uniquement UE13_E pour le semestre 1, UE23_E
    pour le semestre 2, UE33_E pour le semestre 3 et UE43_E pour le semestre
    4) à raison
    de <em>bonus = (option - 10)/10</em>.
    </li>
    <li> BUT : la meilleure note d'option, si elle est supérieure à 10, bonifie
    les moyennes d'UE à raison de <em>bonus = (option - 10) * 3%</em>.</li>
    </ul>
    """

    name = "bonus_cachan1"
    displayed_name = "IUT de Cachan 1"
    seuil_moy_gen = 10.0  # tous les points sont comptés
    proportion_point = 0.03
    classic_use_bonus_ues = True
    ues_bonifiables_cachan = {"UE13_E", "UE23_E", "UE33_E", "UE43_E"}

    def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
        """calcul du bonus, avec réglage différent suivant le type de formation"""
        # Prend la note de chaque modimpl, sans considération d'UE
        if len(sem_modimpl_moys_inscrits.shape) > 2:  # apc
            sem_modimpl_moys_inscrits = sem_modimpl_moys_inscrits[:, :, 0]
        # ici sem_modimpl_moys_inscrits est nb_etuds x nb_mods_bonus, en APC et en classic
        note_bonus_max = np.max(sem_modimpl_moys_inscrits, axis=1)  # 1d, nb_etuds
        ues = self.formsemestre.query_ues(with_sport=False).all()
        ues_idx = [ue.id for ue in ues]

        if self.formsemestre.formation.is_apc():  # --- BUT
            bonus_moy_arr = np.where(
                note_bonus_max > self.seuil_moy_gen,
                (note_bonus_max - self.seuil_moy_gen) * self.proportion_point,
                0.0,
            )
            self.bonus_ues = pd.DataFrame(
                np.stack([bonus_moy_arr] * len(ues)).T,
                index=self.etuds_idx,
                columns=ues_idx,
                dtype=float,
            )
        else:  # --- DUT
            # pareil mais proportion différente et application à certaines UEs
            proportion_point = 0.1
            bonus_moy_arr = np.where(
                note_bonus_max > self.seuil_moy_gen,
                (note_bonus_max - self.seuil_moy_gen) * proportion_point,
                0.0,
            )
            self.bonus_ues = pd.DataFrame(
                np.stack([bonus_moy_arr] * len(ues)).T,
                index=self.etuds_idx,
                columns=ues_idx,
                dtype=float,
            )
            # Applique bonus seulement sur certaines UE de code connu:
            for ue in ues:
                if ue.ue_code not in self.ues_bonifiables_cachan:
                    self.bonus_ues[ue.id] = 0.0  # annule


class BonusCaen(BonusSportAdditif):
    """Calcul bonus modules optionnels (sport, culture), règle IUT de Caen Normandie.

    Les étudiants de l'IUT de Caen Normandie peuvent suivre des enseignements
    optionnels non rattachés à une unité d'enseignement:
    <ul>
    <li><b>Sport</b>.
    <li><b>Engagement étudiant</b>
    </ul>
    Les points au-dessus de 10 sur 20 obtenus dans chacune de ces matières
    optionnelles sont cumulés et donnent lieu à un bonus sur chaque UE de 5%
    des points au dessus de 10 (soit +0,1 point pour chaque tranche de 2 points au
    dessus de 10).
    """

    name = "bonus_caen"
    displayed_name = "IUT de Caen Normandie"
    bonus_max = 1.0
    seuil_moy_gen = 10.0  # au dessus de 10
    proportion_point = 0.05  # 5%


class BonusCalais(BonusSportAdditif):
    """Calcul bonus modules optionnels (sport, culture), règle IUT LCO.

    Les étudiants de l'IUT LCO peuvent suivre des enseignements optionnels 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. 6% de ces points cumulés s'ajoutent :
    <ul>
    <li><b>en DUT</b> à la moyenne générale du semestre déjà obtenue par l'étudiant.
    </li>
    <li><b>en BUT et LP</b> à la moyenne des UE dont l'acronyme fini par <b>BS</b>
    (ex : UE2.1BS, UE32BS)
    </li>
    </ul>
    """

    name = "bonus_calais"
    displayed_name = "IUT du Littoral"
    bonus_max = 0.6
    seuil_moy_gen = 10.0  # au dessus de 10
    proportion_point = 0.06  # 6%

    def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
        parcours = self.formsemestre.formation.get_cursus()
        # Variantes de DUT ?
        if (
            isinstance(parcours, CursusDUT)
            or parcours.TYPE_CURSUS == CursusDUTMono.TYPE_CURSUS
        ):  # DUT
            super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
        else:
            self.classic_use_bonus_ues = True  # pour les LP
            super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
            ues = self.formsemestre.query_ues(with_sport=False).all()
            ues_sans_bs = [
                ue for ue in ues if ue.acronyme[-2:].upper() != "BS"
            ]  # les 2 derniers cars forcés en majus
            for ue in ues_sans_bs:
                self.bonus_ues[ue.id] = 0.0


class BonusColmar(BonusSportAdditif):
    """Calcul bonus modules optionnels (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"
    displayed_name = "IUT de Colmar"
    bonus_max = 0.5
    seuil_moy_gen = 10.0  # tous les points sont comptés
    proportion_point = 0.05


class BonusGrenobleIUT1(BonusSportMultiplicatif):
    """Bonus IUT1 de Grenoble

    <p>
    À compter de sept. 2021:
    La note de sport est sur 20, et on calcule une bonification (en %)
    qui va s'appliquer à la moyenne de chaque UE du semestre en appliquant
    la formule : bonification (en %) = (note-10)*0,5.
    </p><p>
    <em>La bonification ne s'applique que si la note est supérieure à 10.</em>
    </p><p>
    (Une note de 10 donne donc 0% de bonif, et une note de 20 : 5% de bonif)
    </p><p>
    Avant sept. 2021, la note de sport allait de 0 à 5 points (sur 20).
    Chaque point correspondait à 0.25% d'augmentation de la moyenne
    générale.
    Par exemple : note de sport 2/5 : la moyenne générale était augmentée de 0.5%.
    </p>
    """

    name = "bonus_iut1grenoble_2017"
    displayed_name = "IUT de Grenoble 1"
    # C'est un bonus "multiplicatif": on l'exprime en additif,
    # sur chaque moyenne d'UE m_0
    # Augmenter de 5% correspond à multiplier par a=1.05
    # m_1 = a . m_0
    # m_1 = m_0 + bonus
    # bonus = m_0 (a - 1)
    def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
        """calcul du bonus, avec réglage différent suivant la date"""

        if self.formsemestre.date_debut > datetime.date(2021, 7, 15):
            self.seuil_moy_gen = 10.0
            self.amplitude = 0.005
        else:  # anciens semestres
            self.seuil_moy_gen = 0.0
            self.amplitude = 1 / 400.0

        super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)


class BonusIUTRennes1(BonusSportAdditif):
    """Calcul bonus optionnels (sport, langue vivante, engagement étudiant),
    règle IUT de l'Université de Rennes 1 (Lannion, Rennes, St Brieuc, St Malo).

    <ul>
    <li>Les étudiants peuvent suivre un ou plusieurs activités optionnelles notées
    dans les semestres pairs.<br>
    La meilleure des notes obtenue est prise en compte, si elle est supérieure à 10/20.
    </li>
    <li>Le vingtième des points au dessus de 10 est ajouté à la moyenne de chaque UE
    en BUT, ou à la moyenne générale pour les autres formations.
    </li>
    <li> Exemple: un étudiant ayant 16/20 bénéficiera d'un bonus de (16-10)/20 = 0,3 points
    sur chaque UE.
    </li>
    </ul>
    """

    name = "bonus_iut_rennes1"
    displayed_name = "IUTs de Rennes 1 (Lannion, Rennes, St Brieuc, St Malo)"
    seuil_moy_gen = 10.0
    proportion_point = 1 / 20.0
    classic_use_bonus_ues = False
    # S'applique aussi en classic, sur la moy. gen.
    def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
        """calcul du bonus"""
        # Prend la note de chaque modimpl, sans considération d'UE
        if len(sem_modimpl_moys_inscrits.shape) > 2:  # apc
            sem_modimpl_moys_inscrits = sem_modimpl_moys_inscrits[:, :, 0]
        # ici sem_modimpl_moys_inscrits est nb_etuds x nb_mods_bonus, en APC et en classic
        note_bonus_max = np.max(sem_modimpl_moys_inscrits, axis=1)  # 1d, nb_etuds
        nb_ues = self.formsemestre.query_ues(with_sport=False).count()

        bonus_moy_arr = np.where(
            note_bonus_max > self.seuil_moy_gen,
            (note_bonus_max - self.seuil_moy_gen) * self.proportion_point,
            0.0,
        )
        # Seuil: bonus dans [min, max] (défaut [0,20])
        bonus_max = self.bonus_max or 20.0
        np.clip(bonus_moy_arr, self.bonus_min, bonus_max, out=bonus_moy_arr)
        if self.formsemestre.formation.is_apc():
            bonus_moy_arr = np.stack([bonus_moy_arr] * nb_ues).T

        self.bonus_additif(bonus_moy_arr)


# juste pour compatibilité (nom bonus en base):
class BonusStBrieuc(BonusIUTRennes1):
    name = "bonus_iut_stbrieuc"
    displayed_name = "IUTs de Rennes 1/St-Brieuc"
    __doc__ = BonusIUTRennes1.__doc__


class BonusStMalo(BonusIUTRennes1):
    name = "bonus_iut_stmalo"
    displayed_name = "IUTs de Rennes 1/St-Malo"
    __doc__ = BonusIUTRennes1.__doc__


class BonusLaRocheSurYon(BonusSportAdditif):
    """Bonus IUT de La Roche-sur-Yon

    Si une note de bonus est saisie, l'étudiant est gratifié de 0,2 points
    sur sa moyenne générale ou, en BUT, sur la moyenne de chaque UE.
    """

    name = "bonus_larochesuryon"
    displayed_name = "IUT de La Roche-sur-Yon"
    seuil_moy_gen = 0.0
    seuil_comptage = 0.0
    proportion_point = 1e10  # le moindre point sature le bonus
    bonus_max = 0.2  # à 0.2


class BonusLaRochelle(BonusSportAdditif):
    """Calcul bonus modules optionnels (sport, culture), règle IUT de La Rochelle.

    <ul>
    <li>Si la note de sport est comprise entre 0 et 10 : pas d'ajout de point.</li>
    <li>Si la note de sport est comprise entre 10 et 20 :
        <ul>
            <li>Pour le BUT, application pour chaque UE du semestre :
                <ul>
                <li>pour une note entre 18 et 20 => + 0,10 points</li>
                <li>pour une note entre 16 et 17,99 => + 0,08 points</li>
                <li>pour une note entre 14 et 15,99 => + 0,06 points</li>
                <li>pour une note entre 12 et 13,99 => + 0,04 points</li>
                <li>pour une note entre 10 et 11,99 => + 0,02 points</li>
                </ul>
            </li>
            <li>Pour les DUT/LP :
                ajout de 1% de la note sur la moyenne générale du semestre
            </li>
        </ul>
    </li>
    </ul>
    """

    name = "bonus_iutlr"
    displayed_name = "IUT de La Rochelle"

    seuil_moy_gen = 10.0  # si bonus > 10,
    seuil_comptage = 0.0  # tous les points sont comptés
    proportion_point = 0.01  # 1%

    def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
        """calcul du bonus"""
        # La date du semestre ?
        if self.formsemestre.formation.is_apc():
            if 0 in sem_modimpl_moys_inscrits.shape:
                # pas d'étudiants ou pas d'UE ou pas de module...
                return
            # Calcule moyenne pondérée des notes de sport:
            with np.errstate(invalid="ignore"):  # ignore les 0/0 (-> NaN)
                bonus_moy_arr = np.sum(
                    sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1
                ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
            np.nan_to_num(bonus_moy_arr, nan=0.0, copy=False)
            bonus_moy_arr[bonus_moy_arr < 10.0] = 0.0
            bonus_moy_arr[bonus_moy_arr >= 18.0] = 0.10
            bonus_moy_arr[bonus_moy_arr >= 16.0] = 0.08
            bonus_moy_arr[bonus_moy_arr >= 14.0] = 0.06
            bonus_moy_arr[bonus_moy_arr >= 12.0] = 0.04
            bonus_moy_arr[bonus_moy_arr >= 10.0] = 0.02
            self.bonus_additif(bonus_moy_arr)
        else:
            # DUT et LP:
            return super().compute_bonus(
                sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan
            )


class BonusLeHavre(BonusSportAdditif):
    """Bonus sport IUT du Havre sur les moyennes d'UE

    <p>
    Les enseignements optionnels de langue, préprofessionnalisation,
    PIX (compétences numériques), l'entrepreneuriat étudiant, l'engagement
    bénévole au sein d'association dès lors qu'une grille d'évaluation des
    compétences existe ainsi que les activités sportives et culturelles
    seront traités au niveau semestriel.
    </p><p>
    Le maximum de bonification qu'un étudiant peut obtenir sur sa moyenne
    est plafonné à 0.5 point.
    </p><p>
    Lorsqu'un étudiant suit plus de deux matières qui donnent droit à
    bonification, l'étudiant choisit les deux notes à retenir.
    </p><p>
    Les points bonus ne sont acquis que pour une note supérieure à 10/20.
    </p><p>
    La bonification est calculée de la manière suivante :<br>

    Pour chaque matière (max. 2) donnant lieu à bonification :<br>

    Bonification =  (N-10) x 0,05,
    N étant  la note de l'activité sur 20.
    </p>
    """

    # note: ScoDoc ne vérifie pas que le nombre de modules avec inscription n'excède pas 2
    name = "bonus_iutlh"
    displayed_name = "IUT du Havre"
    classic_use_bonus_ues = True  # sur les UE, même en DUT et LP
    seuil_moy_gen = 10.0  # seuls les points au dessus du seuil sont comptés
    proportion_point = 0.05
    bonus_max = 0.5  #


class BonusLeMans(BonusSportAdditif):
    """Calcul bonus modules optionnels (sport, culture), règle IUT Le Mans.

    <p>Les points au-dessus de 10 sur 20 obtenus dans chacune des matières
    optionnelles sont cumulés.
    </p>
    <ul>
    <li>En BUT: la moyenne de chacune des UE du semestre est augmentée de
    2% du cumul des points de bonus;</li>

    <li>En DUT/LP: la moyenne générale est augmentée de 5% du cumul des points bonus.
    </li>
    </ul>
    <p>Dans tous les cas, le bonus est dans la limite de 0,5 point.</p>
    """

    name = "bonus_iutlemans"
    displayed_name = "IUT du Mans"
    seuil_moy_gen = 10.0  # points comptés au dessus de 10.
    bonus_max = 0.5  #

    def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
        """calcul du bonus"""
        # La date du semestre ?
        if self.formsemestre.formation.is_apc():
            self.proportion_point = 0.02
        else:
            self.proportion_point = 0.05
        return super().compute_bonus(
            sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan
        )


# Bonus simple, mais avec changement de paramètres en 2010 !
class BonusLille(BonusSportAdditif):
    """Calcul bonus modules optionnels (sport, culture), règle IUT Villeneuve d'Ascq

    <p>Les étudiants de l'IUT peuvent suivre des enseignements optionnels
    de l'Université Lille (sports, etc) non rattachés à une unité d'enseignement.
    </p><p>
    Les points au-dessus de 10 sur 20 obtenus dans chacune des matières
    optionnelles sont cumulés et 4% (2% avant août 2010) de ces points cumulés
    s'ajoutent à la moyenne générale du semestre déjà obtenue par l'étudiant.
    </p>
    """

    name = "bonus_lille"
    displayed_name = "IUT de Lille"
    seuil_moy_gen = 10.0  # points comptés au dessus de 10.

    def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
        """calcul du bonus"""
        # La date du semestre ?
        if self.formsemestre.date_debut > datetime.date(2010, 8, 1):
            self.proportion_point = 0.04
        else:
            self.proportion_point = 0.02
        return super().compute_bonus(
            sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan
        )


class BonusLimousin(BonusSportAdditif):
    """Calcul bonus modules optionnels (sport, culture) à l'IUT du Limousin

    Les points au-dessus de 10 sur 20 obtenus dans chacune des matières optionnelles
    sont cumulés.

    La moyenne de chacune des UE du semestre pair est augmentée de 5% du
    cumul des points de bonus.
    Le maximum de points bonus est de 0,5.
    """

    name = "bonus_limousin"
    displayed_name = "IUT du Limousin"
    proportion_point = 0.05
    bonus_max = 0.5


class BonusLyon(BonusSportAdditif):
    """Calcul bonus modules optionnels (sport, culture) à l'IUT de Lyon (2022)

    <p><b>À partir de 2022-2023 :</b>
    des points de bonification seront ajoutés aux moyennes de toutes les UE
    du semestre concerné (3/100e de point par point au-dessus de 10).<br>
    Cette bonification ne pourra excéder 1/2 point sur chacune des UE
    </p>
    <ul>
    <li>Exemple 1 :<br>
    <tt>
    Sport 12/20 => +0.06<br>
    LV2 13/20   => +0.09<br>
    Bonus total = +0.15 appliqué à toutes les UE du semestre
    </tt>
    </li>
    <li>Exemple 2 :<br>
    <tt>
    Sport 20/20 => +0.30<br>
    LV2 18/20   => +0.24<br>
    Bonus total = +0.50 appliqué à toutes les UE du semestre
    </tt></li>
    </ul>

    <p><b>Jusqu'en 2021-2022 :</b>
    Les points au-dessus de 10 sur 20 obtenus dans chacune des matières
    optionnelles sont cumulés et 1,8% de ces points cumulés
    s'ajoutent aux moyennes générales, dans la limite d'1/2 point.
    </p>
    """

    name = "bonus_lyon_provisoire"
    displayed_name = "IUT de Lyon"
    seuil_moy_gen = 10.0  # points comptés au dessus de 10.
    bonus_max = 0.5

    def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
        if self.formsemestre.date_debut > datetime.date(2022, 8, 1):
            self.classic_use_bonus_ues = True  # pour les LP
            self.proportion_point = 0.03
        else:
            self.classic_use_bonus_ues = False
            self.proportion_point = 0.018
        return super().compute_bonus(
            sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan
        )


class BonusMantes(BonusSportAdditif):
    """Calcul bonus modules optionnels (investissement, ...), IUT de Mantes en Yvelines.

    <p>
    Soit N la note attribuée, le bonus (ou malus) correspond à :
    (N-10) x 0,05
    appliqué sur chaque UE du semestre sélectionné pour le BUT
    ou appliqué sur la moyenne générale du semestre sélectionné pour le DUT.
    </p>
    <p>Exemples :</p>
    <ul>
        <li> pour une note de 20 : bonus de + 0,5</li>
        <li> pour une note de 15 : bonus de + 0,25</li>
        <li> note de 10 : Ni bonus, ni malus (+0)</li>
        <li> note de 5, malus :  - 0,25</li>
        <li> note de 0,malus : - 0,5</li>
    </ul>
    """

    name = "bonus_mantes"
    displayed_name = "IUT de Mantes en Yvelines"
    bonus_min = -0.5  # peut être NEGATIF !
    bonus_max = 0.5
    seuil_moy_gen = 0.0  # tous les points comptent
    seuil_comptage = 10.0  # pivot à 10.
    proportion_point = 0.05


class BonusMulhouse(BonusSportAdditif):
    """Calcul bonus modules optionnels (sport, culture) à l'IUT de Mulhouse

    La moyenne  de chacune des UE du  semestre sera  majorée à  hauteur de
    5% du  cumul des points supérieurs à 10 obtenus en matières optionnelles,
    dans la limite de 0,5 point.
    """

    name = "bonus_iutmulhouse"
    displayed_name = "IUT de Mulhouse"
    seuil_moy_gen = 10.0  # points comptés au dessus de 10.
    proportion_point = 0.05
    bonus_max = 0.5  #


class BonusNantes(BonusSportAdditif):
    """IUT de Nantes (Septembre 2018)

    <p>Nous avons différents types de bonification
    (sport, culture, engagement citoyen).
    </p><p>
    Nous ajoutons aux moyennes une bonification de 0,2 pour chaque item
    la bonification totale ne doit pas excéder les 0,5 point.
    Sur le bulletin nous ne mettons pas une note sur 20 mais directement les bonifications.
    </p><p>
    Dans ScoDoc: on a déclarera une UE "sport&culture" dans laquelle on aura
    des modules pour chaque activité (Sport, Associations, ...)
    avec à chaque fois une note (ScoDoc l'affichera comme une note sur 20,
    mais en fait ce sera la valeur de la bonification: entrer 0,1/20 signifiera
    un bonus de 0,1 point la moyenne générale).
    </p>
    """

    name = "bonus_nantes"
    displayed_name = "IUT de Nantes"
    seuil_moy_gen = 0.0  # seuls les points au dessus du seuil sont comptés
    proportion_point = 1  # multiplie les points au dessus du seuil
    bonus_max = 0.5  # plafonnement à 0.5 points


class BonusOrleans(BonusSportAdditif):
    """Calcul bonus modules optionnels (sport, culture), règle IUT d'Orléans
    <p><b>Cadre général :</b>
    En reconnaissance de l'engagement des étudiants dans la vie associative,
    sociale ou professionnelle, l'IUT d'Orléans accorde, sous conditions,
    une bonification aux étudiants inscrits qui en font la demande en début
    d'année universitaire.
    </p>
    <p>Cet engagement doit être régulier et correspondre à une activité réelle
    et sérieuse qui bénéficie à toute la communauté étudiante de l'IUT,
    de l'Université ou à l'ensemble de la collectivité.</p>
    <p><b>Bonification :</b>
    Pour les DUT et LP, cette bonification interviendra sur la moyenne générale
    des semestres pairs :
    <ul><li> du 2ème semestre pour les étudiants de 1ère année de DUT</li>
    <li> du 4ème semestre pour les étudiants de 2nde année de DUT</li>
    <li> du 6ème semestre pour les étudiants en LP</li>
    </ul>
    Pour le BUT, cette bonification interviendra sur la moyenne de chacune
    des UE des semestre pairs :
    <ul><li> du 2ème semestre pour les étudiants de 1ère  année de BUT</li>
    <li> du 4ème semestre pour les étudiants de 2ème  année de BUT</li>
    <li> du 6ème semestre pour les étudiants de 3ème  année de BUT</li>
    </ul>
    <em>La bonification ne peut dépasser +0,5 points par année universitaire.</em>
    </p>
    <p><b> Avant février 2020 :</b>
    Un bonus de 2,5% de la note de sport est accordé à la moyenne générale.
    </p>
    """

    name = "bonus_iutorleans"
    displayed_name = "IUT d'Orléans"
    bonus_max = 0.5
    seuil_moy_gen = 0.0  # seuls les points au dessus du seuil sont comptés
    proportion_point = 1
    classic_use_bonus_ues = False

    def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
        if self.formsemestre.date_debut > datetime.date(2020, 2, 1):
            self.proportion_point = 1.0
        else:
            self.proportion_point = 2.5 / 100.0
        return super().compute_bonus(
            sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan
        )


class BonusPoitiers(BonusSportAdditif):
    """Calcul bonus optionnels (sport, culture), règle IUT de Poitiers.

    Les deux notes d'option supérieure à 10, bonifient les moyennes de chaque UE.

    bonus = (option1 - 10)*5% + (option2 - 10)*5%
    """

    name = "bonus_poitiers"
    displayed_name = "IUT de Poitiers"
    proportion_point = 0.05


class BonusRoanne(BonusSportAdditif):
    """IUT de Roanne.

    Le bonus est compris entre 0 et 0.6 points
    et est toujours appliqué aux UEs.
    """

    name = "bonus_iutr"
    displayed_name = "IUT de Roanne"
    seuil_moy_gen = 0.0
    bonus_max = 0.6  # plafonnement à 0.6 points
    classic_use_bonus_ues = True  # sur les UE, même en DUT et LP
    proportion_point = 1


class BonusSceaux(BonusSportAdditif):  # atypique
    """IUT de Sceaux

    L’IUT de Sceaux (Université de Paris-Saclay) propose aux étudiants un seul enseignement
    non rattaché aux UE : l’option Sport.
    <p>
    Cette option donne à l’étudiant qui la suit une bonification qui s’applique uniquement
    si sa note est supérieure à 10.
    </p>
    <p>
    Cette bonification s’applique sur l’ensemble des UE d’un semestre de la façon suivante :
    </p>
    <p>
    <tt>
    [ (Note – 10) / Nb UE du semestre ] / Total des coefficients de chaque UE
    </tt>
    </p>

    <p>
    Exemple : un étudiant qui a obtenu 16/20 à l’option Sport en S1
    (composé par exemple de 3 UE:UE1.1, UE1.2 et UE1.3)
    aurait les bonifications suivantes :
    </p>
    <ul>
    <li>UE1.1 (Total des coefficients : 15) ⇒ Bonification UE1.1 = <tt>[ (16 – 10) / 3 ] /15
    </tt>
    </li>
    <li>UE1.2 (Total des coefficients : 14) ⇒ Bonification UE1.2 = <tt>[ (16 – 10) / 3 ] /14
    </tt>
    </li>
    <li>UE1.3 (Total des coefficients : 12,5) ⇒ Bonification UE1.3 = <tt>[ (16 – 10) / 3 ] /12,5
    </tt>
    </li>
    </ul>
    """

    name = "bonus_iut_sceaux"
    displayed_name = "IUT de Sceaux"
    proportion_point = 1.0

    def __init__(
        self,
        formsemestre: "FormSemestre",
        sem_modimpl_moys: np.array,
        ues: list,
        modimpl_inscr_df: pd.DataFrame,
        modimpl_coefs: np.array,
        etud_moy_gen,
        etud_moy_ue,
    ):
        # Pour ce bonus, il faut conserver:
        # - le nombre d'UEs
        self.nb_ues = len([ue for ue in ues if ue.type != UE_SPORT])
        #  - le total des coefs de chaque UE
        # modimpl_coefs : DataFrame, lignes modimpl, col UEs (sans sport)
        self.sum_coefs_ues = modimpl_coefs.sum()  # Series, index ue_id
        super().__init__(
            formsemestre,
            sem_modimpl_moys,
            ues,
            modimpl_inscr_df,
            modimpl_coefs,
            etud_moy_gen,
            etud_moy_ue,
        )

    def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
        """Calcul du bonus IUT de Sceaux 2023
        sem_modimpl_moys_inscrits: les notes de sport
            En APC: ndarray (nb_etuds, nb_mod_sport, nb_ues_non_bonus)
            En classic: ndarray (nb_etuds, nb_mod_sport)

        Attention: si la somme des coefs de modules dans une UE est nulle, on a un bonus Inf
        (moyenne d'UE cappée à 20).
        """
        if (0 in sem_modimpl_moys_inscrits.shape) or (self.nb_ues == 0):
            # pas d'étudiants ou pas d'UE ou pas de module...
            return
        super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
        if self.bonus_ues is not None:
            self.bonus_ues = (self.bonus_ues / self.nb_ues) / self.sum_coefs_ues


class BonusStEtienne(BonusSportAdditif):
    """IUT de Saint-Etienne.

    Le bonus est compris entre 0 et 0.6 points.
    """

    name = "bonus_iutse"
    displayed_name = "IUT de Saint-Etienne"
    seuil_moy_gen = 0.0
    bonus_max = 0.6  # plafonnement à 0.6 points
    proportion_point = 1


class BonusStDenis(BonusSportAdditif):
    """Calcul bonus modules optionnels (sport, culture), règle IUT Saint-Denis

    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, dans la limite
    d'1/2 point.
    """

    name = "bonus_iut_stdenis"
    displayed_name = "IUT de Saint-Denis"
    bonus_max = 0.5


class BonusStNazaire(BonusSport):
    """IUT de Saint-Nazaire

    Trois bonifications sont possibles : sport, culture et engagement citoyen
    (qui seront déclarées comme des modules séparés de l'UE bonus).
    <ul>
    <li>Chaque bonus est compris entre 0 et 20 points -> 4pt = 1%<br>
    (note 4/20: 1%, 8/20: 2%, 12/20: 3%, 16/20: 4%, 20/20: 5%)
    </li>
    <li>Le total des 3 bonus ne peut excéder 10%</li>
    <li>La somme des bonus s'applique à la moyenne de chaque UE</li>
    </ul>
    <p>Exemple: une moyenne d'UE de 10/20 avec un total des bonus de 6% donne
     une moyenne de 10,6.</p>
    <p>Les bonifications s'appliquent aussi au classement général du semestre
    et de l'année.
    </p>
    """

    name = "bonus_iutSN"
    displayed_name = "IUT de Saint-Nazaire"
    classic_use_bonus_ues = True  # s'applique aux UEs en DUT et LP
    amplitude = 0.01 / 4  # 4pt => 1%
    factor_max = 0.1  # 10% max
    # Modifié 2022-11-29: calculer chaque bonus
    # (de 1 à 3 modules) séparément.
    def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
        """Calcul du bonus St Nazaire 2022
        sem_modimpl_moys_inscrits: les notes de sport
            En APC: ndarray (nb_etuds, nb_mod_sport, nb_ues_non_bonus)
            En classic: ndarray (nb_etuds, nb_mod_sport)
        """
        if 0 in sem_modimpl_moys_inscrits.shape:
            # pas d'étudiants ou pas d'UE ou pas de module...
            return
        # Prend les 3 premiers bonus trouvés
        # ignore les coefficients
        bonus_mod_moys = sem_modimpl_moys_inscrits[:, :3]
        bonus_mod_moys = np.nan_to_num(bonus_mod_moys, copy=False)
        factor = bonus_mod_moys * self.amplitude
        # somme les bonus:
        factor = factor.sum(axis=1)
        # et limite à 10%:
        factor.clip(0.0, self.factor_max, out=factor)

        # Applique aux moyennes d'UE
        if len(factor.shape) == 1:  # classic
            factor = factor[:, np.newaxis]
        bonus = self.etud_moy_ue * factor
        self.bonus_ues = bonus  # DataFrame

        # Les bonus multiplicatifs ne s'appliquent pas à la moyenne générale
        self.bonus_moy_gen = None


class BonusTarbes(BonusIUTRennes1):
    """Calcul bonus optionnels (sport, culture), règle IUT de Tarbes.

    <ul>
    <li>Les étudiants opeuvent suivre un ou plusieurs activités optionnelles notées.
    La meilleure des notes obtenue est prise en compte, si elle est supérieure à 10/20.
    </li>
    <li>Le trentième des points au dessus de 10 est ajouté à la moyenne des UE en BUT,
    ou à la moyenne générale en DUT et LP.
    </li>
    <li> Exemple: un étudiant ayant 16/20 bénéficiera d'un bonus de (16-10)/30 = 0,2 points
    sur chaque UE.
    </li>
    </ul>
    """

    name = "bonus_tarbes"
    displayed_name = "IUT de Tarbes"
    seuil_moy_gen = 10.0
    proportion_point = 1 / 30.0
    classic_use_bonus_ues = True


class BonusTours(BonusDirect):
    """Calcul bonus sport & culture IUT Tours.

    <p>Les notes des UE bonus (ramenées sur 20) sont sommées
    et 1/40 (2,5%) est ajouté  aux moyennes: soit à la moyenne générale,
    soit pour le BUT à chaque moyenne d'UE.
    </p><p>
    <em>Attention: en GEII, facteur 1/40, ailleurs facteur 1.</em>
    </p><p>
    Le bonus total est limité à 1 point.
    </p>
    """

    name = "bonus_tours"
    displayed_name = "IUT de Tours"
    bonus_max = 1.0  #
    seuil_moy_gen = 0.0  # seuls les points au dessus du seuil sont comptés
    proportion_point = 1.0 / 40.0

    def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
        """calcul différencié selon le département !"""
        if g.scodoc_dept == "GEII":
            self.proportion_point = 1.0 / 40.0
        else:
            self.proportion_point = 1.0
        return super().compute_bonus(
            sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan
        )


class BonusIUTvannes(BonusSportAdditif):
    """Calcul bonus modules optionels (sport, culture), règle IUT Vannes

    <p><b>Ne concerne actuellement que les DUT et LP</b></p>
    <p>Les étudiants de l'IUT peuvent suivre des enseignements optionnels
    de l'U.B.S.  (sports, musique, deuxième langue, culture, etc) non
    rattachés à une unité d'enseignement.
    </p><p>
    Les points au-dessus de 10 sur 20 obtenus dans chacune des matières
    optionnelles sont cumulés.
    </p><p>
    3% de ces points cumulés s'ajoutent à la moyenne générale du semestre
    déjà obtenue par l'étudiant.
    </p>
    """

    name = "bonus_iutvannes"
    displayed_name = "IUT de Vannes"
    seuil_moy_gen = 10.0
    proportion_point = 0.03  # 3%
    classic_use_bonus_ues = False  # seulement sur moy gen.


class BonusValenciennes(BonusDirect):
    """Article 7 des RCC de l'IUT de Valenciennes

    <p>
    Une bonification maximale de 0.25 point (1/4 de point) peut être ajoutée
    à la moyenne de chaque Unité d'Enseignement pour :
    </p>
    <ul>
    <li>l'engagement citoyen ;</li>
    <li>la participation à un module de sport.</li>
    </ul>

    <p>
    Une bonification accordée par la commission des sports de l'UPHF peut être attribuée
    aux sportifs de haut niveau. Cette bonification est appliquée à l'ensemble des
    Unités d'Enseignement. Ce bonus est :
    </p>
    <ul>
    <li> 0.5 pour la catégorie <em>or</em> (sportif inscrit sur liste ministérielle
    jeunesse et sport) ;
    </li>
    <li> 0.45 pour la catégorie <em>argent</em> (sportif en club professionnel) ;
    </li>
    <li> 0.40 pour le <em>bronze</em> (sportif de niveau départemental, régional ou national).
    </li>
    </ul>
    <p>Le cumul de bonifications est possible mais ne peut excéder 0.5 point (un demi-point).
    </p>
    <p><em>Dans ScoDoc, saisir directement la valeur désirée du bonus
        dans une évaluation notée sur 20.</em>
    </p>
    """

    name = "bonus_valenciennes"
    displayed_name = "IUT de Valenciennes"
    bonus_max = 0.5


class BonusVilleAvray(BonusSportAdditif):
    """Bonus modules optionnels (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.
    <ul>
    <li>Si la note est >= 10 et < 12, bonus de 0.1 point</li>
    <li>Si la note est >= 12 et < 16, bonus de 0.2 point</li>
    <li>Si la note est >= 16, bonus de 0.3 point</li>
    </ul>
    <p>Ce bonus s'ajoute à la moyenne générale du semestre déjà obtenue par
    l'étudiant.</p>
    """

    name = "bonus_iutva"
    displayed_name = "IUT de Ville d'Avray"

    def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
        """calcul du bonus"""
        if 0 in sem_modimpl_moys_inscrits.shape:
            # pas d'étudiants ou pas d'UE ou pas de module...
            return
        # Calcule moyenne pondérée des notes de sport:
        with np.errstate(invalid="ignore"):  # ignore les 0/0 (-> NaN)
            bonus_moy_arr = np.sum(
                sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1
            ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
        np.nan_to_num(bonus_moy_arr, nan=0.0, copy=False)
        bonus_moy_arr[bonus_moy_arr < 10.0] = 0.0
        bonus_moy_arr[bonus_moy_arr >= 16.0] = 0.3
        bonus_moy_arr[bonus_moy_arr >= 12.0] = 0.2
        bonus_moy_arr[bonus_moy_arr >= 10.0] = 0.1

        self.bonus_additif(bonus_moy_arr)


class BonusIUTV(BonusSportAdditif):
    """Calcul bonus modules optionnels (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"
    displayed_name = "IUT de Villetaneuse"
    # c'est le bonus par défaut: aucune méthode à surcharger


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
