# Copyright (c) 2015, Max Fillinger <max@max-fillinger.net>
# 
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
# 
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.

# The epub format specification is available at http://idpf.org/epub/201

'''Contains the EpubBuilder class to build epub2.0.1 files with the getebook
module.'''

import html
import re
import datetime
import getebook
import os.path
import re
import zipfile

__all__ = ['EpubBuilder', 'EpubTOC', 'Author']

def _normalize(name):
    '''Transform "Firstname [Middlenames] Lastname" into
    "Lastname, Firstname [Middlenames]".'''
    split = name.split()
    if len(split) == 1:
        return name
    return split[-1] + ', ' + ' '.join(name[0:-1])

def _make_starttag(tag, attrs):
    'Write a starttag.'
    out = '<' + tag
    for key in attrs:
        out += ' {}="{}"'.format(key, html.escape(attrs[key]))
    out += '>'
    return out

def _make_xml_elem(tag, text, attr = []):
    'Write a flat xml element.'
    out = '    <' + tag
    for (key, val) in attr:
        out += ' {}="{}"'.format(key, val)
    if text:
        out += '>{}</{}>\n'.format(text, tag)
    else:
        out += ' />\n'
    return out

class EpubTOC(getebook.TOC):
    'Table of contents.'
    _head = ((
      '<?xml version="1.0" encoding="UTF-8"?>\n'
      '<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1" xml:lang="en-US">\n'
      '  <head>\n'
      '    <meta name="dtb:uid" content="{}" />\n'
      '    <meta name="dtb:depth" content="{}" />\n'
      '    <meta name="dtb:totalPageCount" content="0" />\n'
      '    <meta name="dtb:maxPageNumber" content="0" />\n'
      '  </head>\n'
      '  <docTitle>\n'
      '    <text>{}</text>\n'
      '  </docTitle>\n'
    ))
    _doc_author = ((
      '  <docAuthor>\n'
      '    <text>{}</text>\n'
      '  </docAuthor>\n'
    ))
    _navp = ((
      '{0}<navPoint id="nav{1}">\n'
      '{0}  <navLabel>\n'
      '{0}    <text>{2}</text>\n'
      '{0}  </navLabel>\n'
      '{0}  <content src="{3}" />\n'
    ))

    def _navp_xml(self, entry, indent_lvl):
        'Write xml for an entry and all its subentries.'
        xml = self._navp.format('  '*indent_lvl, str(entry.no), entry.text,
          entry.target)
        for sub in entry.entries:
            xml += self._navp_xml(sub, indent_lvl+1)
        xml += '  '*indent_lvl + '</navPoint>\n'
        return xml

    def write_xml(self, uid, title, authors):
        'Write the xml code for the table of contents.'
        xml = self._head.format(uid, self.max_depth, title)
        for aut in authors:
            xml += self._doc_author.format(aut)
        xml += '  <navMap>\n'
        for entry in self.entries:
            xml += self._navp_xml(entry, 2)
        xml += '  </navMap>\n</ncx>'
        return xml

class _Fileinfo:
    'Information about a component file of an epub.'
    def __init__(self, name, in_spine = True, guide_title = None,
                 guide_type = None):
        '''Initialize the object. If the file does not belong in the
        reading order, in_spine should be set to False. If it should
        appear in the guide, set guide_title and guide_type.'''
        self.name = name
        (self.ident, ext) = os.path.splitext(name)
        name_split = name.rsplit('.', 1)
        self.ident = name_split[0]
        self.in_spine = in_spine
        self.guide_title = guide_title
        self.guide_type = guide_type
        # Infer media-type from file extension
        ext = ext.lower()
        if ext in ('.htm', '.html', '.xhtml'):
            self.media_type = 'application/xhtml+xml'
        elif ext in ('.png', '.gif', '.jpeg'):
            self.media_type = 'image/' + ext
        elif ext == '.jpg':
            self.media_type = 'image/jpeg'
        elif ext == '.css':
            self.media_type = 'text/css'
        elif ext == '.ncx':
            self.media_type = 'application/x-dtbncx+xml'
        else:
            raise ValueError('Can\'t infer media-type from extension: %s' % ext)
    def manifest_entry(self):
        'Write the XML element for the manifest.'
        return _make_xml_elem('item', '',
          [
            ('href', self.name),
            ('id', self.ident),
            ('media-type', self.media_type)
          ])
    def spine_entry(self):
        '''Write the XML element for the spine.
        (Empty string if in_spine is False.)'''
        if self.in_spine:
            return _make_xml_elem('itemref', '', [('idref', self.ident)])
        else:
            return ''
    def guide_entry(self):
        '''Write the XML element for the guide.
        (Empty string if no guide title and type are given.)'''
        if self.guide_title and self.guide_type:
            return _make_xml_elem('reference', '',
              [
                ('title', self.guide_title),
                ('type', self.guide_type),
                ('href', self.name)
              ])
        else:
            return ''

class _EpubMeta:
    'Metadata entry for an epub file.'
    def __init__(self, tag, text, *args):
        '''The metadata entry is an XML element. *args is used for
        supplying the XML element's attributes as (key, value) pairs.'''
        self.tag = tag
        self.text = text
        self.attr = args
    def write_xml(self):
        'Write the XML element.'
        return _make_xml_elem(self.tag, self.text, self.attr)
    def __repr__(self):
        'Returns the text.'
        return self.text
    def __str__(self):
        'Returns the text.'
        return self.text

class _EpubDate(_EpubMeta):
    'Metadata element for the publication date.'
    _date_re = re.compile('^([0-9]{4})(-[0-9]{2}(-[0-9]{2})?)?$')
    def __init__(self, date):
        '''date must be a string of the form "YYYY[-MM[-DD]]". If it is
        not of this form, or if the date is invalid, ValueError is
        raised.'''
        m = self._date_re.match(date) 
        if not m:
            raise ValueError('invalid date format')
        year = int(m.group(1))
        try:
            mon = int(m.group(2)[1:])
            if mon < 0 or mon > 12:
                raise ValueError('month must be in 1..12')
        except IndexError:
            pass
        try:
            day = int(m.group(3)[1:])
            datetime.date(year, mon, day) # raises ValueError if invalid
        except IndexError:
            pass
        self.tag = 'dc:date'
        self.text = date
        self.attr = ()

class _EpubLang(_EpubMeta):
    'Metadata element for the language of the book.'
    _lang_re = re.compile('^[a-z]{2}(-[A-Z]{2})?$')
    def __init__(self, lang):
        '''lang must be a lower-case two-letter language code,
        optionally followed by a "-" and a upper-case two-letter country
        code. (e.g., "en", "en-US", "en-UK", "de", "de-DE", "de-AT")'''
        if self._lang_re.match(lang):
            self.tag = 'dc:language'
            self.text = lang
            self.attr = ()
        else:
            raise ValueError('invalid language format')

class Author(_EpubMeta):
    '''To control the file-as and role attribute for the authors, pass
    an Author object to the EpubBuilder instead of