#! /usr/bin/python3

import labels
from reportlab.graphics import shapes
from reportlab.graphics.barcode import createBarcodeDrawing
from reportlab.lib.colors import toColor
from reportlab.graphics import renderSVG

import reportlab.rl_config

from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
reportlab.rl_config.warnOnMissingFontGlyphs = 0

import platform, hashlib

if platform.system() != "Windows":
    pdfmetrics.registerFont(TTFont('Times-Roman',
                                   '/usr/share/fonts/truetype/liberation/LiberationSerif-Regular.ttf'))
    pdfmetrics.registerFont(TTFont('Times-Bold',
                                   '/usr/share/fonts/truetype/liberation/LiberationSerif-Bold.ttf'))
    pdfmetrics.registerFont(TTFont('Times-Italic',
                                   '/usr/share/fonts/truetype/liberation/LiberationSerif-Italic.ttf'))
    pdfmetrics.registerFont(TTFont('Times-BoldItalic',
                                   '/usr/share/fonts/truetype/liberation/LiberationSerif-BoldItalic.ttf'))

    pdfmetrics.registerFont(TTFont('Helvetica',
                                   '/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf'))
    pdfmetrics.registerFont(TTFont('Helvetica-Bold',
                                   '/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf'))
    pdfmetrics.registerFont(TTFont('Helvetica-Italic',
                                   '/usr/share/fonts/truetype/liberation/LiberationSans-Italic.ttf'))
    pdfmetrics.registerFont(TTFont('Helvetica-BoldItalic',
                                   '/usr/share/fonts/truetype/liberation/LiberationSans-BoldItalic.ttf'))

PADDING = 1

def pt2mm(long):
    """
    Convertit une longueur donnée en points vers des millimètres
    """
    return 0.3528 * long

def mm2pt(long):
    """
    Convertit une longueur donnée en millimètres vers des points
    """
    return long / 0.3528

class QRimages():
    "pour enregistrer des QR-codes correspondant à des données"

    dico = {}
    tempfiles = []

    @staticmethod
    def qr(data, size = 140, x = 0, y = 0):
        """
        inscrit un QR-code dans une image, et ajoute ça dans
        QRimages.dico
        
        @param data une donnée à encoder
        @param size largeur et hauteur du QR-code (~50 mm par défaut)
        @param x abscisse du début (0mm par défaut)
        @param y ordonnée du début (0mm par défaut)
        """
        if data not in QRimages.dico:
            import qrcode
            import tempfile
            from qrcode.image.styledpil import StyledPilImage
            from qrcode.image.styles.moduledrawers.pil import RoundedModuleDrawer

            qr = qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_H)
            qr.add_data(data)
            img = qr.make_image(
                image_factory=StyledPilImage,
                module_drawer=RoundedModuleDrawer()
            )
            code = hashlib.md5(data.encode()).hexdigest()
            tmp = tempfile.NamedTemporaryFile(prefix="etiq-"+code, suffix=".png")
            img.save(tmp.name, format="PNG")
            QRimages.tempfiles.append(tmp) # so the tempfile lives longer than this call
            QRimages.dico[data] = shapes.Image(x, y, size, size, tmp.name)
        return QRimages.dico[data]
    
def transform2labelPos(transform, specs):
    """
    Déduit d'une matrice de transformation, la position d'une
    étiquette dans une page.
    @param transform l'attribut d'un élément SVG "g", contenant une étiquette
    @param specs une instance de labels.Specification
    @return un dictionnaire qui donne la ligne et la colonne, numérotées
       à partir de zéro
    """
    pattern = re.compile(r" *matrix\(.*,([.\d]+),([.\d]+)\)")
    m = pattern.match(transform)
    x_mm = pt2mm(float(m.group(1)))
    y_mm = pt2mm(float(m.group(2)))
    col = round((x_mm - float(specs.left_margin)) /
                float(specs.label_width + specs.column_gap))
    row = round((float(
        specs.sheet_height - specs.top_margin - specs.label_height) - y_mm) /
                float(specs.label_height + specs.row_gap))
    return {"row": row, "col": col}

class EtiquetteBase:
    """
    Structure de données commune à toutes les impressions sur des
    planches d'étiquettes, ou microperforées.
    """
    
    """
    @var specs une instance de labels.Specification, ajustée pour le format
    "Avery J8159", vendu aussi sous le nom "A12" autrement.
    Voir https://www.avery.fr/produit/etiquette-pour-timbres-j8159-10
    """
    specs = labels.Specification(
        210, 297,                              # format A4
        3, 8,                                  # 3 colonnes, 9 lignes
        63.5, 33.9,                            # largeur, hauteur de l'étiquette
        corner_radius=2,                       # arrondi
        left_margin=7, right_margin=7,         # marges gauche et droite
        top_margin=12.9,                         # marge en haut
        left_padding=PADDING,                  # espacement intérieur
        right_padding=6.5,                     # coquetterie de GemaSCO ?
        top_padding=PADDING,                   #
        bottom_padding=PADDING,                #
        row_gap=0                              # espace entre lignes
    )

    def __init__(self):
        pass
    
    def adjustXY(self, drawable, x, y):
        """
        Ajuste l'origine d'une instance dessinable de reportlab.
        L'ajustement ne se fait que si l'origine initiale est (0,0)
        @param drawable une instace dessinable de reportlab
        @param x abscisse du début en mm, ou "center"
        @param y ordonnée du début en mm, ou "center"
        """
        x1, y1, x2, y2 = drawable.getBounds()
        w = x2 - x1
        h = y2 - y1
        if x1 == 0 and y1 <= 0:
            x = (self.w - w)/2 if x=="center" else mm2pt(x)
            y = (self.h - h)/2 if y=="center" else mm2pt(y)
            if isinstance(drawable, shapes.Drawing):
                drawable.translate(x, y)
            elif isinstance(drawable, shapes.String):
                drawable.x = x
                drawable.y = y
            elif isinstance(drawable, shapes.Image):
                drawable.x = x
                drawable.y = y
        return

    def addParagraph(self,p, x, y, lineheight, maxLines,
                     fontSize = 10, join = ""):
        """
        Ajoute un paragraphe sur le pylabel
        @param p un paragraphe qui peut contenir des retours à la ligne "/n",
                 et qui sont censées être courtes (tenir sur la lageur de
                 l'étiquette pour chaque ligne)
        @param x abscisse où commence le paragraphe
        @para y ordonnée où commence le paragraphe
        @param lineheight hauteur d'un passage à la ligne
        @param maxLines nombre de lignes à ne pas dépasser
        @param fontSize taille de fonte en pt (10 par défaut)
        @param join (vide par défaut) ; s'il n'est pas vide, c'est une chaîne
                    courte qui peut servir à rassembler des morceaux du
                    paragraphe sur une même ligne tant qu'on ne dépasse pas.
        """
        width = self.specs.label_width - \
            (self.specs.left_padding + self.specs.right_padding)
        lignes = [li.strip() for li in p.split("\n") if li.strip()]
        index = 0
        n_ligne=0
        while index < len(lignes):
            printable = lignes[index]
            s = self.string(printable, fontSize=fontSize)
            x1 ,y1, x2, y2 = s.getBounds()
            if x + pt2mm(x2 - x1) >= width:
                self.adjustXY(s, x, y + n_ligne * lineheight)
                self.pylabel.add(s)
                n_ligne +=1
                if n_ligne > maxLines:
                    print("Error: too many lines")
                    return
                index +=1
                continue
            else:
                accu = printable
                while join and index < len(lignes)-1:
                    accu += join + lignes[index+1]
                    s = self.string(accu, fontSize=fontSize)
                    x1 ,y1, x2, y2 = s.getBounds()
                    if x + pt2mm(x2 - x1) >= width:
                        break
                    else:
                        printable = accu
                        index +=1
                # on a fini d'ajouter des morceaux, il est temps
                # d'écrire sur le label
                s = self.string(printable, fontSize=fontSize)
                self.adjustXY(s, x, y - n_ligne * lineheight)
                self.pylabel.add(s)
                n_ligne +=1
                index += 1
                if n_ligne > maxLines:
                    print("Error: too many lines")
                    return
        return

    def drawBarcode(self, width = 72, height = 6, x = 'center', y = 10):
        """
        Inscrit un code barre dans un pylabel ;
        @param width largeur du code-barre, en mm (47 mm par défaut)
        @param height hauteur du code-barre, en mm (6 mm par défaut)
        @param x abscisse du début ("center" par défaut)
        @param y ordonnée du début (10 mm par défaut)
        """
        bc = createBarcodeDrawing(
            'Code128', value="{code:9d}".format(**self.__dict__),
            width = mm2pt(width), height = mm2pt(height))
        dx = -12 # ce décalage a été réglé expérientalement, au pif
        bc.translate(dx, mm2pt(10))
        self.pylabel.add(bc)
        return

    def string(self, s, fontName="Helvetica", fontSize=6):
        """
        Crée une instance de shapes.String avec x,y = 0,0
        @param s la chaîne à afficher
        @param fontname la police de caractères, Helvetica par défaut
        @param la taille de la police, 6 pt par défaut
        """
        return shapes.String(0, 0, s, fontName=fontName, fontSize=fontSize)

    def image(self, path, width, height):
        """
        Crée une instance de shapes.Image avec x,y = 0,0
        @param path chemin de l'image
        @param width largeur
        @param height hauteur
        """
        if path[0] != "/":
            path = os.path.join(BASE_DIR, path)
        return shapes.Image(0, 0, width, height, path)

    @staticmethod
    def draw_etiquette(label, w, h, instance):
        """
        Fonction de rappel qui crée effectivement le dessin d'une étiquette
        @param label une instance de reportlab.graphics.shapes.Drawing qui
           est gérée automatiquement lors du rappel
        @param w la largeur disponible dans l'objet généré automatiquement
        @param h la hauteur disponible dans l'objet généré automatiquement
        @param instance une instance d'Etiquette
        """
        instance.make_label(label, w, h)
        return

class PlanchePostits(EtiquetteBase):
    """
    Structure de données pour imprimer 6 post-its placés sur une feuille A4
    Les paramètres du constructeur sont :
    @param url l'url qui doit former le QR-code
    """
    specs = labels.Specification(
        210, 297,                              # format A4
        2, 3,                                  # 2 colonnes, 3 lignes
        75, 75,                                # largeur, hauteur de l'étiquette
        corner_radius=2,                       # arrondi
        left_margin=15, right_margin=15,       # marges gauche et droite
        top_margin=16,                         # marge en haut
        left_padding=10,                       # espacement intérieur
        right_padding=10,                      #
        top_padding=10,                        #
        bottom_padding=10,                     #
        row_gap=15,                            # espace entre lignes
    )
    
    def __init__(self, url):
        self.url = url
        return

    def drawQR(self):
        s = self.string("JE PARTICIPE", fontSize=20)
        self.adjustXY(s, 0, 50)
        self.pylabel.add(s)
        self.pylabel.add(QRimages.qr(self.url))
        s = self.string(self.url, fontSize=4.7)
        self.pylabel.add(s)
        return

    def make_label(self, label, w, h):
        """
        Enchaîne toutes les étapes de création de l'étiquette, pour
        la bibliothèque pylabels.
        """
        self.pylabel, self.w, self.h = label, w, h
        self.drawQR()
        return


def makePDF(data, tabou):
    """
    Produit un fichier PDF de planches d'étiquettes
    @param data une liste d'instances d'EtiquetteJeanBart
    @param tabou une liste de positions où on ne doit pas imprimer
       d'étiquette ; chaque position est donnée par un dictionnaire
       "row" : entier, "col": entier (numéros débutant à un)
    @return un pseudo-fichier, ouvert en lecteur, initialisé à son début
    """
    sheet = labels.Sheet(
        PlanchePostits.specs, PlanchePostits.draw_etiquette, border=False)
    sheet.partial_page(1,[(int(pos["row"]), int(pos["col"])) for pos in tabou])
    for etiquette in data:
        sheet.add_label(etiquette)
    buffer = io.BytesIO()
    sheet.save(buffer)
    buffer.seek(0)
    return buffer

def demo():
    # démonstration !
    # création des feuilles à imprimer
    sheet = labels.Sheet(
        PlanchePostits.specs, PlanchePostits.draw_etiquette, border=True)

    # Ajout de quelques étiquettes
    for n in range(6):
        e = PlanchePostits(
            "https://moodle.freeduc.science/static/"
            "connexion-moodle-freeduc.html")
        sheet.add_label(e)


    # Enregistrement du fichier et message
    sheet.save('essai.pdf')
    print("{0:d} étiquette(s) imprimées, sur {1:d} page(s).".format(
        sheet.label_count, sheet.page_count))
    return
    

if __name__ == "__main__":
    demo()
