#!/usr/bin/env python3

import io
import os
import re
import smtplib
import ssl
import collections

from xml.sax.saxutils import escape

from collections import OrderedDict
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email.mime.text import MIMEText
from email import encoders
from email.utils import formatdate

import requests
try:
    from escpos import printer
except ImportError:
    printer = None

from reportlab.platypus import BaseDocTemplate, Frame, PageTemplate, Image
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import cm
from reportlab.lib.pagesizes import letter
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
from reportlab.platypus import Paragraph, Table, TableStyle, Spacer
from reportlab.pdfgen import canvas


CANCEL = False


#~ https://github.com/kennethreitz/requests/blob/v1.2.3/requests/structures.py#L37
class CaseInsensitiveDict(collections.MutableMapping):
    """A case-insensitive ``dict``-like object.
    Implements all methods and operations of
    ``collections.MutableMapping`` as well as dict's ``copy``. Also
    provides ``lower_items``.
    All keys are expected to be strings. The structure remembers the
    case of the last key to be set, and ``iter(instance)``,
    ``keys()``, ``items()``, ``iterkeys()``, and ``iteritems()``
    will contain case-sensitive keys. However, querying and contains
    testing is case insensitive::
        cid = CaseInsensitiveDict()
        cid['Accept'] = 'application/json'
        cid['aCCEPT'] == 'application/json'  # True
        list(cid) == ['Accept']  # True
    For example, ``headers['content-encoding']`` will return the
    value of a ``'Content-Encoding'`` response header, regardless
    of how the header name was originally stored.
    If the constructor, ``.update``, or equality comparison
    operations are given keys that have equal ``.lower()``s, the
    behavior is undefined.
    """

    def __init__(self, data=None, **kwargs):
        self._store = OrderedDict()
        if data is None:
            data = {}
        self.update(data, **kwargs)

    def __setitem__(self, key, value):
        # Use the lowercased key for lookups, but store the actual
        # key alongside the value.
        self._store[key.lower()] = (key, value)

    def __getitem__(self, key):
        return self._store[key.lower()][1]

    def __delitem__(self, key):
        del self._store[key.lower()]

    def __iter__(self):
        return (casedkey for casedkey, mappedvalue in self._store.values())

    def __len__(self):
        return len(self._store)

    def lower_items(self):
        """Like iteritems(), but with all lowercase keys."""
        return (
            (lowerkey, keyval[1])
            for (lowerkey, keyval)
            in self._store.items()
        )

    def __eq__(self, other):
        if isinstance(other, collections.Mapping):
            other = CaseInsensitiveDict(other)
        else:
            return NotImplemented
        # Compare insensitively
        return dict(self.lower_items()) == dict(other.lower_items())

    # Copy is required
    def copy(self):
        return CaseInsensitiveDict(self._store.values())

    def __repr__(self):
        return str(dict(self.items()))


class NumLet(object):

    def __init__(self, value, moneda, **args):
        self._letras = self._letters(value, moneda)

    @property
    def letras(self):
        return self._letras.upper()

    def _letters(self, numero, currency='MXN'):
        # ~ print (currency)
        monedas = {
            'MXN': 'peso',
            'USD': 'dólar',
            'EUR': 'euro',
        }
        moneda = monedas.get(currency, currency)
        tf = {
            'MXN': 'm.n.',
        }

        texto_inicial = '-('
        texto_final = '/100 {})-'.format(tf.get(currency, currency))
        fraccion_letras = False
        fraccion = ''

        enletras = texto_inicial
        numero = abs(numero)
        numtmp = '%015d' % numero

        if numero < 1:
            enletras += 'cero ' + self._plural(moneda) + ' '
        else:
            enletras += self._numlet(numero)
            if numero == 1 or numero < 2:
                enletras += moneda + ' '
            elif int(''.join(numtmp[3:])) == 0 or int(''.join(numtmp[9:])) == 0:
                enletras += 'de ' + self._plural(moneda) + ' '
            else:
                enletras += self._plural(moneda) + ' '

        decimal = '%0.2f' % numero
        decimal = decimal.split('.')[1]
        #~ decimal = int((numero-int(numero))*100)
        if fraccion_letras:
            if decimal == 0:
                enletras += 'con cero ' + self._plural(fraccion)
            elif decimal == 1:
                enletras += 'con un ' + fraccion
            else:
                enletras += 'con ' + self._numlet(int(decimal)) + self.plural(fraccion)
        else:
            enletras += decimal

        enletras += texto_final
        return enletras

    def _numlet(self, numero):
        numtmp = '%015d' % numero
        co1=0
        letras = ''
        leyenda = ''
        for co1 in range(0,5):
            inicio = co1*3
            cen = int(numtmp[inicio:inicio+1][0])
            dec = int(numtmp[inicio+1:inicio+2][0])
            uni = int(numtmp[inicio+2:inicio+3][0])
            letra3 = self.centena(uni, dec, cen)
            letra2 = self.decena(uni, dec)
            letra1 = self.unidad(uni, dec)

            if co1 == 0:
                if (cen+dec+uni) == 1:
                    leyenda = 'billon '
                elif (cen+dec+uni) > 1:
                    leyenda = 'billones '
            elif co1 == 1:
                if (cen+dec+uni) >= 1 and int(''.join(numtmp[6:9])) == 0:
                    leyenda = "mil millones "
                elif (cen+dec+uni) >= 1:
                    leyenda = "mil "
            elif co1 == 2:
                if (cen+dec) == 0 and uni == 1:
                    leyenda = 'millon '
                elif cen > 0 or dec > 0 or uni > 1:
                    leyenda = 'millones '
            elif co1 == 3:
                if (cen+dec+uni) >= 1:
                    leyenda = 'mil '
            elif co1 == 4:
                if (cen+dec+uni) >= 1:
                    leyenda = ''

            letras += letra3 + letra2 + letra1 + leyenda
            letra1 = ''
            letra2 = ''
            letra3 = ''
            leyenda = ''
        return letras

    def centena(self, uni, dec, cen):
        letras = ''
        numeros = ["","","doscientos ","trescientos ","cuatrocientos ","quinientos ","seiscientos ","setecientos ","ochocientos ","novecientos "]
        if cen == 1:
            if (dec+uni) == 0:
                letras = 'cien '
            else:
                letras = 'ciento '
        elif cen >= 2 and cen <= 9:
            letras = numeros[cen]
        return letras

    def decena(self, uni, dec):
        letras = ''
        numeros = ["diez ","once ","doce ","trece ","catorce ","quince ","dieci","dieci","dieci","dieci"]
        decenas = ["","","","treinta ","cuarenta ","cincuenta ","sesenta ","setenta ","ochenta ","noventa "]
        if dec == 1:
            letras = numeros[uni]
        elif dec == 2:
            if uni == 0:
                letras = 'veinte '
            elif uni > 0:
                letras = 'veinti'
        elif dec >= 3 and dec <= 9:
            letras = decenas[dec]
        if uni > 0 and dec > 2:
            letras = letras+'y '
        return letras

    def unidad(self, uni, dec):
        letras = ''
        numeros = ["","un ","dos ","tres ","cuatro ","cinco ","seis ","siete ","ocho ","nueve "]
        if dec != 1:
            if uni > 0 and uni <= 5:
                letras = numeros[uni]
        if uni >= 6 and uni <= 9:
            letras = numeros[uni]
        return letras

    def _plural(self, palabra):
        if re.search('[aeiou]$', palabra):
            return re.sub('$', 's', palabra)
        else:
            return palabra + 'es'


class SendMail(object):

    def __init__(self, config):
        self._config = config
        self._server = None
        self._error = ''
        self._is_connect = self._login()

    @property
    def is_connect(self):
        return self._is_connect

    @property
    def error(self):
        return self._error

    def _login(self):
        try:
            if self._config['ssl'] and self._config['starttls']:
                self._server = smtplib.SMTP(
                    self._config['servidor'],
                    self._config['puerto'], timeout=10)
                self._server.ehlo()
                self._server.starttls()
                self._server.ehlo()
            elif self._config['ssl']:
                self._server = smtplib.SMTP_SSL(
                    self._config['servidor'],
                    self._config['puerto'], timeout=10)
                self._server.ehlo()
            else:
                self._server = smtplib.SMTP(
                    self._config['servidor'],
                    self._config['puerto'], timeout=10)
            self._server.login(self._config['usuario'], self._config['contra'])
            return True
        except smtplib.SMTPAuthenticationError as e:
            if '535' in str(e):
                self._error = 'Nombre de usuario o contraseña inválidos'
                return False
            # ~ print (e)
            if '534' in str(e) and 'gmail' in self._config['servidor']:
                self._error = 'Necesitas activar el acceso a otras ' \
                    'aplicaciones en tu cuenta de GMail'
                return False
        except smtplib.SMTPException as e:
            self._error = str(e)
            return False
        except Exception as e:
            self._error = str(e)
            return False
        return

    def send(self, options):
        try:
            message = MIMEMultipart()
            message['From'] = self._config['usuario']
            message['To'] = options['para']
            message['CC'] = options['copia']
            message['Subject'] = options['asunto']
            message['Date'] = formatdate(localtime=True)
            if options['confirmar']:
                message['Disposition-Notification-To'] = message['From']
            message.attach(MIMEText(options['mensaje'], 'html'))
            for f in options['files']:
                part = MIMEBase('application', 'octet-stream')
                if isinstance(f[0], str):
                    part.set_payload(f[0].encode('utf-8'))
                else:
                    part.set_payload(f[0])
                encoders.encode_base64(part)
                part.add_header(
                    'Content-Disposition',
                    "attachment; filename={}".format(f[1]))
                message.attach(part)

            receivers = options['para'].split(',') + options['copia'].split(',')
            self._server.sendmail(
                self._config['usuario'], receivers, message.as_string())
            return ''
        except Exception as e:
            return str(e)

    def close(self):
        try:
            self._server.quit()
        except:
            pass
        return


class NumberedCanvas(canvas.Canvas):
    X = 20.59 * cm
    XC = 21.6 * cm / 2 + 1.5 * cm
    Y = 1.5 * cm

    def __init__(self, *args, **kwargs):
        canvas.Canvas.__init__(self, *args, **kwargs)
        self._saved_page_states = []

    def showPage(self):
        self._saved_page_states.append(dict(self.__dict__))
        self._startPage()
        return

    def save(self):
        """add page info to each page (page x of y)"""
        page_count = len(self._saved_page_states)
        for state in self._saved_page_states:
            self.__dict__.update(state)
            self.draw_page_number(page_count)
            self.draw_cancel()
            canvas.Canvas.showPage(self)
        canvas.Canvas.save(self)
        return

    def draw_page_number(self, page_count):
        self.setFont('Helvetica', 8)
        # ~ self.setFillColor(colors.darkred)
        text = f'Página {self._pageNumber} de {page_count}'
        self.drawRightString(self.X, self.Y, text)

        text = 'Este documento es una representación impresa de un CFDI'
        self.drawCentredString(self.XC, self.Y, text)

        text = 'Factura elaborada con software libre'
        self.drawString(1.5 * cm, 1.5 * cm, text)
        return

    def draw_cancel(self):
        global CANCEL
        if CANCEL:
            self.rotate(45)
            self.setFont('Helvetica', 100)
            self.setFillColor(colors.red)
            text = 'Cancelada'
            self.drawCentredString(20 * cm, 3 * cm, text)
        return

class TemplateInvoice(BaseDocTemplate):

    def __init__(self, *args, **kwargs):
        # letter 21.6 x 27.9
        kwargs['pagesize'] = letter
        kwargs['rightMargin'] = 1 * cm
        kwargs['leftMargin'] = 1 * cm
        kwargs['topMargin'] = 1.5 * cm
        kwargs['bottomMargin'] = 1.5 * cm
        BaseDocTemplate.__init__(self, *args, **kwargs)
        self._data = {}
        self._rows = []

    def _set_rect(self, style):
        color = style.pop('color', 'black')
        if isinstance(color, str):
            self.canv.setFillColor(getattr(colors, color))
        else:
            self.canv.setFillColorRGB(*color)

        keys = ('x', 'y', 'width', 'height', 'radius')
        for k in keys:
            style[k] = style[k] * cm

        self.canv.roundRect(**style)
        return

    def _set_text(self, styles, value):
        text = styles.pop('valor', '')
        if not value:
            value = text

        rect = styles['rectangulo']
        if value:
            self.canv.setFillColor(colors.white)
            self.canv.setStrokeColor(colors.white)
            ps = ParagraphStyle(**styles['estilo'])
            p = Paragraph(value, ps)
            p.wrap(rect['width'] * cm, rect['height'] * cm)
            p.drawOn(self.canv, rect['x'] * cm, rect['y'] * cm)

        self._set_rect(rect)
        return

    def _emisor(self, styles, data):
        logo_path = data.pop('logo', '')
        logo_style = styles.pop('logo', {})
        logo_path2 = data.pop('logo2', '')
        logo_style2 = styles.pop('logo2', {})
        sucursales = styles.pop('sucursales', {})

        for k, v in styles.items():
            self._set_text(styles[k], data.get(k, ''))

        if logo_path and logo_style:
            rect = logo_style['rectangulo']
            keys = ('x', 'y', 'width', 'height')
            for k in keys:
                rect[k] = rect[k] * cm
            self.canv.drawImage(logo_path, **rect)

        if logo_path2 and logo_style2:
            rect = logo_style2['rectangulo']
            keys = ('x', 'y', 'width', 'height')
            for k in keys:
                rect[k] = rect[k] * cm
            self.canv.drawImage(logo_path2, **rect)

        if sucursales:
            for k, sucursal in sucursales.items():
                values = sucursal.pop('textos')
                x = sucursal['rectangulo']['x']
                y = sucursal['rectangulo']['y']
                w = sucursal['rectangulo']['width']
                h = sucursal['rectangulo']['height']
                for v in values:
                    self._set_text(sucursal, v)
                    y -= h
                    sucursal['rectangulo']['x'] = x
                    sucursal['rectangulo']['y'] = y
                    sucursal['rectangulo']['width'] = w
                    sucursal['rectangulo']['height'] = h
        return

    def _receptor(self, styles, data):
        title = styles.pop('titulo', {})

        for k, v in styles.items():
            self._set_text(styles[k], data.get(k, ''))

        if title:
            rect = title['rectangulo']
            self.canv.saveState()
            self.canv.rotate(90)
            value = title.pop('valor', '')
            title['rectangulo']['x'], title['rectangulo']['y'] = \
                title['rectangulo']['y'], title['rectangulo']['x'] * -1
            self._set_text(title, value)
            self.canv.restoreState()
        return

    def _comprobante1(self, styles, data):
        title = styles.pop('titulo', {})
        self.canv.setTitle(f"Factura {data.get('seriefolio', '')}")

        for k, v in styles.items():
            self._set_text(styles[k], data.get(k, ''))

        if title:
            rect = title['rectangulo']
            self.canv.saveState()
            self.canv.rotate(90)
            value = title.pop('valor', '')
            title['rectangulo']['x'], title['rectangulo']['y'] = \
                title['rectangulo']['y'], title['rectangulo']['x'] * -1
            self._set_text(title, value)
            self.canv.restoreState()
        return

    def afterPage(self):
        encabezado = self._custom_styles['encabezado']
        self.canv.saveState()
        self._emisor(encabezado['emisor'], self._data['emisor'])
        self._receptor(encabezado['receptor'], self._data['receptor'])
        self._comprobante1(encabezado['comprobante'], self._data['comprobante'])
        self.canv.restoreState()
        return

    def _currency(self, value, simbol='$'):
        return '{} {:,.2f}'.format(simbol, float(value))

    def _format(self, value, field):
        fields = ('valorunitario', 'importe')
        if field in fields:
            return self._currency(value)

        if field == 'descripcion':
            html = '<font color="black" size=7>{}</font>'
            style_bt = getSampleStyleSheet()['BodyText']
            return Paragraph(html.format(value), style_bt)

        return value

    def _conceptos(self, conceptos):
        headers = (('Clave', 'Descripción', 'Unidad', 'Cantidad',
            'Valor Unitario', 'Importe'),)
        fields = ('noidentificacion', 'descripcion', 'unidad', 'cantidad',
            'valorunitario', 'importe')
        rows = []
        for concepto in conceptos:
            row = tuple([self._format(concepto[f], f) for f in fields])
            rows.append(row)
        return headers + tuple(rows)

    def _totales(self, values):
        #~ print (values)
        rows = [('', 'Subtotal', self._currency(values['subtotal']))]

        for tax in values['traslados']:
            row = ('', tax[0], self._currency(tax[1]))
            rows.append(row)
        for tax in values['retenciones']:
            row = ('', tax[0], self._currency(tax[1]))
            rows.append(row)
        for tax in values['taxlocales']:
            row = ('', tax[0], self._currency(tax[1]))
            rows.append(row)

        row = ('', 'Total', self._currency(values['total']))
        rows.append(row)

        widths = [12.5 * cm, 4 * cm, 3 * cm]
        table_styles = [
            ('GRID', (0, 0), (-1, -1), 0.05 * cm, colors.white),
            ('ALIGN', (1, 0), (-1, -1), 'RIGHT'),
            ('FONTSIZE', (0, 0), (-1, -1), 8),
            ('BACKGROUND', (1, 0), (-1, -1), colors.lightgrey),
            ('TEXTCOLOR', (1, 0), (-1, -1), colors.black),
            ('FACE', (1, 0), (-1, -1), 'Helvetica-Bold'),
        ]
        table = Table(rows, colWidths=widths, spaceBefore=0.25*cm)
        table.setStyle(TableStyle(table_styles))
        return table

    def _comprobante2(self, styles, data):
        leyendas = styles.pop('leyendas', {})

        ls = []
        for k, v in styles.items():
            if k in data:
                if 'spaceBefore' in v['estilo']:
                    v['estilo']['spaceBefore'] = v['estilo']['spaceBefore'] * cm
                ps = ParagraphStyle(**v['estilo'])
                p = Paragraph(escape(data[k]), ps)
                ls.append(p)
            elif k=='formametodopago':
                ps = ParagraphStyle(**v['estilo'])
                v = f"{data['formadepago']} - {data['metododepago']}"
                p = Paragraph(v, ps)
                ls.append(p)
            elif k=='monedatipocambio':
                ps = ParagraphStyle(**v['estilo'])
                v = f"{data['moneda']} - {data['tipocambio']}"
                p = Paragraph(v, ps)
                ls.append(p)

        cbb = Image(data['cbb'])
        cbb.drawHeight = 4 * cm
        cbb.drawWidth = 4 * cm

        style_bt = getSampleStyleSheet()['BodyText']
        style_bt.leading = 8
        html_t = '<b><font size=6>{}</font></b>'
        html = '<font color="black" size=5>{}</font>'
        msg = 'Cadena original del complemento de certificación digital del SAT'
        rows = [
            (cbb, Paragraph(html_t.format('Sello Digital del CFDI'), style_bt)),
            ('', Paragraph(html.format(data['sellocfd']), style_bt)),
            ('', Paragraph(html_t.format('Sello Digital del SAT'), style_bt)),
            ('', Paragraph(html.format(data['sellosat']), style_bt)),
            ('', Paragraph(html_t.format(msg), style_bt)),
            ('', Paragraph(html.format(data['cadenaoriginal']), style_bt)),
        ]

        widths = [4 * cm, 15.5 * cm]
        table_styles = [
            ('FONTSIZE', (0, 0), (-1, -1), 6),
            ('SPAN', (0, 0), (0, -1)),
            ('FACE', (1, 0), (1, 0), 'Helvetica-Bold'),
            ('BACKGROUND', (1, 1), (1, 1), colors.lightgrey),
            ('TEXTCOLOR', (1, 1), (1, 1), colors.darkred),
            ('FACE', (1, 2), (1, 2), 'Helvetica-Bold'),
            ('BACKGROUND', (1, 3), (1, 3), colors.lightgrey),
            ('TEXTCOLOR', (1, 3), (1, 3), colors.darkred),
            ('FACE', (1, 4), (1, 4), 'Helvetica-Bold'),
            ('BACKGROUND', (1, 5), (1, 5), colors.lightgrey),
            ('TEXTCOLOR', (1, 5), (1, 5), colors.darkred),
            ('ALIGN', (0, 0), (0, 0), 'CENTER'),
            ('VALIGN', (0, 0), (0, 0), 'MIDDLE'),
        ]
        table = Table(rows, colWidths=widths)
        table.setStyle(TableStyle(table_styles))
        ls.append(table)

        if leyendas:
            if 'spaceBefore' in leyendas['estilo']:
                leyendas['estilo']['spaceBefore'] = \
                    leyendas['estilo']['spaceBefore'] * cm
            for t in leyendas['textos']:
                ps = ParagraphStyle(**leyendas['estilo'])
                p = Paragraph(t, ps)
                ls.append(p)

        return ls

    @property
    def custom_styles(self):
        return self._custom_styles
    @custom_styles.setter
    def custom_styles(self, values):
        self._custom_styles = values

    @property
    def data(self):
        return self._data
    @data.setter
    def data(self, values):
        global CANCEL
        # ~ print (values)
        self._data = values
        CANCEL = self._data['cancelada']

        rows = self._conceptos(self._data['conceptos'])
        widths = [2 * cm, 9 * cm, 1.5 * cm, 2 * cm, 2 * cm, 3 * cm]
        table_styles = [
            ('GRID', (0, 0), (-1, -1), 0.05 * cm, colors.white),
            ('FONTSIZE', (0, 0), (-1, 0), 7),
            ('ALIGN', (0, 0), (-1, 0), 'CENTER'),
            ('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
            ('FACE', (0, 0), (-1, 0), 'Helvetica-Bold'),
            ('BACKGROUND', (0, 0), (-1, 0), colors.grey),
            ('FONTSIZE', (0, 1), (-1, -1), 7),
            ('VALIGN', (0, 1), (-1, -1), 'TOP'),
            ('ALIGN', (0, 1), (0, -1), 'CENTER'),
            ('ALIGN', (2, 1), (2, -1), 'CENTER'),
            ('ALIGN', (3, 1), (5, -1), 'RIGHT'),
            ('LINEBELOW', (0, 1), (-1, -1), 0.05 * cm, colors.grey),
            ('LINEBEFORE', (0, 1), (-1, -1), 0.05 * cm, colors.white),
        ]
        table_conceptos = Table(rows, colWidths=widths, repeatRows=1)
        table_conceptos.setStyle(TableStyle(table_styles))

        totales = self._totales(self.data['totales'])
        comprobante = self._comprobante2(
            self._custom_styles['comprobante'], self.data['comprobante'])

        self._rows = [Spacer(0, 5.7 * cm), table_conceptos, totales] + comprobante

    def render(self):
        frame = Frame(self.leftMargin, self.bottomMargin,
            self.width, self.height, id='normal')
        template = PageTemplate(id='invoice', frames=frame)
        self.addPageTemplates([template])
        self.build(self._rows, canvasmaker=NumberedCanvas)
        return


class ReportTemplate(BaseDocTemplate):
    """Override the BaseDocTemplate class to do custom handle_XXX actions"""

    def __init__(self, *args, **kwargs):
        # letter 21.6 x 27.9
        kwargs['pagesize'] = letter
        kwargs['rightMargin'] = 1 * cm
        kwargs['leftMargin'] = 1 * cm
        kwargs['topMargin'] = 1.5 * cm
        kwargs['bottomMargin'] = 1.5 * cm
        BaseDocTemplate.__init__(self, *args, **kwargs)
        self.styles = getSampleStyleSheet()
        self.header = {}
        self.data = []

    def afterPage(self):
        """Called after each page has been processed"""
        self.canv.saveState()
        date = datetime.datetime.today().strftime('%A, %d de %B del %Y')
        self.canv.setStrokeColorRGB(0.5, 0, 0)
        self.canv.setFont("Helvetica", 8)
        self.canv.drawRightString(20.59 * cm, 26.9 * cm, date)
        self.canv.line(1 * cm, 26.4 * cm, 20.6 * cm, 26.4 * cm)

        path_cur = os.path.dirname(os.path.realpath(__file__))
        path_img = os.path.join(path_cur, 'logo.png')
        try:
            self.canv.drawImage(path_img, 1 * cm, 24.2 * cm, 4 * cm, 2 * cm)
        except:
            pass

        self.canv.roundRect(
            5 * cm, 25.4 * cm, 15.5 * cm, 0.6 * cm, 0.15 * cm,
            stroke=True, fill=False)
        self.canv.setFont('Helvetica-BoldOblique', 10)
        self.canv.drawCentredString(12.75 * cm, 25.6 * cm, self.header['emisor'])

        self.canv.roundRect(
            5 * cm, 24.4 * cm, 15.5 * cm, 0.6 * cm, 0.15 * cm,
            stroke=True, fill=False)
        self.canv.setFont('Helvetica-BoldOblique', 9)
        self.canv.drawCentredString(12.75 * cm, 24.6 * cm, self.header['title'])

        self.canv.line(1 * cm, 1.5 * cm, 20.6 * cm, 1.5 * cm)
        self.canv.restoreState()
        return

    def set_data(self, data):
        self.header['emisor'] = data['emisor']
        self.header['title'] = data['title']
        cols = len(data['rows'][0])
        widths = []
        for w in data['widths']:
            widths.append(float(w) * cm)
        t_styles = [
            ('GRID', (0, 0), (-1, -1), 0.25, colors.darkred),
            ('FONTSIZE', (0, 0), (-1, 0), 9),
            ('BOX', (0, 0), (-1, 0), 1, colors.darkred),
            ('TEXTCOLOR', (0, 0), (-1, 0), colors.black),
            ('FONTSIZE', (0, 1), (-1, -1), 8),
            ('ALIGN', (0, 0), (-1, 0), 'CENTER'),
            ('ALIGN', (0, 0), (0, -1), 'RIGHT'),
        ]
        if cols == 6:
            t_styles += [
                ('ALIGN', (1, 1), (1, -1), 'CENTER'),
                ('ALIGN', (3, 1), (3, -1), 'CENTER'),
                ('ALIGN', (4, 1), (4, -1), 'RIGHT'),
            ]
        elif cols == 3:
            t_styles += [
                ('ALIGN', (-1, 0), (-1, -1), 'RIGHT'),
                ('ALIGN', (-2, 0), (-2, -1), 'RIGHT'),
                ('ALIGN', (0, 0), (-1, 0), 'CENTER'),
            ]
        elif cols == 2:
            t_styles += [
                ('ALIGN', (-1, 0), (-1, -1), 'RIGHT'),
                ('ALIGN', (0, 0), (-1, 0), 'CENTER'),
            ]
        rows = []
        for i, r in enumerate(data['rows']):
            n = i + 1
            rows.append(('{}.-'.format(n),) + r)
            if cols == 6:
                if r[4] == 'Cancelado':
                    t_styles += [
                        ('GRID', (0, n), (-1, n), 0.25, colors.red),
                        ('TEXTCOLOR', (0, n), (-1, n), colors.red),
                    ]
        rows.insert(0, data['titles'])
        t = Table(rows, colWidths=widths, repeatRows=1)
        t.setStyle(TableStyle(t_styles))

        text = 'Total este reporte = $ {}'.format(data['total'])
        ps = ParagraphStyle(
            name='Total',
            fontSize=12,
            fontName='Helvetica-BoldOblique',
            textColor=colors.darkred,
            spaceBefore=0.5 * cm,
            spaceAfter=0.5 * cm)
        p1 = Paragraph(text, ps)
        text = 'Nota: esta suma no incluye documentos cancelados'
        ps = ParagraphStyle(
            name='Note',
            fontSize=7,
            fontName='Helvetica-BoldOblique')
        p2 = Paragraph(text, ps)
        self.data = [t, p1, p2]
        return

    def render(self):
        frame = Frame(self.leftMargin, self.bottomMargin,
            self.width, self.height, id='normal')
        template = PageTemplate(id='report', frames=frame)
        self.addPageTemplates([template])
        self.build(self.data, canvasmaker=NumberedCanvas)
        return


class SeaFileAPI(object):
    FILE_DOES_NOT_EXIST = 441

    def __init__(self, url, username, password):
        self._url = url
        self._headers = self._get_auth(username, password)

    @property
    def is_connect(self):
        return bool(self._headers)

    def _open(self, path):
        return open(path, 'rb')

    def _get_auth(self, username, password):
        url = self._url + 'auth-token/'
        data = {
            'username': username,
            'password': password,
        }
        resp = requests.post(url, data=data)
        if resp.status_code != requests.codes.ok:
            msg = 'Token Error: {}'.format(resp.status_code)
            print (msg)
            return {}

        headers = {'Authorization': 'Token {}'.format(resp.json()['token'])}
        return headers

    def _get_upload_link(self, repo_id):
        if not self._headers:
            return ''

        url = '{}repos/{}/upload-link/'.format(self._url, repo_id)
        resp = requests.get(url, headers=self._headers)
        if resp.status_code != requests.codes.ok:
            msg = 'Error: {}'.format(resp.status_code)
            print (msg)
            return ''

        return resp.json()

    def _get_update_link(self, repo_id):
        if not self._headers:
            return ''

        url = '{}repos/{}/update-link/'.format(self._url, repo_id)
        data = {'p': '/'}
        resp = requests.get(url, data=data, headers=self._headers)
        if resp.status_code != requests.codes.ok:
            msg = 'Error: {}'.format(resp.status_code)
            print (msg)
            return ''

        return resp.json()

    def _decrypt(self, repo_id, password):
        if not self._headers:
            return False

        url = '{}repos/{}/'.format(self._url, repo_id)
        data = {'password': password}
        resp = requests.post(url, data=data, headers=self._headers)
        if resp.status_code != requests.codes.ok:
            msg = 'Error: {}'.format(resp.status_code)
            print (msg)
            return False

        return True

    def upload_file(self, path_file, repo_id, relative_path, password=''):
        if not self._headers:
            return False

        if password:
            if not self._decrypt(repo_id, password):
                return False

        upload_link = self._get_upload_link(repo_id)
        if isinstance(path_file, tuple):
            obj_file = path_file[0]
            filename = path_file[1]
        else:
            obj_file = self._open(path_file)
            _, filename = self._info_path(path_file)

        data = {
            # ~ 'filename': filename,
            'parent_dir': relative_path,
            'relative_path': '',
        }
        files = {'file': (filename, obj_file)}

        resp = requests.post(
            upload_link, data=data, files=files, headers=self._headers)
        if resp.status_code != requests.codes.ok:
            msg = 'Upload Code: {}\n{}'.format(resp.status_code, resp.text)
            print (msg)
            return False

        return True

    def _info_path(self, path):
        path, filename = os.path.split(path)
        return path, filename

    def list_directory(self, repo_id):
        if not self._headers:
            return False

        url = '{}repos/{}/dir/'.format(self._url, repo_id)
        params = {'p': '/', 't': 'd', 'recursive': '1'}
        try:
            resp = requests.get(url, params=params, headers=self._headers)
            return resp.json()
        except Exception as e:
            return False

    def create_dir(self, repo_id, name):
        url = '{}repos/{}/dir/'.format(self._url, repo_id)
        data = {'operation': 'mkdir'}
        params = {'p': name}
        resp = requests.post(url, data=data, headers=self._headers, params=params)
        if resp.status_code != requests.codes.created:
            msg = 'Create Dir Error: {}'.format(resp.status_code)
            print (msg)
            return False

        return True

    def _validate_directory(self, repo_id, target_file):
        if target_file == '/':
            return True

        names = target_file.split('/')[:-1]
        directories = self.list_directory(repo_id)

        if isinstance(directories, bool):
            return False

        exists = False
        parent_dir = '/'
        name_dir = ''
        for name in names:
            name_dir += '/' + name
            for directory in directories:
                if name == directory['name'] and parent_dir == directory['parent_dir']:
                    exists = True
                    break
            if exists:
                exists = False
            else:
                self.create_dir(repo_id, name_dir)
            if parent_dir == '/':
                parent_dir += name
            else:
                parent_dir += '/' + name

        return True

    def update_file(self, path_file, repo_id, target_file, password, create=True):
        if not self._headers:
            return False

        if not self._validate_directory(repo_id, target_file):
            return False

        if password:
            if not self._decrypt(repo_id, password):
                return False

        update_link = self._get_update_link(repo_id)
        if isinstance(path_file, tuple):
            obj_file = path_file[0]
            filename = path_file[1]
        else:
            obj_file = self._open(path_file)
            _, filename = self._info_path(path_file)

        files = {
            'file': (filename, obj_file),
            'filename': (None, filename),
            'target_file': (None, '{}{}'.format(target_file, filename))
        }

        resp = requests.post(update_link, files=files, headers=self._headers)
        if resp.status_code != requests.codes.ok:
            msg = 'Update Code: {}\n{}'.format(resp.status_code, resp.text)
            print (msg)
            if resp.status_code == self.FILE_DOES_NOT_EXIST and create:
                relative_path = '/'
                if target_file != '/':
                    relative_path += target_file
                return self.upload_file(path_file, repo_id, relative_path, password)
            else:
                return False

        return True

    def ping(self):
        url = self._url + 'ping/'
        resp = requests.get(url)
        return resp.json()


    def auth_ping(self):
        url = self._url + 'auth/ping/'
        resp = requests.get(url, headers=self._headers)
        return resp.json()


class PrintTicket(object):
    LINE = '------------------------------------------------\n'
    TITLES = 'CANT.     U    ARTICULO           P.U.     TOTAL\n'
    LEYENDA = 'GRACIAS POR SU COMPRA\n\nGuarde este ticket para cualquier ' \
    'aclaración.\nComprobante simplificado de operación con\npúblico en ' \
    'general de acuerdo al Art. 37\nFracc II inc. v del Reglamento del\n' \
    'Código Fiscal de la Federación.\n\n'

    def __init__(self, info):
        self.p = self._init_printer(info)

    def _init_printer(self, info):
        try:
            if info['ip']:
                p = printer.Network(info['ip'])
            else:
                p = printer.Usb(*info['usb'])
            p.codepage = 'cp850'
            return p
        except Exception as e:
            print (e)
        return

    def _set(self, *data):
        self.p.set(*data)
        return

    def _t(self, text):
        self.p.text(text)
        return

    def _l(self):
        self._t(self.LINE)
        return

    def printer(self, data):
        if self.p is None:
            return False

        self._emisor(data['emisor'])
        self._receptor(data['receptor'])
        self._ticket(data['ticket'])
        self._products(data['products'])
        self._footer(data['ticket'])
        self.p.cut()
        return True

    def _emisor(self, data):
        self._set('center', 'B', 'B', 2, 2)
        self._t(data['name'])
        self._set('center', 'B', 'B', 2)
        self._t(data['rfc'])
        self._set('center', 'A')
        self._t(data['regimen'])
        self._t(data['address'])
        return

    def _receptor(self, data):
        self._set('center', 'B', 'B', 2)
        self._t(data['name'])
        return

    def _ticket(self, data):
        self._set('left', 'B', 'B', 2)
        self._t(data['title'])
        self._set('left', 'A')
        self._t(data['date'])
        return

    def _products(self, data):
        self._l()
        self._t(self.TITLES)
        self._l()
        for p in data:
            self._t(p)
        self._l()
        self._t('Total artículos: {}\n'.format(len(data)))
        self._l()
        return

    def _footer(self, data):
        self._set('right', 'B', 'B', 2)
        self._t(data['total'])
        self._set('center', 'A')
        self._t(data['letters'])
        self._t(self.LEYENDA)
        self._set('center', 'A', 'B')
        self._t('empresalibre.net')
        return
