/************************************************************************
**
**  Copyright (C) 2013              John Schember <john@nachtimwald.com>
**  Copyright (C) 2009, 2010, 2011  Strahinja Markovic  <strahinja.markovic@gmail.com>
**
**  This file is part of Sigil.
**
**  Sigil 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 3 of the License, or
**  (at your option) any later version.
**
**  Sigil 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 Sigil.  If not, see <http://www.gnu.org/licenses/>.
**
*************************************************************************/

#include <boost/tuple/tuple.hpp>
// XercesExtensions
#include <XmlUtils.h>

#include <QtCore/QBuffer>
#include <QtCore/QDate>
#include <QtCore/QDir>
#include <QtCore/QFileInfo>
#include <QtCore/QUuid>
#include <QRegularExpression>

#include "BookManipulation/CleanSource.h"
#include "BookManipulation/XercesCppUse.h"
#include "BookManipulation/XhtmlDoc.h"
#include "Misc/Language.h"
#include "Misc/Utility.h"
#include "ResourceObjects/HTMLResource.h"
#include "ResourceObjects/ImageResource.h"
#include "ResourceObjects/NCXResource.h"
#include "ResourceObjects/OPFResource.h"
#include "sigil_constants.h"

using boost::tie;
namespace xe = XercesExt;

static const QString SIGIL_VERSION_META_NAME  = "Sigil version";
static const QString OPF_XML_NAMESPACE        = "http://www.idpf.org/2007/opf";
static const QString FALLBACK_MIMETYPE        = "text/plain";
static const QString ITEM_ELEMENT_TEMPLATE    = "<item id=\"%1\" href=\"%2\" media-type=\"%3\"/>";
static const QString ITEMREF_ELEMENT_TEMPLATE = "<itemref idref=\"%1\"/>";
static const QString OPF_REWRITTEN_COMMENT    = "<!-- Your OPF file was broken so Sigil "
        "tried to rebuild it for you. -->";

static const QString TEMPLATE_TEXT =
    "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
    "<package version=\"2.0\" xmlns=\"http://www.idpf.org/2007/opf\" unique-identifier=\"BookId\">\n\n"
    "  <metadata xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:opf=\"http://www.idpf.org/2007/opf\">\n"
    "    <dc:identifier opf:scheme=\"UUID\" id=\"BookId\">urn:uuid:%1</dc:identifier>\n"
    "  </metadata>\n\n"
    "  <manifest>\n"
    "    <item id=\"ncx\" href=\"toc.ncx\" media-type=\"application/x-dtbncx+xml\"/>\n"
    "  </manifest>\n\n"
    "  <spine toc=\"ncx\">\n"
    "  </spine>\n\n"
    "  <guide>\n\n</guide>\n\n"
    "</package>";


OPFResource::OPFResource(const QString &mainfolder, const QString &fullfilepath, QObject *parent)
    : XMLResource(mainfolder, fullfilepath, parent)
{
    CreateMimetypes();
    FillWithDefaultText();
    // Make sure the file exists on disk.
    // Among many reasons, this also solves the problem
    // with the Book Browser not displaying an icon for this resource.
    SaveToDisk();
}


bool OPFResource::RenameTo(const QString &new_filename)
{
    // The user is not allowed to rename the OPF file.
    return false;
}


Resource::ResourceType OPFResource::Type() const
{
    return Resource::OPFResourceType;
}


GuideSemantics::GuideSemanticType OPFResource::GetGuideSemanticTypeForResource(const Resource &resource) const
{
    QReadLocker locker(&GetLock());
    shared_ptr<xc::DOMDocument> document = GetDocument();
    return GetGuideSemanticTypeForResource(resource, *document);
}

QString OPFResource::GetGuideSemanticNameForResource(Resource *resource)
{
    return GuideSemantics::Instance().GetGuideName(GetGuideSemanticTypeForResource(*resource));
}

QHash <QString, QString>  OPFResource::GetGuideSemanticNameForPaths()
{
    QHash <QString, QString> semantic_types;

    QReadLocker locker(&GetLock());
    shared_ptr<xc::DOMDocument> document = GetDocument();

    QList<xc::DOMElement *> references =
        XhtmlDoc::GetTagMatchingDescendants(*document, "reference", OPF_XML_NAMESPACE);

    foreach(xc::DOMElement *reference, references) {
        const QString &href = XtoQ(reference->getAttribute(QtoX("href")));
        QStringList parts = href.split('#', QString::KeepEmptyParts);

        const QString &type_text = XtoQ(reference->getAttribute(QtoX("type")));
        GuideSemantics::GuideSemanticType type =
            GuideSemantics::Instance().MapReferenceTypeToGuideEnum(type_text);
        semantic_types[parts.at(0)] = GuideSemantics::Instance().GetGuideName(type);
    }

    // Cover image semantics don't use reference
    xc::DOMElement *meta = GetCoverMeta(*document);
    if (meta) {

        QString cover_id = XtoQ(meta->getAttribute(QtoX("content")));

        QList<xc::DOMElement *> items =
            XhtmlDoc::GetTagMatchingDescendants(*document, "item", OPF_XML_NAMESPACE);
        foreach(xc::DOMElement *item, items) {
            QString id = XtoQ(item->getAttribute(QtoX("id")));

            if (id == cover_id) {
                QString href = XtoQ(item->getAttribute(QtoX("href")));
                GuideSemantics::GuideSemanticType type =
                    GuideSemantics::Instance().MapReferenceTypeToGuideEnum("cover");
                semantic_types[href] = GuideSemantics::Instance().GetGuideName(type);
            }
        }
    }

    return semantic_types;
}

QHash <Resource *, int>  OPFResource::GetReadingOrderAll( const QList <Resource *> resources)
{
    QHash <Resource *, int> reading_order;

    QReadLocker locker(&GetLock());
    shared_ptr<xc::DOMDocument> document = GetDocument();

    QList<xc::DOMElement *> itemrefs =
        XhtmlDoc::GetTagMatchingDescendants(*document, "itemref", OPF_XML_NAMESPACE);

    QHash<QString, int> id_order;
    for (int i = 0; i < itemrefs.count(); ++i) {
        QString idref = XtoQ(itemrefs[ i ]->getAttribute(QtoX("idref")));
        id_order[idref] = i;
    }

    QHash<Resource *, QString> id_mapping = GetResourceManifestIDMapping(resources, *document);

    foreach(Resource *resource, resources) {
        reading_order[resource] = id_order[id_mapping[resource]];
    }

    return reading_order;
}

int OPFResource::GetReadingOrder(const ::HTMLResource &html_resource) const
{
    QReadLocker locker(&GetLock());
    shared_ptr<xc::DOMDocument> document = GetDocument();
    const Resource &resource = *static_cast<const Resource *>(&html_resource);
    QString resource_id = GetResourceManifestID(resource, *document);
    QList<xc::DOMElement *> itemrefs =
        XhtmlDoc::GetTagMatchingDescendants(*document, "itemref", OPF_XML_NAMESPACE);

    for (int i = 0; i < itemrefs.count(); ++i) {
        QString idref = XtoQ(itemrefs[ i ]->getAttribute(QtoX("idref")));

        if (resource_id == idref) {
            return i;
        }
    }

    return -1;
}


QString OPFResource::GetMainIdentifierValue() const
{
    QReadLocker locker(&GetLock());
    shared_ptr<xc::DOMDocument> document = GetDocument();
    return XtoQ(GetMainIdentifier(*document).getTextContent());
}


void OPFResource::SaveToDisk(bool book_wide_save)
{
    QString text = GetText();
    // Work around for covers appearing on the Nook. Issue 942.
    text = text.replace(QRegularExpression("<meta content=\"([^\"]+)\" name=\"cover\""), "<meta name=\"cover\" content=\"\\1\"");
    SetText(text);
    TextResource::SaveToDisk(book_wide_save);
}

QString OPFResource::GetUUIDIdentifierValue()
{
    EnsureUUIDIdentifierPresent();
    QReadLocker locker(&GetLock());
    shared_ptr<xc::DOMDocument> document = GetDocument();
    QList<xc::DOMElement *> identifiers =
        XhtmlDoc::GetTagMatchingDescendants(*document, "identifier", DUBLIN_CORE_NS);
    foreach(xc::DOMElement * identifier, identifiers) {
        QString value = XtoQ(identifier->getTextContent()).remove("urn:uuid:");

        if (!QUuid(value).isNull()) {
            return value;
        }
    }
    // EnsureUUIDIdentifierPresent should ensure we
    // never reach here.
    Q_ASSERT(false);
    return QString();
}


void OPFResource::EnsureUUIDIdentifierPresent()
{
    QWriteLocker locker(&GetLock());
    shared_ptr<xc::DOMDocument> document = GetDocument();
    QList<xc::DOMElement *> identifiers = XhtmlDoc::GetTagMatchingDescendants(*document, "identifier", DUBLIN_CORE_NS);

    foreach(xc::DOMElement *identifier, identifiers) {
        QString value = XtoQ(identifier->getTextContent()).remove("urn:uuid:");

        if (!QUuid(value).isNull()) {
            return;
        }
    }
    QString uuid = Utility::CreateUUID();
    WriteIdentifier("UUID", uuid, *document);
    UpdateTextFromDom(*document);
}

QString OPFResource::AddNCXItem(const QString &ncx_path)
{
    QWriteLocker locker(&GetLock());
    QString path_to_oebps_folder = QFileInfo(GetFullPath()).absolutePath() + "/";
    QString ncx_oebps_path  = Utility::URLEncodePath(QString(ncx_path).remove(path_to_oebps_folder));
    shared_ptr<xc::DOMDocument> document = GetDocument();
    QHash<QString, QString> attributes;
    attributes[ "id"         ] = GetUniqueID("ncx", *document);
    attributes[ "href"       ] = Utility::URLEncodePath(ncx_oebps_path);
    attributes[ "media-type" ] = "application/x-dtbncx+xml";
    xc::DOMElement *new_item = XhtmlDoc::CreateElementInDocument(
                                   "item", OPF_XML_NAMESPACE, *document, attributes);
    xc::DOMElement *manifest = GetManifestElement(*document);
    if (!manifest) {
        return QString();
    }
    manifest->appendChild(new_item);
    UpdateTextFromDom(*document);
    return attributes[ "id" ];
}

void OPFResource::UpdateNCXOnSpine(const QString &new_ncx_id)
{
    QWriteLocker locker(&GetLock());
    shared_ptr<xc::DOMDocument> document = GetDocument();
    xc::DOMElement *spine = GetSpineElement(*document);
    if (!spine) {
        return;
    }
    QString ncx_id = XtoQ(spine->getAttribute(QtoX("toc")));

    if (new_ncx_id != ncx_id) {
        spine->setAttribute(QtoX("toc"), QtoX(new_ncx_id));
        UpdateTextFromDom(*document);
    }
}

void OPFResource::UpdateNCXLocationInManifest(const ::NCXResource &ncx)
{
    QWriteLocker locker(&GetLock());
    shared_ptr<xc::DOMDocument> document = GetDocument();
    xc::DOMElement *spine = GetSpineElement(*document);
    QString ncx_id = XtoQ(spine->getAttribute(QtoX("toc")));
    QList<xc::DOMElement *> items = XhtmlDoc::GetTagMatchingDescendants(*document, "item", OPF_XML_NAMESPACE);
    foreach(xc::DOMElement * item, items) {
        QString id = XtoQ(item->getAttribute(QtoX("id")));

        if (id == ncx_id) {
            item->setAttribute(QtoX("href"), QtoX(ncx.Filename()));
            break;
        }
    }
    UpdateTextFromDom(*document);
}


void OPFResource::AddSigilVersionMeta()
{
    QWriteLocker locker(&GetLock());
    shared_ptr<xc::DOMDocument> document = GetDocument();
    QList<xc::DOMElement *> metas =
        XhtmlDoc::GetTagMatchingDescendants(*document, "meta", OPF_XML_NAMESPACE);
    foreach(xc::DOMElement * meta, metas) {
        QString name = XtoQ(meta->getAttribute(QtoX("name")));

        if (name == SIGIL_VERSION_META_NAME) {
            meta->setAttribute(QtoX("content"), QtoX(SIGIL_VERSION));
            UpdateTextFromDom(*document);
            return;
        }
    }
    xc::DOMElement *element = document->createElementNS(QtoX(OPF_XML_NAMESPACE), QtoX("meta"));
    element->setAttribute(QtoX("name"),    QtoX("Sigil version"));
    element->setAttribute(QtoX("content"), QtoX(SIGIL_VERSION));
    xc::DOMElement *metadata = GetMetadataElement(*document);
    if (!metadata) {
        return;
    }
    metadata->appendChild(element);
    UpdateTextFromDom(*document);
}


bool OPFResource::IsCoverImage(const ::ImageResource &image_resource) const
{
    QReadLocker locker(&GetLock());
    shared_ptr<xc::DOMDocument> document = GetDocument();
    return IsCoverImageCheck(image_resource, *document);
}

bool OPFResource::IsCoverImageCheck(const Resource &resource, xc::DOMDocument &document) const
{
    QString resource_id = GetResourceManifestID(resource, document);
    return IsCoverImageCheck(resource_id, document);
}

bool OPFResource::IsCoverImageCheck(QString resource_id, xc::DOMDocument &document) const
{
    xc::DOMElement *meta = GetCoverMeta(document);

    if (meta) {
        return XtoQ(meta->getAttribute(QtoX("content"))) == resource_id;
    }

    return false;
}


bool OPFResource::CoverImageExists() const
{
    QReadLocker locker(&GetLock());
    shared_ptr<xc::DOMDocument> document = GetDocument();
    return GetCoverMeta(*document) != NULL;
}


void OPFResource::AutoFixWellFormedErrors()
{
    QWriteLocker locker(&GetLock());
    UpdateTextFromDom(*CreateOPFFromScratch());
}


QStringList OPFResource::GetSpineOrderFilenames() const
{
    QReadLocker locker(&GetLock());
    shared_ptr<xc::DOMDocument> document = GetDocument();
    QList<xc::DOMElement *> items =
        XhtmlDoc::GetTagMatchingDescendants(*document, "item", OPF_XML_NAMESPACE);
    QHash<QString, QString> id_to_filename_mapping;
    foreach(xc::DOMElement * item, items) {
        QString id   = XtoQ(item->getAttribute(QtoX("id")));
        QString href = XtoQ(item->getAttribute(QtoX("href")));
        id_to_filename_mapping[ id ] = QFileInfo(href).fileName();
    }
    QList<xc::DOMElement *> itemrefs =
        XhtmlDoc::GetTagMatchingDescendants(*document, "itemref", OPF_XML_NAMESPACE);
    QStringList filenames_in_reading_order;
    foreach(xc::DOMElement * itemref, itemrefs) {
        QString idref = XtoQ(itemref->getAttribute(QtoX("idref")));

        if (id_to_filename_mapping.contains(idref)) {
            filenames_in_reading_order.append(Utility::URLDecodePath(id_to_filename_mapping[ idref ]));
        }
    }
    return filenames_in_reading_order;
}


void OPFResource::SetSpineOrderFromFilenames(const QStringList spineOrder)
{
    QWriteLocker locker(&GetLock());
    shared_ptr<xc::DOMDocument> document = GetDocument();
    QList<xc::DOMElement *> items =
        XhtmlDoc::GetTagMatchingDescendants(*document, "item", OPF_XML_NAMESPACE);
    QHash<QString, QString> filename_to_id_mapping;
    foreach(xc::DOMElement * item, items) {
        QString id   = XtoQ(item->getAttribute(QtoX("id")));
        QString href = XtoQ(item->getAttribute(QtoX("href")));
    }
    QList<xc::DOMElement *> itemrefs =
        XhtmlDoc::GetTagMatchingDescendants(*document, "itemref", OPF_XML_NAMESPACE);
    QList<xc::DOMElement *> newSpine;
    foreach(QString spineItem, spineOrder) {
        QString id = filename_to_id_mapping[ spineItem ];
        bool found = false;
        QListIterator<xc::DOMElement *> spineElementSearch(itemrefs);

        while (spineElementSearch.hasNext() && !found) {
            xc::DOMElement *spineElement = spineElementSearch.next();

            if (XtoQ(spineElement->getAttribute(QtoX("idref"))) == spineItem) {
                newSpine.append(spineElement);
                found = true;
            }
        }
    }
    xc::DOMElement *spine = GetSpineElement(*document);
    if (!spine) {
        return;
    }
    XhtmlDoc::RemoveChildren(*spine);
    QListIterator<xc::DOMElement *> spineWriter(newSpine);

    while (spineWriter.hasNext()) {
        spine->appendChild(spineWriter.next());
    }

    UpdateTextFromDom(*document);
}


QList<Metadata::MetaElement> OPFResource::GetDCMetadata() const
{
    QReadLocker locker(&GetLock());
    shared_ptr<xc::DOMDocument> document = GetDocument();
    QList<xc::DOMElement *> dc_elements =
        XhtmlDoc::GetTagMatchingDescendants(*document, "*", DUBLIN_CORE_NS);
    QList<Metadata::MetaElement> metadata;
    foreach(xc::DOMElement * dc_element, dc_elements) {
        // Map the names in the OPF file to internal names
        Metadata::MetaElement book_meta = Metadata::Instance().MapToBookMetadata(*dc_element);

        if (!book_meta.name.isEmpty() && !book_meta.value.toString().isEmpty()) {
            metadata.append(book_meta);
        }
    }
    return metadata;
}


QList<QVariant> OPFResource::GetDCMetadataValues(QString text) const
{
    QList<QVariant> values;
    foreach(Metadata::MetaElement meta, GetDCMetadata()) {
        if (meta.name == text) {
            values.append(meta.value);
        }
    }
    return values;
}


void OPFResource::SetDCMetadata(const QList<Metadata::MetaElement> &metadata)
{
    QWriteLocker locker(&GetLock());
    shared_ptr<xc::DOMDocument> document = GetDocument();
    RemoveDCElements(*document);
    foreach(Metadata::MetaElement book_meta, metadata) {
        MetadataDispatcher(book_meta, *document);
    }
    SetMetaElementsLast(*document);
    UpdateTextFromDom(*document);
}


void OPFResource::AddResource(const Resource &resource)
{
    QWriteLocker locker(&GetLock());
    shared_ptr<xc::DOMDocument> document = GetDocument();
    QHash<QString, QString> attributes;
    attributes[ "id"         ] = GetUniqueID(GetValidID(resource.Filename()), *document);
    attributes[ "href"       ] = Utility::URLEncodePath(resource.GetRelativePathToOEBPS());
    attributes[ "media-type" ] = GetResourceMimetype(resource);
    xc::DOMElement *new_item = XhtmlDoc::CreateElementInDocument(
                                   "item", OPF_XML_NAMESPACE, *document, attributes);
    xc::DOMElement *manifest = GetManifestElement(*document);
    if (!manifest) {
        return;
    }
    manifest->appendChild(new_item);

    if (resource.Type() == Resource::HTMLResourceType) {
        AppendToSpine(attributes[ "id" ], *document);
    }

    UpdateTextFromDom(*document);
}

void OPFResource::RemoveCoverMetaForImage(const Resource &resource, xc::DOMDocument &document)
{
    xc::DOMElement *meta = GetCoverMeta(document);
    QString resource_id = GetResourceManifestID(resource, document);

    // Remove entry if there is a cover in meta and if this file is marked as cover
    if (meta && XtoQ(meta->getAttribute(QtoX("content"))) == resource_id) {
        GetMetadataElement(document)->removeChild(meta);
    }
}

void OPFResource::AddCoverMetaForImage(const Resource &resource, xc::DOMDocument &document)
{
    xc::DOMElement *meta = GetCoverMeta(document);
    QString resource_id = GetResourceManifestID(resource, document);

    // If a cover entry exists, update its id, else create one
    if (meta) {
        meta->setAttribute(QtoX("content"), QtoX(resource_id));
    } else {
        QHash<QString, QString> attributes;
        attributes[ "name"    ] = "cover";
        attributes[ "content" ] = resource_id;
        xc::DOMElement *new_meta = XhtmlDoc::CreateElementInDocument(
                                       "meta", OPF_XML_NAMESPACE, document, attributes);
        xc::DOMElement *metadata = GetMetadataElement(document);
        metadata->appendChild(new_meta);
    }
}

void OPFResource::RemoveResource(const Resource &resource)
{
    QWriteLocker locker(&GetLock());
    shared_ptr<xc::DOMDocument> document  = GetDocument();
    xc::DOMElement *manifest                = GetManifestElement(*document);
    if (!manifest) {
        return;
    }
    std::vector<xc::DOMElement *> children = xe::GetElementChildren(*manifest);
    QString resource_oebps_path             = Utility::URLEncodePath(resource.GetRelativePathToOEBPS());
    QString item_id;

    // Delete the meta tag for cover images before deleting the manifest entry
    if (resource.Type() == Resource::ImageResourceType) {
        RemoveCoverMetaForImage(resource, *document);
    }

    foreach(xc::DOMElement * child, children) {
        QString href = XtoQ(child->getAttribute(QtoX("href")));

        if (href == resource_oebps_path) {
            item_id = XtoQ(child->getAttribute(QtoX("id")));
            manifest->removeChild(child);
            break;
        }
    }

    if (resource.Type() == Resource::HTMLResourceType) {
        RemoveFromSpine(item_id, *document);
        RemoveGuideReferenceForResource(resource, *document);
    }

    UpdateTextFromDom(*document);
}


void OPFResource::AddGuideSemanticType(
    const ::HTMLResource &html_resource,
    GuideSemantics::GuideSemanticType new_type)
{
    QWriteLocker locker(&GetLock());
    shared_ptr<xc::DOMDocument> document         = GetDocument();
    GuideSemantics::GuideSemanticType current_type = GetGuideSemanticTypeForResource(html_resource, *document);

    if (current_type != new_type) {
        RemoveDuplicateGuideTypes(new_type, *document);
        SetGuideSemanticTypeForResource(new_type, html_resource, *document);
    }
    // If the current type is the same as the new one,
    // we toggle it off.
    else {
        RemoveGuideReferenceForResource(html_resource, *document);
    }

    UpdateTextFromDom(*document);
}


void OPFResource::SetResourceAsCoverImage(const ::ImageResource &image_resource)
{
    QWriteLocker locker(&GetLock());
    shared_ptr<xc::DOMDocument> document = GetDocument();

    if (IsCoverImageCheck(image_resource, *document)) {
        RemoveCoverMetaForImage(image_resource, *document);
    } else {
        AddCoverMetaForImage(image_resource, *document);
    }

    UpdateTextFromDom(*document);
}


void OPFResource::UpdateSpineOrder(const QList<::HTMLResource *> html_files)
{
    QWriteLocker locker(&GetLock());
    shared_ptr<xc::DOMDocument> document = GetDocument();
    QHash<::HTMLResource *, xc::DOMElement *> itemref_mapping = GetItemrefsForHTMLResources(html_files, *document);
    xc::DOMElement *spine = GetSpineElement(*document);
    if (!spine) {
        return;
    }
    XhtmlDoc::RemoveChildren(*spine);
    foreach(::HTMLResource * resource, html_files) {
        xc::DOMElement *itemref = itemref_mapping.value(resource, NULL);

        if (itemref) {
            spine->appendChild(itemref);
        }
    }
    UpdateTextFromDom(*document);
}


void OPFResource::ResourceRenamed(const Resource &resource, QString old_full_path)
{
    QWriteLocker locker(&GetLock());
    shared_ptr<xc::DOMDocument> document = GetDocument();
    QString path_to_oebps_folder = QFileInfo(GetFullPath()).absolutePath() + "/";
    QString resource_oebps_path  = Utility::URLEncodePath(QString(old_full_path).remove(path_to_oebps_folder));
    QList<xc::DOMElement *> items =
        XhtmlDoc::GetTagMatchingDescendants(*document, "item", OPF_XML_NAMESPACE);
    QString old_id;
    QString new_id;
    foreach(xc::DOMElement * item, items) {
        QString href = XtoQ(item->getAttribute(QtoX("href")));

        if (href == resource_oebps_path) {
            item->setAttribute(QtoX("href"), QtoX(Utility::URLEncodePath(resource.GetRelativePathToOEBPS())));
            old_id = XtoQ(item->getAttribute(QtoX("id")));
            new_id = GetUniqueID(GetValidID(resource.Filename()), *document);
            item->setAttribute(QtoX("id"), QtoX(new_id));
            break;
        }
    }
    UpdateItemrefID(old_id, new_id, *document);

    if (resource.Type() == Resource::ImageResourceType) {
        // Change meta entry for cover if necessary
        // Check using IDs since file is already renamed
        if (IsCoverImageCheck(old_id, *document)) {
            // Add will automatically replace an existing id
            // Assumes only one cover but removing duplicates
            // can cause timing issues
            AddCoverMetaForImage(resource, *document);
        }
    }

    UpdateTextFromDom(*document);
}


void OPFResource::AppendToSpine(const QString &id, xc::DOMDocument &document)
{
    QHash<QString, QString> attributes;
    attributes[ "idref" ] = id;
    xc::DOMElement *new_item = XhtmlDoc::CreateElementInDocument(
                                   "itemref", OPF_XML_NAMESPACE, document, attributes);
    xc::DOMElement *spine = GetSpineElement(document);
    if (!spine) {
        return;
    }
    spine->appendChild(new_item);
}


void OPFResource::RemoveFromSpine(const QString &id, xc::DOMDocument &document)
{
    xc::DOMElement *spine = GetSpineElement(document);
    if (!spine) {
        return;
    }
    std::vector<xc::DOMElement *> children = xe::GetElementChildren(*spine);
    foreach(xc::DOMElement * child, children) {
        QString idref = XtoQ(child->getAttribute(QtoX("idref")));

        if (idref == id) {
            spine->removeChild(child);
            break;
        }
    }
}


void OPFResource::UpdateItemrefID(const QString &old_id, const QString &new_id, xc::DOMDocument &document)
{
    xc::DOMElement *spine = GetSpineElement(document);
    std::vector<xc::DOMElement *> children = xe::GetElementChildren(*spine);
    foreach(xc::DOMElement * child, children) {
        QString idref = XtoQ(child->getAttribute(QtoX("idref")));

        if (idref == old_id) {
            child->setAttribute(QtoX("idref"), QtoX(new_id));
            break;
        }
    }
}


shared_ptr<xc::DOMDocument> OPFResource::GetDocument() const
{
    // The call to ProcessXML is needed because even though we have well-formed
    // checks tied to "focus lost" events of the OPF tab, on Win XP those events
    // are sometimes not delivered at all. Blame MS. In the mean time, this
    // work-around makes sure we get valid XML into Xerces no matter what.
    shared_ptr<xc::DOMDocument> document =
        XhtmlDoc::LoadTextIntoDocument(CleanSource::ProcessXML(GetText()));

    if (!BasicStructurePresent(*document)) {
        document = CreateOPFFromScratch(document.get());
    }

    // For NCX files, the default of standalone == false should remain
    document->setXmlStandalone(true);
    return document;
}


xc::DOMElement *OPFResource::GetPackageElement(const xc::DOMDocument &document)
{
    QList<xc::DOMElement *> packages = XhtmlDoc::GetTagMatchingDescendants(document, "package", OPF_XML_NAMESPACE);
    if (!packages.isEmpty()) {
        return packages[0];
    }
    return NULL;
}


xc::DOMElement *OPFResource::GetMetadataElement(const xc::DOMDocument &document)
{
    QList<xc::DOMElement *> metadatas = XhtmlDoc::GetTagMatchingDescendants(document, "metadata", OPF_XML_NAMESPACE);
    if (!metadatas.isEmpty()) {
        return metadatas[0];
    }
    return NULL;
}


xc::DOMElement *OPFResource::GetManifestElement(const xc::DOMDocument &document)
{
    QList<xc::DOMElement *> manifests = XhtmlDoc::GetTagMatchingDescendants(document, "manifest", OPF_XML_NAMESPACE);
    if (!manifests.isEmpty()) {
        return manifests[0];
    }
    return NULL;
}


xc::DOMElement *OPFResource::GetSpineElement(const xc::DOMDocument &document)
{
    QList<xc::DOMElement *> spines = XhtmlDoc::GetTagMatchingDescendants(document, "spine", OPF_XML_NAMESPACE);
    if (!spines.isEmpty()) {
        return spines[0];
    }
    return NULL;
}


xc::DOMElement *OPFResource::GetGuideElement(const xc::DOMDocument &document)
{
    QList<xc::DOMElement *> guides = XhtmlDoc::GetTagMatchingDescendants(document, "guide", OPF_XML_NAMESPACE);
    if (!guides.isEmpty()) {
        return guides[0];
    }
    return NULL;
}


xc::DOMElement *OPFResource::GetGuideElement(xc::DOMDocument &document)
{
    QList<xc::DOMElement *> guides = XhtmlDoc::GetTagMatchingDescendants(document, "guide", OPF_XML_NAMESPACE);

    if (!guides.isEmpty()) {
        return guides[0];
    }

    xc::DOMElement *guide = XhtmlDoc::CreateElementInDocument("guide", OPF_XML_NAMESPACE, document, QHash<QString, QString>());
    xc::DOMElement &package = *document.getDocumentElement();
    package.appendChild(guide);
    return guide;
}


xc::DOMElement *OPFResource::GetGuideReferenceForResource(const Resource &resource, const xc::DOMDocument &document)
{
    QString resource_oebps_path         = Utility::URLEncodePath(resource.GetRelativePathToOEBPS());
    QList<xc::DOMElement *> references =
        XhtmlDoc::GetTagMatchingDescendants(document, "reference", OPF_XML_NAMESPACE);
    foreach(xc::DOMElement * reference, references) {
        const QString &href = XtoQ(reference->getAttribute(QtoX("href")));
        QStringList parts = href.split('#', QString::KeepEmptyParts);

        if (parts.at(0) == resource_oebps_path) {
            return reference;
        }
    }
    return NULL;
}


void OPFResource::RemoveGuideReferenceForResource(const Resource &resource, xc::DOMDocument &document)
{
    xc::DOMElement *guide = GetGuideElement(document);
    if (!guide) {
        return;
    }
    xc::DOMElement *elem = GetGuideReferenceForResource(resource, document);

    if (elem) {
        guide->removeChild(elem);
    }
}


GuideSemantics::GuideSemanticType OPFResource::GetGuideSemanticTypeForResource(
    const Resource &resource,
    xc::DOMDocument &document)
{
    xc::DOMElement *reference = GetGuideReferenceForResource(resource, document);

    if (reference) {
        QString type = XtoQ(reference->getAttribute(QtoX("type")));
        return GuideSemantics::Instance().MapReferenceTypeToGuideEnum(type);
    }

    return GuideSemantics::NoType;
}


void OPFResource::SetGuideSemanticTypeForResource(
    GuideSemantics::GuideSemanticType type,
    const Resource &resource,
    xc::DOMDocument &document)
{
    xc::DOMElement *reference = GetGuideReferenceForResource(resource, document);
    QString type_attribute;
    QString title_attribute;
    tie(type_attribute, title_attribute) = GuideSemantics::Instance().GetGuideTypeMapping()[ type ];

    if (reference) {
        reference->setAttribute(QtoX("type"), QtoX(type_attribute));
        reference->setAttribute(QtoX("title"), QtoX(title_attribute));
    } else {
        QHash<QString, QString> attributes;
        attributes[ "type"  ] = type_attribute;
        attributes[ "title" ] = title_attribute;
        attributes[ "href"  ] = Utility::URLEncodePath(resource.GetRelativePathToOEBPS());
        xc::DOMElement *new_item = XhtmlDoc::CreateElementInDocument(
                                       "reference", OPF_XML_NAMESPACE, document, attributes);
        xc::DOMElement *guide = GetGuideElement(document);
        if (guide) {
            guide->appendChild(new_item);
        }
    }
}


void OPFResource::RemoveDuplicateGuideTypes(
    GuideSemantics::GuideSemanticType new_type,
    xc::DOMDocument &document)
{
    // Industry best practice is to have only one
    // <guide> reference type instance per book.
    // For NoType, there is nothing to remove.
    if (new_type == GuideSemantics::NoType) {
        return;
    }

    xc::DOMElement *guide = GetGuideElement(document);
    if (!guide) {
        return;
    }
    QList<xc::DOMElement *> references = XhtmlDoc::GetTagMatchingDescendants(document, "reference", OPF_XML_NAMESPACE);
    foreach(xc::DOMElement * reference, references) {
        QString type_text = XtoQ(reference->getAttribute(QtoX("type")));
        GuideSemantics::GuideSemanticType current_type = GuideSemantics::Instance().MapReferenceTypeToGuideEnum(type_text);

        if (current_type == new_type) {
            guide->removeChild(reference);
            // There is no "break" statement here because we might
            // load an epub that has several instance of one guide type.
            // We preserve them on load, but if the user is intent on
            // changing them, then we enforce "one type instance per book".
        }
    }
}

// If there is no itemref for the resource, one will be created (but NOT
// attached to the spine element!).
// Also, it's possible that a NULL will be set as an itemref for a resource
// if that resource doesns't have an entry in the manifest.
QHash<::HTMLResource *, xc::DOMElement *> OPFResource::GetItemrefsForHTMLResources(
    const QList<::HTMLResource *> html_files,
    xc::DOMDocument &document)
{
    QList<xc::DOMElement *> itemrefs =
        XhtmlDoc::GetTagMatchingDescendants(document, "itemref", OPF_XML_NAMESPACE);
    QList<Resource *> resource_list;
    foreach(::HTMLResource * html_resource, html_files) {
        resource_list.append(static_cast<Resource *>(html_resource));
    }
    QHash<Resource *, QString> id_mapping = GetResourceManifestIDMapping(resource_list, document);
    QList<Resource *> htmls_without_itemrefs;
    QHash<::HTMLResource *, xc::DOMElement *> itmeref_mapping;
    foreach(Resource * resource, resource_list) {
        ::HTMLResource *html_resource = qobject_cast<::HTMLResource *>(resource);
        QString resource_id = id_mapping.value(resource, "");
        foreach(xc::DOMElement * itemref, itemrefs) {
            QString idref = XtoQ(itemref->getAttribute(QtoX("idref")));

            if (idref == resource_id) {
                itmeref_mapping[ html_resource ] = itemref;
                break;
            }
        }

        if (!itmeref_mapping.contains(html_resource)) {
            htmls_without_itemrefs.append(resource);
        }
    }
    foreach(Resource * resource, htmls_without_itemrefs) {
        QHash<QString, QString> attributes;
        QString resource_id = id_mapping.value(resource, "");
        ::HTMLResource *html_resource = qobject_cast<::HTMLResource *>(resource);

        if (resource_id.isEmpty()) {
            itmeref_mapping[ html_resource ] = NULL;
        }

        attributes[ "idref" ] = resource_id;
        xc::DOMElement *new_itemref = XhtmlDoc::CreateElementInDocument(
                                          "itemref", OPF_XML_NAMESPACE, document, attributes);
        itmeref_mapping[ html_resource ] = new_itemref;
    }
    return itmeref_mapping;
}


xc::DOMElement *OPFResource::GetCoverMeta(const xc::DOMDocument &document)
{
    QList<xc::DOMElement *> metas =
        XhtmlDoc::GetTagMatchingDescendants(document, "meta", OPF_XML_NAMESPACE);
    foreach(xc::DOMElement * meta, metas) {
        QString name = XtoQ(meta->getAttribute(QtoX("name")));

        if (name == "cover") {
            return meta;
        }
    }
    return NULL;
}


xc::DOMElement &OPFResource::GetMainIdentifier(const xc::DOMDocument &document)
{
    xc::DOMElement *identifier = GetMainIdentifierUnsafe(document);
    Q_ASSERT(identifier);
    return *identifier;
}


// This is here because we use it to check for the presence of the main identifier
// during DOM validation. But after that, the GetMainIdentifier func should be used.
xc::DOMElement *OPFResource::GetMainIdentifierUnsafe(const xc::DOMDocument &document)
{
    xc::DOMElement *package = GetPackageElement(document);
    if (!package) {
        return NULL;
    }
    QString unique_identifier = XtoQ(package->getAttribute(QtoX("unique-identifier")));
    QList<xc::DOMElement *> identifiers = XhtmlDoc::GetTagMatchingDescendants(document, "identifier", DUBLIN_CORE_NS);
    foreach(xc::DOMElement * identifier, identifiers) {
        QString id = XtoQ(identifier->getAttribute(QtoX("id")));

        if (id == unique_identifier) {
            return identifier;
        }
    }
    return NULL;
}

QString OPFResource::GetResourceManifestID(const Resource &resource, const xc::DOMDocument &document)
{
    QString oebps_path = Utility::URLEncodePath(resource.GetRelativePathToOEBPS());
    QList<xc::DOMElement *> items =
        XhtmlDoc::GetTagMatchingDescendants(document, "item", OPF_XML_NAMESPACE);
    foreach(xc::DOMElement * item, items) {
        QString href = XtoQ(item->getAttribute(QtoX("href")));

        if (href == oebps_path) {
            return XtoQ(item->getAttribute(QtoX("id")));
        }
    }
    return QString();
}


QHash<Resource *, QString> OPFResource::GetResourceManifestIDMapping(
    const QList<Resource *> resources,
    const xc::DOMDocument &document)
{
    QHash<Resource *, QString> id_mapping;
    QList<xc::DOMElement *> items =
        XhtmlDoc::GetTagMatchingDescendants(document, "item", OPF_XML_NAMESPACE);
    foreach(Resource * resource, resources) {
        QString oebps_path = Utility::URLEncodePath(resource->GetRelativePathToOEBPS());
        foreach(xc::DOMElement * item, items) {
            QString href = XtoQ(item->getAttribute(QtoX("href")));

            if (href == oebps_path) {
                id_mapping[ resource ] = XtoQ(item->getAttribute(QtoX("id")));
                break;
            }
        }
    }
    return id_mapping;
}


void OPFResource::SetMetaElementsLast(xc::DOMDocument &document)
{
    QList<xc::DOMElement *> metas =
        XhtmlDoc::GetTagMatchingDescendants(document, "meta", OPF_XML_NAMESPACE);
    xc::DOMElement *metadata = GetMetadataElement(document);
    if (!metadata) {
        return;
    }
    foreach(xc::DOMElement * meta, metas) {
        // This makes sure that the <meta> elements come last
        metadata->removeChild(meta);
        metadata->appendChild(meta);
    }
}


void OPFResource::RemoveDCElements(xc::DOMDocument &document)
{
    QList<xc::DOMElement *> dc_elements = XhtmlDoc::GetTagMatchingDescendants(document, "*", DUBLIN_CORE_NS);
    xc::DOMElement &main_identifier = GetMainIdentifier(document);
    foreach(xc::DOMElement * dc_element, dc_elements) {
        // We preserve the original main identifier. Users
        // complain when we don't.
        if (dc_element->isSameNode(&main_identifier)) {
            continue;
        }

        xc::DOMNode *parent = dc_element->getParentNode();

        if (parent) {
            parent->removeChild(dc_element);
        }
    }
}


void OPFResource::MetadataDispatcher(const Metadata::MetaElement &book_meta, xc::DOMDocument &document)
{
    // We ignore badly formed meta elements.
    if (book_meta.name.isEmpty() || book_meta.value.isNull()) {
        return;
    }

    // Write Relator codes (always write author as relator code)
    if (Metadata::Instance().IsRelator(book_meta.name) || book_meta.name == "author") {
        WriteCreatorOrContributor(book_meta, document);
    }
    // There is a relator for the publisher, but there is
    // also a special publisher element that we would rather use
    else if (book_meta.name == "pub") {
        WriteSimpleMetadata("publisher", book_meta.value.toString(), document);
    } else if (book_meta.name  == "language") {
        WriteSimpleMetadata(book_meta.name,
                            Language::instance()->GetLanguageCode(book_meta.value.toString()),
                            document);
    } else if (book_meta.name  == "identifier") {
        WriteIdentifier(book_meta.file_as, book_meta.value.toString(), document);
    } else if (book_meta.name == "date") {
        WriteDate(book_meta.file_as, book_meta.value, document);
    } else {
        WriteSimpleMetadata(book_meta.name, book_meta.value.toString(), document);
    }
}


void OPFResource::WriteCreatorOrContributor(const Metadata::MetaElement book_meta, xc::DOMDocument &document)
{
    QString value = book_meta.value.toString();
    QString file_as = book_meta.file_as;
    QString role_type = book_meta.role_type;
    QString name = book_meta.name;

    if (name == "author") {
        name = "aut";
    }

    // Must have a role type
    if (role_type.isEmpty()) {
        role_type = "contributor";
    }

    // This assumes that the "dc" prefix has been declared for the DC namespace
    xc::DOMElement *element = document.createElementNS(QtoX(DUBLIN_CORE_NS), QtoX("dc:" + role_type));
    element->setAttributeNS(QtoX(OPF_XML_NAMESPACE), QtoX("opf:role"), QtoX(name));

    if (!file_as.isEmpty()) {
        element->setAttributeNS(QtoX(OPF_XML_NAMESPACE), QtoX("opf:file-as"), QtoX(file_as));
    }

    element->setTextContent(QtoX(value));
    xc::DOMElement *metadata = GetMetadataElement(document);
    if (!metadata) {
        return;
    }
    metadata->appendChild(element);
}


void OPFResource::WriteSimpleMetadata(
    const QString &metaname,
    const QString &metavalue,
    xc::DOMDocument &document)
{
    try {
        // This assumes that the "dc" prefix has been declared for the DC namespace
        xc::DOMElement *element = document.createElementNS(QtoX(DUBLIN_CORE_NS), QtoX("dc:" + metaname));
        element->setTextContent(QtoX(metavalue));
        xc::DOMElement *metadata = GetMetadataElement(document);
        if (!metadata) {
            return;
        }
        metadata->appendChild(element);
    } catch (...) {
        // "dc" prefix isn't always declared when importing HTML.
    }
}


void OPFResource::WriteIdentifier(
    const QString &metaname,
    const QString &metavalue,
    xc::DOMDocument &document)
{
    xc::DOMElement &main_identifier = GetMainIdentifier(document);

    // There's a possibility that this identifier is a duplicate
    // of the main identifier that we preserved, so we don't write
    // it out if it is.
    if (metavalue == XtoQ(main_identifier.getTextContent()) &&
        metaname == XtoQ(main_identifier.getAttributeNS(QtoX(OPF_XML_NAMESPACE), QtoX("scheme")))) {
        return;
    }

    // This assumes that the "dc" prefix has been declared for the DC namespace
    xc::DOMElement *element = document.createElementNS(QtoX(DUBLIN_CORE_NS), QtoX("dc:identifier"));
    element->setAttributeNS(QtoX(OPF_XML_NAMESPACE), QtoX("opf:scheme"), QtoX(metaname));

    if (metaname.toLower() == "uuid" && !metavalue.contains("urn:uuid:")) {
        element->setTextContent(QtoX("urn:uuid:" + metavalue));
    } else {
        element->setTextContent(QtoX(metavalue));
    }

    xc::DOMElement *metadata = GetMetadataElement(document);
    if (!metadata) {
        return;
    }
    metadata->appendChild(element);
}

void OPFResource::AddModificationDateMeta()
{
    QString date = QDate::currentDate().toString("yyyy-MM-dd");
    QWriteLocker locker(&GetLock());
    shared_ptr<xc::DOMDocument> document = GetDocument();
    QList<xc::DOMElement *> metas =
        XhtmlDoc::GetTagMatchingDescendants(*document, "date", DUBLIN_CORE_NS);
    foreach(xc::DOMElement * meta, metas) {
        QString name = XtoQ(meta->getAttribute(QtoX("opf:event")));

        if (name == "modification") {
            meta->setTextContent(QtoX(date));
            UpdateTextFromDom(*document);
            return;
        }
    }
    xc::DOMElement *element = document->createElementNS(QtoX(DUBLIN_CORE_NS), QtoX("dc:date"));
    element->setAttributeNS(QtoX(OPF_XML_NAMESPACE), QtoX("opf:event"), QtoX("modification"));
    element->setTextContent(QtoX(date));
    xc::DOMElement *metadata = GetMetadataElement(*document);
    if (!metadata) {
        return;
    }
    metadata->appendChild(element);
    UpdateTextFromDom(*document);
}

void OPFResource::WriteDate(
    const QString &metaname,
    const QVariant &metavalue,
    xc::DOMDocument &document)
{
    QString date = metavalue.toDate().toString("yyyy-MM-dd");
    // This assumes that the "dc" prefix has been declared for the DC namespace
    xc::DOMElement *element = document.createElementNS(QtoX(DUBLIN_CORE_NS), QtoX("dc:date"));
    element->setAttributeNS(QtoX(OPF_XML_NAMESPACE), QtoX("opf:event"), QtoX(metaname));
    element->setTextContent(QtoX(date));
    xc::DOMElement *metadata = GetMetadataElement(document);
    if (!metadata) {
        return;
    }
    metadata->appendChild(element);
}


bool OPFResource::BasicStructurePresent(const xc::DOMDocument &document)
{
    QList<xc::DOMElement *> packages =
        XhtmlDoc::GetTagMatchingDescendants(document, "package", OPF_XML_NAMESPACE);

    if (packages.count() != 1) {
        return false;
    }

    QList<xc::DOMElement *> metadatas =
        XhtmlDoc::GetTagMatchingDescendants(document, "metadata", OPF_XML_NAMESPACE);

    if (metadatas.count() != 1) {
        return false;
    }

    QList<xc::DOMElement *> manifests =
        XhtmlDoc::GetTagMatchingDescendants(document, "manifest", OPF_XML_NAMESPACE);

    if (manifests.count() != 1) {
        return false;
    }

    QList<xc::DOMElement *> spines =
        XhtmlDoc::GetTagMatchingDescendants(document, "spine", OPF_XML_NAMESPACE);

    if (spines.count() != 1) {
        return false;
    }

    if (!GetMainIdentifierUnsafe(document)) {
        return false;
    }

    return true;
}

shared_ptr<xc::DOMDocument> OPFResource::CreateOPFFromScratch(const xc::DOMDocument *d) const
{
    xc::DOMElement *elem;
    QList<xc::DOMElement *> children;
    QString xml_source;
    QString manifest;
    QString spine;
    QString metadata_content;
    QList<std::pair<QString, QString>> manifest_file;
    QList<std::pair<QString, QString>> manifest_recovered;
    QString manifest_content;
    QList<QString> ids_in_manifest;
    QList<QString> spine_file;
    QList<QString> spine_recovered;
    QString spine_content;
    QString guide_content;
    QString item_id;
    QString path;
    QStringList relative_oebps_paths;
    QString id;
    QString href;
    QString mime;
    bool exists;

    // Try to pull as much as we can out of the originial OPF is present.
    if (d) {
        metadata_content = XhtmlDoc::GetNodeChildrenAsString(GetMetadataElement(*d));
        guide_content = XhtmlDoc::GetNodeChildrenAsString(GetGuideElement(*d));

        elem = GetManifestElement(*d);
        if (elem) {
            children = XhtmlDoc::GetTagMatchingDescendants(*elem, "item");;
            for (int i=0; i<children.length(); ++i) {
                if (XtoQ(children.at(i)->getAttribute(QtoX("media-type"))).toLower() == NCX_MIMETYPE) {
                    continue;
                }
                id = XtoQ(children.at(i)->getAttribute(QtoX("id")));
                href = XtoQ(children.at(i)->getAttribute(QtoX("href")));
                if (!id.isEmpty() && !href.isEmpty()) {
                    manifest_recovered.append(std::pair<QString, QString>(id, href));
                }
            }
        }

        elem = GetSpineElement(*d);
        if (elem) {
            children = XhtmlDoc::GetTagMatchingDescendants(*elem, "itemref");;
            for (int i=0; i<children.length(); ++i) {
                id = XtoQ(children.at(i)->getAttribute(QtoX("idref")));
                if (!id.isEmpty()) {
                    spine_recovered.append(id);
                }
            }
        }
    }

    // Get a list of all items on disk.
    relative_oebps_paths = GetRelativePathsToAllFilesInOEPBS();
    foreach(path, relative_oebps_paths) {
        // The OPF is not allowed to be in the manifest and the NCX
        // is already in the template.
        if (path.contains(OPF_FILE_NAME) || path.contains(NCX_FILE_NAME)) {
            continue;
        }

        item_id = GetValidID(QFileInfo(path).fileName());
        manifest_file.append(std::pair<QString, QString>(item_id, path));

        if (TEXT_EXTENSIONS.contains(QFileInfo(path).suffix().toLower())) {
            spine_file.append(item_id);
        }
    }

    // Compare the recovered with on disk content to ensure we aren't missing anything.
    // Put anything recovered that exists on disk into content.
    for (int i=0; i<manifest_recovered.count(); ++i) {
        std::pair<QString, QString> rec = manifest_recovered.at(i);
        item_id = rec.first;
        path = rec.second;
        for (int j=0; j<manifest_file.count(); ++j) {
            std::pair<QString, QString> frec = manifest_file.at(j);
            if (frec.second == path) {
                mime = GetFileMimetype(path);
                manifest_content.append(QString(ITEM_ELEMENT_TEMPLATE).arg(item_id).arg(path).arg(mime));
                if (mime == "application/xhtml+xml") {
                    ids_in_manifest.append(item_id);
                }
                break;
            }
        }
    }
    // Put anything that exists on disk that isn't in recovered into content.
    for (int i=0; i<manifest_file.count(); ++i) {
        std::pair<QString, QString> frec = manifest_file.at(i);
        exists = false;
        item_id = frec.first;
        path = frec.second;
        for (int j=0; i<manifest_recovered.count(); ++j) {
            std::pair<QString, QString> rec = manifest_recovered.at(j);
            if (rec.second == path) {
                exists = true;
                break;
            }
        }
        if (!exists) {
            mime = GetFileMimetype(path);
            manifest_content.append(QString(ITEM_ELEMENT_TEMPLATE).arg(item_id).arg(path).arg(mime));
            if (mime == "application/xhtml+xml") {
                ids_in_manifest.append(item_id);
            }
        }
    }

    // Compare the recovered sine with the items that are in the manifest. We only
    // use a recovered spine item if the id exists in the manfiest.
    foreach (id, spine_recovered) {
        if (ids_in_manifest.contains(id)) {
            spine_content.append(ITEMREF_ELEMENT_TEMPLATE.arg(id));
        }
    }
    // Add any spine items that are on disk that are not in the recovered spine to the spine.
    foreach (id, spine_file) {
        if (!spine_recovered.contains(id) && ids_in_manifest.contains(id)) {
            spine_content.append(ITEMREF_ELEMENT_TEMPLATE.arg(id));
        }
    }

    // Build the OPF.
    xml_source = GetOPFDefaultText();
    xml_source.replace("</manifest>", manifest_content + "</manifest>")
    .replace("</spine>", spine_content + "</spine>")
    .replace("<metadata", OPF_REWRITTEN_COMMENT + "<metadata")
    .replace("</metadata>", metadata_content + "</metadata>")
    .replace("</guide>", guide_content + "</guide>")
    .replace("<guide>\n\n</guide>\n\n", "");

    shared_ptr<xc::DOMDocument> document = XhtmlDoc::LoadTextIntoDocument(xml_source);
    document->setXmlStandalone(true);
    return document;
}


// Yeah, we could get this list of paths with the GetSortedContentFilesList()
// func from FolderKeeper, but let's not create a strong coupling from
// the opf to the FK just yet. If we can work without that dependency,
// then let's do so.
QStringList OPFResource::GetRelativePathsToAllFilesInOEPBS() const
{
    // The parent folder of the OPF will always be the OEBPS folder.
    QString path_to_oebps_folder = QFileInfo(GetFullPath()).absolutePath();
    QStringList paths = Utility::GetAbsolutePathsToFolderDescendantFiles(path_to_oebps_folder);
    paths.replaceInStrings(Utility::URLEncodePath(path_to_oebps_folder) + "/", "");
    paths.sort();
    return paths;
}


QString OPFResource::GetOPFDefaultText()
{
    return TEMPLATE_TEXT.arg(Utility::CreateUUID());
}


void OPFResource::FillWithDefaultText()
{
    SetText(GetOPFDefaultText());
}


QString OPFResource::GetUniqueID(const QString &preferred_id, const xc::DOMDocument &document) const
{
    xc::DOMElement *element = document.getElementById(QtoX(preferred_id));

    if (!element) {
        return preferred_id;
    }

    return Utility::CreateUUID();
}


QString OPFResource::GetResourceMimetype(const Resource &resource) const
{
    return GetFileMimetype(resource.Filename());
}


QString OPFResource::GetFileMimetype(const QString &filepath) const
{
    return m_Mimetypes.value(QFileInfo(filepath).suffix().toLower(), FALLBACK_MIMETYPE);
}


// Initializes m_Mimetypes
void OPFResource::CreateMimetypes()
{
    m_Mimetypes[ "jpg"   ] = "image/jpeg";
    m_Mimetypes[ "jpeg"  ] = "image/jpeg";
    m_Mimetypes[ "png"   ] = "image/png";
    m_Mimetypes[ "gif"   ] = "image/gif";
    m_Mimetypes[ "tif"   ] = "image/tiff";
    m_Mimetypes[ "tiff"  ] = "image/tiff";
    m_Mimetypes[ "bm"    ] = "image/bmp";
    m_Mimetypes[ "bmp"   ] = "image/bmp";
    m_Mimetypes[ "svg"   ] = "image/svg+xml";
    m_Mimetypes[ "ncx"   ] = NCX_MIMETYPE;
    // We convert all HTML document types to XHTML
    m_Mimetypes[ "xml"   ] = "application/xhtml+xml";
    m_Mimetypes[ "xhtml" ] = "application/xhtml+xml";
    m_Mimetypes[ "html"  ] = "application/xhtml+xml";
    m_Mimetypes[ "htm"   ] = "application/xhtml+xml";
    m_Mimetypes[ "css"   ] = "text/css";
    // Multimedia files
    m_Mimetypes[ "mp3"   ] = "audio/mpeg";
    m_Mimetypes[ "oga"   ] = "audio/ogg";
    m_Mimetypes[ "ogg"   ] = "audio/ogg";
    m_Mimetypes[ "mp4"   ] = "video/mp4";
    m_Mimetypes[ "ogv"   ] = "video/ogg";
    m_Mimetypes[ "webm" ] = "video/webm";
    // Until the standards gods grace us with font mimetypes,
    // these will have to do
    m_Mimetypes[ "otf"   ] = "application/vnd.ms-opentype";
    m_Mimetypes[ "ttf"   ] = "application/x-font-ttf";
    m_Mimetypes[ "ttc"   ] = "application/x-font-truetype-collection";
    m_Mimetypes[ "woff"  ] = "application/font-woff";
}


QString OPFResource::GetRelativePathToRoot() const
{
    QFileInfo info(GetFullPath());
    QDir parent_dir = info.dir();
    QString parent_name = parent_dir.dirName();
    return parent_name + "/" + Filename();
}
