import xml.dom.minidom
from xml.dom import Node
from datetime import datetime as Datetime
import zipfile
import odio
from odio.common import P, H, Span
from tempfile import NamedTemporaryFile


OFFICE_VALUE_TYPE = 'office:value-type'


def escape(data):
    return data.replace("&", "&amp;").replace(">", "&gt;").replace("<", "&lt;")


def quoteattr(data):
    data = escape(data)

    if '"' in data:
        if "'" in data:
            data = '"%s"' % data.replace('"', "&quot;")
        else:
            data = "'%s'" % data
    else:
        data = '"%s"' % data
    return data


class XmlWriter():

    def __init__(self, output):
        self.indentation = 0
        self.output = output
        self._write('<?xml version="1.0" encoding="utf-8"?>\n')

    @staticmethod
    def atts_to_str(attrs):
        if len(attrs) == 0:
            return ''
        else:
            return ' ' + ' '.join(
                k + '=' + quoteattr(v) for k, v in sorted(attrs.items()))

    def _write(self, line, indent=True):
        if indent:
            self.output.write((' ' * self.indentation * 2).encode('utf8'))
        self.output.write(line.encode('utf8'))

    def start_tag(self, name, attrs):
        self._write('<' + name + XmlWriter.atts_to_str(attrs) + '>\n')
        self.indentation += 1

    def end_tag(self, name):
        self.indentation -= 1
        self._write('</' + name + '>\n')

    def simple_tag(self, name, attrs, contents=None):
        if contents is None:
            self._write('<' + name + XmlWriter.atts_to_str(attrs) + '/>\n')
        else:
            self._write('<' + name + XmlWriter.atts_to_str(attrs) + '>')
            self._write(escape(contents), indent=False)
            self._write('</' + name + '>\n', indent=False)


class SpreadsheetWriter():
    def __init__(self, f, compressed):
        self.f = f
        if compressed:
            compression = zipfile.ZIP_DEFLATED
        else:
            compression = zipfile.ZIP_STORED
        self.z = zipfile.ZipFile(f, 'w', compression)
        self.z.writestr(
            'mimetype', 'application/vnd.oasis.opendocument.spreadsheet')
        self.z.writestr(
            'META-INF/manifest.xml',
            """<?xml version="1.0" encoding="UTF-8"?>
<manifest:manifest
    manifest:version="1.2"
    xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0">
  <manifest:file-entry
      manifest:full-path="/"
      manifest:media-type="application/vnd.oasis.opendocument.spreadsheet"/>
  <manifest:file-entry
      manifest:full-path="settings.xml" manifest:media-type="text/xml"/>
  <manifest:file-entry
      manifest:full-path="content.xml" manifest:media-type="text/xml"/>
  <manifest:file-entry
      manifest:full-path="meta.xml" manifest:media-type="text/xml"/>
  <manifest:file-entry
      manifest:full-path="styles.xml" manifest:media-type="text/xml"/>
</manifest:manifest>
""")
        self.z.writestr(
            'meta.xml',
            """<?xml version="1.0" encoding="UTF-8"?>
<office:document-meta
    xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
    xmlns:xlink="http://www.w3.org/1999/xlink"
    xmlns:dc="http://purl.org/dc/elements/1.1/"
    xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0"
    xmlns:of="urn:oasis:names:tc:opendocument:xmlns:of:1.2"
    office:version="1.2">
  <office:meta>
      <meta:generator>Odio</meta:generator>
  </office:meta>
</office:document-meta>
""")

        self.z.writestr(
            'settings.xml',
            """<?xml version="1.0" encoding="UTF-8"?>
<office:document-settings
    xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
    xmlns:xlink="http://www.w3.org/1999/xlink"
    xmlns:config="urn:oasis:names:tc:opendocument:xmlns:config:1.0"
    xmlns:of="urn:oasis:names:tc:opendocument:xmlns:of:1.2"
    office:version="1.2">
</office:document-settings>
""")

        self.z.writestr(
            'styles.xml', """<?xml version="1.0" encoding="UTF-8"?>
<office:document-styles
    xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
    xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"
    xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"
    xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0"
    xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0"
    xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"
    xmlns:xlink="http://www.w3.org/1999/xlink"
    xmlns:dc="http://purl.org/dc/elements/1.1/"
    xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0"
    xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0"
    xmlns:presentation="urn:oasis:names:tc:opendocument:xmlns:presentation:1.0"
    xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0"
    xmlns:chart="urn:oasis:names:tc:opendocument:xmlns:chart:1.0"
    xmlns:dr3d="urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0"
    xmlns:math="http://www.w3.org/1998/Math/MathML"
    xmlns:form="urn:oasis:names:tc:opendocument:xmlns:form:1.0"
    xmlns:script="urn:oasis:names:tc:opendocument:xmlns:script:1.0"
    xmlns:dom="http://www.w3.org/2001/xml-events"
    xmlns:of="urn:oasis:names:tc:opendocument:xmlns:of:1.2"
    xmlns:xhtml="http://www.w3.org/1999/xhtml"
    xmlns:css3t="http://www.w3.org/TR/css3-text/"
    office:version="1.2">
</office:document-styles>
""")
        self.tmp = NamedTemporaryFile()
        self.writer = XmlWriter(self.tmp)
        attrs = {
            'xmlns:office': "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
            'xmlns:office': "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
            'xmlns:style': "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
            'xmlns:text': "urn:oasis:names:tc:opendocument:xmlns:text:1.0",
            'xmlns:table': "urn:oasis:names:tc:opendocument:xmlns:table:1.0",
            'xmlns:draw': "urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",
            'xmlns:fo':
                "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
            'xmlns:xlink': "http://www.w3.org/1999/xlink",
            'xmlns:dc': "http://purl.org/dc/elements/1.1/",
            'xmlns:meta': "urn:oasis:names:tc:opendocument:xmlns:meta:1.0",
            'xmlns:number':
                "urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0",
            'xmlns:presentation':
                "urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",
            'xmlns:svg':
                "urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0",
            'xmlns:chart': "urn:oasis:names:tc:opendocument:xmlns:chart:1.0",
            'xmlns:dr3d': "urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0",
            'xmlns:math': "http://www.w3.org/1998/Math/MathML",
            'xmlns:form': "urn:oasis:names:tc:opendocument:xmlns:form:1.0",
            'xmlns:script': "urn:oasis:names:tc:opendocument:xmlns:script:1.0",
            'xmlns:dom': "http://www.w3.org/2001/xml-events",
            'xmlns:xforms': "http://www.w3.org/2002/xforms",
            'xmlns:xsd': "http://www.w3.org/2001/XMLSchema",
            'xmlns:xsi': "http://www.w3.org/2001/XMLSchema-instance",
            'xmlns:of': 'urn:oasis:names:tc:opendocument:xmlns:of:1.2',
            'xmlns:xhtml': "http://www.w3.org/1999/xhtml",
            'xmlns:css3t': "http://www.w3.org/TR/css3-text/",
            'office:version': "1.2"}
        self.writer.start_tag('office:document-content', attrs)
        self.writer.simple_tag('office:scripts', {})
        self.writer.start_tag('office:automatic-styles', {})
        self.writer.start_tag('number:date-style', {'style:name': "date"})
        self.writer.simple_tag('number:year', {'number:style': "long"})
        self.writer.simple_tag('number:text', {}, '-')
        self.writer.simple_tag('number:month', {'number:style': "long"})
        self.writer.simple_tag('number:text', {}, '-')
        self.writer.simple_tag('number:day', {'number:style': "long"})
        self.writer.simple_tag('number:text', {}, ' ')
        self.writer.simple_tag('number:hours', {'number:style': "long"})
        self.writer.simple_tag('number:text', {}, ':')
        self.writer.simple_tag('number:minutes', {'number:style': "long"})
        self.writer.end_tag('number:date-style')
        self.writer.simple_tag(
            'style:style', {
                'style:name': 'cell_date',
                'style:family': 'table-cell',
                'style:parent-style-name': 'Default',
                'style:data-style-name': 'date'})
        self.writer.end_tag('office:automatic-styles')
        self.writer.start_tag('office:body', {})
        self.writer.start_tag('office:spreadsheet', {})

    def append_table(self, name, rows):
        self.writer.start_tag('table:table', {'table:name': name})
        self.writer.simple_tag('table:table-column', {})
        for row in rows:
            self.writer.start_tag('table:table-row', {})
            cells = []
            for val in row:
                atts = {}
                if isinstance(val, Datetime):
                    atts['office:value-type'] = 'date'
                    atts['office:date-value'] = val.strftime(
                        '%Y-%m-%dT%H:%M:%S')
                    atts['table:style-name'] = 'cell_date'
                elif isinstance(val, str):
                    atts['office:value-type'] = 'string'
                    atts['office:string-value'] = val
                elif isinstance(val, bool):
                    atts['office:value-type'] = 'boolean'
                    atts['office:boolean-value'] = 'true' if val else 'false'
                elif isinstance(val, (float, int)):
                    atts['office:value-type'] = 'float'
                    atts['office:value'] = str(val)
                elif isinstance(val, odio.Formula):
                    atts['table:formula'] = 'of:' + str(val)
                elif val is None:
                    pass
                else:
                    atts['office:value-type'] = 'string'
                    atts['office:string-value'] = str(val)

                if len(cells) > 0 and cells[-1]['atts'] == atts:
                    cells[-1]['count'] += 1
                else:
                    cells.append({'count': 1, 'atts': atts})

            for cell in cells:
                atts = cell['atts']
                if cell['count'] > 1:
                    atts['table:number-columns-repeated'] = str(cell['count'])
                self.writer.simple_tag('table:table-cell', atts)
            self.writer.end_tag('table:table-row')

        self.writer.end_tag('table:table')

    def close(self):
        self.writer.end_tag('office:spreadsheet')
        self.writer.end_tag('office:body')
        self.writer.end_tag('office:document-content')
        self.tmp.flush()
        self.tmp.seek(0)
        self.z.write(self.tmp.name, 'content.xml')
        self.z.close()
        self.tmp.close()

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self.close()


class SpreadsheetReader():
    def __init__(self, spreadsheet_elem):
        self.tables = []
        for table_elem in spreadsheet_elem.getElementsByTagName('table:table'):
            self.tables.append(TableReader(table_elem))


def _get_text(node):
    txt = []
    for child in node.childNodes:
        if child.nodeType == Node.TEXT_NODE:
            txt.append(child.nodeValue.strip())
        else:
            txt.append(_get_text(child))
    return None if len(txt) == 0 else ''.join(txt)


class TableReader():
    def __init__(self, table_elem):
        self.name = table_elem.getAttribute('table:name')
        self.rows = []
        for row_elem in table_elem.getElementsByTagName('table:table-row'):
            row = []
            self.rows.append(row)
            for cell_elem in row_elem.getElementsByTagName('table:table-cell'):
                if cell_elem.hasAttribute('table:formula'):
                    formula = cell_elem.getAttribute('table:formula')
                    eq_idx = formula.index('=')
                    val = odio.Formula(formula[eq_idx:])
                elif cell_elem.hasAttribute(OFFICE_VALUE_TYPE):
                    val_type = cell_elem.getAttribute(OFFICE_VALUE_TYPE)
                    if val_type == 'date':
                        val = Datetime.strptime(
                            cell_elem.getAttribute('office:date-value'),
                            '%Y-%m-%dT%H:%M:%S')
                    elif val_type == 'string':
                        if cell_elem.hasAttribute('office:string-value'):
                            val = cell_elem.getAttribute('office:string-value')
                        else:
                            val = _get_text(cell_elem)
                    elif val_type == 'float':
                        val = float(cell_elem.getAttribute('office:value'))
                    elif val_type == 'boolean':
                        val = cell_elem.getAttribute(
                            'office:boolean-value') == 'true'
                else:
                    val = None

                if cell_elem.hasAttribute('table:number-columns-repeated'):
                    count = int(
                        cell_elem.getAttribute(
                            'table:number-columns-repeated'))
                else:
                    count = 1

                for i in range(count):
                    row.append(val)


class TextWriter():
    def __init__(self, f):
        self.f = f
        self.z = zipfile.ZipFile(f, 'w')
        self.z.writestr(
            'mimetype', 'application/vnd.oasis.opendocument.text')
        self.z.writestr(
            'META-INF/manifest.xml',
            """<?xml version="1.0" encoding="UTF-8"?>
<manifest:manifest
    manifest:version="1.2"
    xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0">
  <manifest:file-entry
      manifest:full-path="/"
      manifest:media-type="application/vnd.oasis.opendocument.text"/>
  <manifest:file-entry
      manifest:full-path="settings.xml" manifest:media-type="text/xml"/>
  <manifest:file-entry
      manifest:full-path="content.xml" manifest:media-type="text/xml"/>
  <manifest:file-entry
      manifest:full-path="meta.xml" manifest:media-type="text/xml"/>
  <manifest:file-entry
      manifest:full-path="styles.xml" manifest:media-type="text/xml"/>
</manifest:manifest>
""")
        self.z.writestr(
            'meta.xml',
            """<?xml version="1.0" encoding="UTF-8"?>
<office:document-meta
    xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
    xmlns:xlink="http://www.w3.org/1999/xlink"
    xmlns:dc="http://purl.org/dc/elements/1.1/"
    xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0"
    xmlns:of="urn:oasis:names:tc:opendocument:xmlns:of:1.2"
    office:version="1.2">
  <office:meta>
      <meta:generator>Odio</meta:generator>
  </office:meta>
</office:document-meta>
""")

        self.z.writestr(
            'settings.xml',
            """<?xml version="1.0" encoding="UTF-8"?>
<office:document-settings
    xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
    xmlns:xlink="http://www.w3.org/1999/xlink"
    xmlns:config="urn:oasis:names:tc:opendocument:xmlns:config:1.0"
    xmlns:of="urn:oasis:names:tc:opendocument:xmlns:of:1.2"
    office:version="1.2">
</office:document-settings>
""")

        self.z.writestr(
            'styles.xml', """<?xml version="1.0" encoding="UTF-8"?>
<office:document-styles
    xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
    xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"
    xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"
    xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0"
    xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0"
    xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"
    xmlns:xlink="http://www.w3.org/1999/xlink"
    xmlns:dc="http://purl.org/dc/elements/1.1/"
    xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0"
    xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0"
    xmlns:presentation="urn:oasis:names:tc:opendocument:xmlns:presentation:1.0"
    xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0"
    xmlns:chart="urn:oasis:names:tc:opendocument:xmlns:chart:1.0"
    xmlns:dr3d="urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0"
    xmlns:math="http://www.w3.org/1998/Math/MathML"
    xmlns:form="urn:oasis:names:tc:opendocument:xmlns:form:1.0"
    xmlns:script="urn:oasis:names:tc:opendocument:xmlns:script:1.0"
    xmlns:dom="http://www.w3.org/2001/xml-events"
    xmlns:of="urn:oasis:names:tc:opendocument:xmlns:of:1.2"
    xmlns:xhtml="http://www.w3.org/1999/xhtml"
    xmlns:css3t="http://www.w3.org/TR/css3-text/"
    office:version="1.2">
</office:document-styles>
""")
        self.doc = xml.dom.minidom.parseString(
            """<?xml version="1.0" encoding="UTF-8"?>
<office:document-content
    xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
    xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"
    xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"
    xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0"
    xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0"
    xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"
    xmlns:xlink="http://www.w3.org/1999/xlink"
    xmlns:dc="http://purl.org/dc/elements/1.1/"
    xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0"
    xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0"
    xmlns:presentation="urn:oasis:names:tc:opendocument:xmlns:presentation:1.0"
    xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0"
    xmlns:chart="urn:oasis:names:tc:opendocument:xmlns:chart:1.0"
    xmlns:dr3d="urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0"
    xmlns:math="http://www.w3.org/1998/Math/MathML"
    xmlns:form="urn:oasis:names:tc:opendocument:xmlns:form:1.0"
    xmlns:script="urn:oasis:names:tc:opendocument:xmlns:script:1.0"
    xmlns:dom="http://www.w3.org/2001/xml-events"
    xmlns:xforms="http://www.w3.org/2002/xforms"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:of="urn:oasis:names:tc:opendocument:xmlns:of:1.2"
    xmlns:xhtml="http://www.w3.org/1999/xhtml"
    xmlns:css3t="http://www.w3.org/TR/css3-text/"
    office:version="1.2">
  <office:scripts/>
  <office:automatic-styles>
    <number:date-style style:name="date">
      <number:year number:style="long"/>
      <number:text>-</number:text>
      <number:month number:style="long"/>
      <number:text>-</number:text>
      <number:day number:style="long"/>
      <number:text> </number:text>
      <number:hours number:style="long"/>
      <number:text>:</number:text>
      <number:minutes number:style="long"/>
    </number:date-style>
    <style:style style:name="cell_date" style:family="table-cell"
      style:parent-style-name="Default" style:data-style-name="date"/>
  </office:automatic-styles>
  <office:body>
    <office:text>
    </office:text>
  </office:body>
</office:document-content>""")
        self.text_elem = self.doc.getElementsByTagName(
            'office:text')[0]

    def append(self, *subnodes):
        for node in subnodes:
            node.attach(self.doc, self.text_elem)

    def close(self):
        self.z.writestr('content.xml', self.doc.toprettyxml(encoding='utf-8'))
        self.z.close()

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self.close()


class TextReader():
    def __init__(self, text_elem):
        self.nodes = []
        self.attributes = {}
        for node_elem in text_elem.childNodes:
            self._parse_node(self, node_elem)

    def _parse_node(self, parent_node, node_dom):
        node_type = node_dom.nodeType
        if node_type == Node.ELEMENT_NODE:
            name = node_dom.tagName
            if name == 'text:p':
                node = P()
            elif name == 'text:h':
                node = H()
            elif name == 'text:span':
                node = Span()
            else:
                raise Exception("Node name " + name + " not recognized.")
            if node_dom.hasAttributes():
                attrs = node_dom.attributes
                for i in range(len(attrs)):
                    attr = attrs.item(i)
                    attr_name = attr.name.replace(':', '_').replace('-', '_')
                    node.attributes[attr_name] = attr.value
            for subnode_dom in node_dom.childNodes:
                self._parse_node(node, subnode_dom)
        elif node_type == Node.TEXT_NODE:
            fnode = node_dom.nodeValue
            snode = fnode.strip()
            if len(snode) == 0:
                return
            node = snode
            rnode = node.rstrip()
            if len(rnode) < len(fnode):
                node += ' '
            lnode = node.lstrip()
            if len(lnode) < len(fnode):
                node = ' ' + node
        else:
            raise Exception("Node type " + str(node_type) + " not recognized.")
        parent_node.nodes.append(node)
