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

"""Photos: trombinoscopes
"""

import io
from zipfile import ZipFile, BadZipfile
import reportlab
from reportlab.lib.units import cm, mm
from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Frame, PageBreak
from reportlab.platypus import Table, TableStyle, Image, KeepInFrame
from reportlab.platypus.flowables import Flowable
from reportlab.platypus.doctemplate import PageTemplate, BaseDocTemplate
from reportlab.lib.pagesizes import A4, landscape
from reportlab.lib import styles
from reportlab.lib.colors import Color
from reportlab.lib import colors
from PIL import Image as PILImage

import flask
from flask import url_for, g

from app.scodoc.notes_log import log
import app.scodoc.sco_utils as scu
from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_pdf import SU
from app.scodoc import html_sco_header
from app.scodoc import htmlutils
from app.scodoc import sco_import_etuds
from app.scodoc import sco_excel
from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups
from app.scodoc import sco_groups_view
from app.scodoc import sco_pdf
from app.scodoc import sco_photos
from app.scodoc import sco_portal_apogee
from app.scodoc import sco_preferences
from app.scodoc import sco_etud


def trombino(
    context,
    REQUEST=None,
    group_ids=[],  # liste des groupes à afficher
    formsemestre_id=None,  # utilisé si pas de groupes selectionné
    etat=None,
    format="html",
    dialog_confirmed=False,
):
    """Trombinoscope"""
    if not etat:
        etat = None  # may be passed as ''
    # Informations sur les groupes à afficher:
    groups_infos = sco_groups_view.DisplayedGroupsInfos(
        context, group_ids, formsemestre_id=formsemestre_id, etat=etat, REQUEST=REQUEST
    )

    #
    if format != "html" and not dialog_confirmed:
        ok, dialog = check_local_photos_availability(
            context, groups_infos, REQUEST, format=format
        )
        if not ok:
            return dialog

    if format == "zip":
        return _trombino_zip(context, groups_infos, REQUEST)
    elif format == "pdf":
        return _trombino_pdf(context, groups_infos, REQUEST)
    elif format == "pdflist":
        return _listeappel_photos_pdf(context, groups_infos, REQUEST)
    else:
        raise Exception("invalid format")
        # return _trombino_html_header(context, REQUEST) + trombino_html(context, group, members, REQUEST=REQUEST) + html_sco_header.sco_footer( REQUEST)


def _trombino_html_header(context, REQUEST):
    return html_sco_header.sco_header(javascripts=["js/trombino.js"])


def trombino_html(context, groups_infos, REQUEST=None):
    "HTML snippet for trombino (with title and menu)"
    menuTrombi = [
        {
            "title": "Charger des photos...",
            "endpoint": "scolar.photos_import_files_form",
            "args": {"group_ids": groups_infos.group_ids},
        },
        {
            "title": "Obtenir archive Zip des photos",
            "endpoint": "scolar.trombino",
            "args": {"group_ids": groups_infos.group_ids, "format": "zip"},
        },
        {
            "title": "Recopier les photos depuis le portail",
            "endpoint": "scolar.trombino_copy_photos",
            "args": {"group_ids": groups_infos.group_ids},
        },
    ]

    if groups_infos.members:
        if groups_infos.tous_les_etuds_du_sem:
            ng = "Tous les étudiants"
        else:
            ng = "Groupe %s" % groups_infos.groups_titles
    else:
        ng = "Aucun étudiant inscrit dans ce groupe !"
    H = [
        '<table style="padding-top: 10px; padding-bottom: 10px;"><tr><td><span style="font-style: bold; font-size: 150%%; padding-right: 20px;">%s</span></td>'
        % (ng)
    ]
    if groups_infos.members:
        H.append(
            "<td>"
            + htmlutils.make_menu("Gérer les photos", menuTrombi, alone=True)
            + "</td>"
        )
    H.append("</tr></table>")
    H.append("<div>")
    i = 0
    for t in groups_infos.members:
        H.append(
            '<span class="trombi_box"><span class="trombi-photo" id="trombi-%s">'
            % t["etudid"]
        )
        if sco_photos.etud_photo_is_local(context, t, size="small"):
            foto = sco_photos.etud_photo_html(context, t, title="", REQUEST=REQUEST)
        else:  # la photo n'est pas immédiatement dispo
            foto = (
                '<span class="unloaded_img" id="%s"><img border="0" height="90" alt="en cours" src="/ScoDoc/static/icons/loading.jpg"/></span>'
                % t["etudid"]
            )
        H.append(
            '<a href="%s">%s</a>'
            % (
                url_for(
                    "scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=t["etudid"]
                ),
                foto,
            )
        )
        H.append("</span>")
        H.append(
            '<span class="trombi_legend"><span class="trombi_prenom">'
            + sco_etud.format_prenom(t["prenom"])
            + '</span><span class="trombi_nom">'
            + sco_etud.format_nom(t["nom"])
        )
        H.append("</span></span></span>")
        i += 1

    H.append("</div>")
    H.append(
        '<div style="margin-bottom:15px;"><a class="stdlink" href="trombino?format=pdf&%s">Version PDF</a></div>'
        % groups_infos.groups_query_args
    )
    return "\n".join(H)


def check_local_photos_availability(context, groups_infos, REQUEST, format=""):
    """Verifie que toutes les photos (des gropupes indiqués) sont copiées localement
    dans ScoDoc (seules les photos dont nous disposons localement peuvent être exportées
    en pdf ou en zip).
    Si toutes ne sont pas dispo, retourne un dialogue d'avertissement pour l'utilisateur.
    """
    nb_missing = 0
    for t in groups_infos.members:
        _ = sco_photos.etud_photo_url(
            context, t, REQUEST=REQUEST
        )  # -> copy distant files if needed
        if not sco_photos.etud_photo_is_local(context, t):
            nb_missing += 1
    if nb_missing > 0:
        parameters = {"group_ids": groups_infos.group_ids, "format": format}
        return (
            False,
            scu.confirm_dialog(
                """<p>Attention: %d photos ne sont pas disponibles et ne peuvent pas être exportées.</p><p>Vous pouvez <a class="stdlink" href="%s">exporter seulement les photos existantes</a>"""
                % (
                    nb_missing,
                    groups_infos.base_url + "&dialog_confirmed=1&format=%s" % format,
                ),
                dest_url="trombino",
                OK="Exporter seulement les photos existantes",
                cancel_url="groups_view?curtab=tab-photos&"
                + groups_infos.groups_query_args,
                parameters=parameters,
            ),
        )
    else:
        return True, ""


def _trombino_zip(context, groups_infos, REQUEST):
    "Send photos as zip archive"
    data = io.BytesIO()
    Z = ZipFile(data, "w")
    # assume we have the photos (or the user acknowledged the fact)
    # Archive originals (not reduced) images, in JPEG
    for t in groups_infos.members:
        im_path = sco_photos.photo_pathname(context, t, size="orig")
        if not im_path:
            continue
        img = open(im_path, "rb").read()
        code_nip = t["code_nip"]
        if code_nip:
            filename = code_nip + ".jpg"
        else:
            filename = t["nom"] + "_" + t["prenom"] + "_" + t["etudid"] + ".jpg"
        Z.writestr(filename, img)
    Z.close()
    size = data.tell()
    log("trombino_zip: %d bytes" % size)
    content_type = "application/zip"
    REQUEST.RESPONSE.setHeader(
        "content-disposition", 'attachement; filename="trombi.zip"'
    )
    REQUEST.RESPONSE.setHeader("content-type", content_type)
    REQUEST.RESPONSE.setHeader("content-length", size)
    return data.getvalue()


# Copy photos from portal to ScoDoc
def trombino_copy_photos(context, group_ids=[], REQUEST=None, dialog_confirmed=False):
    "Copy photos from portal to ScoDoc (overwriting local copy)"
    groups_infos = sco_groups_view.DisplayedGroupsInfos(
        context, group_ids, REQUEST=REQUEST
    )
    back_url = "groups_view?%s&curtab=tab-photos" % groups_infos.groups_query_args

    portal_url = sco_portal_apogee.get_portal_url(context)
    header = html_sco_header.sco_header(page_title="Chargement des photos")
    footer = html_sco_header.sco_footer()
    if not portal_url:
        return (
            header
            + '<p>portail non configuré</p><p><a href="%s">Retour au trombinoscope</a></p>'
            % back_url
            + footer
        )
    if not dialog_confirmed:
        return scu.confirm_dialog(
            """<h2>Copier les photos du portail vers ScoDoc ?</h2>
                <p>Les photos du groupe %s présentes dans ScoDoc seront remplacées par celles du portail (si elles existent).</p>
                <p>(les photos sont normalement automatiquement copiées lors de leur première utilisation, l'usage de cette fonction n'est nécessaire que si les photos du portail ont été modifiées)</p>
                """
            % (groups_infos.groups_titles),
            dest_url="",
            cancel_url=back_url,
            parameters={"group_ids": group_ids},
        )

    msg = []
    nok = 0
    for etud in groups_infos.members:
        path, diag = sco_photos.copy_portal_photo_to_fs(context, etud, REQUEST=REQUEST)
        msg.append(diag)
        if path:
            nok += 1

    msg.append("<b>%d photos correctement chargées</b>" % nok)

    return (
        header
        + "<h2>Chargement des photos depuis le portail</h2><ul><li>"
        + "</li><li>".join(msg)
        + "</li></ul>"
        + '<p><a href="%s">retour au trombinoscope</a>' % back_url
        + footer
    )


def _get_etud_platypus_image(context, t, image_width=2 * cm):
    """Returns aplatypus object for the photo of student t"""
    try:
        path = sco_photos.photo_pathname(context, t, size="small")
        if not path:
            # log('> unknown')
            path = sco_photos.UNKNOWN_IMAGE_PATH
        im = PILImage.open(path)
        w0, h0 = im.size[0], im.size[1]
        if w0 > h0:
            W = image_width
            H = h0 * W / w0
        else:
            H = image_width
            W = w0 * H / h0
        return reportlab.platypus.Image(path, width=W, height=H)
    except:
        log(
            "*** exception while processing photo of %s (%s) (path=%s)"
            % (t["nom"], t["etudid"], path)
        )
        raise


def _trombino_pdf(context, groups_infos, REQUEST):
    "Send photos as pdf page"
    # Generate PDF page
    filename = "trombino_%s" % groups_infos.groups_filename + ".pdf"
    sem = groups_infos.formsemestre  # suppose 1 seul semestre

    PHOTOWIDTH = 3 * cm
    COLWIDTH = 3.6 * cm
    N_PER_ROW = 5  # XXX should be in ScoDoc preferences

    StyleSheet = styles.getSampleStyleSheet()
    report = io.BytesIO()  # in-memory document, no disk file
    objects = [
        Paragraph(
            SU("Trombinoscope " + sem["titreannee"] + " " + groups_infos.groups_titles),
            StyleSheet["Heading3"],
        )
    ]
    L = []
    n = 0
    currow = []
    log("_trombino_pdf %d elements" % len(groups_infos.members))
    for t in groups_infos.members:
        img = _get_etud_platypus_image(context, t, image_width=PHOTOWIDTH)
        elem = Table(
            [
                [img],
                [
                    Paragraph(
                        SU(sco_etud.format_nomprenom(t)),
                        StyleSheet["Normal"],
                    )
                ],
            ],
            colWidths=[PHOTOWIDTH],
        )
        currow.append(elem)
        if n == (N_PER_ROW - 1):
            L.append(currow)
            currow = []
        n = (n + 1) % N_PER_ROW
    if currow:
        currow += [" "] * (N_PER_ROW - len(currow))
        L.append(currow)
    if not L:
        table = Paragraph(SU("Aucune photo à exporter !"), StyleSheet["Normal"])
    else:
        table = Table(
            L,
            colWidths=[COLWIDTH] * N_PER_ROW,
            style=TableStyle(
                [
                    # ('RIGHTPADDING', (0,0), (-1,-1), -5*mm),
                    ("VALIGN", (0, 0), (-1, -1), "TOP"),
                    ("GRID", (0, 0), (-1, -1), 0.25, colors.grey),
                ]
            ),
        )
    objects.append(table)
    # Build document
    document = BaseDocTemplate(report)
    document.addPageTemplates(
        sco_pdf.ScolarsPageTemplate(
            document,
            preferences=sco_preferences.SemPreferences(sem["formsemestre_id"]),
        )
    )
    document.build(objects)
    data = report.getvalue()

    return scu.sendPDFFile(REQUEST, data, filename)


# --------------------- Sur une idée de l'IUT d'Orléans:
def _listeappel_photos_pdf(context, groups_infos, REQUEST):
    "Doc pdf pour liste d'appel avec photos"
    filename = "trombino_%s" % groups_infos.groups_filename + ".pdf"
    sem = groups_infos.formsemestre  # suppose 1 seul semestre

    PHOTOWIDTH = 2 * cm
    # COLWIDTH = 3.6 * cm
    # ROWS_PER_PAGE = 26  # XXX should be in ScoDoc preferences

    StyleSheet = styles.getSampleStyleSheet()
    report = io.BytesIO()  # in-memory document, no disk file
    objects = [
        Paragraph(
            SU(
                sem["titreannee"]
                + " "
                + groups_infos.groups_titles
                + " (%d)" % len(groups_infos.members)
            ),
            StyleSheet["Heading3"],
        )
    ]
    L = []
    n = 0
    currow = []
    log("_listeappel_photos_pdf %d elements" % len(groups_infos.members))
    n = len(groups_infos.members)
    # npages = n / 2*ROWS_PER_PAGE + 1 # nb de pages papier
    # for page in range(npages):
    for i in range(n):  # page*2*ROWS_PER_PAGE, (page+1)*2*ROWS_PER_PAGE):
        t = groups_infos.members[i]
        img = _get_etud_platypus_image(context, t, image_width=PHOTOWIDTH)
        txt = Paragraph(
            SU(sco_etud.format_nomprenom(t)),
            StyleSheet["Normal"],
        )
        if currow:
            currow += [""]
        currow += [img, txt, ""]
        if i % 2:
            L.append(currow)
            currow = []
    if currow:
        currow += [" "] * 3
        L.append(currow)
    if not L:
        table = Paragraph(SU("Aucune photo à exporter !"), StyleSheet["Normal"])
    else:
        table = Table(
            L,
            colWidths=[2 * cm, 4 * cm, 27 * mm, 5 * mm, 2 * cm, 4 * cm, 27 * mm],
            style=TableStyle(
                [
                    # ('RIGHTPADDING', (0,0), (-1,-1), -5*mm),
                    ("VALIGN", (0, 0), (-1, -1), "TOP"),
                    ("GRID", (0, 0), (2, -1), 0.25, colors.grey),
                    ("GRID", (4, 0), (-1, -1), 0.25, colors.grey),
                ]
            ),
        )
    objects.append(table)
    # Build document
    document = BaseDocTemplate(report)
    document.addPageTemplates(
        sco_pdf.ScolarsPageTemplate(
            document,
            preferences=sco_preferences.SemPreferences(sem["formsemestre_id"]),
        )
    )
    document.build(objects)
    data = report.getvalue()

    return scu.sendPDFFile(REQUEST, data, filename)


# ---------------------    Upload des photos de tout un groupe
def photos_generate_excel_sample(context, group_ids=[], REQUEST=None):
    """Feuille excel pour import fichiers photos"""
    fmt = sco_import_etuds.sco_import_format()
    data = sco_import_etuds.sco_import_generate_excel_sample(
        fmt,
        context=context,
        group_ids=group_ids,
        only_tables=["identite"],
        exclude_cols=[
            "date_naissance",
            "lieu_naissance",
            "nationalite",
            "statut",
            "photo_filename",
        ],
        extra_cols=["fichier_photo"],
        REQUEST=REQUEST,
    )
    return sco_excel.send_excel_file(REQUEST, data, "ImportPhotos" + scu.XLSX_SUFFIX)


def photos_import_files_form(context, group_ids=[], REQUEST=None):
    """Formulaire pour importation photos"""
    groups_infos = sco_groups_view.DisplayedGroupsInfos(
        context, group_ids, REQUEST=REQUEST
    )
    back_url = "groups_view?%s&curtab=tab-photos" % groups_infos.groups_query_args

    H = [
        html_sco_header.sco_header(page_title="Import des photos des étudiants"),
        """<h2 class="formsemestre">Téléchargement des photos des étudiants</h2>
         <p><b>Vous pouvez aussi charger les photos individuellement via la fiche de chaque étudiant (menu "Etudiant" / "Changer la photo").</b></p>
         <p class="help">Cette page permet de charger en une seule fois les photos de plusieurs étudiants.<br/>
          Il faut d'abord remplir une feuille excel donnant les noms 
          des fichiers images (une image par étudiant).
         </p>
         <p class="help">Ensuite, réunir vos images dans un fichier zip, puis télécharger 
         simultanément le fichier excel et le fichier zip.
         </p>
        <ol>
        <li><a class="stdlink" href="photos_generate_excel_sample?%s">
        Obtenir la feuille excel à remplir</a>
        </li>
        <li style="padding-top: 2em;">
         """
        % groups_infos.groups_query_args,
    ]
    F = html_sco_header.sco_footer()
    REQUEST.form["group_ids"] = groups_infos.group_ids
    tf = TrivialFormulator(
        REQUEST.URL0,
        REQUEST.form,
        (
            ("xlsfile", {"title": "Fichier Excel:", "input_type": "file", "size": 40}),
            ("zipfile", {"title": "Fichier zip:", "input_type": "file", "size": 40}),
            ("group_ids", {"input_type": "hidden", "type": "list"}),
        ),
    )

    if tf[0] == 0:
        return "\n".join(H) + tf[1] + "</li></ol>" + F
    elif tf[0] == -1:
        return flask.redirect(back_url)
    else:
        return photos_import_files(
            context,
            group_ids=tf[2]["group_ids"],
            xlsfile=tf[2]["xlsfile"],
            zipfile=tf[2]["zipfile"],
            REQUEST=REQUEST,
        )


def photos_import_files(
    context, group_ids=[], xlsfile=None, zipfile=None, REQUEST=None
):
    """Importation des photos"""
    groups_infos = sco_groups_view.DisplayedGroupsInfos(
        context, group_ids, REQUEST=REQUEST
    )
    back_url = "groups_view?%s&curtab=tab-photos" % groups_infos.groups_query_args
    filename_title = "fichier_photo"
    page_title = "Téléchargement des photos des étudiants"

    def callback(context, etud, data, filename, REQUEST):
        sco_photos.store_photo(context, etud, data, REQUEST)

    zip_excel_import_files(
        context, xlsfile, zipfile, REQUEST, callback, filename_title, page_title
    )
    return flask.redirect(back_url + "&head_message=photos%20 importees")


def zip_excel_import_files(
    context,
    xlsfile=None,
    zipfile=None,
    REQUEST=None,
    callback=None,
    filename_title="",  # doit obligatoirement etre specifié
    page_title="",
):
    """Importation de fichiers à partir d'un excel et d'un zip
    La fonction
       callback()
    est appelé pour chaque fichier trouvé.
    """
    # 1- build mapping etudid -> filename
    exceldata = xlsfile.read()
    if not exceldata:
        raise ScoValueError("Fichier excel vide ou invalide")
    _, data = sco_excel.Excel_to_list(exceldata)
    if not data:  # probably a bug
        raise ScoValueError("Fichier excel vide !")
    # on doit avoir une colonne etudid et une colonne filename_title ('fichier_photo')
    titles = data[0]
    try:
        etudid_idx = titles.index("etudid")
        filename_idx = titles.index(filename_title)
    except:
        raise ScoValueError(
            "Fichier excel incorrect (il faut une colonne etudid et une colonne %s) !"
            % filename_title
        )

    def normfilename(fn, lowercase=True):
        "normalisation used to match filenames"
        fn = fn.replace("\\", "/")  # not sure if this is necessary ?
        fn = fn.strip()
        if lowercase:
            fn = scu.strlower(fn)
        fn = fn.split("/")[-1]  # use only last component, not directories
        return fn

    Filename2Etud = {}  # filename : etudid
    for l in data[1:]:
        filename = l[filename_idx].strip()
        if filename:
            Filename2Etud[normfilename(filename)] = l[etudid_idx]

    # 2- Ouvre le zip et
    try:
        z = ZipFile(zipfile)
    except BadZipfile:
        raise ScoValueError("Fichier ZIP incorrect !")
    ignored_zipfiles = []
    stored = []  # [ (etud, filename) ]
    for name in z.namelist():
        if len(name) > 4 and name[-1] != "/" and "." in name:
            data = z.read(name)
            # match zip filename with name given in excel
            normname = normfilename(name)
            if normname in Filename2Etud:
                etudid = Filename2Etud[normname]
                # ok, store photo
                try:
                    etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
                    del Filename2Etud[normname]
                except:
                    raise ScoValueError("ID étudiant invalide: %s" % etudid)

                callback(
                    context,
                    etud,
                    data,
                    normfilename(name, lowercase=False),
                    REQUEST=REQUEST,
                )

                stored.append((etud, name))
            else:
                log("zip: zip name %s not in excel !" % name)
                ignored_zipfiles.append(name)
        else:
            if name[-1] != "/":
                ignored_zipfiles.append(name)
            log("zip: ignoring %s" % name)
    if Filename2Etud:
        # lignes excel non traitées
        unmatched_files = list(Filename2Etud.keys())
    else:
        unmatched_files = []
    # 3- Result page
    H = [
        _trombino_html_header(context, REQUEST),
        """<h2 class="formsemestre">%s</h2>
         <h3>Opération effectuée</h3>
         """
        % page_title,
    ]
    if ignored_zipfiles:
        H.append("<h4>Fichiers ignorés dans le zip:</h4><ul>")
        for name in ignored_zipfiles:
            H.append("<li>%s</li>" % name)
        H.append("</ul>")
    if unmatched_files:
        H.append(
            "<h4>Fichiers indiqués dans feuille mais non trouvés dans le zip:</h4><ul>"
        )
        for name in unmatched_files:
            H.append("<li>%s</li>" % name)
        H.append("</ul>")
    if stored:
        H.append("<h4>Fichiers chargés:</h4><ul>")
        for (etud, name) in stored:
            H.append("<li>%s: <tt>%s</tt></li>" % (etud["nomprenom"], name))
        H.append("</ul>")

    return "\n".join(H)