/*
    SPDX-FileCopyrightText: 2004 Enrico Ros <eros.kde@email.it>

    Work sponsored by the LiMux project of the city of Munich:
    SPDX-FileCopyrightText: 2017 Klarälvdalens Datakonsult AB a KDAB Group company <info@kdab.com>

    SPDX-License-Identifier: GPL-2.0-or-later
*/

#include "page.h"
#include "page_p.h"

// qt/kde includes
#include <QDomDocument>
#include <QDomElement>
#include <QHash>
#include <QPixmap>
#include <QSet>
#include <QString>
#include <QUuid>
#include <QVariant>

#include <QDebug>

// local includes
#include "action.h"
#include "annotations.h"
#include "annotations_p.h"
#include "debug_p.h"
#include "document.h"
#include "document_p.h"
#include "form.h"
#include "form_p.h"
#include "observer.h"
#include "pagecontroller_p.h"
#include "pagesize.h"
#include "pagetransition.h"
#include "rotationjob_p.h"
#include "textpage_p.h"
#include "tile.h"
#include "tilesmanager_p.h"
#include "utils_p.h"

#include <limits>

#ifdef PAGE_PROFILE
#include <QTime>
#endif

using namespace Okular;

static const double distanceConsideredEqual = 25; // 5px

static void deleteObjectRects(QLinkedList<ObjectRect *> &rects, const QSet<ObjectRect::ObjectType> &which)
{
    QLinkedList<ObjectRect *>::iterator it = rects.begin(), end = rects.end();
    for (; it != end;)
        if (which.contains((*it)->objectType())) {
            delete *it;
            it = rects.erase(it);
        } else
            ++it;
}

PagePrivate::PagePrivate(Page *page, uint n, double w, double h, Rotation o)
    : m_page(page)
    , m_number(n)
    , m_orientation(o)
    , m_width(w)
    , m_height(h)
    , m_doc(nullptr)
    , m_boundingBox(0, 0, 1, 1)
    , m_rotation(Rotation0)
    , m_text(nullptr)
    , m_transition(nullptr)
    , m_textSelections(nullptr)
    , m_openingAction(nullptr)
    , m_closingAction(nullptr)
    , m_duration(-1)
    , m_isBoundingBoxKnown(false)
{
    // avoid Division-By-Zero problems in the program
    if (m_width <= 0)
        m_width = 1;

    if (m_height <= 0)
        m_height = 1;
}

PagePrivate::~PagePrivate()
{
    qDeleteAll(formfields);
    delete m_openingAction;
    delete m_closingAction;
    delete m_text;
    delete m_transition;
}

PagePrivate *PagePrivate::get(Page *page)
{
    return page ? page->d : nullptr;
}

void PagePrivate::imageRotationDone(RotationJob *job)
{
    TilesManager *tm = tilesManager(job->observer());
    if (tm) {
        QPixmap *pixmap = new QPixmap(QPixmap::fromImage(job->image()));
        tm->setPixmap(pixmap, job->rect(), job->isPartialUpdate());
        delete pixmap;
        return;
    }

    QMap<DocumentObserver *, PixmapObject>::iterator it = m_pixmaps.find(job->observer());
    if (it != m_pixmaps.end()) {
        PixmapObject &object = it.value();
        (*object.m_pixmap) = QPixmap::fromImage(job->image());
        object.m_rotation = job->rotation();
        object.m_isPartialPixmap = job->isPartialUpdate();
    } else {
        PixmapObject object;
        object.m_pixmap = new QPixmap(QPixmap::fromImage(job->image()));
        object.m_rotation = job->rotation();
        object.m_isPartialPixmap = job->isPartialUpdate();

        m_pixmaps.insert(job->observer(), object);
    }
}

QTransform PagePrivate::rotationMatrix() const
{
    return Okular::buildRotationMatrix(m_rotation);
}

/** class Page **/

Page::Page(uint pageNumber, double w, double h, Rotation o)
    : d(new PagePrivate(this, pageNumber, w, h, o))
{
}

Page::~Page()
{
    if (d) {
        deletePixmaps();
        deleteRects();
        d->deleteHighlights();
        deleteAnnotations();
        d->deleteTextSelections();
        deleteSourceReferences();

        delete d;
    }
}

int Page::number() const
{
    return d->m_number;
}

Rotation Page::orientation() const
{
    return d->m_orientation;
}

Rotation Page::rotation() const
{
    return d->m_rotation;
}

Rotation Page::totalOrientation() const
{
    return (Rotation)(((int)d->m_orientation + (int)d->m_rotation) % 4);
}

double Page::width() const
{
    return d->m_width;
}

double Page::height() const
{
    return d->m_height;
}

double Page::ratio() const
{
    return d->m_height / d->m_width;
}

NormalizedRect Page::boundingBox() const
{
    return d->m_boundingBox;
}

bool Page::isBoundingBoxKnown() const
{
    return d->m_isBoundingBoxKnown;
}

void Page::setBoundingBox(const NormalizedRect &bbox)
{
    if (d->m_isBoundingBoxKnown && d->m_boundingBox == bbox)
        return;

    // Allow tiny rounding errors (happens during rotation)
    static const double epsilon = 0.00001;
    Q_ASSERT(bbox.left >= -epsilon && bbox.top >= -epsilon && bbox.right <= 1 + epsilon && bbox.bottom <= 1 + epsilon);

    d->m_boundingBox = bbox & NormalizedRect(0., 0., 1., 1.);
    d->m_isBoundingBoxKnown = true;
}

bool Page::hasPixmap(DocumentObserver *observer, int width, int height, const NormalizedRect &rect) const
{
    TilesManager *tm = d->tilesManager(observer);
    if (tm) {
        if (width != tm->width() || height != tm->height()) {
            // FIXME hasPixmap should not be calling setSize on the TilesManager this is not very "const"
            // as this function claims to be
            if (width != -1 && height != -1) {
                tm->setSize(width, height);
            }
            return false;
        }

        return tm->hasPixmap(rect);
    }

    QMap<DocumentObserver *, PagePrivate::PixmapObject>::const_iterator it = d->m_pixmaps.constFind(observer);
    if (it == d->m_pixmaps.constEnd())
        return false;

    if (width == -1 || height == -1)
        return true;

    if (it.value().m_isPartialPixmap)
        return false;

    const QPixmap *pixmap = it.value().m_pixmap;

    return (pixmap->width() == width && pixmap->height() == height);
}

bool Page::hasTextPage() const
{
    return d->m_text != nullptr;
}

RegularAreaRect *Page::wordAt(const NormalizedPoint &p, QString *word) const
{
    if (d->m_text)
        return d->m_text->wordAt(p, word);

    return nullptr;
}

RegularAreaRect *Page::textArea(TextSelection *selection) const
{
    if (d->m_text)
        return d->m_text->textArea(selection);

    return nullptr;
}

bool Page::hasObjectRect(double x, double y, double xScale, double yScale) const
{
    if (m_rects.isEmpty())
        return false;

    QLinkedList<ObjectRect *>::const_iterator it = m_rects.begin(), end = m_rects.end();
    for (; it != end; ++it)
        if ((*it)->distanceSqr(x, y, xScale, yScale) < distanceConsideredEqual)
            return true;

    return false;
}

bool Page::hasHighlights(int s_id) const
{
    // simple case: have no highlights
    if (m_highlights.isEmpty())
        return false;
    // simple case: we have highlights and no id to match
    if (s_id == -1)
        return true;
    // iterate on the highlights list to find an entry by id
    QLinkedList<HighlightAreaRect *>::const_iterator it = m_highlights.begin(), end = m_highlights.end();
    for (; it != end; ++it)
        if ((*it)->s_id == s_id)
            return true;
    return false;
}

bool Page::hasTransition() const
{
    return d->m_transition != nullptr;
}

bool Page::hasAnnotations() const
{
    return !m_annotations.isEmpty();
}

RegularAreaRect *Page::findText(int id, const QString &text, SearchDirection direction, Qt::CaseSensitivity caseSensitivity, const RegularAreaRect *lastRect) const
{
    RegularAreaRect *rect = nullptr;
    if (text.isEmpty() || !d->m_text)
        return rect;

    rect = d->m_text->findText(id, text, direction, caseSensitivity, lastRect);
    return rect;
}

QString Page::text(const RegularAreaRect *area) const
{
    return text(area, TextPage::AnyPixelTextAreaInclusionBehaviour);
}

QString Page::text(const RegularAreaRect *area, TextPage::TextAreaInclusionBehaviour b) const
{
    QString ret;

    if (!d->m_text)
        return ret;

    if (area) {
        RegularAreaRect rotatedArea = *area;
        rotatedArea.transform(d->rotationMatrix().inverted());

        ret = d->m_text->text(&rotatedArea, b);
    } else
        ret = d->m_text->text(nullptr, b);

    return ret;
}

TextEntity::List Page::words(const RegularAreaRect *area, TextPage::TextAreaInclusionBehaviour b) const
{
    TextEntity::List ret;

    if (!d->m_text)
        return ret;

    if (area) {
        RegularAreaRect rotatedArea = *area;
        rotatedArea.transform(d->rotationMatrix().inverted());

        ret = d->m_text->words(&rotatedArea, b);
    } else
        ret = d->m_text->words(nullptr, b);

    for (auto &retI : ret) {
        const TextEntity *orig = retI;
        retI = new TextEntity(orig->text(), new Okular::NormalizedRect(orig->transformedArea(d->rotationMatrix())));
        delete orig;
    }

    return ret;
}

void PagePrivate::rotateAt(Rotation orientation)
{
    if (orientation == m_rotation)
        return;

    deleteTextSelections();

    if (((int)m_orientation + (int)m_rotation) % 2 != ((int)m_orientation + (int)orientation) % 2)
        qSwap(m_width, m_height);

    Rotation oldRotation = m_rotation;
    m_rotation = orientation;

    /**
     * Rotate the images of the page.
     */
    QMapIterator<DocumentObserver *, PagePrivate::PixmapObject> it(m_pixmaps);
    while (it.hasNext()) {
        it.next();

        const PagePrivate::PixmapObject &object = it.value();

        RotationJob *job = new RotationJob(object.m_pixmap->toImage(), object.m_rotation, m_rotation, it.key());
        job->setPage(this);
        m_doc->m_pageController->addRotationJob(job);
    }

    /**
     * Rotate tiles manager
     */
    QMapIterator<const DocumentObserver *, TilesManager *> i(m_tilesManagers);
    while (i.hasNext()) {
        i.next();

        TilesManager *tm = i.value();
        if (tm)
            tm->setRotation(m_rotation);
    }

    /**
     * Rotate the object rects on the page.
     */
    const QTransform matrix = rotationMatrix();
    for (ObjectRect *objRect : qAsConst(m_page->m_rects))
        objRect->transform(matrix);

    const QTransform highlightRotationMatrix = Okular::buildRotationMatrix((Rotation)(((int)m_rotation - (int)oldRotation + 4) % 4));
    for (HighlightAreaRect *hlar : qAsConst(m_page->m_highlights)) {
        hlar->transform(highlightRotationMatrix);
    }
}

void PagePrivate::changeSize(const PageSize &size)
{
    if (size.isNull() || (size.width() == m_width && size.height() == m_height))
        return;

    m_page->deletePixmaps();
    //    deleteHighlights();
    //    deleteTextSelections();

    m_width = size.width();
    m_height = size.height();
    if (m_rotation % 2)
        qSwap(m_width, m_height);
}

const ObjectRect *Page::objectRect(ObjectRect::ObjectType type, double x, double y, double xScale, double yScale) const
{
    // Walk list in reverse order so that annotations in the foreground are preferred
    QLinkedListIterator<ObjectRect *> it(m_rects);
    it.toBack();
    while (it.hasPrevious()) {
        const ObjectRect *objrect = it.previous();
        if ((objrect->objectType() == type) && objrect->distanceSqr(x, y, xScale, yScale) < distanceConsideredEqual)
            return objrect;
    }

    return nullptr;
}

QLinkedList<const ObjectRect *> Page::objectRects(ObjectRect::ObjectType type, double x, double y, double xScale, double yScale) const
{
    QLinkedList<const ObjectRect *> result;

    QLinkedListIterator<ObjectRect *> it(m_rects);
    it.toBack();
    while (it.hasPrevious()) {
        const ObjectRect *objrect = it.previous();
        if ((objrect->objectType() == type) && objrect->distanceSqr(x, y, xScale, yScale) < distanceConsideredEqual)
            result.append(objrect);
    }

    return result;
}

const ObjectRect *Page::nearestObjectRect(ObjectRect::ObjectType type, double x, double y, double xScale, double yScale, double *distance) const
{
    ObjectRect *res = nullptr;
    double minDistance = std::numeric_limits<double>::max();

    QLinkedList<ObjectRect *>::const_iterator it = m_rects.constBegin(), end = m_rects.constEnd();
    for (; it != end; ++it) {
        if ((*it)->objectType() == type) {
            double d = (*it)->distanceSqr(x, y, xScale, yScale);
            if (d < minDistance) {
                res = (*it);
                minDistance = d;
            }
        }
    }

    if (distance)
        *distance = minDistance;
    return res;
}

const PageTransition *Page::transition() const
{
    return d->m_transition;
}

QLinkedList<Annotation *> Page::annotations() const
{
    return m_annotations;
}

Annotation *Page::annotation(const QString &uniqueName) const
{
    for (Annotation *a : m_annotations) {
        if (a->uniqueName() == uniqueName)
            return a;
    }
    return nullptr;
}

const Action *Page::pageAction(PageAction action) const
{
    switch (action) {
    case Page::Opening:
        return d->m_openingAction;
        break;
    case Page::Closing:
        return d->m_closingAction;
        break;
    }

    return nullptr;
}

QLinkedList<FormField *> Page::formFields() const
{
    return d->formfields;
}

void Page::setPixmap(DocumentObserver *observer, QPixmap *pixmap, const NormalizedRect &rect)
{
    d->setPixmap(observer, pixmap, rect, false /*isPartialPixmap*/);
}

void PagePrivate::setPixmap(DocumentObserver *observer, QPixmap *pixmap, const NormalizedRect &rect, bool isPartialPixmap)
{
    if (m_rotation == Rotation0) {
        TilesManager *tm = tilesManager(observer);
        if (tm) {
            tm->setPixmap(pixmap, rect, isPartialPixmap);
            delete pixmap;
            return;
        }

        QMap<DocumentObserver *, PagePrivate::PixmapObject>::iterator it = m_pixmaps.find(observer);
        if (it != m_pixmaps.end()) {
            delete it.value().m_pixmap;
        } else {
            it = m_pixmaps.insert(observer, PagePrivate::PixmapObject());
        }
        it.value().m_pixmap = pixmap;
        it.value().m_rotation = m_rotation;
        it.value().m_isPartialPixmap = isPartialPixmap;
    } else {
        // it can happen that we get a setPixmap while closing and thus the page controller is gone
        if (m_doc->m_pageController) {
            RotationJob *job = new RotationJob(pixmap->toImage(), Rotation0, m_rotation, observer);
            job->setPage(this);
            job->setRect(TilesManager::toRotatedRect(rect, m_rotation));
            job->setIsPartialUpdate(isPartialPixmap);
            m_doc->m_pageController->addRotationJob(job);
        }

        delete pixmap;
    }
}

void Page::setTextPage(TextPage *textPage)
{
    delete d->m_text;

    d->m_text = textPage;
    if (d->m_text) {
        d->m_text->d->m_page = this;
        // Correct/optimize text order for search and text selection
        d->m_text->d->correctTextOrder();
    }
}

void Page::setObjectRects(const QLinkedList<ObjectRect *> &rects)
{
    QSet<ObjectRect::ObjectType> which;
    which << ObjectRect::Action << ObjectRect::Image;
    deleteObjectRects(m_rects, which);

    /**
     * Rotate the object rects of the page.
     */
    const QTransform matrix = d->rotationMatrix();

    QLinkedList<ObjectRect *>::const_iterator objectIt = rects.begin(), end = rects.end();
    for (; objectIt != end; ++objectIt)
        (*objectIt)->transform(matrix);

    m_rects << rects;
}

void PagePrivate::setHighlight(int s_id, RegularAreaRect *rect, const QColor &color)
{
    HighlightAreaRect *hr = new HighlightAreaRect(rect);
    hr->s_id = s_id;
    hr->color = color;

    m_page->m_highlights.append(hr);
}

void PagePrivate::setTextSelections(RegularAreaRect *r, const QColor &color)
{
    deleteTextSelections();
    if (r) {
        HighlightAreaRect *hr = new HighlightAreaRect(r);
        hr->s_id = -1;
        hr->color = color;
        m_textSelections = hr;
        delete r;
    }
}

void Page::setSourceReferences(const QLinkedList<SourceRefObjectRect *> &refRects)
{
    deleteSourceReferences();
    for (SourceRefObjectRect *rect : refRects) {
        m_rects << rect;
    }
}

void Page::setDuration(double seconds)
{
    d->m_duration = seconds;
}

double Page::duration() const
{
    return d->m_duration;
}

void Page::setLabel(const QString &label)
{
    d->m_label = label;
}

QString Page::label() const
{
    return d->m_label;
}

const RegularAreaRect *Page::textSelection() const
{
    return d->m_textSelections;
}

QColor Page::textSelectionColor() const
{
    return d->m_textSelections ? d->m_textSelections->color : QColor();
}

void Page::addAnnotation(Annotation *annotation)
{
    // Generate uniqueName: okular-{UUID}
    if (annotation->uniqueName().isEmpty()) {
        QString uniqueName = QStringLiteral("okular-") + QUuid::createUuid().toString();
        annotation->setUniqueName(uniqueName);
    }
    annotation->d_ptr->m_page = d;
    m_annotations.append(annotation);

    AnnotationObjectRect *rect = new AnnotationObjectRect(annotation);

    // Rotate the annotation on the page.
    const QTransform matrix = d->rotationMatrix();
    annotation->d_ptr->annotationTransform(matrix);

    m_rects.append(rect);
}

bool Page::removeAnnotation(Annotation *annotation)
{
    if (!d->m_doc->m_parent->canRemovePageAnnotation(annotation))
        return false;

    QLinkedList<Annotation *>::iterator aIt = m_annotations.begin(), aEnd = m_annotations.end();
    for (; aIt != aEnd; ++aIt) {
        if ((*aIt) && (*aIt)->uniqueName() == annotation->uniqueName()) {
            int rectfound = false;
            QLinkedList<ObjectRect *>::iterator it = m_rects.begin(), end = m_rects.end();
            for (; it != end && !rectfound; ++it)
                if (((*it)->objectType() == ObjectRect::OAnnotation) && ((*it)->object() == (*aIt))) {
                    delete *it;
                    it = m_rects.erase(it);
                    rectfound = true;
                }
            qCDebug(OkularCoreDebug) << "removed annotation:" << annotation->uniqueName();
            annotation->d_ptr->m_page = nullptr;
            m_annotations.erase(aIt);
            break;
        }
    }

    return true;
}

void Page::setTransition(PageTransition *transition)
{
    delete d->m_transition;
    d->m_transition = transition;
}

void Page::setPageAction(PageAction action, Action *link)
{
    switch (action) {
    case Page::Opening:
        delete d->m_openingAction;
        d->m_openingAction = link;
        break;
    case Page::Closing:
        delete d->m_closingAction;
        d->m_closingAction = link;
        break;
    }
}

void Page::setFormFields(const QLinkedList<FormField *> &fields)
{
    qDeleteAll(d->formfields);
    d->formfields = fields;
    for (FormField *ff : qAsConst(d->formfields)) {
        ff->d_ptr->setDefault();
        ff->d_ptr->m_page = this;
    }
}

void Page::deletePixmap(DocumentObserver *observer)
{
    TilesManager *tm = d->tilesManager(observer);
    if (tm) {
        delete tm;
        d->m_tilesManagers.remove(observer);
    } else {
        PagePrivate::PixmapObject object = d->m_pixmaps.take(observer);
        delete object.m_pixmap;
    }
}

void Page::deletePixmaps()
{
    QMapIterator<DocumentObserver *, PagePrivate::PixmapObject> it(d->m_pixmaps);
    while (it.hasNext()) {
        it.next();
        delete it.value().m_pixmap;
    }

    d->m_pixmaps.clear();

    qDeleteAll(d->m_tilesManagers);
    d->m_tilesManagers.clear();
}

void Page::deleteRects()
{
    // delete ObjectRects of type Link and Image
    QSet<ObjectRect::ObjectType> which;
    which << ObjectRect::Action << ObjectRect::Image;
    deleteObjectRects(m_rects, which);
}

void PagePrivate::deleteHighlights(int s_id)
{
    // delete highlights by ID
    QLinkedList<HighlightAreaRect *>::iterator it = m_page->m_highlights.begin(), end = m_page->m_highlights.end();
    while (it != end) {
        HighlightAreaRect *highlight = *it;
        if (s_id == -1 || highlight->s_id == s_id) {
            it = m_page->m_highlights.erase(it);
            delete highlight;
        } else
            ++it;
    }
}

void PagePrivate::deleteTextSelections()
{
    delete m_textSelections;
    m_textSelections = nullptr;
}

void Page::deleteSourceReferences()
{
    deleteObjectRects(m_rects, QSet<ObjectRect::ObjectType>() << ObjectRect::SourceRef);
}

void Page::deleteAnnotations()
{
    // delete ObjectRects of type Annotation
    deleteObjectRects(m_rects, QSet<ObjectRect::ObjectType>() << ObjectRect::OAnnotation);
    // delete all stored annotations
    qDeleteAll(m_annotations);
    m_annotations.clear();
}

bool PagePrivate::restoreLocalContents(const QDomNode &pageNode)
{
    bool loadedAnything = false; // set if something actually gets loaded

    // iterate over all children (annotationList, ...)
    QDomNode childNode = pageNode.firstChild();
    while (childNode.isElement()) {
        QDomElement childElement = childNode.toElement();
        childNode = childNode.nextSibling();

        // parse annotationList child element
        if (childElement.tagName() == QLatin1String("annotationList")) {
#ifdef PAGE_PROFILE
            QTime time;
            time.start();
#endif
            // Clone annotationList as root node in restoredLocalAnnotationList
            const QDomNode clonedNode = restoredLocalAnnotationList.importNode(childElement, true);
            restoredLocalAnnotationList.appendChild(clonedNode);

            // iterate over all annotations
            QDomNode annotationNode = childElement.firstChild();
            while (annotationNode.isElement()) {
                // get annotation element and advance to next annot
                QDomElement annotElement = annotationNode.toElement();
                annotationNode = annotationNode.nextSibling();

                // get annotation from the dom element
                Annotation *annotation = AnnotationUtils::createAnnotation(annotElement);

                // append annotation to the list or show warning
                if (annotation) {
                    m_doc->performAddPageAnnotation(m_number, annotation);
                    qCDebug(OkularCoreDebug) << "restored annot:" << annotation->uniqueName();
                    loadedAnything = true;
                } else
                    qCWarning(OkularCoreDebug).nospace() << "page (" << m_number << "): can't restore an annotation from XML.";
            }
#ifdef PAGE_PROFILE
            qCDebug(OkularCoreDebug).nospace() << "annots: XML Load time: " << time.elapsed() << "ms";
#endif
        }
        // parse formList child element
        else if (childElement.tagName() == QLatin1String("forms")) {
            // Clone forms as root node in restoredFormFieldList
            const QDomNode clonedNode = restoredFormFieldList.importNode(childElement, true);
            restoredFormFieldList.appendChild(clonedNode);

            if (formfields.isEmpty())
                continue;

            QHash<int, FormField *> hashedforms;
            for (FormField *ff : qAsConst(formfields)) {
                hashedforms[ff->id()] = ff;
            }

            // iterate over all forms
            QDomNode formsNode = childElement.firstChild();
            while (formsNode.isElement()) {
                // get annotation element and advance to next annot
                QDomElement formElement = formsNode.toElement();
                formsNode = formsNode.nextSibling();

                if (formElement.tagName() != QLatin1String("form"))
                    continue;

                bool ok = true;
                int index = formElement.attribute(QStringLiteral("id")).toInt(&ok);
                if (!ok)
                    continue;

                QHash<int, FormField *>::const_iterator wantedIt = hashedforms.constFind(index);
                if (wantedIt == hashedforms.constEnd())
                    continue;

                QString value = formElement.attribute(QStringLiteral("value"));
                (*wantedIt)->d_ptr->setValue(value);
                loadedAnything = true;
            }
        }
    }

    return loadedAnything;
}

void PagePrivate::saveLocalContents(QDomNode &parentNode, QDomDocument &document, PageItems what) const
{
    // create the page node and set the 'number' attribute
    QDomElement pageElement = document.createElement(QStringLiteral("page"));
    pageElement.setAttribute(QStringLiteral("number"), m_number);

    // add annotations info if has got any
    if ((what & AnnotationPageItems) && (what & OriginalAnnotationPageItems)) {
        const QDomElement savedDocRoot = restoredLocalAnnotationList.documentElement();
        if (!savedDocRoot.isNull()) {
            // Import and append node in target document
            const QDomNode importedNode = document.importNode(savedDocRoot, true);
            pageElement.appendChild(importedNode);
        }
    } else if ((what & AnnotationPageItems) && !m_page->m_annotations.isEmpty()) {
        // create the annotationList
        QDomElement annotListElement = document.createElement(QStringLiteral("annotationList"));

        // add every annotation to the annotationList
        QLinkedList<Annotation *>::const_iterator aIt = m_page->m_annotations.constBegin(), aEnd = m_page->m_annotations.constEnd();
        for (; aIt != aEnd; ++aIt) {
            // get annotation
            const Annotation *a = *aIt;
            // only save okular annotations (not the embedded in file ones)
            if (!(a->flags() & Annotation::External)) {
                // append an filled-up element called 'annotation' to the list
                QDomElement annElement = document.createElement(QStringLiteral("annotation"));
                AnnotationUtils::storeAnnotation(a, annElement, document);
                annotListElement.appendChild(annElement);
                qCDebug(OkularCoreDebug) << "save annotation:" << a->uniqueName();
            }
        }

        // append the annotationList element if annotations have been set
        if (annotListElement.hasChildNodes())
            pageElement.appendChild(annotListElement);
    }

    // add forms info if has got any
    if ((what & FormFieldPageItems) && (what & OriginalFormFieldPageItems)) {
        const QDomElement savedDocRoot = restoredFormFieldList.documentElement();
        if (!savedDocRoot.isNull()) {
            // Import and append node in target document
            const QDomNode importedNode = document.importNode(savedDocRoot, true);
            pageElement.appendChild(importedNode);
        }
    } else if ((what & FormFieldPageItems) && !formfields.isEmpty()) {
        // create the formList
        QDomElement formListElement = document.createElement(QStringLiteral("forms"));

        // add every form data to the formList
        QLinkedList<FormField *>::const_iterator fIt = formfields.constBegin(), fItEnd = formfields.constEnd();
        for (; fIt != fItEnd; ++fIt) {
            // get the form field
            const FormField *f = *fIt;

            QString newvalue = f->d_ptr->value();
            if (f->d_ptr->m_default == newvalue)
                continue;

            // append an filled-up element called 'annotation' to the list
            QDomElement formElement = document.createElement(QStringLiteral("form"));
            formElement.setAttribute(QStringLiteral("id"), f->id());
            formElement.setAttribute(QStringLiteral("value"), newvalue);
            formListElement.appendChild(formElement);
        }

        // append the annotationList element if annotations have been set
        if (formListElement.hasChildNodes())
            pageElement.appendChild(formListElement);
    }

    // append the page element only if has children
    if (pageElement.hasChildNodes())
        parentNode.appendChild(pageElement);
}

const QPixmap *Page::_o_nearestPixmap(DocumentObserver *observer, int w, int h) const
{
    Q_UNUSED(h)

    const QPixmap *pixmap = nullptr;

    // if a pixmap is present for given id, use it
    QMap<DocumentObserver *, PagePrivate::PixmapObject>::const_iterator itPixmap = d->m_pixmaps.constFind(observer);
    if (itPixmap != d->m_pixmaps.constEnd())
        pixmap = itPixmap.value().m_pixmap;
    // else find the closest match using pixmaps of other IDs (great optim!)
    else if (!d->m_pixmaps.isEmpty()) {
        int minDistance = -1;
        QMap<DocumentObserver *, PagePrivate::PixmapObject>::const_iterator it = d->m_pixmaps.constBegin(), end = d->m_pixmaps.constEnd();
        for (; it != end; ++it) {
            int pixWidth = (*it).m_pixmap->width(), distance = pixWidth > w ? pixWidth - w : w - pixWidth;
            if (minDistance == -1 || distance < minDistance) {
                pixmap = (*it).m_pixmap;
                minDistance = distance;
            }
        }
    }

    return pixmap;
}

bool Page::hasTilesManager(const DocumentObserver *observer) const
{
    return d->tilesManager(observer) != nullptr;
}

QList<Tile> Page::tilesAt(const DocumentObserver *observer, const NormalizedRect &rect) const
{
    TilesManager *tm = d->m_tilesManagers.value(observer);
    if (tm)
        return tm->tilesAt(rect, TilesManager::PixmapTile);
    else
        return QList<Tile>();
}

TilesManager *PagePrivate::tilesManager(const DocumentObserver *observer) const
{
    return m_tilesManagers.value(observer);
}

void PagePrivate::setTilesManager(const DocumentObserver *observer, TilesManager *tm)
{
    TilesManager *old = m_tilesManagers.value(observer);
    delete old;

    m_tilesManagers.insert(observer, tm);
}

void PagePrivate::adoptGeneratedContents(PagePrivate *oldPage)
{
    rotateAt(oldPage->m_rotation);

    m_pixmaps = oldPage->m_pixmaps;
    oldPage->m_pixmaps.clear();

    m_tilesManagers = oldPage->m_tilesManagers;
    oldPage->m_tilesManagers.clear();

    m_boundingBox = oldPage->m_boundingBox;
    m_isBoundingBoxKnown = oldPage->m_isBoundingBoxKnown;
    m_text = oldPage->m_text;
    oldPage->m_text = nullptr;

    m_textSelections = oldPage->m_textSelections;
    oldPage->m_textSelections = nullptr;

    restoredLocalAnnotationList = oldPage->restoredLocalAnnotationList;
    restoredFormFieldList = oldPage->restoredFormFieldList;
}

FormField *PagePrivate::findEquivalentForm(const Page *p, FormField *oldField)
{
    // given how id is not very good of id (at least for pdf) we do a few passes
    // same rect, type and id
    for (FormField *f : qAsConst(p->d->formfields)) {
        if (f->rect() == oldField->rect() && f->type() == oldField->type() && f->id() == oldField->id())
            return f;
    }
    // same rect and type
    for (FormField *f : qAsConst(p->d->formfields)) {
        if (f->rect() == oldField->rect() && f->type() == oldField->type())
            return f;
    }
    // fuzzy rect, same type and id
    for (FormField *f : qAsConst(p->d->formfields)) {
        if (f->type() == oldField->type() && f->id() == oldField->id() && qFuzzyCompare(f->rect().left, oldField->rect().left) && qFuzzyCompare(f->rect().top, oldField->rect().top) &&
            qFuzzyCompare(f->rect().right, oldField->rect().right) && qFuzzyCompare(f->rect().bottom, oldField->rect().bottom)) {
            return f;
        }
    }
    // fuzzy rect and same type
    for (FormField *f : qAsConst(p->d->formfields)) {
        if (f->type() == oldField->type() && qFuzzyCompare(f->rect().left, oldField->rect().left) && qFuzzyCompare(f->rect().top, oldField->rect().top) && qFuzzyCompare(f->rect().right, oldField->rect().right) &&
            qFuzzyCompare(f->rect().bottom, oldField->rect().bottom)) {
            return f;
        }
    }
    return nullptr;
}
