#!/usr/bin/env python2
# vim:fileencoding=utf-8
from __future__ import (unicode_literals, division, absolute_import,
                        print_function)

__license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'

from lxml import etree

from calibre import prepare_string_for_xml as xml
from calibre.ebooks.oeb.polish.check.base import BaseError, WARN
from calibre.ebooks.oeb.polish.utils import guess_type
from calibre.ebooks.oeb.base import OPF, OPF2_NS, DC, DC11_NS, XHTML_MIME

class MissingSection(BaseError):

    def __init__(self, name, section_name):
        BaseError.__init__(self, _('The <%s> section is missing from the OPF') % section_name, name)
        self.HELP = xml(_(
            'The <%s> section is required in the OPF file. You have to create one.') % section_name)

class IncorrectIdref(BaseError):

    def __init__(self, name, idref, lnum):
        BaseError.__init__(self, _('idref="%s" points to unknown id') % idref, name, lnum)
        self.HELP = xml(_(
            'The idref="%s" points to an id that does not exist in the OPF') % idref)

class IncorrectCover(BaseError):

    def __init__(self, name, lnum, cover):
        BaseError.__init__(self, _('The meta cover tag points to an non-existent item'), name, lnum)
        self.HELP = xml(_(
            'The meta cover tag points to an item with id="%s" which does not exist in the manifest') % cover)

class NookCover(BaseError):

    HELP = _(
            'Some ebook readers such as the Nook fail to recognize covers if'
            ' the content attribute comes before the name attribute.'
            ' For maximum compatibility move the name attribute before the content attribute.')
    INDIVIDUAL_FIX = _('Move the name attribute before the content attribute')

    def __init__(self, name, lnum):
        BaseError.__init__(self, _('The meta cover tag has content before name'), name, lnum)

    def __call__(self, container):
        for cover in container.opf_xpath('//opf:meta[@name="cover" and @content]'):
            cover.set('content', cover.attrib.pop('content'))
        container.dirty(container.opf_name)
        return True

class IncorrectToc(BaseError):

    def __init__(self, name, lnum, bad_idref=None, bad_mimetype=None):
        if bad_idref is not None:
            msg = _('The item identified as the Table of Contents (%s) does not exist') % bad_idref
            self.HELP = _('There is no item with id="%s" in the manifest.') % bad_idref
        else:
            msg = _('The item identified as the Table of Contents has an incorrect media-type (%s)') % bad_mimetype
            self.HELP = _('The media type for the table of contents must be %s') % guess_type('a.ncx')
        BaseError.__init__(self, msg, name, lnum)

class MissingHref(BaseError):

    HELP = _('A file listed in the manifest is missing, you should either remove'
             ' it from the manifest or add the missing file to the book.')

    def __init__(self, name, href, lnum):
        BaseError.__init__(self, _('Item (%s) in manifest is missing') % href, name, lnum)
        self.bad_href = href
        self.INDIVIDUAL_FIX = _('Remove the entry for %s from the manifest') % href

    def __call__(self, container):
        [container.remove_from_xml(elem) for elem in container.opf_xpath('/opf:package/opf:manifest/opf:item[@href]')
         if elem.get('href') == self.bad_href]
        container.dirty(container.opf_name)
        return True

class NonLinearItems(BaseError):

    level = WARN
    has_multiple_locations = True

    HELP = xml(_('There are items marked as non-linear in the <spine>.'
                 ' These will be displayed in random order by different ebook readers.'
                 ' Some will ignore the non-linear attribute, some will display'
                 ' them at the end or the beginning of the book and some will'
                 ' fail to display them at all. Instead of using non-linear items'
                 ' simply place the items in the order you want them to be displayed.'))

    INDIVIDUAL_FIX = _('Mark all non-linear items as linear')

    def __init__(self, name, locs):
        BaseError.__init__(self, _('Non-linear items in the spine'), name)
        self.all_locations = [(name, x, None) for x in locs]

    def __call__(self, container):
        [elem.attrib.pop('linear') for elem in container.opf_xpath('//opf:spine/opf:itemref[@linear]')]
        container.dirty(container.opf_name)
        return True

class DuplicateHref(BaseError):

    has_multiple_locations = True

    INDIVIDUAL_FIX = _(
        'Remove all but the first duplicate item')

    def __init__(self, name, eid, locs, for_spine=False):
        loc = 'spine' if for_spine else 'manifest'
        BaseError.__init__(self, _('Duplicate item in {0}: {1}').format(loc, eid), name)
        self.HELP = _(
            'The item {0} is present more than once in the {2} in {1}. This is'
            ' not allowed.').format(eid, name, loc)
        self.all_locations = [(name, lnum, None) for lnum in sorted(locs)]
        self.duplicate_href = eid
        self.xpath = '/opf:package/opf:' + ('spine/opf:itemref[@idref]' if for_spine else 'manifest/opf:item[@href]')
        self.attr = 'idref' if for_spine else 'href'

    def __call__(self, container):
        items = [e for e in container.opf_xpath(self.xpath) if e.get(self.attr) == self.duplicate_href]
        [container.remove_from_xml(e) for e in items[1:]]
        container.dirty(self.name)
        return True

class MultipleCovers(BaseError):

    has_multiple_locations = True
    HELP = xml(_(
        'There is more than one <meta name="cover"> tag defined. There should be only one.'))
    INDIVIDUAL_FIX = _('Remove all but the first meta cover tag')

    def __init__(self, name, locs):
        BaseError.__init__(self, _('There is more than one cover defined'), name)
        self.all_locations = [(name, lnum, None) for lnum in sorted(locs)]

    def __call__(self, container):
        items = [e for e in container.opf_xpath('/opf:package/opf:metadata/opf:meta[@name="cover"]')]
        [container.remove_from_xml(e) for e in items[1:]]
        container.dirty(self.name)
        return True

class NoUID(BaseError):

    HELP = xml(_(
        'The OPF must have a unique identifier, i.e. a <dc:identifier> element whose id is referenced'
        ' by the <package> element'))
    INDIVIDUAL_FIX = _('Auto-generate a unique identifier')

    def __init__(self, name):
        BaseError.__init__(self, _('The OPF has no unique identifier'), name)

    def __call__(self, container):
        import uuid
        opf = container.opf
        uid = str(uuid.uuid4())
        opf.set('unique-identifier', uid)
        m = container.opf_xpath('/opf:package/opf:metadata')
        if not m:
            m = [container.opf.makeelement(OPF('metadata'), nsmap={'dc':DC11_NS})]
            container.insert_into_xml(container.opf, m[0], 0)
        m = m[0]
        dc = m.makeelement(DC('identifier'), id=uid, nsmap={'opf':OPF2_NS})
        dc.set(OPF('scheme'), 'uuid')
        dc.text = uid
        container.insert_into_xml(m, dc)
        container.dirty(container.opf_name)
        return True

class BadSpineMime(BaseError):

    def __init__(self, name, iid, mt, lnum, opf_name):
        BaseError.__init__(self, _('Incorrect media-type for spine item'), opf_name, lnum)
        self.HELP = _(
            'The item {0} present in the spine has the media-type {1}. '
            ' Most ebook software cannot handle non-HTML spine items. '
            ' If the item is actually HTML, you should change its media-type to {2}.'
            ' If it is not-HTML you should consider replacing it with an HTML item, as it'
            ' is unlikely to work in most readers.').format(name, mt, XHTML_MIME)
        if iid is not None:
            self.INDIVIDUAL_FIX = _('Change the media-type to %s') % XHTML_MIME
            self.iid = iid

    def __call__(self, container):
        container.opf_xpath('/opf:package/opf:manifest/opf:item[@id=%r]' % self.iid)[0].set(
            'media-type', XHTML_MIME)
        container.dirty(container.opf_name)
        container.refresh_mime_map()
        return True

def check_opf(container):
    errors = []

    if container.opf.tag != OPF('package'):
        err = BaseError(_('The OPF does not have the correct root element'), container.opf_name)
        err.HELP = xml(_(
            'The opf must have the root element <package> in namespace {0}, like this: <package xmlns="{0}">')).format(OPF2_NS)
        errors.append(err)

    for tag in ('metadata', 'manifest', 'spine'):
        if not container.opf_xpath('/opf:package/opf:' + tag):
            errors.append(MissingSection(container.opf_name, tag))

    all_ids = set(container.opf_xpath('//*/@id'))
    for elem in container.opf_xpath('//*[@idref]'):
        if elem.get('idref') not in all_ids:
            errors.append(IncorrectIdref(container.opf_name, elem.get('idref'), elem.sourceline))

    nl_items = [elem.sourceline for elem in container.opf_xpath('//opf:spine/opf:itemref[@linear="no"]')]
    if nl_items:
        errors.append(NonLinearItems(container.opf_name, nl_items))

    seen, dups = {}, {}
    for item in container.opf_xpath('/opf:package/opf:manifest/opf:item[@href]'):
        href = item.get('href')
        if not container.exists(container.href_to_name(href, container.opf_name)):
            errors.append(MissingHref(container.opf_name, href, item.sourceline))
        if href in seen:
            if href not in dups:
                dups[href] = [seen[href]]
            dups[href].append(item.sourceline)
        else:
            seen[href] = item.sourceline
    errors.extend(DuplicateHref(container.opf_name, eid, locs) for eid, locs in dups.iteritems())

    seen, dups = {}, {}
    for item in container.opf_xpath('/opf:package/opf:spine/opf:itemref[@idref]'):
        ref = item.get('idref')
        if ref in seen:
            if ref not in dups:
                dups[ref] = [seen[ref]]
            dups[ref].append(item.sourceline)
        else:
            seen[ref] = item.sourceline
    errors.extend(DuplicateHref(container.opf_name, eid, locs, for_spine=True) for eid, locs in dups.iteritems())

    spine = container.opf_xpath('/opf:package/opf:spine[@toc]')
    if spine:
        spine = spine[0]
        mitems = [x for x in container.opf_xpath('/opf:package/opf:manifest/opf:item[@id]') if x.get('id') == spine.get('toc')]
        if mitems:
            mitem = mitems[0]
            if mitem.get('media-type', '') != guess_type('a.ncx'):
                errors.append(IncorrectToc(container.opf_name, mitem.sourceline, bad_mimetype=mitem.get('media-type')))
        else:
            errors.append(IncorrectToc(container.opf_name, spine.sourceline, bad_idref=spine.get('toc')))

    covers = container.opf_xpath('/opf:package/opf:metadata/opf:meta[@name="cover"]')
    if len(covers) > 0:
        if len(covers) > 1:
            errors.append(MultipleCovers(container.opf_name, [c.sourceline for c in covers]))
        manifest_ids = set(container.opf_xpath('/opf:package/opf:manifest/opf:item/@id'))
        for cover in covers:
            if cover.get('content', None) not in manifest_ids:
                errors.append(IncorrectCover(container.opf_name, cover.sourceline, cover.get('content', '')))
            raw = etree.tostring(cover)
            try:
                n, c = raw.index('name="'), raw.index('content="')
            except ValueError:
                n = c = -1
            if n > -1 and c > -1 and n > c:
                errors.append(NookCover(container.opf_name, cover.sourceline))

    uid = container.opf.get('unique-identifier', None)
    if uid is None or not container.opf_xpath('/opf:package/opf:metadata/dc:identifier[@id=%r]' % uid):
        errors.append(NoUID(container.opf_name))

    for item, name, linear in container.spine_iter:
        mt = container.mime_map[name]
        if mt != XHTML_MIME:
            iid = item.get('idref', None)
            lnum = None
            if iid:
                mitem = container.opf_xpath('/opf:package/opf:manifest/opf:item[@id=%r]' % iid)
                if mitem:
                    lnum = mitem[0].sourceline
                else:
                    iid = None
            errors.append(BadSpineMime(name, iid, mt, lnum, container.opf_name))

    return errors
