# -*- 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@gmail.com
#
##############################################################################

"""Génération des bulletins de note: super-classe pour les générateurs (HTML et PDF)

class BulletinGenerator:
 description
 supported_formats = [ 'pdf', 'html' ]
 .bul_title_pdf()
 .bul_table(fmt)
 .bul_part_below(fmt)
 .bul_signatures_pdf()

 .__init__ et .generate(fmt) methodes appelees par le client (sco_bulletin)

La préférence 'bul_class_name' donne le nom de la classe generateur.
La préférence 'bul_pdf_class_name' est obsolete (inutilisée).


"""
import collections
import io
import time
import traceback


import reportlab
from reportlab.platypus import (
    SimpleDocTemplate,
    DocIf,
    Paragraph,
    Spacer,
    Frame,
    PageBreak,
)
from reportlab.platypus import Table, TableStyle, Image, KeepInFrame

from flask import request
from flask_login import current_user

from app.models import FormSemestre, Identite, ScoDocSiteConfig
from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import NoteProcessError
from app import log
from app.scodoc import sco_formsemestre
from app.scodoc import sco_pdf
from app.scodoc.sco_pdf import PDFLOCK
import sco_version


class BulletinGenerator:
    "Virtual superclass for PDF bulletin generators" ""
    # Here some helper methods
    # see sco_bulletins_standard.BulletinGeneratorStandard subclass for real methods
    supported_formats = []  # should list supported formats, eg [ 'html', 'pdf' ]
    description = "superclass for bulletins"  # description for user interface
    list_in_menu = True  # la classe doit-elle est montrée dans le menu de config ?
    scale_table_in_page = True  # rescale la table sur 1 page
    multi_pages = False

    def __init__(
        self,
        bul_dict,
        authuser=None,
        etud: Identite = None,
        filigranne=None,
        formsemestre: FormSemestre = None,
        server_name=None,
        version="long",
        with_img_signatures_pdf: bool = True,
    ):
        from app.scodoc import sco_preferences

        if version not in scu.BULLETINS_VERSIONS:
            raise ValueError("invalid version code !")
        self.bul_dict = bul_dict
        self.infos = bul_dict  # legacy code compat
        # authuser nécessaire pour version HTML qui contient liens dépendants de l'utilisateur
        self.authuser = authuser
        self.etud: Identite = etud
        self.filigranne = filigranne
        self.formsemestre: FormSemestre = formsemestre
        self.server_name = server_name
        self.version = version
        self.with_img_signatures_pdf = with_img_signatures_pdf
        # Store preferences for convenience:
        formsemestre_id = self.bul_dict["formsemestre_id"]
        self.preferences = sco_preferences.SemPreferences(formsemestre_id)
        self.diagnostic = None  # error message if any problem
        # Common PDF styles:
        #  - Pour tous les champs du bulletin sauf les cellules de table:
        self.style_field = reportlab.lib.styles.ParagraphStyle({})
        self.style_field.fontName = self.preferences["SCOLAR_FONT_BUL_FIELDS"]
        self.style_field.fontSize = self.preferences["SCOLAR_FONT_SIZE"]
        self.style_field.firstLineIndent = 0
        # Champ signatures
        self.style_signature = self.style_field
        #  - Pour les cellules de table:
        self.CellStyle = reportlab.lib.styles.ParagraphStyle({})
        self.CellStyle.fontSize = self.preferences["SCOLAR_FONT_SIZE"]
        self.CellStyle.fontName = self.preferences["SCOLAR_FONT"]
        self.CellStyle.leading = (
            1.0 * self.preferences["SCOLAR_FONT_SIZE"]
        )  # vertical space
        # Marges du document PDF
        self.margins = (
            self.preferences["left_margin"],
            self.preferences["top_margin"],
            self.preferences["right_margin"],
            self.preferences["bottom_margin"],
        )

    def get_filename(self):
        """Build a filename to be proposed to the web client"""
        sem = sco_formsemestre.get_formsemestre(self.bul_dict["formsemestre_id"])
        return scu.bul_filename_old(sem, self.bul_dict["etud"], "pdf")

    def generate(self, fmt="", stand_alone=True):
        """Return bulletin in specified format"""
        if not fmt in self.supported_formats:
            raise ValueError(f"unsupported bulletin format ({fmt})")
        try:
            PDFLOCK.acquire()  # this lock is necessary since reportlab is not re-entrant
            if fmt == "html":
                return self.generate_html()
            elif fmt == "pdf":
                return self.generate_pdf(stand_alone=stand_alone)
            else:
                raise ValueError(f"invalid bulletin format ({fmt})")
        finally:
            PDFLOCK.release()

    def generate_html(self):
        """Return bulletin as an HTML string"""
        H = ['<div class="notes_bulletin">']
        # table des notes:
        H.append(self.bul_table(fmt="html"))  # pylint: disable=no-member
        # infos sous la table:
        H.append(self.bul_part_below(fmt="html"))  # pylint: disable=no-member
        H.append("</div>")
        return "\n".join(H)

    def generate_pdf(self, stand_alone=True):
        """Build PDF bulletin from distinct parts
        Si stand_alone, génère un doc PDF complet et renvoie une string
        Sinon, renvoie juste une liste d'objets PLATYPUS pour intégration
        dans un autre document.
        """
        from app.scodoc import sco_preferences

        formsemestre_id = self.bul_dict["formsemestre_id"]
        nomprenom = self.bul_dict["etud"]["nomprenom"]
        etat_civil = self.bul_dict["etud"]["etat_civil"]
        marque_debut_bulletin = sco_pdf.DebutBulletin(
            etat_civil,
            filigranne=self.bul_dict["filigranne"],
            footer_content=f"""ScoDoc - Bulletin de {nomprenom} - {time.strftime("%d/%m/%Y %H:%M")}""",
        )
        story = []
        # partie haute du bulletin
        story += self.bul_title_pdf()  # pylint: disable=no-member
        index_obj_debut = len(story)

        # table des notes
        story += self.bul_table(fmt="pdf")  # pylint: disable=no-member
        # infos sous la table
        story += self.bul_part_below(fmt="pdf")  # pylint: disable=no-member
        # signatures
        story += self.bul_signatures_pdf()  # pylint: disable=no-member
        if self.scale_table_in_page:
            # Réduit sur une page
            story = [marque_debut_bulletin, KeepInFrame(0, 0, story, mode="shrink")]
        else:
            # Insere notre marqueur qui permet de générer les bookmarks et filigrannes:
            story.insert(index_obj_debut, marque_debut_bulletin)

        if ScoDocSiteConfig.is_bul_pdf_disabled():
            story = [Paragraph("<p>Export des PDF interdit par l'administrateur</p>")]

        #
        # objects.append(sco_pdf.FinBulletin())
        if not stand_alone:
            if self.multi_pages:
                # Bulletins sur plusieurs page, force début suivant sur page impaire
                story.append(
                    DocIf("doc.page%2 == 1", [PageBreak(), PageBreak()], [PageBreak()])
                )
            else:
                story.append(PageBreak())  # insert page break at end

            return story
        else:
            # Generation du document PDF
            sem = sco_formsemestre.get_formsemestre(formsemestre_id)
            report = io.BytesIO()  # in-memory document, no disk file
            document = sco_pdf.BaseDocTemplate(report)
            document.addPageTemplates(
                sco_pdf.ScoDocPageTemplate(
                    document,
                    author="%s %s (E. Viennet) [%s]"
                    % (sco_version.SCONAME, sco_version.SCOVERSION, self.description),
                    title=f"""Bulletin {sem["titremois"]} de {etat_civil}""",
                    subject="Bulletin de note",
                    margins=self.margins,
                    server_name=self.server_name,
                    filigranne=self.filigranne,
                    preferences=sco_preferences.SemPreferences(formsemestre_id),
                )
            )
            document.build(story)
            data = report.getvalue()
        return data

    def buildTableObject(self, P, pdfTableStyle, colWidths):
        """Utility used by some old-style generators.
        Build a platypus Table instance from a nested list of cells, style and widths.
        P: table, as a list of lists
        PdfTableStyle: commandes de style pour la table (reportlab)
        """
        try:
            # put each table cell in a Paragraph
            Pt = [
                [Paragraph(sco_pdf.SU(x), self.CellStyle) for x in line] for line in P
            ]
        except:
            # enquête sur exception intermittente...
            log("*** bug in PDF buildTableObject:")
            log("P=%s" % P)
            # compris: reportlab is not thread safe !
            #   see http://two.pairlist.net/pipermail/reportlab-users/2006-June/005037.html
            # (donc maintenant protégé dans ScoDoc par un Lock global)
            self.diagnostic = "erreur lors de la génération du PDF<br>"
            self.diagnostic += "<pre>" + traceback.format_exc() + "</pre>"
            return []
        return Table(Pt, colWidths=colWidths, style=pdfTableStyle)


# ---------------------------------------------------------------------------
def make_formsemestre_bulletin_etud(
    bul_dict,
    etud: Identite = None,
    formsemestre: FormSemestre = None,
    version=None,  # short, long, selectedevals
    fmt="pdf",  # html, pdf
    stand_alone=True,
    with_img_signatures_pdf: bool = True,
):
    """Bulletin de notes

    Appelle une fonction générant le bulletin au format spécifié à partir des informations infos,
    selon les préférences du semestre.

    """
    from app.scodoc import sco_preferences

    version = version or "long"
    if version not in scu.BULLETINS_VERSIONS:
        raise ValueError("invalid version code !")

    formsemestre_id = bul_dict["formsemestre_id"]
    bul_class_name = sco_preferences.get_preference("bul_class_name", formsemestre_id)
    gen_class = None
    for bul_class_name in (
        sco_preferences.get_preference("bul_class_name", formsemestre_id),
        # si pas trouvé (modifs locales bizarres ,), ré-essaye avec la valeur par défaut
        bulletin_default_class_name(),
    ):
        if bul_dict.get("type") == "BUT" and fmt.startswith("pdf"):
            gen_class = bulletin_get_class(bul_class_name + "BUT")
        if gen_class is None and bul_dict.get("type") != "BUT":
            gen_class = bulletin_get_class(bul_class_name)
        if gen_class is not None:
            break

    if gen_class is None:
        raise ValueError(f"Type de bulletin PDF invalide (paramètre: {bul_class_name})")

    try:
        PDFLOCK.acquire()
        bul_generator = gen_class(
            bul_dict,
            authuser=current_user,
            etud=etud,
            filigranne=bul_dict["filigranne"],
            formsemestre=formsemestre,
            server_name=request.url_root,
            version=version,
            with_img_signatures_pdf=with_img_signatures_pdf,
        )
        if fmt not in bul_generator.supported_formats:
            # use standard generator
            log(
                "Bulletin format %s not supported by %s, using %s"
                % (fmt, bul_class_name, bulletin_default_class_name())
            )
            bul_class_name = bulletin_default_class_name()
            gen_class = bulletin_get_class(bul_class_name)
            bul_generator = gen_class(
                bul_dict,
                authuser=current_user,
                etud=etud,
                filigranne=bul_dict["filigranne"],
                formsemestre=formsemestre,
                server_name=request.url_root,
                version=version,
                with_img_signatures_pdf=with_img_signatures_pdf,
            )
        data = bul_generator.generate(fmt=fmt, stand_alone=stand_alone)
    finally:
        PDFLOCK.release()

    if bul_generator.diagnostic:
        log(f"bul_error: {bul_generator.diagnostic}")
        raise NoteProcessError(bul_generator.diagnostic)

    filename = bul_generator.get_filename()
    return data, filename


####

# Liste des types des classes de générateurs de bulletins PDF:
BULLETIN_CLASSES = collections.OrderedDict()


def register_bulletin_class(klass):
    BULLETIN_CLASSES[klass.__name__] = klass


def bulletin_class_descriptions():
    return [
        BULLETIN_CLASSES[class_name].description
        for class_name in BULLETIN_CLASSES
        if BULLETIN_CLASSES[class_name].list_in_menu
    ]


def bulletin_class_names() -> list[str]:
    "Liste les noms des classes de bulletins à présenter à l'utilisateur"
    return [
        class_name
        for class_name in BULLETIN_CLASSES
        if BULLETIN_CLASSES[class_name].list_in_menu
    ]


def bulletin_default_class_name():
    return bulletin_class_names()[0]


def bulletin_get_class(class_name: str) -> BulletinGenerator:
    """La class de génération de bulletin de ce nom,
    ou None si pas trouvée
    """
    return BULLETIN_CLASSES.get(class_name)


def bulletin_get_class_name_displayed(formsemestre_id):
    """Le nom du générateur utilisé, en clair"""
    from app.scodoc import sco_preferences

    bul_class_name = sco_preferences.get_preference("bul_class_name", formsemestre_id)
    gen_class = bulletin_get_class(bul_class_name)
    if gen_class is None:
        return "invalide ! (voir paramètres)"
    return gen_class.description
