#####################################################################
# zopyx.authoring
# (C) 2011, ZOPYX Limited, D-72070 Tuebingen. All rights reserved
#####################################################################

import os  
import time
import hashlib
import tempfile
import cssutils
from zipfile import ZipFile, is_zipfile

import zope.event
from zope.interface import directlyProvides, directlyProvidedBy
from Products.Five.browser import BrowserView
from Products.CMFCore.utils import getToolByName
from DateTime.DateTime import DateTime
from plone.i18n.normalizer.interfaces import IUserPreferredURLNormalizer

from zopyx.smartprintng.plone import Transformer
from zopyx.smartprintng.plone.interfaces import IArchiveFolder
from .. import authoringMessageFactory as _
from .. import HAVE_TIDY, HAVE_CURL
from ..logger import LOG
from ..subscribers.events import AfterOfficeImport
import util
import word_util

def cleanup_html(html):
    """ cleanup HTML generated by OpenOffice """

    if not HAVE_TIDY:
        raise RuntimeError('Tidy is not available')

    filename = tempfile.mktemp()
    filename_out = filename + '.out'
    file(filename, 'wb').write(html)
    cmd = 'tidy -utf8 -c %s >%s' % (filename, filename_out)
    LOG.info('Running %s' % cmd)
    status = os.system(cmd)
    LOG.info('tidy exit code: %d' % status)
    if not os.path.exists(filename_out) or os.path.getsize(filename_out) == 0:
        raise RuntimeError('Running "tidy" failed (cmd: %s)' % cmd)

    # Perform some transformations
    html = file(filename_out).read()
    transformations = ['shiftHeadings', 
                       'cleanupEmptyElements', 
                       'removeEmptyNodesFromWord',
                       'mergeSingleSpanIntoParagraph',
                       'convertWordFootnotes',
                       'convertWordFootnotes2',
                       'convertWordEndnotes',
                       'fixHeadingsAfterOfficeImport',
                       'cleanupTables',
                       'adjustImagesAfterOfficeImport',
                       'addUUIDsToAllTags',
                       ]
    T = Transformer(transformations)
    result_html = T(html, input_encoding='utf-8', return_body=True)

    os.unlink(filename)
    os.unlink(filename_out)
    return result_html


ignored_styles = (
    'font-family',
    'orphans',
    'direction',
    'widows',
    'border',
    'border-top',
    'border-bottom',
    'border-left',
    'border-right',
    'padding',
    'padding-top',
    'padding-bottom',
    'padding-left',
    'padding-right',
    'margin',
    'margin-top',
    'margin-bottom',
    'margin-left',
    'margin-right',
    'so-language',
    'page-break-before',
    'page-break-after',
    'font-size', 
    'text-indent',
    'line-height',
)

def cleanup_css(css):
    sheet = cssutils.parseString(css)
    cssutils.ser.prefs.indent = '  '
    
    for i, rule in enumerate(sheet):
        if isinstance(rule, cssutils.css.CSSStyleRule):
            remove_props = list()
            remove_props = [prop.name for prop in rule.style if prop.name.lower() in ignored_styles]
            for name in remove_props:
                rule.style.removeProperty(name)

    for i, rule in enumerate(sheet):
        if isinstance(rule, cssutils.css.CSSPageRule):
            sheet.deleteRule(rule)
            continue

    return sheet.cssText


class OfficeUpload(BrowserView):
    """ This view accepts an uploaded office document (.docx, .docx)
        and sends it to an external hosted conversion service at fafalter.de.
        The result is returned as ZIP archive containing a HTML and the
        extracted images (both will be reimported into Plone).
    """

    def __call__(self):

        if not HAVE_CURL:
            raise RuntimeError('curl is not available')

        # url and credentials are stored on the property sheet
        pp = getToolByName(self.context, 'portal_properties')
        zp = getToolByName(pp, 'zopyxauthoring_properties')

        if self.request.get('do_archive'):
            ids = self.context.objectIds()
            if ids:
                archive_id = 'archive-%s' %DateTime().strftime('%Y%m%dT%H%M%S')
                self.context.invokeFactory('AuthoringContentFolder', id=archive_id, title=archive_id)
                archive_folder = self.context[archive_id]
                directlyProvides(archive_folder, directlyProvidedBy(archive_folder), IArchiveFolder)
                cb = self.context.manage_copyObjects(ids)
                archive_folder.manage_pasteObjects(cb)

        # Tabula-Rasa first if requested
        clear_options = self.request.get('clear_options')
        if clear_options == 'clear_all':
            self.context.manage_delObjects([o.getId() for o in self.context.contentValues() if o.portal_type not in ('AuthoringContentFolder',)])
        elif clear_options == 'keep_images':
            self.context.manage_delObjects([o.getId() for o in self.context.contentValues() if o.portal_type not in ('Image', 'AuthoringContentFolder')])

        # store uploaded file first on the filesystem
        if not 'doc' in self.request.form:
            self.context.plone_utils.addPortalMessage(_(u'label_no_file_uploaded', 
                                                        u'No uploaded file found'))
            return self.request.response.redirect(self.context.absolute_url() + '/@@import-office-document-form')

        doc_filename = self.request['doc'].filename
        if not doc_filename.lower().endswith('docx'):
            self.context.plone_utils.addPortalMessage(_(u'label_upload_is_not_a_docx_file', 
                                                        u'Uploaded file must be a .DOCX file'), 'error')
            return self.request.response.redirect(self.context.absolute_url() + '/@@folder_contents')

        data = self.request['doc'].read()
        if not data:
            self.context.plone_utils.addPortalMessage(_(u'label_no_file_given', 
                                                        u'No file given'), 'error')
            return self.request.response.redirect(self.context.absolute_url() + '/@@import-office-document-form')

        doc_filename = os.path.basename(self.request['doc'].filename)
        doc_filename, doc_ext = os.path.splitext(doc_filename)
        doc_filename = unicode(doc_filename, 'utf-8')
        dest_id = IUserPreferredURLNormalizer(self.request).normalize(doc_filename)

        if doc_ext.lower() != '.docx':
            self.context.plone_utils.addPortalMessage(_(u'label_only_docx_files', 
                                                        u'Imported file must be a DOCX file'), 'error')
            return self.request.response.redirect(self.context.absolute_url() + '/@@import-office-document-form')


        input_fn= tempfile.mktemp(prefix='%s-%s-' %(dest_id, DateTime().strftime('%Y%m%dT%H%M%S')))
        zip_fn = input_fn + '.zip'
        file(input_fn, 'wb').write(data)
        LOG.info('Received office document (%d bytes), stored as %s' % (len(data), input_fn))

        hash2image = dict()
        image_original_dir = None

        if doc_ext.lower() == '.zip':
            # Upload of a ZIP file containing the result of a local OO conversion
            zip_fn = input_fn

        else:

            # First perform a replacement of all hires images into lo-res image
            # replaceImages() will return a minimized version of the original DOCX
            # document. 'hash2image' will be used later to replace the minimized images
            # with the original images for import into Plone. The matching is based on 
            # the md5 hash of the thumnail images

            docx_filename, hash2image, image_original_dir = word_util.replaceImages(input_fn)

            # POST file to Fafalter server and trigger conversion
            ts = time.time()
            cmd = 'curl -s -u "%s:%s" --form "doc=@%s" -o "%s" "%s"' % \
                   (zp.oo_server_username, zp.oo_server_password, docx_filename, zip_fn, zp.oo_server_url)
            LOG.info('Running %s' % cmd)
            status = os.system(cmd)
            LOG.info('curl exit code: %d, duration=%2.2f seconds' % (status, time.time() - ts))

            if not is_zipfile(zip_fn):
                msg = 'File returned from external conversion is not a ZIP file'
                LOG.error(msg)
                LOG.error('Curl output:\n%s\n' % file(zip_fn).read())
                raise RuntimeError(msg)

        content_folder_title = self.context.getContentFolder().Title()

        # import converted files (html + images) from ZIP file 
        # back into Plone
        ZF = ZipFile(zip_fn)
        LOG.info('ZIP contents: %s' % ZF.namelist())
        image_counter = 1
        office_styles = ''
        for name in ZF.namelist():
            basename, ext = os.path.splitext(name)
            if ext == '.html':
                self.context.invokeFactory('AuthoringContentPage', id=dest_id, title=content_folder_title)
                LOG.info('Added Document %s' % dest_id)
                doc = self.context[dest_id]
                html = ZF.read(name)
                html = cleanup_html(html)
                doc.setText(html)
                doc.setContentType('text/html')
                doc.getField('text').setContentType(doc, 'text/html')
                doc.reindexObject()
            elif ext in ('.jpg', '.gif', '.png'):
#                img_id = '%s-%d.%s' % (dest_id, image_counter, ext)
                title = '%s (Image %d)' % (dest_id, image_counter)
                self.context.invokeFactory('Image', id=name, title=title)
                image_counter += 1
                LOG.info('Added Image %s' % name)
                img = self.context[name]

                # Check if the image is a thumbnail (as replaced through replaceImages())
                img_data = ZF.read(name)
                img_data_hash = hashlib.md5(img_data).hexdigest()

                # Img/thumbnail hash matches a former replacment? If yes
                # then restore the original image content into Plone
                real_image = hash2image.get(img_data_hash)
                if real_image:
                    img_data = file(real_image, 'rb').read()

                img.setImage(img_data)
                img.reindexObject()
            elif ext in('.css'):
                office_styles = cleanup_css(ZF.read(name))

        # attach stylesheet
        self.context[dest_id].setOfficeStyles(office_styles)

        # house-keeping
        ZF.close()
        util.safe_unlink(zip_fn)
        util.safe_unlink(input_fn)
        util.safe_unlink(image_original_dir)
        zope.event.notify(AfterOfficeImport(self.context))

        self.context.plone_utils.addPortalMessage(_(u'label_upload_and_conversion_completed', 
                                                    u'Upload and conversion completed'))
        return self.request.response.redirect(self.context.absolute_url() + '/@@folder_contents')

    def usage(self):
        """ Return back references """
