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

##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2024 Emmanuel Viennet.  All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
#   Emmanuel Viennet      emmanuel.viennet@viennet.net
#
##############################################################################

##############################################################################
#  Module "Avis de poursuite d'étude"
#  conçu et développé par Cléo Baras (IUT de Grenoble)
##############################################################################

"""
Created on Thu Sep  8 09:36:33 2016

@author: barasc
"""

import os
import datetime
import re
import unicodedata

import pandas as pd
from flask import g

import app.scodoc.sco_utils as scu

from app.models import FormSemestre
from app.pe.rcss.pe_rcs import TYPES_RCS
from app.scodoc import sco_formsemestre
from app.scodoc.sco_logos import find_logo


# Generated LaTeX files are encoded as:
PE_LATEX_ENCODING = "utf-8"

# /opt/scodoc/tools/doc_poursuites_etudes
REP_DEFAULT_AVIS = os.path.join(scu.SCO_TOOLS_DIR, "doc_poursuites_etudes/")
REP_LOCAL_AVIS = os.path.join(scu.SCODOC_CFG_DIR, "doc_poursuites_etudes/")

PE_DEFAULT_AVIS_LATEX_TMPL = REP_DEFAULT_AVIS + "distrib/modeles/un_avis.tex"
PE_LOCAL_AVIS_LATEX_TMPL = REP_LOCAL_AVIS + "local/modeles/un_avis.tex"
PE_DEFAULT_FOOTER_TMPL = REP_DEFAULT_AVIS + "distrib/modeles/un_footer.tex"
PE_LOCAL_FOOTER_TMPL = REP_LOCAL_AVIS + "local/modeles/un_footer.tex"

# ----------------------------------------------------------------------------------------

"""
Descriptif d'un parcours classique BUT

TODO:: A améliorer si BUT en moins de 6 semestres
"""

NBRE_SEMESTRES_DIPLOMANT = 6
AGGREGAT_DIPLOMANT = (
    "6S"  # aggrégat correspondant à la totalité des notes pour le diplôme
)
TOUS_LES_SEMESTRES = TYPES_RCS[AGGREGAT_DIPLOMANT]["aggregat"]


# ----------------------------------------------------------------------------------------
def calcul_age(born: datetime.date) -> int:
    """Calcule l'age connaissant la date de naissance ``born``. (L'age est calculé
    à partir de l'horloge système).

    Args:
        born: La date de naissance

    Return:
        L'age (au regard de la date actuelle)
    """
    if not born or not isinstance(born, datetime.date):
        return None

    today = datetime.date.today()
    return today.year - born.year - ((today.month, today.day) < (born.month, born.day))


# Nota: scu.suppress_accents fait la même chose mais renvoie un str et non un bytes
def remove_accents(input_unicode_str: str) -> bytes:
    """Supprime les accents d'une chaine unicode"""
    nfkd_form = unicodedata.normalize("NFKD", input_unicode_str)
    only_ascii = nfkd_form.encode("ASCII", "ignore")
    return only_ascii


def escape_for_latex(s):
    """Protège les caractères pour inclusion dans du source LaTeX"""
    if not s:
        return ""
    conv = {
        "&": r"\&",
        "%": r"\%",
        "$": r"\$",
        "#": r"\#",
        "_": r"\_",
        "{": r"\{",
        "}": r"\}",
        "~": r"\textasciitilde{}",
        "^": r"\^{}",
        "\\": r"\textbackslash{}",
        "<": r"\textless ",
        ">": r"\textgreater ",
    }
    exp = re.compile(
        "|".join(
            re.escape(key)
            for key in sorted(list(conv.keys()), key=lambda item: -len(item))
        )
    )
    return exp.sub(lambda match: conv[match.group()], s)


# ----------------------------------------------------------------------------------------
def list_directory_filenames(path: str) -> list[str]:
    """List of regular filenames (paths) in a directory (recursive)
    Excludes files and directories begining with .
    """
    paths = []
    for root, dirs, files in os.walk(path, topdown=True):
        dirs[:] = [d for d in dirs if d[0] != "."]
        paths += [os.path.join(root, fn) for fn in files if fn[0] != "."]
    return paths


def add_local_file_to_zip(zipfile, ziproot, pathname, path_in_zip):
    """Read pathname server file and add content to zip under path_in_zip"""
    rooted_path_in_zip = os.path.join(ziproot, path_in_zip)
    zipfile.write(filename=pathname, arcname=rooted_path_in_zip)
    # data = open(pathname).read()
    # zipfile.writestr(rooted_path_in_zip, data)


def add_refs_to_register(register, directory):
    """Ajoute les fichiers trouvés dans directory au registre (dictionaire) sous la forme
    filename => pathname
    """
    length = len(directory)
    for pathname in list_directory_filenames(directory):
        filename = pathname[length + 1 :]
        register[filename] = pathname


def add_pe_stuff_to_zip(zipfile, ziproot):
    """Add auxiliary files to (already opened) zip
    Put all local files found under config/doc_poursuites_etudes/local
    and config/doc_poursuites_etudes/distrib
    If a file is present in both subtrees, take the one in local.

    Also copy logos
    """
    register = {}
    # first add standard (distrib references)
    distrib_dir = os.path.join(REP_DEFAULT_AVIS, "distrib")
    add_refs_to_register(register=register, directory=distrib_dir)
    # then add local references (some oh them may overwrite distrib refs)
    local_dir = os.path.join(REP_LOCAL_AVIS, "local")
    add_refs_to_register(register=register, directory=local_dir)
    # at this point register contains all refs (filename, pathname) to be saved
    for filename, pathname in register.items():
        add_local_file_to_zip(zipfile, ziproot, pathname, "avis/" + filename)

    # Logos: (add to logos/ directory in zip)
    logos_names = ["header", "footer"]
    for name in logos_names:
        logo = find_logo(logoname=name, dept_id=g.scodoc_dept_id)
        if logo is not None:
            add_local_file_to_zip(
                zipfile, ziproot, logo.filepath, "avis/logos/" + logo.filename
            )


# ----------------------------------------------------------------------------------------
def get_annee_diplome_semestre(
    sem_base: FormSemestre | dict, nbre_sem_formation: int = 6
) -> int:
    """Pour un semestre ``sem_base`` donné (supposé être un semestre d'une formation BUT
    à 6 semestres) et connaissant le numéro du semestre, ses dates de début et de fin du
    semestre, prédit l'année à laquelle sera remis le diplôme BUT des étudiants qui y
    sont scolarisés (en supposant qu'il n'y ait pas de redoublement à venir).

    **Remarque sur le calcul** : Les semestres de 1ère partie d'année (S1, S3, S5 ou S4,
    S6 pour des semestres décalés)
    s'étalent sur deux années civiles ; contrairement au semestre de seconde partie
    d'année universitaire.

    Par exemple :

    * S5 débutant en 2025 finissant en 2026 : diplome en 2026
    * S3 debutant en 2025 et finissant en 2026 : diplome en 2027

    La fonction est adaptée au cas des semestres décalés.

    Par exemple :

    * S5 décalé débutant en 2025 et finissant en 2025 : diplome en 2026
    * S3 décalé débutant en 2025 et finissant en 2025 : diplome en 2027

    Args:
        sem_base: Le semestre à partir duquel est prédit l'année de diplomation, soit :

                 * un ``FormSemestre`` (Scodoc9)
                 * un dict (format compatible avec Scodoc7)

        nbre_sem_formation: Le nombre de semestre prévu dans la formation (par défaut 6 pour un BUT)
    """

    if isinstance(sem_base, FormSemestre):
        sem_id = sem_base.semestre_id
        annee_fin = sem_base.date_fin.year
        annee_debut = sem_base.date_debut.year
    else:  # sem_base est un dictionnaire (Scodoc 7)
        sem_id = sem_base["semestre_id"]
        annee_fin = int(sem_base["annee_fin"])
        annee_debut = int(sem_base["annee_debut"])
    if (
        1 <= sem_id <= nbre_sem_formation
    ):  # Si le semestre est un semestre BUT => problème si formation BUT en 1 an ??
        nb_sem_restants = (
            nbre_sem_formation - sem_id
        )  # nombre de semestres restant avant diplome
        nb_annees_restantes = (
            nb_sem_restants // 2
        )  # nombre d'annees restant avant diplome
        # Flag permettant d'activer ou désactiver un increment
        # à prendre en compte en cas de semestre décalé
        # avec 1 - delta = 0 si semestre de 1ere partie d'année / 1 sinon
        delta = annee_fin - annee_debut
        decalage = nb_sem_restants % 2  # 0 si S4, 1 si S3, 0 si S2, 1 si S1
        increment = decalage * (1 - delta)
        return annee_fin + nb_annees_restantes + increment


def get_cosemestres_diplomants(annee_diplome: int) -> dict[int, FormSemestre]:
    """Ensemble des cosemestres donnant lieu à diplomation à l'``annee_diplome``.

    **Définition** : Un co-semestre est un semestre :

    * dont l'année de diplômation prédite (sans redoublement) est la même
    * dont la formation est la même (optionnel)
    * qui a des étudiants inscrits

    Args:
        annee_diplome: L'année de diplomation

    Returns:
        Un dictionnaire {fid: FormSemestre(fid)} contenant les cosemestres
    """
    tous_les_sems = (
        sco_formsemestre.do_formsemestre_list()
    )  # tous les semestres memorisés dans scodoc

    cosemestres_fids = {
        sem["id"]
        for sem in tous_les_sems
        if get_annee_diplome_semestre(sem) == annee_diplome
    }

    cosemestres = {}
    for fid in cosemestres_fids:
        cosem = FormSemestre.get_formsemestre(fid)
        if len(cosem.etuds_inscriptions) > 0:
            cosemestres[fid] = cosem

    return cosemestres


def tri_semestres_par_rang(cosemestres: dict[int, FormSemestre]):
    """Partant d'un dictionnaire de cosemestres, les tri par rang (semestre_id) dans un
    dictionnaire {rang: [liste des semestres du dit rang]}"""
    cosemestres_tries = {}
    for sem in cosemestres.values():
        cosemestres_tries[sem.semestre_id] = cosemestres_tries.get(
            sem.semestre_id, []
        ) + [sem]
    return cosemestres_tries


def find_index_and_columns_communs(
    df1: pd.DataFrame, df2: pd.DataFrame
) -> (list, list):
    """Partant de 2 DataFrames ``df1`` et ``df2``, renvoie les indices de lignes
    et de colonnes, communes aux 2 dataframes

    Args:
        df1: Un dataFrame
        df2: Un dataFrame
    Returns:
        Le tuple formé par la liste des indices de lignes communs et la liste des indices
        de colonnes communes entre les 2 dataFrames
    """
    indices1 = df1.index
    indices2 = df2.index
    indices_communs = list(df1.index.intersection(df2.index))
    colonnes1 = df1.columns
    colonnes2 = df2.columns
    colonnes_communes = list(set(colonnes1) & set(colonnes2))
    return indices_communs, colonnes_communes


def get_dernier_semestre_en_date(semestres: dict[int, FormSemestre]) -> FormSemestre:
    """Renvoie le dernier semestre en **date de fin** d'un dictionnaire
    de semestres (potentiellement non trié) de la forme ``{fid: FormSemestre(fid)}``.

    Args:
        semestres: Un dictionnaire de semestres

    Return:
        Le FormSemestre du semestre le plus récent
    """
    if semestres:
        fid_dernier_semestre = list(semestres.keys())[0]
        dernier_semestre: FormSemestre = semestres[fid_dernier_semestre]
        for fid in semestres:
            if semestres[fid].date_fin > dernier_semestre.date_fin:
                dernier_semestre = semestres[fid]
        return dernier_semestre
    return None
