/*

Copyright 2021 S. Razi Alavizadeh
Copyright 2020 Johan Björklund
Copyright 2012-2015, 2021 Adam Reichold

This file is part of qpdfview.

qpdfview 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.

qpdfview 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 qpdfview.  If not, see <http://www.gnu.org/licenses/>.

*/

#include "pageitem.h"

#include <QApplication>
#include <QClipboard>
#include <QtConcurrentRun>
#include <QFileDialog>
#include <QGraphicsProxyWidget>
#include <QGraphicsScene>
#include <QGraphicsSceneHoverEvent>
#include <qmath.h>
#include <QMenu>
#include <QMessageBox>
#include <QPainter>
#include <QStyleOptionGraphicsItem>
#include <QTimer>
#include <QToolTip>
#include <QUrl>

#include "settings.h"
#include "model.h"
#include "tileitem.h"

namespace qpdfview
{

namespace
{

const int largeTilesThreshold = 8;
const int veryLargeTilesThreshold = 16;

const qreal proxyPadding = 2.0;

inline bool modifiersAreActive(const QGraphicsSceneMouseEvent* event, Qt::KeyboardModifiers modifiers)
{
    if(modifiers == Qt::NoModifier)
    {
        return false;
    }

    return event->modifiers() == modifiers || (event->buttons() & modifiers) != 0;
}

inline bool modifiersUseMouseButton(Settings* settings, Qt::MouseButton mouseButton)
{
    return ((settings->pageItem().copyToClipboardModifiers() | settings->pageItem().addAnnotationModifiers()) & mouseButton) != 0;
}

} // anonymous

Settings* PageItem::s_settings = 0;

PageItem::PageItem(Model::Page* page, int index, PaintMode paintMode, QGraphicsItem* parent) : QGraphicsObject(parent),
    m_page(page),
    m_size(page->size()),
    m_cropRect(),
    m_index(index),
    m_paintMode(paintMode),
    m_highlights(),
    m_loadInteractiveElements(0),
    m_links(),
    m_annotations(),
    m_formFields(),
    m_rubberBandMode(ModifiersMode),
    m_rubberBand(),
    m_annotationOverlay(),
    m_formFieldOverlay(),
    m_renderParam(RenderParam::defaultInstance),
    m_transform(),
    m_normalizedTransform(),
    m_boundingRect(),
    m_tileItems()
{
    if(s_settings == 0)
    {
        s_settings = Settings::instance();
    }

    setAcceptHoverEvents(true);

    setFlag(QGraphicsItem::ItemUsesExtendedStyleOption, useTiling());

    if(!useTiling())
    {
        m_tileItems.resize(1);
        m_tileItems.squeeze();

        m_tileItems.replace(0, new TileItem(this));
    }

    prepareGeometry();
}

PageItem::~PageItem()
{
    if(m_loadInteractiveElements != 0)
    {
        m_loadInteractiveElements->waitForFinished();

        delete m_loadInteractiveElements;
        m_loadInteractiveElements = 0;
    }

    hideAnnotationOverlay(false);
    hideFormFieldOverlay(false);

    TileItem::dropCachedPixmaps(this);

    qDeleteAll(m_links);
    qDeleteAll(m_annotations);
    qDeleteAll(m_formFields);

    qDeleteAll(m_tileItems);
}

QRectF PageItem::boundingRect() const
{
    if(m_cropRect.isNull())
    {
        return m_boundingRect;
    }

    QRectF boundingRect;

    boundingRect.setLeft(m_boundingRect.left() + m_cropRect.left() * m_boundingRect.width());
    boundingRect.setTop(m_boundingRect.top() + m_cropRect.top() * m_boundingRect.height());
    boundingRect.setWidth(m_cropRect.width() * m_boundingRect.width());
    boundingRect.setHeight(m_cropRect.height() * m_boundingRect.height());

    return boundingRect;
}

void PageItem::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget*)
{
    paintPage(painter, option->exposedRect);

    paintLinks(painter);
    paintFormFields(painter);

    paintHighlights(painter);
    paintRubberBand(painter);
}

QSizeF PageItem::displayedSize(const RenderParam& renderParam) const
{
    const bool rotationChanged = m_renderParam.rotation() != renderParam.rotation();

    const bool flagsChanged = m_renderParam.flags() != renderParam.flags();

    const bool useCropRect = !m_cropRect.isNull() && !rotationChanged && !flagsChanged;

    const qreal cropWidth = useCropRect ? m_cropRect.width() : 1.0;
    const qreal cropHeight = useCropRect ? m_cropRect.height() : 1.0;

    switch(renderParam.rotation())
    {
    default:
    case RotateBy0:
    case RotateBy180:
        return QSizeF(renderParam.resolutionX() / 72.0 * cropWidth * m_size.width(),
                      renderParam.resolutionY() / 72.0 * cropHeight * m_size.height());
    case RotateBy90:
    case RotateBy270:
        return QSizeF(renderParam.resolutionX() / 72.0 * cropHeight * m_size.height(),
                      renderParam.resolutionY() / 72.0 * cropWidth * m_size.width());
    }
}

void PageItem::setHighlights(const QList< QRectF >& highlights)
{
    m_highlights = highlights;

    update();
}

void PageItem::setRubberBandMode(RubberBandMode rubberBandMode)
{
    if(m_rubberBandMode != rubberBandMode && rubberBandMode >= 0 && rubberBandMode < NumberOfRubberBandModes)
    {
        m_rubberBandMode = rubberBandMode;

        if(m_rubberBandMode == ModifiersMode)
        {
            unsetCursor();
        }
        else
        {
            setCursor(Qt::CrossCursor);
        }
    }
}

void PageItem::setRenderParam(const RenderParam& renderParam)
{
    if(m_renderParam != renderParam)
    {
        const bool resolutionChanged = m_renderParam.resolutionX() != renderParam.resolutionX()
                || m_renderParam.resolutionY() != renderParam.resolutionY()
                || !qFuzzyCompare(m_renderParam.devicePixelRatio(), renderParam.devicePixelRatio())
                || !qFuzzyCompare(m_renderParam.scaleFactor(), renderParam.scaleFactor());

        const bool rotationChanged = m_renderParam.rotation() != renderParam.rotation();

        const RenderFlags changedFlags = m_renderParam.flags() ^ renderParam.flags();

        refresh(!rotationChanged && changedFlags == 0);

        m_renderParam = renderParam;

        if(resolutionChanged || rotationChanged)
        {
            prepareGeometryChange();
            prepareGeometry();
        }

        if(changedFlags.testFlag(TrimMargins))
        {
            setFlag(QGraphicsItem::ItemClipsToShape, m_renderParam.trimMargins());
        }
    }
}

void PageItem::refresh(bool keepObsoletePixmaps, bool dropCachedPixmaps)
{
    if(!useTiling())
    {
        m_tileItems.first()->refresh(keepObsoletePixmaps);
    }
    else
    {
        foreach(TileItem* tile, m_tileItems)
        {
            tile->refresh(keepObsoletePixmaps);
        }
    }

    if(!keepObsoletePixmaps)
    {
        prepareGeometryChange();

        m_cropRect = QRectF();
    }

    if(dropCachedPixmaps)
    {
        TileItem::dropCachedPixmaps(this);
    }

    update();
}

int PageItem::startRender(bool prefetch)
{
    int cost = 0;

    if(!useTiling())
    {
        cost += m_tileItems.first()->startRender(prefetch);
    }
    else
    {
        foreach(TileItem* tile, m_tileItems)
        {
            cost += tile->startRender(prefetch);
        }
    }

    return cost;
}

void PageItem::cancelRender()
{
    if(!useTiling())
    {
        m_tileItems.first()->cancelRender();
    }
    else
    {
        foreach(TileItem* tile, m_exposedTileItems)
        {
            tile->cancelRender();
        }

        m_exposedTileItems.clear();
    }
}

void PageItem::showAnnotationOverlay(Model::Annotation* selectedAnnotation)
{
    if(s_settings->pageItem().annotationOverlay())
    {
        showOverlay(m_annotationOverlay, SLOT(hideAnnotationOverlay()), m_annotations, selectedAnnotation);
    }
    else
    {
        hideAnnotationOverlay(false);

        addProxy(m_annotationOverlay, SLOT(hideAnnotationOverlay()), selectedAnnotation);
        m_annotationOverlay.value(selectedAnnotation)->widget()->setFocus();
    }
}

void PageItem::hideAnnotationOverlay(bool deleteLater)
{
    hideOverlay(m_annotationOverlay, deleteLater);
}

void PageItem::updateAnnotationOverlay()
{
    updateOverlay(m_annotationOverlay);
}

void PageItem::showFormFieldOverlay(Model::FormField* selectedFormField)
{
    if(s_settings->pageItem().formFieldOverlay())
    {
        showOverlay(m_formFieldOverlay, SLOT(hideFormFieldOverlay()), m_formFields, selectedFormField);
    }
    else
    {
        hideFormFieldOverlay(false);

        addProxy(m_formFieldOverlay, SLOT(hideFormFieldOverlay()), selectedFormField);
        m_formFieldOverlay.value(selectedFormField)->widget()->setFocus();
    }
}

void PageItem::updateFormFieldOverlay()
{
    updateOverlay(m_formFieldOverlay);
}

void PageItem::hideFormFieldOverlay(bool deleteLater)
{
    hideOverlay(m_formFieldOverlay, deleteLater);
}

void PageItem::hoverEnterEvent(QGraphicsSceneHoverEvent*)
{
}

void PageItem::hoverMoveEvent(QGraphicsSceneHoverEvent* event)
{
    if(m_rubberBandMode == ModifiersMode && event->modifiers() == Qt::NoModifier)
    {
        // links

        foreach(const Model::Link* link, m_links)
        {
            if(m_normalizedTransform.map(link->boundary).contains(event->pos()))
            {
                if(link->page != -1 && (link->urlOrFileName.isNull() || !presentationMode()))
                {
                    setCursor(Qt::PointingHandCursor);

                    if(link->urlOrFileName.isNull())
                    {
                        QToolTip::showText(event->screenPos(), tr("Go to page %1.").arg(link->page));
                    }
                    else
                    {
                        QToolTip::showText(event->screenPos(), tr("Go to page %1 of file '%2'.").arg(link->page).arg(link->urlOrFileName));
                    }

                    return;
                }
                else if(!link->urlOrFileName.isNull() && !presentationMode())
                {
                    setCursor(Qt::PointingHandCursor);
                    QToolTip::showText(event->screenPos(), tr("Open '%1'.").arg(link->urlOrFileName));

                    return;
                }
            }
        }

        if(presentationMode())
        {
            unsetCursor();
            QToolTip::hideText();

            return;
        }

        // annotations

        foreach(const Model::Annotation* annotation, m_annotations)
        {
            if(m_normalizedTransform.mapRect(annotation->boundary()).contains(event->pos()))
            {
                setCursor(Qt::PointingHandCursor);
                QToolTip::showText(event->screenPos(), annotation->contents());

                return;
            }
        }

        // form fields

        foreach(const Model::FormField* formField, m_formFields)
        {
            if(m_normalizedTransform.mapRect(formField->boundary()).contains(event->pos()))
            {
                setCursor(Qt::PointingHandCursor);
                QToolTip::showText(event->screenPos(), tr("Edit form field '%1'.").arg(formField->name()));

                return;
            }
        }

        unsetCursor();
        QToolTip::hideText();
    }
}

void PageItem::hoverLeaveEvent(QGraphicsSceneHoverEvent*)
{
}

void PageItem::mousePressEvent(QGraphicsSceneMouseEvent* event)
{
    const bool leftButtonActive = event->button() == Qt::LeftButton;
    const bool middleButtonActive = event->button() == Qt::MiddleButton;
    const bool anyButtonActive = leftButtonActive || middleButtonActive;

    const bool noModifiersActive = event->modifiers() == Qt::NoModifier;
    const bool copyToClipboardModifiersActive = modifiersAreActive(event, s_settings->pageItem().copyToClipboardModifiers());
    const bool addAnnotationModifiersActive = modifiersAreActive(event, s_settings->pageItem().addAnnotationModifiers());
    const bool zoomToSelectionModifiersActive = modifiersAreActive(event, s_settings->pageItem().zoomToSelectionModifiers());
    const bool rubberBandModifiersActive = copyToClipboardModifiersActive || addAnnotationModifiersActive || zoomToSelectionModifiersActive;
    const bool openInSourceEditorModifiersActive = modifiersAreActive(event, s_settings->pageItem().openInSourceEditorModifiers());

    // rubber band

    if(rubberBandModifiersActive && leftButtonActive && !presentationMode())
    {
        if(m_rubberBandMode == ModifiersMode)
        {
            setCursor(Qt::CrossCursor);

            if(copyToClipboardModifiersActive)
            {
                m_rubberBandMode = CopyToClipboardMode;
            }
            else if(addAnnotationModifiersActive)
            {
                m_rubberBandMode = AddAnnotationMode;
            }
            else if(zoomToSelectionModifiersActive)
            {
                m_rubberBandMode = ZoomToSelectionMode;
            }
        }
    }

    if(m_rubberBandMode != ModifiersMode)
    {
        m_rubberBand = QRectF(event->pos(), QSizeF());

        emit rubberBandStarted();

        update();

        event->accept();
        return;
    }

    if(openInSourceEditorModifiersActive && leftButtonActive && !presentationMode())
    {
        emit openInSourceEditor(m_index + 1, sourcePos(event->pos()));

        event->accept();
        return;
    }

    if(noModifiersActive && anyButtonActive)
    {
        // links

        foreach(const Model::Link* link, m_links)
        {
            if(m_normalizedTransform.map(link->boundary).contains(event->pos()))
            {
                unsetCursor();

                if(link->page != -1 && (link->urlOrFileName.isNull() || !presentationMode()))
                {
                    if(link->urlOrFileName.isNull())
                    {
                        emit linkClicked(middleButtonActive, link->page, link->left, link->top);
                    }
                    else
                    {
                        emit linkClicked(middleButtonActive, link->urlOrFileName, link->page);
                    }

                    event->accept();
                    return;
                }
                else if(!link->urlOrFileName.isNull() && !presentationMode())
                {
                    emit linkClicked(link->urlOrFileName);

                    event->accept();
                    return;
                }
            }
        }
    }

    if(noModifiersActive && leftButtonActive && !presentationMode())
    {
        // annotations

        foreach(Model::Annotation* annotation, m_annotations)
        {
            if(m_normalizedTransform.mapRect(annotation->boundary()).contains(event->pos()))
            {
                unsetCursor();

                showAnnotationOverlay(annotation);

                event->accept();
                return;
            }
        }

        hideAnnotationOverlay();

        // form fields

        foreach(Model::FormField* formField, m_formFields)
        {
            if(m_normalizedTransform.mapRect(formField->boundary()).contains(event->pos()))
            {
                unsetCursor();

                showFormFieldOverlay(formField);

                event->accept();
                return;
            }
        }

        hideFormFieldOverlay();
    }

    event->ignore();
}

void PageItem::mouseMoveEvent(QGraphicsSceneMouseEvent* event)
{
    if(!m_rubberBand.isNull())
    {
        const QPointF pos = event->pos();

        if(m_boundingRect.contains(pos))
        {
            QRectF rubberBand = m_rubberBand;
            rubberBand.setBottomRight(pos);

            if(!rubberBand.isNull())
            {
                m_rubberBand = rubberBand;

                update();
            }

            event->accept();
            return;
        }
    }

    event->ignore();
}

void PageItem::mouseReleaseEvent(QGraphicsSceneMouseEvent* event)
{
    if(!m_rubberBand.isNull())
    {
        unsetCursor();

        m_rubberBand = m_rubberBand.normalized();

        if(m_rubberBandMode == CopyToClipboardMode)
        {
            copyToClipboard(event->screenPos());
        }
        else if(m_rubberBandMode == AddAnnotationMode)
        {
            addAnnotation(event->screenPos());
        }
        else if(m_rubberBandMode == ZoomToSelectionMode)
        {
            emit zoomToSelection(m_index + 1, m_normalizedTransform.inverted().mapRect(m_rubberBand));
        }

        m_rubberBandMode = ModifiersMode;
        m_rubberBand = QRectF();

        emit rubberBandFinished();

        update();

        event->accept();
        return;
    }

    event->ignore();
}

void PageItem::contextMenuEvent(QGraphicsSceneContextMenuEvent* event)
{
    if(event->reason() == QGraphicsSceneContextMenuEvent::Mouse && modifiersUseMouseButton(s_settings, Qt::RightButton))
    {
        event->accept();
        return;
    }

    if(presentationMode())
    {
        event->ignore();
        return;
    }

    foreach(Model::Link* link, m_links)
    {
        if(m_normalizedTransform.map(link->boundary).contains(event->pos()))
        {
            unsetCursor();

            showLinkContextMenu(link, event->screenPos());

            event->accept();
            return;
        }
    }

    foreach(Model::Annotation* annotation, m_annotations)
    {
        if(m_normalizedTransform.mapRect(annotation->boundary()).contains(event->pos()))
        {
            unsetCursor();

            showAnnotationContextMenu(annotation, event->screenPos());

            event->accept();
            return;
        }
    }

    event->ignore();
}

void PageItem::on_loadInteractiveElements_finished()
{
    update();
}

void PageItem::updateCropRect()
{
    QRectF cropRect;

    if(!useTiling())
    {
        cropRect = m_tileItems.first()->cropRect();
    }
    else
    {
        foreach(TileItem* tile, m_tileItems)
        {
            const QRect& tileRect = tile->rect();
            const QRectF& tileCropRect = tile->cropRect();

            if(tileCropRect.isNull())
            {
                cropRect = QRectF();
                break;
            }

            const qreal left = (tileRect.left() + tileCropRect.left() * tileRect.width()) / m_boundingRect.width();
            const qreal top = (tileRect.top() + tileCropRect.top() * tileRect.height()) / m_boundingRect.height();
            const qreal width = tileCropRect.width() * tileRect.width() / m_boundingRect.width();
            const qreal height = tileCropRect.height() * tileRect.height() / m_boundingRect.height();

            cropRect = cropRect.united(QRectF(left, top, width, height));
        }
    }

    if(m_cropRect.isNull() && !cropRect.isNull())
    {
        prepareGeometryChange();

        m_cropRect = cropRect;

        emit cropRectChanged();
    }
}

inline bool PageItem::presentationMode() const
{
    return m_paintMode == PresentationMode;
}

inline bool PageItem::thumbnailMode() const
{
    return m_paintMode == ThumbnailMode;
}

bool PageItem::useTiling() const
{
    return m_paintMode != ThumbnailMode && s_settings->pageItem().useTiling();
}

void PageItem::startLoadInteractiveElements()
{
    if(thumbnailMode() || m_loadInteractiveElements != 0)
    {
        return;
    }

    m_loadInteractiveElements = new QFutureWatcher< void >(this);
    connect(m_loadInteractiveElements, SIGNAL(finished()), SLOT(on_loadInteractiveElements_finished()));

#if QT_VERSION >= QT_VERSION_CHECK(6,0,0)

    m_loadInteractiveElements->setFuture(QtConcurrent::run(std::bind(&PageItem::loadInteractiveElements, this)));

#else

    m_loadInteractiveElements->setFuture(QtConcurrent::run(this, &PageItem::loadInteractiveElements));

#endif // QT_VERSION
}

void PageItem::loadInteractiveElements()
{
    m_links = m_page->links();

    if(presentationMode())
    {
        return;
    }

    PageItem* const parent = this;
    QThread* const parentThread = parent->thread();

    const QList< Model::Annotation* > annotations = m_page->annotations();

    foreach(Model::Annotation* annotation, annotations)
    {
        annotation->moveToThread(parentThread);
        connect(annotation, SIGNAL(wasModified()), parent, SIGNAL(wasModified()));
    }

    m_annotations = annotations;

    const QList< Model::FormField* > formFields = m_page->formFields();

    foreach(Model::FormField* formField, formFields)
    {
        formField->moveToThread(parentThread);
        connect(formField, SIGNAL(wasModified()), parent, SIGNAL(wasModified()));
    }

    m_formFields = formFields;
}

void PageItem::copyToClipboard(QPoint screenPos)
{
    QMenu menu;

    QAction* copyTextAction = menu.addAction(tr("Copy &text"));
    QAction* selectTextAction = menu.addAction(tr("&Select text"));
    QAction* appendTextToBookmarkCommentAction = menu.addAction(tr("&Append text to bookmark comment..."));
    const QAction* copyImageAction = menu.addAction(tr("Copy &image"));
    const QAction* saveImageToFileAction = menu.addAction(tr("Save image to &file..."));

    const QString text = m_page->text(m_transform.inverted().mapRect(m_rubberBand));

    copyTextAction->setVisible(!text.isEmpty());
    selectTextAction->setVisible(!text.isEmpty() && QApplication::clipboard()->supportsSelection());
    appendTextToBookmarkCommentAction->setVisible(!text.isEmpty());

    const QAction* action = menu.exec(screenPos);

    if(action == copyTextAction || action == selectTextAction)
    {
        if(action == copyTextAction)
        {
            QApplication::clipboard()->setText(text);
        }
        else
        {
            QApplication::clipboard()->setText(text, QClipboard::Selection);
        }
    }
    else if(action == appendTextToBookmarkCommentAction)
    {
        emit appendTextToBookmarkComment(m_index + 1, text);
    }
    else if(action == copyImageAction || action == saveImageToFileAction)
    {
        const QRect rect = m_rubberBand.translated(-m_boundingRect.topLeft()).toRect();
        const QImage image = m_page->render(m_renderParam.resolutionX() * m_renderParam.scaleFactor(),
                                            m_renderParam.resolutionY() * m_renderParam.scaleFactor(),
                                            m_renderParam.rotation(), rect);

        if(!image.isNull())
        {
            if(action == copyImageAction)
            {
                QApplication::clipboard()->setImage(image);
            }
            else if(action == saveImageToFileAction)
            {
                const QString fileName = QFileDialog::getSaveFileName(0, tr("Save image to file"), QDir::homePath(), "Portable network graphics (*.png)");

                if(!image.save(fileName, "PNG"))
                {
                    QMessageBox::warning(0, tr("Warning"), tr("Could not save image to file '%1'.").arg(fileName));
                }
            }
        }
    }
}

void PageItem::addAnnotation(QPoint screenPos)
{
    if(m_page->canAddAndRemoveAnnotations())
    {
        QMenu menu;

        const QAction* addTextAction = menu.addAction(tr("Add &text"));
        const QAction* addHighlightAction = menu.addAction(tr("Add &highlight"));

        const QAction* action = menu.exec(screenPos);

        if(action == addTextAction || action == addHighlightAction)
        {
            QRectF boundary = m_normalizedTransform.inverted().mapRect(m_rubberBand);

            Model::Annotation* annotation = 0;

            if(action == addTextAction)
            {
                boundary.setWidth(24.0 / m_size.width());
                boundary.setHeight(24.0 / m_size.height());

                annotation = m_page->addTextAnnotation(boundary, s_settings->pageItem().annotationColor());
            }
            else if(action == addHighlightAction)
            {
                annotation = m_page->addHighlightAnnotation(boundary, s_settings->pageItem().annotationColor());
            }

            m_annotations.append(annotation);
            connect(annotation, SIGNAL(wasModified()), SIGNAL(wasModified()));

            refresh(false, true);
            emit wasModified();

            if(action == addTextAction)
            {
                showAnnotationOverlay(annotation);
            }
        }
    }
}

void PageItem::showLinkContextMenu(Model::Link* link, QPoint screenPos)
{
    if(link->page == -1)
    {
        QMenu menu;

        const QAction* copyLinkAddressAction = menu.addAction(tr("&Copy link address"));
        QAction* selectLinkAddressAction = menu.addAction(tr("&Select link address"));

        selectLinkAddressAction->setVisible(QApplication::clipboard()->supportsSelection());

        const QAction* action = menu.exec(screenPos);

        if(action == copyLinkAddressAction)
        {
            QApplication::clipboard()->setText(link->urlOrFileName);
        }
        else if(action == selectLinkAddressAction)
        {
            QApplication::clipboard()->setText(link->urlOrFileName, QClipboard::Selection);
        }
    }
}

void PageItem::showAnnotationContextMenu(Model::Annotation* annotation, QPoint screenPos)
{
    if(m_page->canAddAndRemoveAnnotations())
    {
        QMenu menu;

        const QAction* removeAnnotationAction = menu.addAction(tr("&Remove annotation"));

        const QAction* action = menu.exec(screenPos);

        if(action == removeAnnotationAction)
        {
            m_annotations.removeAll(annotation);
            m_page->removeAnnotation(annotation);

            annotation->deleteLater();

            refresh(false, true);
            emit wasModified();
        }
    }
}

template< typename Overlay, typename Element >
void PageItem::showOverlay(Overlay& overlay, const char* hideOverlay, const QList< Element* >& elements, Element* selectedElement)
{
    foreach(Element* element, elements)
    {
        if(!overlay.contains(element))
        {
            addProxy(overlay, hideOverlay, element);
        }

        if(element == selectedElement)
        {
            overlay.value(element)->widget()->setFocus();
        }
    }
}

template< typename Overlay, typename Element >
void PageItem::addProxy(Overlay& overlay, const char* hideOverlay, Element* element)
{
    QGraphicsProxyWidget* proxy = new QGraphicsProxyWidget(this);
    proxy->setWidget(element->createWidget());

#if QT_VERSION >= QT_VERSION_CHECK(4,7,0)

    proxy->setAutoFillBackground(true);

#endif // QT_VERSION

    overlay.insert(element, proxy);
    setProxyGeometry(element, proxy);

    connect(proxy, SIGNAL(visibleChanged()), hideOverlay);
}

template< typename Overlay >
void PageItem::hideOverlay(Overlay& overlay, bool deleteLater)
{
#if QT_VERSION >= QT_VERSION_CHECK(4,8,0)

    Overlay discardedOverlay;
    discardedOverlay.swap(overlay);

#else

    Overlay discardedOverlay(overlay);
    overlay = Overlay();

#endif // QT_VERSION

    if(!discardedOverlay.isEmpty())
    {
        for(typename Overlay::const_iterator i = discardedOverlay.constBegin(); i != discardedOverlay.constEnd(); ++i)
        {
            if(deleteLater)
            {
                i.value()->deleteLater();
            }
            else
            {
                delete i.value();
            }
        }

        refresh(false, true);
    }
}

template< typename Overlay >
void PageItem::updateOverlay(const Overlay& overlay) const
{
    for(typename Overlay::const_iterator i = overlay.constBegin(); i != overlay.constEnd(); ++i)
    {
        setProxyGeometry(i.key(), i.value());
    }
}

void PageItem::setProxyGeometry(Model::Annotation* annotation, QGraphicsProxyWidget* proxy) const
{
    const QPointF center = m_normalizedTransform.map(annotation->boundary().center());

    qreal x = center.x() - 0.5 * proxy->preferredWidth();
    qreal y = center.y() - 0.5 * proxy->preferredHeight();
    qreal width = proxy->preferredWidth();
    qreal height = proxy->preferredHeight();

    x = qMax(x, m_boundingRect.left() + proxyPadding);
    y = qMax(y, m_boundingRect.top() + proxyPadding);

    width = qMin(width, m_boundingRect.right() - proxyPadding - x);
    height = qMin(height, m_boundingRect.bottom() - proxyPadding - y);

    proxy->setGeometry(QRectF(x, y, width, height));
}

void PageItem::setProxyGeometry(Model::FormField* formField, QGraphicsProxyWidget* proxy) const
{
    QRectF rect = m_normalizedTransform.mapRect(formField->boundary());

    qreal x = rect.x();
    qreal y = rect.y();
    qreal width = rect.width();
    qreal height = rect.height();

    switch(m_renderParam.rotation())
    {
    default:
    case RotateBy0:
        proxy->setRotation(0.0);
        break;
    case RotateBy90:
        x += width;
        qSwap(width, height);

        proxy->setRotation(90.0);
        break;
    case RotateBy180:
        x += width;
        y += height;

        proxy->setRotation(180.0);
        break;
    case RotateBy270:
        y += height;
        qSwap(width, height);

        proxy->setRotation(270.0);
        break;
    }

    width /= m_renderParam.scaleFactor();
    height /= m_renderParam.scaleFactor();

    proxy->setScale(m_renderParam.scaleFactor());

    proxy->setGeometry(QRectF(x - proxyPadding, y - proxyPadding, width + proxyPadding, height + proxyPadding));
}

void PageItem::prepareGeometry()
{
    m_transform.reset();

    m_transform.scale(m_renderParam.resolutionX() * m_renderParam.scaleFactor() / 72.0,
                      m_renderParam.resolutionY() * m_renderParam.scaleFactor() / 72.0);

    switch(m_renderParam.rotation())
    {
    default:
    case RotateBy0:
        break;
    case RotateBy90:
        m_transform.rotate(90.0);
        break;
    case RotateBy180:
        m_transform.rotate(180.0);
        break;
    case RotateBy270:
        m_transform.rotate(270.0);
        break;
    }

    m_normalizedTransform = m_transform;
    m_normalizedTransform.scale(m_size.width(), m_size.height());


    m_boundingRect = m_transform.mapRect(QRectF(QPointF(), m_size));

    m_boundingRect.setWidth(qRound(m_boundingRect.width()));
    m_boundingRect.setHeight(qRound(m_boundingRect.height()));


    prepareTiling();

    updateAnnotationOverlay();
    updateFormFieldOverlay();
}

void PageItem::prepareTiling()
{
    if(!useTiling())
    {
        m_tileItems.first()->setRect(QRect(0, 0, m_boundingRect.width(), m_boundingRect.height()));

        return;
    }


    const qreal pageWidth = m_boundingRect.width();
    const qreal pageHeight = m_boundingRect.height();
    const qreal pageSize = qMax(pageWidth, pageHeight);

    int tileSize = s_settings->pageItem().tileSize();

    if(tileSize * veryLargeTilesThreshold < pageSize)
    {
        tileSize *= 4;
    }
    else if(tileSize * largeTilesThreshold < pageSize)
    {
        tileSize *= 2;
    }

    int tileWidth = pageWidth < pageHeight ? tileSize * pageWidth / pageHeight : tileSize;
    int tileHeight = pageHeight < pageWidth ? tileSize * pageHeight / pageWidth : tileSize;

    const int columnCount = qCeil(pageWidth / tileWidth);
    const int rowCount = qCeil(pageHeight / tileHeight);

    tileWidth = qCeil(pageWidth / columnCount);
    tileHeight = qCeil(pageHeight / rowCount);


    const int newCount = columnCount * rowCount;
    const int oldCount = m_tileItems.count();

    if(oldCount != newCount)
    {
        for(int index = newCount; index < oldCount; ++index)
        {
            m_tileItems.at(index)->deleteAfterRender();
        }

        m_tileItems.resize(newCount);

        for(int index = oldCount; index < newCount; ++index)
        {
            m_tileItems.replace(index, new TileItem(this));
        }

        foreach(TileItem* tile, m_tileItems)
        {
            tile->dropObsoletePixmap();
        }
    }

    m_exposedTileItems.clear();


    for(int column = 0; column < columnCount; ++column)
    {
        for(int row = 0; row < rowCount; ++row)
        {
            const int left = column > 0 ? column * tileWidth : 0.0;
            const int top = row > 0 ? row * tileHeight : 0.0;

            const int width = column < (columnCount - 1) ? tileWidth : pageWidth - left;
            const int height = row < (rowCount - 1) ? tileHeight : pageHeight - top;

            m_tileItems.at(column * rowCount + row)->setRect(QRect(left, top, width, height));
        }
    }
}

inline void PageItem::paintPage(QPainter* painter, const QRectF& exposedRect) const
{
    if(s_settings->pageItem().decoratePages() && !presentationMode())
    {
        // background

        QColor paperColor = s_settings->pageItem().paperColor();

        if(m_renderParam.invertColors())
        {
            paperColor.setRgb(~paperColor.rgb());
        }

        if(m_renderParam.invertLightness())
        {
            paperColor.setRgb(~paperColor.rgb());
        }

        painter->fillRect(m_boundingRect, QBrush(paperColor));
    }

    // tiles

    if(!useTiling())
    {
        TileItem* tile = m_tileItems.first();

        if(tile->paint(painter, m_boundingRect.topLeft()))
        {
            tile->dropPixmap();
        }
    }
    else
    {
        const QRectF& translatedExposedRect = exposedRect.translated(-m_boundingRect.topLeft());

        foreach(TileItem* tile, m_tileItems)
        {
            const bool intersects = translatedExposedRect.intersects(tile->rect());
            const bool contains = m_exposedTileItems.contains(tile);

            if(intersects && !contains)
            {
                m_exposedTileItems.insert(tile);
            }
            else if(!intersects && contains)
            {
                m_exposedTileItems.remove(tile);

                tile->cancelRender();
            }
        }

        bool allExposedPainted = true;

        foreach(TileItem* tile, m_exposedTileItems)
        {
            if(!tile->paint(painter, m_boundingRect.topLeft()))
            {
                allExposedPainted = false;
            }
        }

        if(allExposedPainted)
        {
            foreach(TileItem* tile, m_exposedTileItems)
            {
                tile->dropPixmap();
            }
        }
    }

    if(s_settings->pageItem().decoratePages() && !presentationMode())
    {
        // border

        painter->save();

        painter->setClipping(false);

        painter->drawRect(m_renderParam.trimMargins() ? PageItem::boundingRect() : PageItem::uncroppedBoundingRect());

        painter->restore();
    }
}

inline void PageItem::paintLinks(QPainter* painter) const
{
    if(s_settings->pageItem().decorateLinks() && !presentationMode() && !m_links.isEmpty())
    {
        painter->save();

        painter->setTransform(m_normalizedTransform, true);
        painter->setPen(QPen(Qt::red, 0.0));

        foreach(const Model::Link* link, m_links)
        {
            painter->drawPath(link->boundary);
        }

        painter->restore();
    }
}

inline void PageItem::paintFormFields(QPainter* painter) const
{
    if(s_settings->pageItem().decorateFormFields() && !presentationMode() && !m_formFields.isEmpty())
    {
        painter->save();

        painter->setTransform(m_normalizedTransform, true);
        painter->setPen(QPen(Qt::blue, 0.0));

        foreach(const Model::FormField* formField, m_formFields)
        {
            painter->drawRect(formField->boundary());
        }

        painter->restore();
    }
}

inline void PageItem::paintHighlights(QPainter* painter) const
{
    if(!m_highlights.isEmpty())
    {
        painter->save();

        painter->setTransform(m_transform, true);
        painter->setPen(QPen(s_settings->pageItem().highlightColor(), 0.0));
        painter->setBrush(QBrush(s_settings->pageItem().highlightColor()));
        painter->setCompositionMode(QPainter::CompositionMode_Multiply);

        foreach(const QRectF highlight, m_highlights)
        {
            painter->drawRect(highlight.normalized());
        }

        painter->restore();
    }
}

inline void PageItem::paintRubberBand(QPainter* painter) const
{
    if(!m_rubberBand.isNull())
    {
        painter->save();

        painter->setPen(QPen(Qt::white, 0.0, Qt::DashLine));
        painter->setCompositionMode(QPainter::CompositionMode_Difference);

        painter->drawRect(m_rubberBand);

        painter->restore();
    }
}

} // qpdfview
