## $Id: utils.py 12802 2015-03-20 18:04:32Z henrik $
##
## Copyright (C) 2014 Uli Fouquet & Henrik Bettermann
## 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
##
"""General helper functions and utilities for the customer section.
"""
import re
import os
import grok
from copy import deepcopy
from StringIO import StringIO
from reportlab.lib import colors
from reportlab.lib.units import cm
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.platypus import Paragraph, Image, Table, Spacer
from reportlab.platypus.flowables import PageBreak
from PyPDF2 import PdfFileMerger, PdfFileReader, PdfFileWriter
from zope.event import notify
from zope.schema.interfaces import ConstraintNotSatisfied
from zope.component import getUtility, createObject
from zope.formlib.form import setUpEditWidgets
from zope.i18n import translate
from waeup.ikoba.interfaces import MessageFactory as _
from waeup.ikoba.interfaces import (
    STARTED, CREATED, REQUESTED, APPROVED, SUBMITTED, VERIFIED, REJECTED,
    EXPIRED, PROVISIONALLY, AWAITING,
    IIkobaUtils, IExtFileStore)
from waeup.ikoba.customers.interfaces import ICustomersUtils
from waeup.ikoba.browser.interfaces import IPDFCreator
from waeup.ikoba.browser.pdf import (
    ENTRY1_STYLE, format_html, NOTE_STYLE, HEADING_STYLE, HEADLINE1_STYLE,
    get_signature_tables, get_qrcode)
from waeup.ikoba.payments.interfaces import STATE_PAID

RE_CUSTID_NON_NUM = re.compile('[^\d]+')

def generate_customer_id():
    customers = grok.getSite()['customers']
    new_id = customers.unique_customer_id
    return new_id

def path_from_custid(customer_id):
    """Convert a customer_id into a predictable relative folder path.

    Used for storing files.

    Returns the name of folder in which files for a particular customer
    should be stored. This is a relative path, relative to any general
    customers folder with 5 zero-padded digits (except when customer_id
    is overlong).

    We normally map 1,000 different customer ids into one single
    path. For instance ``K1000000`` will give ``01000/K1000000``,
    ``K1234567`` will give ``0123/K1234567`` and ``K12345678`` will
    result in ``1234/K12345678``.

    For lower numbers < 10**6 we return the same path for up to 10,000
    customer_ids. So for instance ``KM123456`` will result in
    ``00120/KM123456`` (there will be no path starting with
    ``00123``).

    Works also with overlong number: here the leading zeros will be
    missing but ``K123456789`` will give reliably
    ``12345/K123456789`` as expected.
    """
    # remove all non numeric characters and turn this into an int.
    num = int(RE_CUSTID_NON_NUM.sub('', customer_id))
    if num < 10**6:
        # store max. of 10000 custs per folder and correct num for 5 digits
        num = num / 10000 * 10
    else:
        # store max. of 1000 custs per folder
        num = num / 1000
    # format folder name to have 5 zero-padded digits
    folder_name = u'%05d' % num
    folder_name = os.path.join(folder_name, customer_id)
    return folder_name

SLIP_STYLE = [
    ('VALIGN',(0,0),(-1,-1),'TOP'),
    #('FONT', (0,0), (-1,-1), 'Helvetica', 11),
    ]

CONTENT_STYLE = [
    ('VALIGN',(0,0),(-1,-1),'TOP'),
    #('FONT', (0,0), (-1,-1), 'Helvetica', 8),
    #('TEXTCOLOR',(0,0),(-1,0),colors.white),
    #('BACKGROUND',(0,0),(-1,0),colors.black),
    ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black),
    ('BOX', (0,0), (-1,-1), 1, colors.black),

    ]

FONT_SIZE = 10
FONT_COLOR = 'black'

def trans(text, lang):
    # shortcut
    return translate(text, 'waeup.ikoba', target_language=lang)


def formatted_text(text, color=FONT_COLOR, lang='en'):
    """Turn `text`, `color` and `size` into an HTML snippet.

    The snippet is suitable for use with reportlab and generating PDFs.
    Wraps the `text` into a ``<font>`` tag with passed attributes.

    Also non-strings are converted. Raw strings are expected to be
    utf-8 encoded (usually the case for widgets etc.).

    Finally, a br tag is added if widgets contain div tags
    which are not supported by reportlab.

    The returned snippet is unicode type.
    """
    if not isinstance(text, unicode):
        if isinstance(text, basestring):
            text = text.decode('utf-8')
        else:
            text = unicode(text)
    if text == 'None':
        text = ''
    # Mainly for boolean values we need our customized
    # localisation of the zope domain
    text = translate(text, 'zope', target_language=lang)
    text = text.replace('</div>', '<br /></div>')
    tag1 = u'<font color="%s">' % (color)
    return tag1 + u'%s</font>' % text

def render_customer_data(customerview, context, omit_fields=(),
                        lang='en', slipname=None):
    """Render customer table for an existing frame.
    """
    width, height = A4
    set_up_widgets(customerview, ignore_request=True)
    data_left = []
    data_middle = []
    style = getSampleStyleSheet()
    img = getUtility(IExtFileStore).getFileByContext(
        customerview.context, attr='passport.jpg')
    if img is None:
        from waeup.ikoba.browser import DEFAULT_PASSPORT_IMAGE_PATH
        img = open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb')
    doc_img = Image(img.name, width=4*cm, height=4*cm, kind='bound')
    data_left.append([doc_img])
    #data.append([Spacer(1, 12)])

    f_label = trans(_('Name:'), lang)
    f_label = Paragraph(f_label, ENTRY1_STYLE)
    f_text = formatted_text(customerview.context.display_fullname)
    f_text = Paragraph(f_text, ENTRY1_STYLE)
    data_middle.append([f_label,f_text])

    for widget in customerview.widgets:
        if 'name' in widget.name:
            continue
        f_label = translate(
            widget.label.strip(), 'waeup.ikoba',
            target_language=lang)
        f_label = Paragraph('%s:' % f_label, ENTRY1_STYLE)
        f_text = formatted_text(widget(), lang=lang)
        f_text = Paragraph(f_text, ENTRY1_STYLE)
        data_middle.append([f_label,f_text])

    if getattr(customerview.context, 'certcode', None):
        if not 'date_of_birth' in omit_fields:
            f_label = trans(_('Date of Birth:'), lang)
            f_label = Paragraph(f_label, ENTRY1_STYLE)
            date_of_birth = customerview.context.date_of_birth
            tz = getUtility(IIkobaUtils).tzinfo
            date_of_birth = to_timezone(date_of_birth, tz)
            if date_of_birth is not None:
                date_of_birth = date_of_birth.strftime("%d/%m/%Y")
            f_text = formatted_text(date_of_birth)
            f_text = Paragraph(f_text, ENTRY1_STYLE)
            data_middle.append([f_label,f_text])

    # append QR code to the right
    if slipname:
        url = customerview.url(context, slipname)
        data_right = [[get_qrcode(url, width=70.0)]]
        table_right = Table(data_right,style=SLIP_STYLE)
    else:
        table_right = None

    table_left = Table(data_left,style=SLIP_STYLE)
    table_middle = Table(data_middle,style=SLIP_STYLE, colWidths=[5*cm, 5*cm])
    table = Table([[table_left, table_middle, table_right],],style=SLIP_STYLE)
    return table

def render_table_data(tableheader, tabledata, lang='en'):
    """Render children table for an existing frame.
    """
    data = []
    #data.append([Spacer(1, 12)])
    line = []
    style = getSampleStyleSheet()
    for element in tableheader:
        field = '<strong>%s</strong>' % formatted_text(element[0], lang=lang)
        field = Paragraph(field, style["Normal"])
        line.append(field)
    data.append(line)
    for ticket in tabledata:
        line = []
        for element in tableheader:
              field = formatted_text(getattr(ticket,element[1],u' '))
              field = Paragraph(field, style["Normal"])
              line.append(field)
        data.append(line)
    table = Table(data,colWidths=[
        element[2]*cm for element in tableheader], style=CONTENT_STYLE)
    return table

def docs_as_flowables(view, lang='en'):
    """Create reportlab flowables out of scanned docs.
    """
    # XXX: fix circular import problem
    from waeup.ikoba.browser.fileviewlets import FileManager
    from waeup.ikoba.browser import DEFAULT_IMAGE_PATH
    style = getSampleStyleSheet()
    data = []

    # Collect viewlets
    fm = FileManager(view.context, view.request, view)
    fm.update()
    if fm.viewlets:
        sc_translation = trans(_('Connected Files'), lang)
        data.append(Paragraph(sc_translation, HEADING_STYLE))
        # Insert list of scanned documents
        table_data = []
        for viewlet in fm.viewlets:
            if viewlet.file_exists:
                # Show viewlet only if file exists
                f_label = Paragraph(trans(viewlet.label, lang), ENTRY1_STYLE)
                img_path = getattr(getUtility(IExtFileStore).getFileByContext(
                    view.context, attr=viewlet.download_name), 'name', None)
                #f_text = Paragraph(trans(_('(not provided)'),lang), ENTRY1_STYLE)
                if img_path is None:
                    pass
                elif img_path[-4:] == '.pdf':
                    f_text = Paragraph('%s (see file attached)' % (
                        viewlet.title,), ENTRY1_STYLE)
                elif not img_path[-4:] in ('.jpg', '.JPG'):
                    # reportlab requires jpg images, I think.
                    f_text = Paragraph('%s (no preview available)' % (
                        viewlet.title,), ENTRY1_STYLE)
                else:
                    f_text = Image(img_path, width=2*cm, height=1*cm, kind='bound')
                table_data.append([f_label, f_text])
        if table_data:
            # safety belt; empty tables lead to problems.
            data.append(Table(table_data, style=SLIP_STYLE))
    return data


def set_up_widgets(view, ignore_request=False):
    view.adapters = {}
    view.widgets = setUpEditWidgets(
        view.form_fields, view.prefix, view.context, view.request,
        adapters=view.adapters, for_display=True,
        ignore_request=ignore_request
        )

def generate_contract_id():
    new_id = grok.getSite().unique_contract_id
    return new_id

class CustomersUtils(grok.GlobalUtility):
    """A collection of methods subject to customization.
    """
    grok.implements(ICustomersUtils)

    #: A prefix used when generating new customer ids. Each customer id will
    #: start with this string. The default is 'K'.
    CUSTOMER_ID_PREFIX = u'K'

    CUSTMANAGE_CUSTOMER_STATES = (STARTED,)

    DOCMANAGE_CUSTOMER_STATES = (REQUESTED, APPROVED, PROVISIONALLY)

    DOCMANAGE_DOCUMENT_STATES = (CREATED,)

    CONMANAGE_CUSTOMER_STATES = deepcopy(DOCMANAGE_CUSTOMER_STATES)

    CONMANAGE_CONTRACT_STATES = (CREATED,)

    TRANSLATED_CUSTOMER_STATES = {
        CREATED: _('created'),
        STARTED: _('started'),
        REQUESTED: _('requested'),
        APPROVED: _('approved'),
        PROVISIONALLY: _('provisionally approved'),
        }

    TRANSLATED_CONTRACT_STATES = {
        CREATED: _('created'),
        AWAITING: _('awaiting payment'),
        SUBMITTED: _('submitted for approval'),
        APPROVED: _('approved'),
        REJECTED: _('rejected'),
        EXPIRED:_('expired')
        }

    TRANSLATED_DOCUMENT_STATES = {
        CREATED: _('created'),
        SUBMITTED: _('submitted for verification'),
        VERIFIED: _('verified'),
        REJECTED: _('rejected'),
        EXPIRED:_('expired')
        }

    DOCTYPES_DICT = {
        'CustomerSampleDocument': _('Sample Document'),
        'CustomerPDFDocument': _('PDF Document'),
        }

    CONTYPES_DICT = {
        'SampleContract': _('Sample Contract'),
        }

    SELECTABLE_DOCTYPES_DICT = deepcopy(DOCTYPES_DICT)

    SELECTABLE_CONTYPES_DICT = deepcopy(CONTYPES_DICT)

    EXPORTER_NAMES = ('customers', 'customersampledocuments', 'samplecontracts')

    def getPDFCreator(self, context=None):
        """Get a pdf creator suitable for `context`.

        The default implementation always returns the default creator.
        """
        return getUtility(IPDFCreator)

    def renderPDF(self, view, filename='slip.pdf', customer=None,
                  customerview=None,
                  tableheader=[], tabledata=[],
                  note=None, signatures=None, sigs_in_footer=(),
                  show_scans=True, show_history=True, topMargin=1.5,
                  omit_fields=(), mergefiles=None, watermark=None):
        """Render pdf slips for various pages.
        """
        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
        style = getSampleStyleSheet()
        creator = self.getPDFCreator()
        data = []
        doc_title = view.label
        author = '%s (%s)' % (view.request.principal.title,
                              view.request.principal.id)
        footer_text = view.label.split('\n')
        if len(footer_text) > 2:
            # We can add a department in first line
            footer_text = footer_text[1]
        else:
            # Only the first line is used for the footer
            footer_text = footer_text[0]
        if getattr(customer, 'customer_id', None) is not None:
            footer_text = "%s - %s - " % (customer.customer_id, footer_text)

        # Insert customer data table
        if customer is not None:
            if view.__name__ == 'contract_slip.pdf':
                bd_translation = trans(_('Contractor'), portal_language)
                data.append(Paragraph(bd_translation, HEADING_STYLE))
            if view.__name__ == 'document_slip.pdf':
                bd_translation = trans(_('Document Owner'), portal_language)
                data.append(Paragraph(bd_translation, HEADING_STYLE))
            data.append(render_customer_data(
                customerview, view.context, omit_fields, lang=portal_language,
                slipname=filename))

        # Insert widgets
        if view.form_fields:
            if view.__name__.startswith('contract'):
                data_header = trans(_('Contract Data'), portal_language)
                data.append(Paragraph(data_header, HEADING_STYLE))
            separators = getattr(self, 'SEPARATORS_DICT', {})
            table = creator.getWidgetsTable(
                view.form_fields, view.context, None, lang=portal_language,
                separators=separators)
            data.append(table)

        # Insert payment data
        if getattr(view, 'payment_tuples', None) is not None:
            data_header = trans(_('Payments'), portal_language)
            data.append(Paragraph(data_header, HEADING_STYLE))
            payment_number = 0
            for payment_tuple in getattr(view, 'payment_tuples', None):
                payment_number += 1
                payment = payment_tuple[0]
                payment_form_fields = payment_tuple[1]
                if payment.state != STATE_PAID:
                    continue
                data.append(
                    Paragraph("Payment %s" % payment_number, HEADLINE1_STYLE))
                separators = getattr(self, 'SEPARATORS_DICT', {})
                table = creator.getWidgetsTable(
                    payment_form_fields, payment, None, lang=portal_language,
                    separators=separators)
                data.append(table)

        # Insert scanned docs
        if show_scans:
            data.extend(docs_as_flowables(view, portal_language))

        # Insert history
        if show_history:
            if getattr(view.context, 'history', None):
                hist_translation = trans(_('Transaction History'),
                    portal_language)
                data.append(Paragraph(hist_translation, HEADING_STYLE))
                data.extend(
                    creator.fromStringList(view.context.history.messages))
            else:
                hist_translation = trans(_('Registration History'),
                    portal_language)
                data.append(Paragraph(hist_translation, HEADING_STYLE))
                data.extend(creator.fromStringList(customer.history.messages))

        # Insert validity period
        if getattr(view, 'validity_period', None):
            vp_translation = trans(_('Validity Period'), portal_language)
            data.append(Paragraph(vp_translation, HEADING_STYLE))
            vp = view.validity_period
            data.append(Paragraph(vp, NOTE_STYLE))

        # Insert content tables (optionally on second page)
        if getattr(view, 'tabletitle', None):
            for i in range(len(view.tabletitle)):
                if tabledata[i] and tableheader[i]:
                    #data.append(PageBreak())
                    #data.append(Spacer(1, 20))
                    data.append(Paragraph(view.tabletitle[i], HEADING_STYLE))
                    data.append(Spacer(1, 8))
                    contenttable = render_table_data(tableheader[i],tabledata[i])
                    data.append(contenttable)

        # Insert terms and conditions
        if getattr(view, 'terms_and_conditions', None):
            tc_translation = trans(_('Terms and Conditions'), portal_language)
            data.append(Paragraph(tc_translation, HEADING_STYLE))
            tc = format_html(view.terms_and_conditions)
            data.append(Paragraph(tc, NOTE_STYLE))

        # Insert signatures
        # XXX: We are using only sigs_in_footer in waeup.ikoba, so we
        # do not have a test for the following lines.
        if signatures and not sigs_in_footer:
            data.append(Spacer(1, 20))
            # Render one signature table per signature to
            # get date and signature in line.
            for signature in signatures:
                signaturetables = get_signature_tables(signature)
                data.append(signaturetables[0])

        view.response.setHeader(
            'Content-Type', 'application/pdf')
        try:
            pdf_stream = creator.create_pdf(
                data, None, doc_title, author=author, footer=footer_text,
                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
        except IOError:
            view.flash('Error in image file.')
            return view.redirect(view.url(view.context))

        if mergefiles:
            merger = PdfFileMerger()
            merger.append(StringIO(pdf_stream))
            if watermark:
                watermark = PdfFileReader(watermark)
            for file in mergefiles:
                if watermark:
                    # Pass through all pages of each file
                    # and merge with watermark page.
                    marked_file = PdfFileWriter()
                    orig_file = PdfFileReader(file[1])
                    num_pages = orig_file.getNumPages()
                    for num in range(num_pages):
                        page = orig_file.getPage(num)
                        page.mergePage(watermark.getPage(0))
                        marked_file.addPage(page)
                    # Save into a file-like object
                    tmp1 = StringIO()
                    marked_file.write(tmp1)
                    # Append the file-like object
                    merger.append(tmp1)
                else:
                    # Just append the file object
                    merger.append(file[1])
            # Save into a file-like object
            tmp2 = StringIO()
            merger.write(tmp2)
            return tmp2.getvalue()

        return pdf_stream
