#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
.. codeauthor:: Daniel STAN <dstan@crans.org>
.. codeauthor:: Antoine Durand-Gasselin <adg@crans.org>

Pour envoyer des jobs d'impression à l'imprimante canon via son interface web.

License: GPLv3
"""
# impression_hp.py
#

import sys, os
import logging
import logging.handlers
import BeautifulSoup
import requests
import math

if '/usr/scripts' not in sys.path:
    sys.path.append('/usr/scripts')

from base import FichierInvalide, PrintError, SettingsError

from gestion.config import impression as config_impression
from subprocess import Popen, PIPE, check_output
import livret
import lc_ldap.shortcuts
import lc_ldap.crans_utils as crans_utils

## Pour les codes qui se connectent direct à l'imprimante en http
BASE_URL = 'https://imprimante.adm.crans.org'
URL_JOBLIST = BASE_URL + '/hp/device/JobLogReport/Index'
URL_STATE = BASE_URL + '/'
CA = '/etc/ssl/certs/cacert.org.pem'

##
logger = logging.getLogger('impression_hp')
logger.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(name)s: [%(levelname)s] %(message)s')
handler = logging.handlers.SysLogHandler(address = '/dev/log')
try:
    handler.addFormatter(formatter)
except AttributeError:
    handler.formatter = formatter
logger.addHandler(handler)

DEBUG = bool(os.getenv('DBG_PRINTER', False))

class Option(object):
    # Valeur par défaut
    value = None

    def __str__(self):
        return str(self.value)

    name = "option"
    pretty_name = u"option quelconque"

    def parse(self, v):
        self.value = v

class BooleanOption(Option):
    value = False
    def parse(self, v):
        self.value = bool(v)
        if str(v).lower() == 'false':
            self.value = False

class Couleur(BooleanOption):
    name = "couleur"
    pretty_name = u"impression couleur"

class Livret(BooleanOption):
    name = "livret"
    pretty_name = u"livret piqûres à cheval"

    def nb(self):
        return 2*int(self.value)

class Duplex(BooleanOption):
    name = "recto_verso"
    pretty_name = u"Impression recto verso"

class Copies(Option):
    name = "copies"
    pretty_name = u"nombre d'exemplaires"

    value = 1

    def parse(self, v):
        self.value = int(v)

class SelectOption(Option):
    possibilities={
        None: u'Aucun',
    }

    value = None

    def parse(self, v):
        if v == 'None':
            v = None
        if v not in self.possibilities:
            return
        self.value = v

class Agrafage(SelectOption):
    name = "agrafage"
    pretty_name = u"agrafage"

    possibilities = {
      None : "aucune agrafe",
      "TopLeft" : u"agrafe en haut à gauche",
      "TopRight" : u"agrafe en haut à droite",
      #"BottomLeft" : u"agrafe en bas à gauche", # n'existe pas
      #"BottomRight" : u"agrafe en bas à droite", # n' existe pas
      "Left": u"deux agrafes sur le bord gauche",
      "Right" : u"deux agrafes sur le bord droit",
      #"Top" : u"deux agrafes sur le bord supérieur", #pas faisable
      #"Bottom" : u"deux agrafes sur le bord inférieur", #n'existe pas
    }

    def nb(self):
        if not self.value:
            return 0
        if 'Left' in self.value or 'Right' in self.value:
            return 2 - int('Top' in self.value or 'Bottom' in self.value)
        return 2

    def HP_name(self):
        if 'Top' in self.value:
            return '1Staple%sAngled' % (self.value.replace('Top', ''))
        else:
            return '2Staples%s' % self.value

class Papier(SelectOption):
    name = "papier"
    pretty_name = u"format papier"

    value = 'A4'

    possibilities = {
      'A4' : "Papier A4 ordinaire",
      'A3' : "Papier A3 ordinaire",
    }


class Perforation(SelectOption):
    name = "perforation"
    pretty_name = u"perforation"
    possibilities = {
      None: u"Aucune",
      "2HolePunchLeft": u"2HolePunchLeft",
      "2HolePunchRight": u"2HolePunchRight",
      "2HolePunchTop": u"2HolePunchTop",
      "2HolePunchBottom": u"2HolePunchBottom",
      "3HolePunchLeft": u"3HolePunchLeft",
      "3HolePunchRight": u"3HolePunchRight",
      "3HolePunchTop": u"3HolePunchTop",
      "4HolePunchLeft": u"4HolePunchLeft",
      "4HolePunchRight": u"4HolePunchRight",
      "4HolePunchTop": u"4HolePunchTop",
    }



# ######################################################## #
#                    CLASSE IMPRESSION                     #
# ######################################################## #
#
#
class impression(object):
    """impression d'un fichier avec l'imprimante HP MFP M880

    Un objet impression correspond à un fichier pdf et un adhérent.
    """
    # fichier (chemin)
    _fichier = ""
    # adhérent (instance)
    _adh = None
    # paramètres
    _settings_list = [
        Agrafage,
        Papier,
        Couleur,
        Duplex,
        Livret,
        Copies,
        Perforation,
    ]

    _settings = {}

    # le prix de l'impression
    _prix = 0.0
    _pages = 0
    # le cout de base encre pour une impression en couleurs/n&b
    #    (prix pour papier A4)
    _base_prix_nb = 0.0
    _base_prix_couleurs = 0.0

    # Format du pdf, tout droit sorti de pdfinfo
    # (les dimensions sont donc en pts)
    _format = '(A4)'
    _width = 595.28
    _height = 841.89

    # Jid unique, à définir avant l'impression
    _jid = 0

    def __init__(self, path_to_pdf, adh = None):
        """impression(path_to_pdf [, adh])

        Crée un nouvel objet impression à partir du fichier pdf pointé
        par path_to_pdf. Si adh ext donné, il peut être soit une
        instance d'un objet adhérent de crans_ldap soit le login de
        l'adhérent. Lève l'exception FichierInvalide si le fichier
        n'existe pas ou si ce n'est pas un pdf.
        """

        # On génère la liste des options
        self._settings = {cls.name: cls() for cls in self._settings_list}

        self._fichier = path_to_pdf
        self._adh = adh

        # on verifie que le fichier existe
        if not os.path.isfile(path_to_pdf):
            raise FichierInvalide, ("Fichier introuvable", path_to_pdf)
        if not open(path_to_pdf).read().startswith("%PDF"):
            raise FichierInvalide, ("Le fichier ne semble pas etre un PDF", path_to_pdf)

        # on compte les pages et on regarde le format
        proc = Popen(["pdfinfo",self._fichier],stdout=PIPE,stderr=PIPE)
        pdfinfo = proc.communicate()
        if proc.returncode:
            raise FichierInvalide(u"pdfinfo n'arrive pas a lire le fichier (il est peut-etre corrompu ou protege par un mot de passe), https://wiki.crans.org/VieCrans/ImpressionReseau#Format_des_fichiers",path_to_pdf)
        self._pages = -1
        for line in pdfinfo[0].split('\n'):
            if line.startswith('Pages'):
                self._pages = int(line.split()[1])
            elif line.startswith('Page size'):
                size = line.split()
                if len(size) <= 6:
                    self._format = "Unknown"
                else:
                    self._format = size[6]

                self._width = float(size[2])
                self._height = float(size[4])
                # Hack pour mieux reconnaître les formats
                w = min(self._width,self._height)
                h = max(self._width,self._height)
                err = 100
                if abs(w - 595) < err and abs(h - 842) < err:
                    self._format = "(A4)"
                elif abs(w - 842) < err and abs(h - 1180) < err:
                    self._format = "(A3)"
        if self._pages <= 0:
            raise FichierInvalide(u"Impossible de lire le nombre de pages",path_to_pdf)

        if not self._format in ['(A4)','(A3)']:
            pass
            #raise FichierInvalide, u"Seuls les formats A3 et A4 sont supportes"
        # calcule le prix de l'encre tout de suite
        self._calcule_prix()

    def _uniq_jid(self):
        """ Alloue un jid unique """
        fname = '/var/impression/jid'
        ## Maybe need a lock ?
        try:
            f = file(fname,'r+')
            cur = int(f.read())+1
            f.seek(0)
        except (IOError,ValueError):
            cur = 0
            f = file(fname,'w')
        f.write(str(cur))
        f.close()
        return cur

    def _pdfbook(self):
        """Génère un pdf livret équivalent et renvoie le chemin"""
        newfile = '/tmp/' + self.fileName()[-4] + '-book.pdf'
        newfile = '/tmp/book.pdf'
        check_output(
            ['/usr/bin/pdfjam', self._fichier,
                livret.pdfjam_order(self._pages),
                '-o', newfile,
            ])
        return newfile

    def changeSettings(self, **kw):
        """changeSettings([keyword=value])

        Change les parametres de l'impression, recalcule et renvoie le nouveau prix.
        """
        for name in kw:
            if name not in self._settings:
                raise Exception('unknown %s option' % name)
            self._settings[name].parse(kw[name])

        if self._settings['livret'].value:
            self._settings['recto_verso'].parse(True)
            self._settings['agrafage'].parse(None)

        # TODO
        # Check si l'agrafage et cie sont compatibles avec le nb de pages

        return self._calcule_prix()

    def printSettings(self):
        """printSettings()

        Affiche les paramètres courants sur la sortie standard
        """
        for k in self._settings:
            opt = self._settings[k]
            label = u"%s: %s" % (opt.pretty_name.capitalize(), opt.value)
            print label.encode('UTF-8')

    def prix(self):
        """prix()

        Renvoie le prix courrant de l'impression
        """
        return self._prix

    def fileName(self):
        """fileName()

        renvoie le nom du fichier pdf (exemple : monPdf.pdf)
        """
        return os.path.basename(self._fichier)

    def filePath(self):
        """filePath()

        renvoie le chemin d'accès au fichier pdf.
        """
        return self._fichier

    def pages(self):
        """pages()

        renvoie le nombre de pages du document (page au sens nombre de
        faces à imprimer et non le nombre de feuilles)
        """
        return self._pages

    def imprime(self):
        """imprime()

        imprime le document pdf. débite l'adhérent si adhérent il y a.
        (si il a été indiqué à l'initialisation de l'objet)
        """
        self._jid = self._uniq_jid()

        # debite l'adhérent si adherent il y a
        if self._adh != None:
            adh = self._get_adh_write()
            self._calcule_prix() # Normalement inutile, mais évite les races
            with adh:
                adh.solde(-self._prix, u"impression(%d): %s par %s" % (self._jid,self._fichier, self._adh))
                adh.save()
            del adh
        # imprime le document
        self._exec_imprime()

    def get_jid(self):
        """ Retourne le jid, à éxecuter après imprime()"""
        return self._jid

    _doc = None
    def low_tray(self, num):
        """Indique si un bac est vide ou presque vide"""
        if self._doc is None:
            req = requests.get(URL_STATE, verify=CA)
            self._doc = BeautifulSoup.BeautifulSoup(req.text)
        try:
            tray = self._doc.findAll(attrs={'id': 'TrayBinStatus_%d' % num})[0]
        except IndexError:
            return False
        state = tray.attrMap['class']
        if 'morethan40' in state or 'between20and40':
            return False
        if 'empty' in state:
            return True
        logger.info('Unknown Tray State: %s' % state)
        return False

    def _calcule_prix(self):

        faces = self._pages

        if self._settings['livret'].value:
            feuilles = int((faces+3)/4)
            faces = 2 * feuilles
        elif self._settings['recto_verso'].value:
            feuilles = int(faces/2.+0.5)
        else:
            feuilles = faces

        if (self._settings['papier'].value == "A3"):
            c_papier = config_impression.c_a3
            pages = 2*faces
        else:
            pages = faces
            c_papier = config_impression.c_a4

        if self._settings['couleur'].value:
            c_impression = c_papier * feuilles + config_impression.c_face_couleur * pages
        else:
            c_impression = c_papier * feuilles + config_impression.c_face_nb * pages

        # Cout des agrafes
        nb_agrafes = self._settings['agrafage'].nb() + self._settings['livret'].nb()

        c_agrafes = nb_agrafes * config_impression.c_agrafe

        c_total = math.floor(self._settings['copies'].value * ( c_impression +
                      c_agrafes)) # arrondi et facture

        self._prix = float(c_total)/100
        return self._prix

    @lc_ldap.shortcuts.with_ldap_conn(retries=2, delay=5,
        constructor=lc_ldap.shortcuts.lc_ldap_admin)
    def _get_adh_write(self, ldap):
        """Récupère l'adhérent en lecture-écriture"""
        uid = self._adh
        if type(uid) in [str, unicode]:
            uid = uid.split('@')[-1]
            adh = ldap.search(u'uid=%s' % crans_utils.escape(uid), mode='rw')[0]
        return adh

    ## ################################# ##
    ## fonction qui imprime pour de vrai ##
    ## ################################# ##
    ##
    def _exec_imprime(self):
        """ Envoie l'impression a l'imprimante avec les parametres actuels """

        if (self._adh != None):
            logger.info('Impression(%d) [%s] : %s' % (self._jid, self._adh, self._fichier))
        else:
            logger.info("Impression(%d) : %s" % (self._jid, self._fichier))

        # Création  de la liste d'options pour lp
        options = list()


        # Pour spécifier l'imprimante
        options += ['-d', 'MFPM880']

        # Pour spécifier un jobname de la forme jid:adh:nom_du_fichier
        jobname = '%d:%s:%s' % (self._jid, self._adh, self._fichier.split('/')[-1].replace("\"","\\\""))
        options += ['-t', jobname]

        #Pour le nombre de copies et specifie non assemblee
        options += ['-n', str(self._settings['copies'].value)]
        #Et on ne les veux pas assemblées (sic)
        options += ['-o', 'Collate=True']

        if self._settings['papier'].value == 'A4':
            options += ['-o', 'PageSize=A4']
        else:
            options += ['-o', 'pdf-expand',
                        '-o', 'pdf-paper=841x1190',
                        '-o', 'PageSize=A3']

        # Toujours resize (pour éviter que l'imprimante ne deadlock)
        options += ['-o', 'fit-to-page']

        # Quelle alimentation papier utiliser ?
        options.append('-o')
        if self._settings['papier'].value == 'A3':
            options.append('InputSlot=Tray1')
        elif self._settings['livret'].value:
            options.append('InputSlot=Tray2')
        else: # A4, dans Tray3 et Tray4
            ## On utilise en priorité le Tray 4 (plus gros)
            if not self.low_tray(4) or self.low_tray(3):
                options.append('InputSlot=Tray4')
            else:
                options.append('InputSlot=Tray3')


        if not self._settings['couleur'].value:
            options += ['-o', 'HPColorAsGray=True']
            options += ['-o', 'ColorModel=Grayscale']

        duplex = self._settings['recto_verso'].value or\
                 self._settings['livret'].value
        paysage = self._width > self._height
        # mode paysage: on indique que c'est landscape (ou livret)
        if paysage:
            options += ['-o', 'landscape']

        # Livret: 2 pages par côté, et on inverse le bord duplex
        if self._settings['livret'].value:
            options += ['-o', 'number-up=2']
            paysage = not paysage

        if duplex:
            if paysage:
                options += ['-o', 'sides=two-sided-short-edge']
            else:
                options += ['-o', 'sides=two-sided-long-edge']

        if not duplex:
            options += ['-o', 'sides=one-sided']

        if self._settings['livret'].value:
            options += ['-o', 'HPStaplerOptions=FoldStitch']
        elif self._settings['agrafage'].value:
            v = self._settings['agrafage'].HP_name()
            options += ['-o', 'HPStaplerOptions=%s' % v]

        if not self._settings['livret'].value:
            if self._settings['perforation'].value:
                v = self._settings['perforation']
                options += ['-o', 'HPPunchingOptions=%s' % v]

        # Que se passe-t-il si le nom de fichier commence par - ?
        # pour éviter cela, on indique la fin des options, avec "--"
        options.append('--')
        if self._settings['livret'].value:
            options.append(self._pdfbook())
        else:
            options.append(self._fichier)

        if not DEBUG:
            logger.info("lp " + " ".join(options))
            check_output(['lp'] + options)
        else:
            logger.info("pretend printing (debug): " + " ".join(options))

