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

"""(Nouvelle) (Nouvelle) gestion des photos d'etudiants

Les images sont stockées dans .../var/scodoc/photos
L'attribut "photo_filename" de la table identite donne le nom du fichier image, 
sans extension (e.g. "F44/RT_EID31545").
Toutes les images sont converties en jpg, et stockées dans photo_filename.jpg en taille originale.
Elles sont aussi réduites en 90 pixels de hauteur, et stockées dans photo_filename.h90.jpg

Les images sont servies par ScoDoc, via la méthode getphotofile?etudid=xxx


## Historique:
 - jusqu'à novembre 2009, les images étaient stockées dans Zope (ZODB). 
 - jusqu'à v1908, stockées dans .../static/photos (et donc accessibles sans authentification).
 - support for legacy ZODB removed in v1909.

"""

import os
import time
import datetime
import random
import urllib2
import traceback
from PIL import Image as PILImage
from cStringIO import StringIO
import glob

from sco_utils import CONFIG, SCO_SRCDIR
from notes_log import log

import scolars
import sco_portal_apogee
from scolog import logdb

# Full paths on server's filesystem. Something like "/opt/scodoc/var/scodoc/photos"
PHOTO_DIR = os.path.join(os.environ["INSTANCE_HOME"], "var", "scodoc", "photos")
ICONS_DIR = os.path.join(SCO_SRCDIR, "static", "icons")
UNKNOWN_IMAGE_PATH = os.path.join(ICONS_DIR, "unknown.jpg")
UNKNOWN_IMAGE_URL = "get_photo_image?etudid="  # with empty etudid => unknown face image
IMAGE_EXT = ".jpg"
JPG_QUALITY = 0.92
REDUCED_HEIGHT = 90  # pixels
MAX_FILE_SIZE = 1024 * 1024  # max allowed size for uploaded image, in bytes
H90 = ".h90"  # suffix for reduced size images


def photo_portal_url(context, etud):
    """Returns external URL to retreive photo on portal,
    or None if no portal configured"""
    photo_url = sco_portal_apogee.get_photo_url(context)
    if photo_url and etud["code_nip"]:
        return photo_url + "?nip=" + etud["code_nip"]
    else:
        return None


def etud_photo_url(context, etud, size="small", fast=False, REQUEST=None):
    """url to the image of the student, in "small" size or "orig" size.
    If ScoDoc doesn't have an image and a portal is configured, link to it.
    """
    photo_url = "get_photo_image?etudid=%s&size=%s" % (etud["etudid"], size)
    if fast:
        return photo_url
    path = photo_pathname(context, etud, size=size)
    if not path:
        # Portail ?
        ext_url = photo_portal_url(context, etud)
        if not ext_url:
            # fallback: Photo "unknown"
            photo_url = UNKNOWN_IMAGE_URL
        else:
            # essaie de copier la photo du portail
            new_path, _ = copy_portal_photo_to_fs(context, etud, REQUEST=REQUEST)
            if not new_path:
                # copy failed, can we use external url ?
                # nb: rarement utile, car le portail est rarement accessible sans authentification
                if CONFIG.PUBLISH_PORTAL_PHOTO_URL:
                    photo_url = ext_url
                else:
                    photo_url = UNKNOWN_IMAGE_URL
    return photo_url


def get_photo_image(context, etudid=None, size="small", REQUEST=None):
    """Returns photo image (HTTP response)
    If not etudid, use "unknown" image
    """
    if not etudid:
        filename = UNKNOWN_IMAGE_PATH
    else:
        etud = context.getEtudInfo(etudid=etudid, filled=1, REQUEST=REQUEST)[0]
        filename = photo_pathname(context, etud, size=size)
        if not filename:
            filename = UNKNOWN_IMAGE_PATH
    return _http_jpeg_file(context, filename, REQUEST=REQUEST)


def _http_jpeg_file(context, filename, REQUEST=None):
    """returns an image.
    This function will be modified when we kill #zope
    """
    st = os.stat(filename)
    last_modified = st.st_mtime  # float timestamp
    last_modified_str = time.strftime(
        "%a, %d %b %Y %H:%M:%S GMT", time.gmtime(last_modified)
    )
    file_size = st.st_size
    RESPONSE = REQUEST.RESPONSE
    RESPONSE.setHeader("Content-Type", "image/jpeg")
    RESPONSE.setHeader("Last-Modified", last_modified_str)
    RESPONSE.setHeader("Cache-Control", "max-age=3600")
    RESPONSE.setHeader("Content-Length", str(file_size))
    header = REQUEST.get_header("If-Modified-Since", None)
    if header is not None:
        header = header.split(";")[0]
        # Some proxies seem to send invalid date strings for this
        # header. If the date string is not valid, we ignore it
        # rather than raise an error to be generally consistent
        # with common servers such as Apache (which can usually
        # understand the screwy date string as a lucky side effect
        # of the way they parse it).
        try:
            dt = datetime.datetime.strptime(header, "%a, %d %b %Y %H:%M:%S GMT")
            mod_since = dt.timestamp()
        except:
            mod_since = None
        if (mod_since is not None) and last_modified <= mod_since:
            RESPONSE.setStatus(304)  # not modified
            return ""

    return open(filename, mode="rb").read()


def etud_photo_is_local(context, etud, size="small"):
    return photo_pathname(context, etud, size=size)


def etud_photo_html(
    context, etud=None, etudid=None, title=None, size="small", REQUEST=None
):
    """HTML img tag for the photo, either in small size (h90)
    or original size (size=="orig")
    """
    if not etud:
        if etudid:
            etud = context.getEtudInfo(etudid=etudid, filled=1, REQUEST=REQUEST)[0]
        else:
            raise ValueError("etud_photo_html: either etud or etudid must be specified")
    photo_url = etud_photo_url(context, etud, size=size, REQUEST=REQUEST)
    nom = etud.get("nomprenom", etud["nom_disp"])
    if title is None:
        title = nom
    if not etud_photo_is_local(context, etud):
        fallback = (
            """onerror='this.onerror = null; this.src="%s"'""" % UNKNOWN_IMAGE_URL
        )
    else:
        fallback = ""
    if size == "small":
        height_attr = 'height="%s"' % REDUCED_HEIGHT
    else:
        height_attr = ""
    return '<img src="%s" alt="photo %s" title="%s" border="0" %s %s />' % (
        photo_url,
        nom,
        title,
        height_attr,
        fallback,
    )


def etud_photo_orig_html(context, etud=None, etudid=None, title=None, REQUEST=None):
    """HTML img tag for the photo, in full size.
    Full-size images are always stored locally in the filesystem.
    They are the original uploaded images, converted in jpeg.
    """
    return etud_photo_html(
        context, etud=etud, etudid=etudid, title=title, size="orig", REQUEST=REQUEST
    )


def photo_pathname(context, etud, size="orig"):
    """Returns full path of image file if etud has a photo (in the filesystem), or False.
    Do not distinguish the cases: no photo, or file missing.
    """
    if size == "small":
        version = H90
    elif size == "orig":
        version = ""
    else:
        raise ValueError("invalid size parameter for photo")
    if not etud["photo_filename"]:
        return False
    path = os.path.join(PHOTO_DIR, etud["photo_filename"]) + version + IMAGE_EXT
    if os.path.exists(path):
        return path
    else:
        return False


def store_photo(context, etud, data, REQUEST=None):
    """Store image for this etud.
    If there is an existing photo, it is erased and replaced.
    data is a string with image raw data.

    Update database to store filename.

    Returns (status, msg)
    """
    # basic checks
    filesize = len(data)
    if filesize < 10 or filesize > MAX_FILE_SIZE:
        return 0, "Fichier image de taille invalide ! (%d)" % filesize
    filename = save_image(context, etud["etudid"], data)
    # update database:
    etud["photo_filename"] = filename
    etud["foto"] = None

    cnx = context.GetDBConnexion()
    scolars.identite_edit_nocheck(cnx, etud)
    cnx.commit()
    #
    if REQUEST:
        logdb(REQUEST, cnx, method="changePhoto", msg=filename, etudid=etud["etudid"])
    #
    return 1, "ok"


def suppress_photo(context, etud, REQUEST=None):
    """Suppress a photo"""
    log("suppress_photo etudid=%s" % etud["etudid"])
    rel_path = photo_pathname(context, etud)
    # 1- remove ref. from database
    etud["photo_filename"] = None
    cnx = context.GetDBConnexion()
    scolars.identite_edit_nocheck(cnx, etud)
    cnx.commit()
    # 2- erase images files
    if rel_path:
        # remove extension and glob
        rel_path = rel_path[: -len(IMAGE_EXT)]
        filenames = glob.glob(rel_path + "*" + IMAGE_EXT)
        for filename in filenames:
            log("removing file %s" % filename)
            os.remove(filename)
    # 3- log
    if REQUEST:
        logdb(
            REQUEST, cnx, method="changePhoto", msg="suppression", etudid=etud["etudid"]
        )


# ---------------------------------------------------------------------------
# Internal functions


def save_image(context, etudid, data):
    """img_file is a file-like object.
    Save image in JPEG in 2 sizes (original and h90).
    Returns filename (relative to PHOTO_DIR), without extension
    """
    data_file = StringIO()
    data_file.write(data)
    data_file.seek(0)
    img = PILImage.open(data_file)
    filename = get_new_filename(context, etudid)
    path = os.path.join(PHOTO_DIR, filename)
    log("saving %dx%d jpeg to %s" % (img.size[0], img.size[1], path))
    img.save(path + IMAGE_EXT, format="JPEG", quality=92)
    # resize:
    img = scale_height(img)
    log("saving %dx%d jpeg to %s.h90" % (img.size[0], img.size[1], filename))
    img.save(path + H90 + IMAGE_EXT, format="JPEG", quality=92)
    return filename


def scale_height(img, W=None, H=REDUCED_HEIGHT):
    if W is None:
        # keep aspect
        W = (img.size[0] * H) / img.size[1]
    img.thumbnail((W, H), PILImage.ANTIALIAS)
    return img


def get_new_filename(context, etudid):
    """Constructs a random filename to store a new image.
    The path is constructed as: Fxx/etudid
    """
    dept = context.DeptId()
    return find_new_dir() + dept + "_" + etudid


def find_new_dir():
    """select randomly a new subdirectory to store a new file.
    We define 100 subdirectories named from F00 to F99.
    Returns a path relative to the PHOTO_DIR.
    """
    d = "F" + "%02d" % random.randint(0, 99)
    path = os.path.join(PHOTO_DIR, d)
    if not os.path.exists(path):
        # ensure photos directory exists
        if not os.path.exists(PHOTO_DIR):
            os.mkdir(PHOTO_DIR)
        # create subdirectory
        log("creating directory %s" % path)
        os.mkdir(path)
    return d + "/"


def copy_portal_photo_to_fs(context, etud, REQUEST=None):
    """Copy the photo from portal (distant website) to local fs.
    Returns rel. path or None if copy failed, with a diagnotic message
    """
    scolars.format_etud_ident(etud)
    url = photo_portal_url(context, etud)
    if not url:
        return None, "%(nomprenom)s: pas de code NIP" % etud
    portal_timeout = context.get_preference("portal_timeout")
    f = None
    try:
        log("copy_portal_photo_to_fs: getting %s" % url)
        f = urllib2.urlopen(url, timeout=portal_timeout)  # python >= 2.7
    except:
        log("download failed: exception:\n%s" % traceback.format_exc())
        log("called from:\n" + "".join(traceback.format_stack()))
        return None, "%s: erreur chargement de %s" % (etud["nomprenom"], url)
    if not f:
        log("download failed")
        return None, "%s: erreur chargement de %s" % (etud["nomprenom"], url)
    data = f.read()
    try:
        status, diag = store_photo(context, etud, data, REQUEST=REQUEST)
    except:
        status = 0
        diag = "Erreur chargement photo du portail"
        log("copy_portal_photo_to_fs: failure (exception in store_photo)!")
    if status == 1:
        log("copy_portal_photo_to_fs: copied %s" % url)
        return photo_pathname(context, etud), "%s: photo chargée" % etud["nomprenom"]
    else:
        return None, "%s: <b>%s</b>" % (etud["nomprenom"], diag)