# -*- 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
#
##############################################################################

"""Ajout/Modification/Suppression modules
(portage from DTML)
"""
import flask
from flask import url_for, render_template
from flask import g, request
from flask_login import current_user
from app.models import Matiere, Module, UniteEns

import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType
from app import log
from app import models
from app.models import Formation
from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import ScoValueError, ScoLockedFormError, ScoGenError
from app.scodoc import html_sco_header
from app.scodoc import sco_codes_parcours
from app.scodoc import sco_edit_matiere
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_news

_moduleEditor = ndb.EditableTable(
    "notes_modules",
    "module_id",
    (
        "module_id",
        "titre",
        "code",
        "abbrev",
        "heures_cours",
        "heures_td",
        "heures_tp",
        "coefficient",
        "ue_id",
        "matiere_id",
        "formation_id",
        "semestre_id",
        "numero",
        "code_apogee",
        "module_type"
        #'ects'
    ),
    sortkey="numero, code, titre",
    output_formators={
        "heures_cours": ndb.float_null_is_zero,
        "heures_td": ndb.float_null_is_zero,
        "heures_tp": ndb.float_null_is_zero,
        "numero": ndb.int_null_is_zero,
        "coefficient": ndb.float_null_is_zero,
        "module_type": ndb.int_null_is_zero
        #'ects' : ndb.float_null_is_null
    },
)


def module_list(*args, **kw):
    "list modules"
    cnx = ndb.GetDBConnexion()
    return _moduleEditor.list(cnx, *args, **kw)


def do_module_create(args) -> int:
    "Create a module. Returns id of new object."
    # create
    from app.scodoc import sco_formations

    cnx = ndb.GetDBConnexion()
    r = _moduleEditor.create(cnx, args)

    # news
    F = sco_formations.formation_list(args={"formation_id": args["formation_id"]})[0]
    sco_news.add(
        typ=sco_news.NEWS_FORM,
        object=args["formation_id"],
        text="Modification de la formation %(acronyme)s" % F,
        max_frequency=3,
    )
    return r


def module_create(matiere_id=None, module_type=None, semestre_id=None):
    """Création d'un module"""
    from app.scodoc import sco_formations
    from app.scodoc import sco_edit_ue

    matiere = Matiere.query.get_or_404(matiere_id)
    if matiere is None:
        raise ScoValueError("invalid matiere !")
    ue = matiere.ue
    parcours = ue.formation.get_parcours()
    is_apc = parcours.APC_SAE
    ues = ue.formation.ues.order_by(
        UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme
    ).all()
    # cherche le numero adéquat (pour placer le module en fin de liste)
    modules = matiere.ue.formation.modules.all()
    if modules:
        default_num = max([m.numero or 0 for m in modules]) + 10
    else:
        default_num = 10

    if is_apc and module_type is not None:
        object_name = scu.MODULE_TYPE_NAMES[module_type]
    else:
        object_name = "Module"
    H = [
        html_sco_header.sco_header(page_title=f"Création {object_name}"),
    ]
    if is_apc:
        H += [
            f"""<h2>Création {object_name} dans la formation {ue.formation.acronyme}</h2>"""
        ]
    else:
        H += [
            f"""<h2>Création {object_name} dans la matière {matiere.titre},
            (UE {ue.acronyme})</h2>
            """
        ]

    H += [
        render_template(
            "scodoc/help/modules.html",
            is_apc=is_apc,
            ue=ue,
            semestre_id=semestre_id,
        )
    ]

    descr = [
        (
            "code",
            {
                "size": 10,
                "explanation": "code du module, ressource ou SAÉ. Exemple M1203, R2.01, ou SAÉ 3.4. Ce code doit être unique dans la formation.",
                "allow_null": False,
                "validator": lambda val, field, formation_id=ue.formation_id: check_module_code_unicity(
                    val, field, formation_id
                ),
            },
        ),
        (
            "titre",
            {
                "size": 30,
                "explanation": "nom du module. Exemple: <em>Introduction à la démarche ergonomique</em>",
            },
        ),
        (
            "abbrev",
            {
                "size": 20,
                "explanation": "nom abrégé (pour les bulletins). Exemple: <em>Intro. à l'ergonomie</em>",
            },
        ),
    ]
    semestres_indices = list(range(1, parcours.NB_SEM + 1))
    if is_apc:  # BUT: choix de l'UE de rattachement (qui donnera le semestre)
        descr += [
            (
                "ue_id",
                {
                    "input_type": "menu",
                    "type": "int",
                    "title": "UE de rattachement",
                    "explanation": "utilisée pour la présentation dans certains documents",
                    "labels": [f"{u.acronyme} {u.titre}" for u in ues],
                    "allowed_values": [u.id for u in ues],
                },
            ),
        ]
    else:
        # Formations classiques: choix du semestre
        descr += [
            (
                "semestre_id",
                {
                    "input_type": "menu",
                    "type": "int",
                    "title": parcours.SESSION_NAME.capitalize(),
                    "explanation": "%s du module" % parcours.SESSION_NAME,
                    "labels": [str(x) for x in semestres_indices],
                    "allowed_values": semestres_indices,
                },
            ),
        ]
    descr += [
        (
            "module_type",
            {
                "input_type": "menu",
                "title": "Type",
                "explanation": "",
                "labels": [x.name.capitalize() for x in scu.ModuleType],
                "allowed_values": [str(int(x)) for x in scu.ModuleType],
            },
        ),
        (
            "heures_cours",
            {
                "title": "Heures de cours",
                "size": 4,
                "type": "float",
                "explanation": "nombre d'heures de cours (optionnel)",
            },
        ),
        (
            "heures_td",
            {
                "title": "Heures de TD",
                "size": 4,
                "type": "float",
                "explanation": "nombre d'heures de Travaux Dirigés (optionnel)",
            },
        ),
        (
            "heures_tp",
            {
                "title": "Heures de TP",
                "size": 4,
                "type": "float",
                "explanation": "nombre d'heures de Travaux Pratiques (optionnel)",
            },
        ),
    ]
    if is_apc:
        descr += [
            (
                "sep_ue_coefs",
                {
                    "input_type": "separator",
                    "title": """
                    <div>(<em>les coefficients vers les UE se fixent sur la page dédiée</em>)
                    </div>""",
                },
            ),
        ]
    else:
        descr += [
            (
                "coefficient",
                {
                    "size": 4,
                    "type": "float",
                    "explanation": "coefficient dans la formation (PPN)",
                    "allow_null": False,
                },
            ),
        ]

    descr += [
        # ('ects', { 'size' : 4, 'type' : 'float', 'title' : 'ECTS', 'explanation' : 'nombre de crédits ECTS (inutilisés: les crédits sont associés aux UE)' }),
        ("formation_id", {"default": ue.formation_id, "input_type": "hidden"}),
        ("ue_id", {"default": ue.id, "input_type": "hidden"}),
        ("matiere_id", {"default": matiere.id, "input_type": "hidden"}),
        (
            "code_apogee",
            {
                "title": "Code Apogée",
                "size": 25,
                "explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules",
            },
        ),
        (
            "numero",
            {
                "size": 2,
                "explanation": "numéro (1,2,3,4...) pour ordre d'affichage",
                "type": "int",
                "default": default_num,
            },
        ),
    ]
    args = scu.get_request_args()
    tf = TrivialFormulator(
        request.base_url,
        args,
        descr,
        submitlabel="Créer ce module",
    )
    if tf[0] == 0:
        return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
    else:
        if is_apc:
            # BUT: l'UE indique le semestre
            selected_ue = UniteEns.query.get(tf[2]["ue_id"])
            if selected_ue is None:
                raise ValueError("UE invalide")
            tf[2]["semestre_id"] = selected_ue.semestre_idx

        _ = do_module_create(tf[2])

        return flask.redirect(
            url_for(
                "notes.ue_table",
                scodoc_dept=g.scodoc_dept,
                formation_id=ue.formation_id,
                semestre_idx=tf[2]["semestre_id"],
            )
        )


def do_module_delete(oid):
    "delete module"
    from app.scodoc import sco_formations

    mod = module_list({"module_id": oid})[0]
    if module_is_locked(mod["module_id"]):
        raise ScoLockedFormError()

    # S'il y a des moduleimpls, on ne peut pas detruire le module !
    mods = sco_moduleimpl.moduleimpl_list(module_id=oid)
    if mods:
        err_page = f"""<h3>Destruction du module impossible car il est utilisé dans des semestres existants !</h3>
        <p class="help">Il faut d'abord supprimer le semestre. Mais il est peut être préférable de 
        laisser ce programme intact et d'en créer une nouvelle version pour la modifier.
        </p>
        <a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept, 
            formation_id=mod["formation_id"])}">reprendre</a>
        """
        raise ScoGenError(err_page)
    # delete
    cnx = ndb.GetDBConnexion()
    _moduleEditor.delete(cnx, oid)

    # news
    F = sco_formations.formation_list(args={"formation_id": mod["formation_id"]})[0]
    sco_news.add(
        typ=sco_news.NEWS_FORM,
        object=mod["formation_id"],
        text="Modification de la formation %(acronyme)s" % F,
        max_frequency=3,
    )


def module_delete(module_id=None):
    """Delete a module"""
    if not module_id:
        raise ScoValueError("invalid module !")
    modules = module_list(args={"module_id": module_id})
    if not modules:
        raise ScoValueError("Module inexistant !")
    mod = modules[0]
    H = [
        html_sco_header.sco_header(page_title="Suppression d'un module"),
        """<h2>Suppression du module %(titre)s (%(code)s)</h2>""" % mod,
    ]

    dest_url = url_for(
        "notes.ue_table",
        scodoc_dept=g.scodoc_dept,
        formation_id=str(mod["formation_id"]),
    )
    tf = TrivialFormulator(
        request.base_url,
        scu.get_request_args(),
        (("module_id", {"input_type": "hidden"}),),
        initvalues=mod,
        submitlabel="Confirmer la suppression",
        cancelbutton="Annuler",
    )
    if tf[0] == 0:
        return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
    elif tf[0] == -1:
        return flask.redirect(dest_url)
    else:
        do_module_delete(module_id)
        return flask.redirect(dest_url)


def do_module_edit(val):
    "edit a module"
    from app.scodoc import sco_edit_formation

    # check
    mod = module_list({"module_id": val["module_id"]})[0]
    if module_is_locked(mod["module_id"]):
        # formation verrouillée: empeche de modifier certains champs:
        protected_fields = ("coefficient", "ue_id", "matiere_id", "semestre_id")
        for f in protected_fields:
            if f in val:
                del val[f]
    # edit
    cnx = ndb.GetDBConnexion()
    _moduleEditor.edit(cnx, val)
    Formation.query.get(mod["formation_id"]).invalidate_cached_sems()


def check_module_code_unicity(code, field, formation_id, module_id=None):
    "true si code module unique dans la formation"
    Mods = module_list(args={"code": code, "formation_id": formation_id})
    if module_id:  # edition: supprime le module en cours
        Mods = [m for m in Mods if m["module_id"] != module_id]

    return len(Mods) == 0


def module_edit(module_id=None):
    """Edit a module"""
    from app.scodoc import sco_formations
    from app.scodoc import sco_tag_module

    if not module_id:
        raise ScoValueError("invalid module !")
    modules = module_list(args={"module_id": module_id})
    if not modules:
        raise ScoValueError("invalid module !")
    module = modules[0]
    a_module = models.Module.query.get(module_id)
    unlocked = not module_is_locked(module_id)
    formation_id = module["formation_id"]
    formation = sco_formations.formation_list(args={"formation_id": formation_id})[0]
    parcours = sco_codes_parcours.get_parcours_from_code(formation["type_parcours"])
    is_apc = parcours.APC_SAE
    ues_matieres = ndb.SimpleDictFetch(
        """SELECT ue.acronyme, mat.*, mat.id AS matiere_id
        FROM notes_matieres mat, notes_ue ue
        WHERE mat.ue_id = ue.id
        AND ue.formation_id = %(formation_id)s
        ORDER BY ue.numero, mat.numero
        """,
        {"formation_id": formation_id},
    )
    mat_names = ["%s / %s" % (x["acronyme"], x["titre"]) for x in ues_matieres]
    ue_mat_ids = ["%s!%s" % (x["ue_id"], x["matiere_id"]) for x in ues_matieres]
    module["ue_matiere_id"] = "%s!%s" % (module["ue_id"], module["matiere_id"])

    semestres_indices = list(range(1, parcours.NB_SEM + 1))

    H = [
        html_sco_header.sco_header(
            page_title="Modification du module %(titre)s" % module,
            cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css"],
            javascripts=[
                "libjs/jQuery-tagEditor/jquery.tag-editor.min.js",
                "libjs/jQuery-tagEditor/jquery.caret.min.js",
                "js/module_tag_editor.js",
            ],
        ),
        """<h2>Modification du module %(titre)s""" % module,
        """ (formation %(acronyme)s, version %(version)s)</h2>""" % formation,
        render_template("scodoc/help/modules.html", is_apc=is_apc),
    ]
    if not unlocked:
        H.append(
            """<div class="ue_warning"><span>Formation verrouillée, seuls certains éléments peuvent être modifiés</span></div>"""
        )

    descr = [
        (
            "code",
            {
                "size": 10,
                "explanation": "code du module (doit être unique dans la formation)",
                "allow_null": False,
                "validator": lambda val, field, formation_id=formation_id: check_module_code_unicity(
                    val, field, formation_id, module_id=module_id
                ),
            },
        ),
        ("titre", {"size": 30, "explanation": "nom du module"}),
        ("abbrev", {"size": 20, "explanation": "nom abrégé (pour bulletins)"}),
        (
            "module_type",
            {
                "input_type": "menu",
                "title": "Type",
                "explanation": "",
                "labels": [x.name.capitalize() for x in scu.ModuleType],
                "allowed_values": [str(int(x)) for x in scu.ModuleType],
                "enabled": unlocked,
            },
        ),
        (
            "heures_cours",
            {"size": 4, "type": "float", "explanation": "nombre d'heures de cours"},
        ),
        (
            "heures_td",
            {
                "size": 4,
                "type": "float",
                "explanation": "nombre d'heures de Travaux Dirigés",
            },
        ),
        (
            "heures_tp",
            {
                "size": 4,
                "type": "float",
                "explanation": "nombre d'heures de Travaux Pratiques",
            },
        ),
    ]
    if is_apc:
        coefs_descr = a_module.ue_coefs_descr()
        if coefs_descr:
            coefs_descr_txt = ", ".join(["%s: %s" % x for x in coefs_descr])
        else:
            coefs_descr_txt = """<span class="missing_value">non définis</span>"""
        descr += [
            (
                "ue_coefs",
                {
                    "readonly": True,
                    "title": "Coefficients vers les UE",
                    "default": coefs_descr_txt,
                    "explanation": "passer par la page d'édition de la formation pour modifier les coefficients",
                },
            )
        ]
    else:  # Module classique avec coef scalaire:
        descr += [
            (
                "coefficient",
                {
                    "size": 4,
                    "type": "float",
                    "explanation": "coefficient dans la formation (PPN)",
                    "allow_null": False,
                    "enabled": unlocked,
                },
            ),
        ]
    descr += [
        ("formation_id", {"input_type": "hidden"}),
        ("ue_id", {"input_type": "hidden"}),
        ("module_id", {"input_type": "hidden"}),
        (
            "ue_matiere_id",
            {
                "input_type": "menu",
                "title": "Rattachement :" if is_apc else "Matière :",
                "explanation": "UE de rattachement, utilisée pour la présentation"
                if is_apc
                else "un module appartient à une seule matière.",
                "labels": mat_names,
                "allowed_values": ue_mat_ids,
                "enabled": unlocked,
            },
        ),
    ]
    if is_apc:
        # le semestre du module est toujours celui de son UE
        descr += [
            (
                "semestre_id",
                {
                    "input_type": "hidden",
                    "type": "int",
                    "readonly": True,
                },
            )
        ]
    else:
        descr += [
            (
                "semestre_id",
                {
                    "input_type": "menu",
                    "type": "int",
                    "title": parcours.SESSION_NAME.capitalize(),
                    "explanation": "%s de début du module dans la formation standard"
                    % parcours.SESSION_NAME,
                    "labels": [str(x) for x in semestres_indices],
                    "allowed_values": semestres_indices,
                    "enabled": unlocked,
                },
            )
        ]
    descr += [
        (
            "code_apogee",
            {
                "title": "Code Apogée",
                "size": 25,
                "explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules",
            },
        ),
        (
            "numero",
            {
                "size": 2,
                "explanation": "numéro (1,2,3,4...) pour ordre d'affichage",
                "type": "int",
            },
        ),
    ]
    # force module semestre_idx to its UE
    if a_module.ue.semestre_idx:
        module["semestre_id"] = a_module.ue.semestre_idx
    # Filet de sécurité si jamais l'UE n'a pas non plus de semestre:
    if not module["semestre_id"]:
        module["semestre_id"] = 1
    tf = TrivialFormulator(
        request.base_url,
        scu.get_request_args(),
        descr,
        html_foot_markup="""<div style="width: 90%;"><span class="sco_tag_edit"><textarea data-module_id="{}" class="module_tag_editor">{}</textarea></span></div>""".format(
            module_id, ",".join(sco_tag_module.module_tag_list(module_id))
        ),
        initvalues=module,
        submitlabel="Modifier ce module",
    )
    if tf[0] == 0:
        return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
    elif tf[0] == -1:
        return flask.redirect(
            url_for(
                "notes.ue_table",
                scodoc_dept=g.scodoc_dept,
                formation_id=formation_id,
                semestre_idx=module["semestre_id"],
            )
        )
    else:
        # l'UE peut changer
        tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!")
        # En APC, force le semestre égal à celui de l'UE
        if is_apc:
            selected_ue = UniteEns.query.get(tf[2]["ue_id"])
            if selected_ue is None:
                raise ValueError("UE invalide")
            tf[2]["semestre_id"] = selected_ue.semestre_idx
        # Check unicité code module dans la formation
        do_module_edit(tf[2])
        return flask.redirect(
            url_for(
                "notes.ue_table",
                scodoc_dept=g.scodoc_dept,
                formation_id=formation_id,
                semestre_idx=tf[2]["semestre_id"],
            )
        )


# Edition en ligne du code Apogee
def edit_module_set_code_apogee(id=None, value=None):
    "Set UE code apogee"
    module_id = id
    value = str(value).strip("-_ \t")
    log("edit_module_set_code_apogee: module_id=%s code_apogee=%s" % (module_id, value))

    modules = module_list(args={"module_id": module_id})
    if not modules:
        return "module invalide"  # should not occur

    do_module_edit({"module_id": module_id, "code_apogee": value})
    if not value:
        value = scu.APO_MISSING_CODE_STR
    return value


def module_table(formation_id):
    """Liste des modules de la formation
    (XXX inutile ou a revoir)
    """
    from app.scodoc import sco_formations

    if not formation_id:
        raise ScoValueError("invalid formation !")
    F = sco_formations.formation_list(args={"formation_id": formation_id})[0]
    H = [
        html_sco_header.sco_header(page_title="Liste des modules de %(titre)s" % F),
        """<h2>Listes des modules dans la formation %(titre)s (%(acronyme)s)</h2>"""
        % F,
        '<ul class="notes_module_list">',
    ]
    editable = current_user.has_permission(Permission.ScoChangeFormation)

    for Mod in module_list(args={"formation_id": formation_id}):
        H.append('<li class="notes_module_list">%s' % Mod)
        if editable:
            H.append('<a href="module_edit?module_id=%(module_id)s">modifier</a>' % Mod)
            H.append(
                '<a href="module_delete?module_id=%(module_id)s">supprimer</a>' % Mod
            )
        H.append("</li>")
    H.append("</ul>")
    H.append(html_sco_header.sco_footer())
    return "\n".join(H)


def module_is_locked(module_id):
    """True if module should not be modified
    (used in a locked formsemestre)
    """
    r = ndb.SimpleDictFetch(
        """SELECT mi.id
        FROM notes_modules mod, notes_formsemestre sem, notes_moduleimpl mi
        WHERE mi.module_id = mod.id
        AND mi.formsemestre_id = sem.id
        AND mi.module_id = %(module_id)s
        AND sem.etat = false
        """,
        {"module_id": module_id},
    )
    return len(r) > 0


def module_count_moduleimpls(module_id):
    "Number of moduleimpls using this module"
    mods = sco_moduleimpl.moduleimpl_list(module_id=module_id)
    return len(mods)


def formation_add_malus_modules(formation_id, titre=None, redirect=True):
    """Création d'un module de "malus" dans chaque UE d'une formation"""
    from app.scodoc import sco_edit_ue

    ues = sco_edit_ue.ue_list(args={"formation_id": formation_id})

    for ue in ues:
        # Un seul module de malus par UE:
        nb_mod_malus = len(
            [
                mod
                for mod in module_list(args={"ue_id": ue["ue_id"]})
                if mod["module_type"] == ModuleType.MALUS
            ]
        )
        if nb_mod_malus == 0:
            ue_add_malus_module(ue["ue_id"], titre=titre)

    if redirect:
        return flask.redirect(
            url_for(
                "notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation_id
            )
        )


def ue_add_malus_module(ue_id, titre=None, code=None):
    """Add a malus module in this ue"""
    from app.scodoc import sco_edit_ue

    ue = sco_edit_ue.ue_list(args={"ue_id": ue_id})[0]

    if titre is None:
        titre = ""
    if code is None:
        code = "MALUS%d" % ue["numero"]

    # Tout module doit avoir un semestre_id (indice 1, 2, ...)
    semestre_ids = sco_edit_ue.ue_list_semestre_ids(ue)
    if semestre_ids:
        semestre_id = semestre_ids[0]
    else:
        # c'est ennuyeux: dans ce cas, on pourrait demander à indiquer explicitement
        # le semestre ? ou affecter le malus au semestre 1 ???
        raise ScoValueError(
            "Impossible d'ajouter un malus s'il n'y a pas d'autres modules"
        )

    # Matiere pour placer le module malus
    Matlist = sco_edit_matiere.matiere_list(args={"ue_id": ue_id})
    numero = max([mat["numero"] for mat in Matlist]) + 10
    matiere_id = sco_edit_matiere.do_matiere_create(
        {"ue_id": ue_id, "titre": "Malus", "numero": numero}
    )

    module_id = do_module_create(
        {
            "titre": titre,
            "code": code,
            "coefficient": 0.0,  # unused
            "ue_id": ue_id,
            "matiere_id": matiere_id,
            "formation_id": ue["formation_id"],
            "semestre_id": semestre_id,
            "module_type": ModuleType.MALUS,
        },
    )

    return module_id