/***************************************************************************
 *   Copyright (C) 2004 by Enrico Ros <eros.kde@email.it>                  *
 *   Copyright (C) 2017    Klarälvdalens Datakonsult AB, a KDAB Group      *
 *                         company, info@kdab.com. Work sponsored by the   *
 *                         LiMux project of the city of Munich             *
 *                                                                         *
 *   This program 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 2 of the License, or     *
 *   (at your option) any later version.                                   *
 ***************************************************************************/

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

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

#include <QDebug>

// local includes
#include "action.h"
#include "annotations.h"
#include "annotations_p.h"
#include "area.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.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();
    }
}

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 );

#if 0
    // add bookmark info if is bookmarked
    if ( d->m_bookmarked )
    {
        // create the pageElement's 'bookmark' child
        QDomElement bookmarkElement = document.createElement( "bookmark" );
        pageElement.appendChild( bookmarkElement );

        // add attributes to the element
        //bookmarkElement.setAttribute( "name", bookmark name );
    }
#endif

    // 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;
}
