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

"""Gestion des images logos (nouveau ScoDoc 9.1)

Les logos sont `logo_header.<ext>`  et `logo_footer.<ext>`
avec `ext` membre de LOGOS_IMAGES_ALLOWED_TYPES (= jpg, png)

SCODOC_LOGOS_DIR   /opt/scodoc-data/config/logos
"""
import glob
import os
import re
import shutil
from pathlib import Path

from flask import current_app, url_for
from PIL import Image as PILImage
import puremagic
from werkzeug.utils import secure_filename

from app import log
from app.models import Departement
from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import ScoValueError

GLOBAL = "_"  # category for server level logos


class Logo:
    """Responsable des opérations (select, create), du calcul des chemins et url
    ainsi que de la récupération des informations sur un logo.
    Usage:
        logo existant: Logo(<name>, <dept_id>, ...).select() (retourne None si fichier non trouvé)
        logo en création: Logo(<name>, <dept_id>, ...).create(stream)
        Les attributs filename, filepath, get_url() ne devraient pas être utilisés avant les opérations
            select ou save (le format n'est pas encore connu à ce moement là)
    """

    def __init__(self, logoname, dept_id=None, prefix=scu.LOGO_FILE_PREFIX):
        """Initialisation des noms et département des logos.
        if prefix = None on recherche simplement une image 'logoname.*'
        Le format est renseigné au moment de la lecture (select) ou de la création (create) de l'objet
        """
        self.logoname = secure_filename(logoname)
        if not self.logoname:
            self.logoname = "*** *** nom de logo invalide *** à changer ! *** ***"
        self.scodoc_dept_id = dept_id
        self.prefix = prefix or ""
        if self.scodoc_dept_id:
            self.dirpath = os.path.sep.join(
                [
                    scu.SCODOC_LOGOS_DIR,
                    scu.LOGOS_DIR_PREFIX + secure_filename(str(dept_id)),
                ]
            )
        else:
            self.dirpath = scu.SCODOC_LOGOS_DIR
        self.basepath = os.path.sep.join(
            [self.dirpath, self.prefix + secure_filename(self.logoname)]
        )
        # next attributes are computed by the select function
        self.suffix = (
            "Not initialized: call the select or create function before access"
        )
        self.filepath = (
            "Not initialized: call the select or create function before access"
        )
        self.filename = (
            "Not initialized: call the select or create function before access"
        )
        self.size = "Not initialized: call the select or create function before access"
        self.aspect_ratio = (
            "Not initialized: call the select or create function before access"
        )
        self.density = (
            "Not initialized: call the select or create function before access"
        )
        self.mm = "Not initialized: call the select or create function before access"

    def __repr__(self) -> str:
        return f"Logo(logoname='{self.logoname}', filename='{self.filename}')"

    def _set_format(self, fmt):
        self.suffix = fmt
        self.filepath = self.basepath + "." + fmt
        self.filename = self.logoname + "." + fmt

    def _ensure_directory_exists(self):
        "create enclosing directory if necessary"
        if not Path(self.dirpath).exists():
            current_app.logger.info("sco_logos creating directory %s", self.dirpath)
            os.mkdir(self.dirpath)

    def create(self, stream):
        "enregistre logo"
        try:
            img_type = guess_image_type(stream)
        except ValueError as exc:
            raise ScoValueError("fichier logo invalide") from exc
        if img_type not in scu.LOGOS_IMAGES_ALLOWED_TYPES:
            raise ScoValueError(f"type d'image invalide ({img_type})")
        self._set_format(img_type)
        self._ensure_directory_exists()
        filename = self.basepath + "." + self.suffix
        with open(filename, "wb") as f:
            f.write(stream.read())
        current_app.logger.info("sco_logos.store_image %s", self.filename)
        # erase other formats if they exists
        for suffix in set(scu.LOGOS_IMAGES_ALLOWED_TYPES) - set([img_type]):
            try:
                os.unlink(self.basepath + "." + suffix)
            except IOError:
                pass

    def _read_info(self, img):
        """computes some properties from the real image
        aspect_ratio assumes that x_density and y_density are equals
        """
        x_size, y_size = img.size
        self.density = img.info.get("dpi", None)
        unit = 1
        if self.density is None:  # no dpi found try jfif infos
            self.density = img.info.get("jfif_density", None)
            unit = img.info.get("jfif_unit", 0)  # 0 = no unit ; 1 = inch ; 2 = mm
        if self.density is not None:
            x_density, y_density = self.density
            if unit != 0 and x_density != 0 and y_density != 0:
                unit2mm = [0, 1 / 0.254, 0.1][unit]
                x_mm = round(x_size * unit2mm / x_density, 2)
                y_mm = round(y_size * unit2mm / y_density, 2)
                self.mm = (x_mm, y_mm)
            else:
                self.mm = None
        else:
            self.mm = None
        self.size = (x_size, y_size)
        self.aspect_ratio = round(float(x_size) / y_size, 2)

    def select(self):
        """
        Récupération des données pour un logo existant
        il doit exister un et un seul fichier image parmi de suffixe/types autorisés
        (sinon on prend le premier trouvé)
        cette opération permet d'affiner le format d'un logo de format inconnu
        """
        for suffix in scu.LOGOS_IMAGES_ALLOWED_TYPES:
            path = Path(self.basepath + "." + suffix)
            if path.exists():
                self._set_format(suffix)
                with open(self.filepath, "rb") as f:
                    img = PILImage.open(f)
                    self._read_info(img)
                return self
        return None

    def get_url(self):
        """Retourne l'URL permettant d'obtenir l'image du logo"""
        return url_for(
            "scodoc.get_logo",
            dept_id=self.scodoc_dept_id,
            name=self.logoname,
            global_if_not_found=False,
        )

    def get_url_small(self):
        """Retourne l'URL permettant d'obtenir l'image du logo sous forme de miniature"""
        return url_for(
            "scodoc.get_logo_small",
            dept_id=self.scodoc_dept_id,
            name=self.logoname,
            global_if_not_found=False,
        )

    def get_usage(self):
        if self.mm is None:
            return f'<logo name="{self.logoname}" width="?? mm" height="?? mm">'
        else:
            return f'<logo name="{self.logoname}" width="{self.mm[0]}mm">'

    def last_modified(self):
        path = Path(self.filepath)
        dt = path.stat().st_mtime
        return path.stat().st_mtime

    def rename(self, new_name):
        """Change le nom (pas le département)
        Les éléments non utiles ne sont pas recalculés (car rechargés lors des accès ultérieurs)
        """
        old_path = Path(self.filepath)
        self.logoname = secure_filename(new_name)
        if not self.logoname:
            self.logoname = "*** *** nom de logo invalide *** à changer ! *** ***"
        else:
            new_path = os.path.sep.join(
                [self.dirpath, self.prefix + self.logoname + "." + self.suffix]
            )
            old_path.rename(new_path)

    def html(self) -> str:
        "élément HTML img affichant ce logo"
        return f"""<img class="sco_logo" src="{self.get_url()}" alt="Logo {self.logoname}">"""


def find_logo(
    logoname: str,
    dept_id: int | None = None,
    strict: bool = False,
    prefix: str = scu.LOGO_FILE_PREFIX,
) -> Logo | None:
    """
    "Recherche un logo 'name' existant.
    Deux strategies:
    si strict:
        recherche uniquement dans le département puis si non trouvé au niveau global
    sinon
        On recherche en local au dept d'abord puis si pas trouvé recherche globale
    quelque soit la stratégie, retourne None si pas trouvé
    :param logoname:  le nom recherche
    :param dept_id:  l'id du département dans lequel se fait la recherche (None si global)
    :param strict: stratégie de recherche (strict = False => dept ou global)
    :param prefix: le prefix utilisé (parmi scu.LOGO_FILE_PREFIX / scu.BACKGROUND_FILE_PREFIX)
    :return: un objet Logo désignant le fichier image trouvé (ou None)
    """
    logo = Logo(logoname, dept_id, prefix).select()
    if logo is None and not strict:
        logo = Logo(logoname=logoname, dept_id=None, prefix=prefix).select()
    return logo


def delete_logo(name, dept_id=None):
    """Delete all files matching logo (dept_id, name) (including all allowed extensions)
    Args:
        name: The name of the logo
        dept_id: the dept_id (if local). Use None to destroy globals logos
    """
    logo = find_logo(logoname=name, dept_id=dept_id)
    while logo is not None:
        os.unlink(logo.select().filepath)
        logo = find_logo(logoname=name, dept_id=dept_id)


def write_logo(stream, name, dept_id=None):
    """Crée le fichier logo sur le serveur.
    Le suffixe du fichier (parmi LOGO_IMAGES_ALLOWED_TYPES) est déduit du contenu du stream
    """
    Logo(logoname=name, dept_id=dept_id).create(stream)


def rename_logo(old_name, new_name, dept_id):
    logo = find_logo(old_name, dept_id, True)
    logo.rename(new_name)


def list_logos():
    """Crée l'inventaire de tous les logos existants.
    L'inventaire se présente comme un dictionnaire de dictionnaire de Logo:
    [None][name] pour les logos globaux
    [dept_id][name] pour les logos propres à un département (attention id numérique du dept)
    Les départements sans logos sont absents du résultat
    """
    inventory = {None: _list_dept_logos()}  # logos globaux (header / footer)
    for dept in Departement.query.filter_by(visible=True).all():
        logos_dept = _list_dept_logos(dept_id=dept.id)
        if logos_dept:
            inventory[dept.id] = _list_dept_logos(dept.id)
    return inventory


def _list_dept_logos(dept_id=None, prefix=scu.LOGO_FILE_PREFIX):
    """Inventorie toutes les images existantes pour un niveau (GLOBAL ou un département).
     retourne un dictionnaire de Logo [logoname] -> Logo
    les noms des fichiers concernés doivent être de la forme: <rep>/<prefix><name>.<suffixe>
    <rep> : répertoire de recherche (déduit du dept_id)
    <prefix>: le prefix (LOGO_FILE_PREFIX pour les logos)
    <suffix>: un des suffixes autorisés
    :param dept_id:  l'id du departement concerné (si None -> global)
    :param prefix:  le préfixe utilisé
    :return: le résultat de la recherche ou None si aucune image trouvée
    """
    allowed_ext = "|".join(scu.LOGOS_IMAGES_ALLOWED_TYPES)
    # parse filename 'logo_<logoname>.<ext> . be carefull: logoname may include '.'
    filename_parser = re.compile(f"{prefix}(([^.]*.)+)({allowed_ext})")
    logos = {}
    path_dir = Path(scu.SCODOC_LOGOS_DIR)
    if dept_id:
        path_dir = Path(
            os.path.sep.join(
                [scu.SCODOC_LOGOS_DIR, scu.LOGOS_DIR_PREFIX + str(dept_id)]
            )
        )
    if path_dir.exists():
        for entry in path_dir.iterdir():
            if os.access(path_dir.joinpath(entry).absolute(), os.R_OK):
                result = filename_parser.match(entry.name)
                if result:
                    logoname = result.group(1)[
                        :-1
                    ]  # retreive logoname from filename (less final dot)
                    logos[logoname] = Logo(logoname=logoname, dept_id=dept_id).select()
    return logos if len(logos.keys()) > 0 else None


def guess_image_type(stream) -> str:
    "guess image type from header in stream"
    ext = puremagic.from_stream(stream)
    if not ext or not ext.startswith("."):
        return None
    fmt = ext[1:]  # remove leading .
    if fmt == "jfif":
        fmt = "jpg"
    return fmt if fmt != "jpeg" else "jpg"


def make_logo_local(logoname, dept_name):
    depts = Departement.query.filter_by(acronym=dept_name).all()
    if len(depts) == 0:
        log(f"no dept {dept_name} found. aborting")
        return
    if len(depts) > 1:
        log(f"several depts {dept_name} found. aborting")
        return
    dept = depts[0]
    log(f"Move logo {logoname}' from global to {dept.acronym}")
    old_path_wild = f"/opt/scodoc-data/config/logos/logo_{logoname}.*"
    new_dir = f"/opt/scodoc-data/config/logos/logos_{dept.id}"
    logos = glob.glob(old_path_wild)
    # checks that there is non local already present
    for logo in logos:
        filename = os.path.split(logo)[1]
        new_name = os.path.sep.join([new_dir, filename])
        if os.path.exists(new_name):
            log("local version of global logo already exists. aborting")
            return
    # create new__dir if necessary
    if not os.path.exists(new_dir):
        log(f"- create {new_dir} directory")
        os.mkdir(new_dir)
    # move global logo (all suffixes) to local dir note: pre existent file (logo_XXX.*) in local dir does not
    # prevent operation if there is no conflict with moved files
    # At this point everything is ok so we can do files manipulation
    for logo in logos:
        shutil.move(logo, new_dir)
    # log(f"moved {n_moves}/{n} etuds")