/*******************************************************************************
**
** SPDX-FileCopyrightText: Copyright 2023 Open Mobile Platform LLC <community@omp.ru>
** SPDX-License-Identifier: BSD-3-Clause
** This file is part of the OfficeViewer project.
**
** Redistribution and use in source and binary forms,
** with or without modification, are permitted provided
** that the following conditions are met:
**
** * Redistributions of source code must retain the above copyright notice,
**   this list of conditions and the following disclaimer.
** * Redistributions in binary form must reproduce the above copyright notice,
**   this list of conditions and the following disclaimer
**   in the documentation and/or other materials provided with the distribution.
** * Neither the name of the copyright holder nor the names of its contributors
**   may be used to endorse or promote products derived from this software
**   without specific prior written permission.
**
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
** AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
** THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
** FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
** IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
** OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
** PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
** LOSS OF USE, DATA, OR PROFITS;
** OR BUSINESS INTERRUPTION)
** HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
** WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE)
** ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
** EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**
*******************************************************************************/


#include <QQuickWindow>
#include <QTimer>
#include <QFutureWatcher>
#include <QtGlobal>
#include <QtConcurrent/QtConcurrentRun>

#include <debug.h>

#include "basedocument.h"
#include "baseannotation.h"
#include "documentmapper.h"
#include "pagecontainer.h"
#include "bookmarksmodel.h"

#include "docview.h"

DocView::DocView(QQuickItem *parent) : QQuickItem(parent),
    m_count(-1),
    m_contentHeight(-1),
    m_contentWidth(-1),
    m_contentY(-1),
    m_contentX(-1),
    m_moveDirection(-1),
    m_paintedItemsSize(-1),
    m_currentIndex(-1),
    m_catchBound(0),
    m_orientation(Qt::Vertical),
    m_itemScale(1.0),
    m_annotationsPaint(false),
    m_documentProvider(nullptr),
    m_documentEdited(false),
    m_topMargin(0),
    m_highLightColor(0, 0, 255, 50),
    m_cursorDelegate(nullptr),
    m_startSelectionMarker(nullptr),
    m_endSelectionMarker(nullptr),
    m_selectionMode(false)
{
    setFlag(QQuickItem::ItemHasContents, true);
    setFlag(QQuickItem::ItemAcceptsInputMethod, true);
    setAcceptedMouseButtons(Qt::AllButtons);

    m_mapper = new DocumentMapper(this);
    connect(m_mapper, &DocumentMapper::contentWidthChanged, this, &DocView::_updateContentSize);
    connect(m_mapper, &DocumentMapper::contentHeightChanged, this, &DocView::_updateContentSize);
    connect(m_mapper, &DocumentMapper::mapEnd, this, &DocView::_calculateVisible);

    connect(this, &DocView::loadPages, this, &DocView::_preparePages);

    connect(this, &QQuickItem::windowChanged, this, [this]() {
        auto *window = this->window();

        if (window != nullptr)
            connect(window, &QQuickWindow::beforeSynchronizing, this, &DocView::_calculateVisible, Qt::DirectConnection);
    });

    m_timer = new QTimer(this);
    m_timer->setSingleShot(true);

    emit statusChanged(BaseDocument::DocumentStatus::Null);

    m_bookmarksModel = new BookmarksModel(this);
}

DocView::~DocView() = default;

QSGNode *DocView::updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *)
{
    return oldNode;
}

void DocView::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry)
{
    QQuickItem::geometryChanged(newGeometry, oldGeometry);

    if (qFuzzyCompare(newGeometry.width(), oldGeometry.width())
            && qFuzzyCompare(newGeometry.height(), oldGeometry.height()))
        return;

    if (m_contentWidth <= 0 && m_contentHeight <= 0)
        return;

    for (auto paintItem : m_pages) {
        auto *page = qobject_cast<PageContainer *>(paintItem);
        if (page == nullptr)
            continue;

        page->setRequestedSize({ width(), height() });
    }

    if (!qFuzzyCompare(oldGeometry.width(), newGeometry.width())) {
        emit widthChanged();
        emit lastPageIndentChanged(lastPageIndent());
    }

    if (!qFuzzyCompare(oldGeometry.height(), newGeometry.height())) {
        emit heightChanged();
        emit lastPageIndentChanged(lastPageIndent());


        if (m_timer->isActive()) {
            _positionPages();
            _positionSelectionMarkers();
            return;
        }

        if (m_orientation == Qt::Vertical) {
            auto newContentY = m_contentY;
            if (m_contentY >= 1.0f)
                newContentY += (oldGeometry.height() - newGeometry.height()) / 2.0f;

            if (m_currentIndex == m_count - 1) {
                auto pagePosition = m_mapper->actualPagePosition(m_count - 1);
                newContentY = pagePosition.start * m_itemScale - m_catchBound + 1;
            }

            setContentY(newContentY);
        } else {
            auto newContentX = m_contentX;
            if (m_contentX >= 1.0f)
                newContentX += (oldGeometry.width() - newGeometry.width()) / 2.0f;

            if (m_currentIndex == m_count - 1) {
                auto pagePosition = m_mapper->actualPagePosition(m_count - 1);
                newContentX = pagePosition.start * m_itemScale - m_catchBound - 1;
            }

            setContentX(newContentX);
        }

        _positionPages();
        _positionSelectionMarkers();
    }

    return;
}

int DocView::count() const
{
    return m_count;
}

qreal DocView::contentHeight() const
{
    return m_contentHeight;
}

qreal DocView::contentWidth() const
{
    return m_contentWidth;
}

qreal DocView::contentY() const
{
    return m_contentY;
}

qreal DocView::contentX() const
{
    return m_contentX;
}

int DocView::currentIndex() const
{
    return m_currentIndex;
}

qreal DocView::catchBound() const
{
    return m_catchBound;
}

Qt::Orientation DocView::typeScroll() const
{
    return m_orientation;
}

qreal DocView::lastPageIndent() const
{
    if (m_orientation == Qt::Vertical)
        return qMax(m_catchBound, height() - m_mapper->lastPageActualSize() / qMax(1.0f, float(m_itemScale)) - m_catchBound + m_topMargin);

    if (m_contentWidth <= width())
        return 0;

    return qMax(m_catchBound, width() - m_mapper->lastPageActualSize() / qMax(1.0f, float(m_itemScale)) - m_catchBound);
}

qreal DocView::itemScale() const
{
    return m_itemScale;
}

bool DocView::annotationsPaint() const
{
    return m_annotationsPaint;
}

bool DocView::notesPaint() const
{
    return m_notesPaint;
}

BaseDocument *DocView::documentProvider() const
{
    return m_documentProvider;
}

qreal DocView::contentTopMargin() const
{
    return m_contentTopMargin;
}

BookmarksModel *DocView::bookmarksModel() const
{
    return m_bookmarksModel;
}

int DocView::fileVersion() const
{
    return m_documentProvider == nullptr ? -1 : m_documentProvider->fileVersion();
}

void DocView::saveDocumentAs(const QString &path,  const QString &format)
{
    auto *watcher = new QFutureWatcher<bool>();
    connect(watcher, &QFutureWatcher<bool>::finished, this, [this, watcher]() {
        if (watcher == nullptr) {
            emit documentSaved(false);
            return;
        }

        if (watcher->isFinished() && !watcher->isCanceled()) {
            emit documentSaved(watcher->result());
        } else {
            emit documentSaved(false);
        }

        if (watcher != nullptr)
            watcher->deleteLater();
    });

    watcher->setFuture(QtConcurrent::run(QThreadPool::globalInstance(), [this, path, format]() {
        return m_documentProvider ? m_documentProvider->saveDocumentAs(path, format) : false;
    }));
}

void DocView::addAnnotation(const QRectF &rect, const QColor &color, const QString &author, const QString &content)
{
    for (const auto &page : m_paintedPages) {
        auto pagePosition = m_mapper->actualPagePosition(page);
        if (m_orientation == Qt::Vertical) {
            if (rect.y() >= pagePosition.start && rect.y() <= pagePosition.end) {
                if (!m_pages.contains(page))
                    return;

                QRectF localRect(rect);
                localRect.setY(rect.y() - pagePosition.start);
                localRect.setHeight(rect.height());
                localRect.setWidth(rect.width());

                auto pageContainer = qobject_cast<PageContainer *>(m_pages.value(page));
                pageContainer->addAnnotation(localRect, color, author, content);
                return;
            }
        } else {
            if (rect.x() > pagePosition.start && rect.x() <= pagePosition.end) {
                if (!m_pages.contains(page))
                    return;

                QRectF localRect(rect);
                localRect.setX(rect.x() - pagePosition.start);
                localRect.setHeight(rect.height());
                localRect.setWidth(rect.width());

                auto pageContainer = qobject_cast<PageContainer *>(m_pages.value(page));
                pageContainer->addAnnotation(localRect, color, author, content);
                return;
            }
        }
    }
}

void DocView::mousePress(const QPointF &pos)
{
    auto internalPos = _posToInternalPosition(pos);
    m_documentProvider->mousePress(internalPos);
}

void DocView::mouseRelease(const QPointF &pos)
{
    auto internalPos = _posToInternalPosition(pos);
    // Disabled because there is an error with coordinates at the libreofice level
    if (false)
        m_documentProvider->mouseRelease(internalPos);
}

void DocView::mouseDoubleClick(const QPointF &pos)
{
    auto internalPos = _posToInternalPosition(pos);
    m_documentProvider->mouseDoubleClick(internalPos);
}

void DocView::startSelectText(const QPointF &pos)
{
    setSelectText(pos, QPointF(m_endSelectionMarker->property("fixedX").toReal(), m_endSelectionMarker->property("fixedY").toReal()));
}

void DocView::endSelectText(const QPointF &pos)
{
    QPointF startPos(m_startSelectionMarker->position().x() + m_startSelectionMarker->width(), m_startSelectionMarker->position().y());
    setSelectText(QPointF(m_startSelectionMarker->property("fixedX").toReal(), m_startSelectionMarker->property("fixedY").toReal()), pos);
}

void DocView::setSelectText(const QPointF &startPos, const QPointF &endPos)
{
    if (startPos.y() > endPos.y() && startPos.x() > endPos.x()) {
        auto markerItem = m_startSelectionMarker;
        m_startSelectionMarker = m_endSelectionMarker;
        m_endSelectionMarker = markerItem;
        m_startSelectionMarker->setProperty("markerTag", "start");
        m_endSelectionMarker->setProperty("markerTag", "end");
    }

    auto startActualPos = startPos;
    startActualPos.setY(startPos.y() + m_contentY);
    startActualPos.setX(startPos.x() + m_contentX);
    auto startInternalPos = _posToInternalPosition(startActualPos);
    m_documentProvider->startSelectText(startInternalPos);

    auto endActualPos = endPos;
    endActualPos.setY(endPos.y() + m_contentY);
    endActualPos.setX(endPos.x() + m_contentX);
    auto endInternalPos = _posToInternalPosition(endActualPos);
    m_documentProvider->endSelectText(endInternalPos);
}

void DocView::resetSelectText()
{
    if (m_documentProvider)
        m_documentProvider->resetSelectText();
}

void DocView::startSearch(const QString &string, int direction)
{
    /* TODO: Add support for start point search
    int pageIndex = -1;
    QPointF actualPos = mapToItemCustom(QPointF(m_contentX, m_contentY), pageIndex);
    auto page = qobject_cast<PageContainer *>(m_pages[pageIndex]);

    if (!page)
        return;

    m_documentProvider->searchString(string, static_cast<BaseDocument::SearchDirection>(direction),
                                     actualPos / page->imageScale());
    */

    m_documentProvider->startSearch(string, static_cast<BaseDocument::SearchDirection>(direction),
                                     QPointF());
}

void DocView::stopSearch()
{
    m_documentProvider->stopSearch();
}

qreal DocView::topMargin() const
{
    return m_topMargin;
}

bool DocView::documentEdited() const
{
    return m_documentEdited;
}

QString DocView::path() const
{
    return m_path;
}

BaseDocument::DocumentStatus DocView::status() const
{
    return m_documentProvider == nullptr ? BaseDocument::DocumentStatus::Null : m_documentProvider->status();
}

void DocView::setContentY(qreal contentY)
{
    if (qFuzzyCompare(double(m_contentY), double(contentY)))
        return;

    if (m_orientation == Qt::Vertical)
        m_moveDirection = m_contentY - contentY;

    m_contentY = contentY;
    emit contentYChanged(m_contentY);

    _positionPages();
    _positionSelectionMarkers();
}

void DocView::setContentX(qreal contentX)
{
    if (qFuzzyCompare(double(m_contentX), double(contentX)))
        return;

    if (m_orientation == Qt::Vertical)
        m_moveDirection = m_contentX - contentX;

    m_contentX = contentX;
    emit contentXChanged(m_contentX);

    _positionPages();
    _positionSelectionMarkers();
}

void DocView::setCatchBound(qreal catchBound)
{
    if (catchBound < 0) {
        qWarning(lcDocviewer()) << "Incorrect value catchBound" << catchBound;
        return;
    }

    if (qFuzzyCompare(m_catchBound, catchBound))
        return;

    m_catchBound = catchBound;
    emit catchBoundChanged(m_catchBound);
    emit lastPageIndentChanged(lastPageIndent());

    _positionPages();
    _positionSelectionMarkers();
}

void DocView::positionViewAtIndex(int index)
{
    if (index < 0 || index > m_count)
        return;

    if (m_currentIndex != index) {
        m_currentIndex = index;
        emit currentIndexChanged(m_currentIndex);
    }

    if (!m_paintedPages.contains(index)) {
        m_paintedPages.clear();
        m_paintedPages.append(index);
    }

    auto pagePosition = m_mapper->actualPagePosition(index);

    if (m_orientation == Qt::Vertical)
        setContentY(pagePosition.start * m_itemScale + m_topMargin - m_catchBound + 1);
    else
        setContentX(pagePosition.start * m_itemScale);

    emit contentChanged();

    m_timer->start(1300);
}

void DocView::setTypeScroll(Qt::Orientation typeScroll)
{
    if (m_orientation == typeScroll)
        return;

    auto currentIndex = m_currentIndex;
    m_orientation = typeScroll;
    m_mapper->setOrientation(m_orientation);
    // TODO: Add page orientation to m_documentProvider
    positionViewAtIndex(currentIndex);
    emit typeScrollChanged(m_orientation);
    _positionPages();
    _positionSelectionMarkers();
}

void DocView::setItemScale(qreal itemScale)
{
    if (qFuzzyCompare(m_itemScale, itemScale))
        return;

    m_itemScale = itemScale;

    _positionPages();
    _positionSelectionMarkers();

    if (m_cursorDelegate) {
        m_cursorDelegate->setProperty("scale", itemScale);
    }

    emit itemScaleChanged(m_itemScale);
    emit lastPageIndentChanged(lastPageIndent());
}

void DocView::scaleAroundPoint(const QPointF &center, qreal newScale)
{
    if ((m_contentWidth * m_itemScale) == 0 || (m_contentHeight * m_itemScale) == 0) {
        qCWarning(lcDocviewer()) << "One of the page sizes is zero" << m_contentWidth << m_contentHeight;
        return;
    }

    m_contentX -= center.x() - (center.x() / (m_contentWidth * m_itemScale)) * (m_contentWidth * newScale);
    m_contentY -= center.y() - (center.y() / (m_contentHeight * m_itemScale)) * (m_contentHeight * newScale);
    m_itemScale = newScale;

    _positionPages();
    _positionSelectionMarkers();

    if (m_cursorDelegate)
        m_cursorDelegate->setProperty("scale", m_itemScale);

    emit contentXChanged(m_contentX);
    emit contentYChanged(m_contentY);
    emit itemScaleChanged(m_itemScale);
}

void DocView::setAnnotationsPaint(bool annotationsPaint)
{
    if (m_annotationsPaint == annotationsPaint)
        return;

    m_annotationsPaint = annotationsPaint;
    emit annotationsPaintChanged(m_annotationsPaint);
}

void DocView::setNotesPaint(bool notesPaint)
{
    if (m_notesPaint == notesPaint)
        return;

    m_notesPaint = notesPaint;
    emit notesPaintChanged(m_notesPaint);
}

void DocView::setDocumentProvider(BaseDocument *documentProvider)
{
    if (m_documentProvider == documentProvider)
        return;

    if (documentProvider == nullptr)
        return;

    m_documentProvider = documentProvider;
    emit documentProviderChanged(m_documentProvider);

    m_absolutePosition.setX(0);
    m_absolutePosition.setY(0);

    m_count = -1;
    emit countChanged(m_count);

    m_contentWidth = -1;
    emit contentWidthChanged(m_contentWidth);

    m_contentHeight = -1;
    emit contentHeightChanged(m_contentHeight);

    emit statusChanged(BaseDocument::DocumentStatus::Null);

    connect(m_documentProvider, &BaseDocument::pageLoaded, this, [&](int pageIndex, BaseDocument::PageLoadStatus loadStatus) {
        if (!m_pages.contains(pageIndex))
            return;

        auto page = qobject_cast<PageContainer *>(m_pages.value(pageIndex));
        if (page == nullptr)
            return;

        if (!page->source() || loadStatus != BaseDocument::PageLoadStatus::Success) {
            auto baseDocument = qobject_cast<BaseDocument *>(m_documentProvider);
            page->setPageSource(baseDocument->loadPage(pageIndex));
        }
    });

    connect(m_documentProvider, &BaseDocument::bookmarksLoaded, this, [this]() {
        m_bookmarksModel->setNewData(m_documentProvider->bookmarks());
        emit bookmarksModelChanged(m_bookmarksModel);
    });

    connect(m_documentProvider, &BaseDocument::statusChanged, this, [this](BaseDocument::DocumentStatus status) {
        if (status == BaseDocument::DocumentStatus::Ready) {
            m_mapper->setDocumentProvider(m_documentProvider);
            m_count = m_documentProvider->count();
            m_documentProvider->startLoadBookmarks();
            emit countChanged(m_count);
        }

        _calculateVisible();
        emit statusChanged(status);
    });

    connect(m_documentProvider, &BaseDocument::fileVersionChanged, this, [this](int fileVersion) {
        if (fileVersion > 0)
            emit fileVersionChanged(m_documentProvider->fileVersion());
    });

    connect(m_documentProvider, &BaseDocument::areaVisibleUpdated, this, [this](const QRect &area) {
        QPointF actualPos(area.x(), area.y());
        int pageIndex = m_mapper->pageIndexAtPosition(actualPos, m_itemScale);
        auto page = qobject_cast<PageContainer *>(m_pages[pageIndex]);
        if (page)
            page->forceUpdateRect(QRectF(area));
    });

    connect(m_documentProvider, &BaseDocument::highlightedUpdated, this, [this](const QList<QRect> &areaRects, int pageIndex) {
        for (const auto &area : areaRects)
            _goToPosition(pageIndex, QPointF(area.x(), area.y()));
    });

    connect(m_documentProvider, &BaseDocument::hyperlinkClicked, this, &DocView::clickedUrl);
    connect(m_documentProvider, &BaseDocument::cellClicked, this, &DocView::cellClicked);
    connect(m_documentProvider, &BaseDocument::cursorChanged, this, &DocView::_positionCursor);
    connect(m_documentProvider, &BaseDocument::selectionMarkerChanged, this, &DocView::_positionSelectionMarkers);
    connect(m_documentProvider, &BaseDocument::selectionTextChanged, this, &DocView::selectionTextChanged);

    connect(this, &DocView::currentIndexChanged, m_documentProvider, &BaseDocument::setPageIndex);

    m_mapper->setWidth(width());
    m_mapper->setHeight(height());

    _loadDocument();
}

void DocView::setPath(const QString &path)
{
    if (m_path == path)
        return;

    m_path = path;
    emit pathChanged(m_path);

    _loadDocument();
}

void DocView::setTopMargin(qreal topMargin)
{
    if (m_topMargin == topMargin)
        return;

    m_topMargin = topMargin;
    emit topMarginChanged(m_topMargin);
    _positionPages();
    _positionSelectionMarkers();
}

void DocView::_updateContentSize()
{
    if (!qFuzzyCompare(double(m_contentWidth), double(m_mapper->contentWidth())) && m_mapper->contentWidth() > 0.0f) {
        m_contentWidth = m_mapper->contentWidth();
        emit contentWidthChanged(m_contentWidth);
    }

    if (!qFuzzyCompare(double(m_contentHeight), double(m_mapper->contentHeight())) && m_mapper->contentHeight() > 0.0f) {
        m_contentHeight = m_mapper->contentHeight();
        emit contentHeightChanged(m_contentHeight);
    }

    emit lastPageIndentChanged(lastPageIndent());
    _restoreAbsolutePosition();
}

void DocView::_positionPages()
{
    auto paintStart = 0.0f;

    if (m_orientation == Qt::Horizontal && m_contentWidth < width())
        paintStart = (width() - m_paintedItemsSize) / 2.0f;

    paintStart += m_topMargin;

    auto maxSize = -1.0f;
    for (auto pageIndex : m_paintedPages) {
        if (!m_pages.contains(pageIndex))
            continue;

        auto page = m_pages.value(pageIndex);
        if (page == nullptr)
            continue;

        if (!page->isVisible())
            page->setVisible(true);

        auto pagePosition = m_mapper->actualPagePosition(pageIndex);

        if (m_orientation == Qt::Vertical) {
            page->setY(paintStart - (qMax(0.0f, float(m_contentY)) - pagePosition.start * m_itemScale));

            if (page->width() <= width())
                page->setX((width() - page->width()) / 2.0f);
            else
                page->setX(-m_contentX);

            auto pageContainer = qobject_cast<PageContainer *>(page);
            auto visibleArea = QRectF(m_contentX - width() / 5, m_contentY - height() / 5, width() * 1.2, height() * 1.2);
            pageContainer->setVisibleArea(visibleArea);

            maxSize = qMax(maxSize, float(page->width()));

            if (!qFuzzyCompare(float(m_contentTopMargin), paintStart)) {
                m_contentTopMargin = paintStart;
                emit contentTopMarginChanged(m_contentTopMargin);
            }
        } else {
            page->setX(paintStart - (qMax(0.0f, float(m_contentX)) - pagePosition.start * m_itemScale));

            if (page->height() <= height())
                page->setY((height() - page->height()) / 2.0f);
            else
                page->setY(-m_contentY);

            auto pageContainer = qobject_cast<PageContainer *>(page);
            pageContainer->setVisibleArea(QRectF(m_contentX, m_contentY, width(), height()));

            maxSize = qMax(maxSize, float(page->height()));

            if (!qFuzzyCompare(m_contentTopMargin, page->y())) {
                m_contentTopMargin = page->y();
                emit contentTopMarginChanged(m_contentTopMargin);
            }
        }
    }

    _updateCurrentIndex();
    _updateAbsolutePosition();

    if (m_orientation == Qt::Vertical) {
        if (qAbs(maxSize - m_contentWidth) < 0.1f)
            return;

        m_contentWidth = maxSize;
        emit contentWidthChanged(m_contentWidth);
    } else {
        if (qAbs(maxSize - m_contentHeight) < 0.1f)
            return;

        m_contentHeight = maxSize;
        emit contentHeightChanged(m_contentHeight);
    }
}

void DocView::_positionSelectionMarkers()
{
    _positionCursor();

    if (!m_documentProvider)
        return;

    if (!m_startSelectionMarker)
        return;

    if (!m_endSelectionMarker)
        return;

    auto selectionMarkers = m_documentProvider->selectionMarkers();
    auto pageForStartMarker = qobject_cast<PageContainer *>(m_pages[selectionMarkers.first.pageIndex]);
    auto pageForEndMarker = qobject_cast<PageContainer *>(m_pages[selectionMarkers.second.pageIndex]);

    setSelectionMode(m_documentProvider->mode() == BaseDocument::Mode::ModeSelect);

    auto changePosMarker = [&] (QQuickItem *markerItem, PageContainer *page, const BaseDocument::Marker &marker) {
        if (selectionMode() && page) {
            markerItem->setVisible(true);
            auto newPosY = page->y() + marker.pos.y() * page->pageRate();
            auto newPosX = page->x() + marker.pos.x() * page->pageRate();
            markerItem->setProperty("fixedY", newPosY);
            markerItem->setProperty("fixedX", newPosX);

            if (page->width() <= width()) {
                markerItem->setProperty("minimumX", (width() - page->width()) / 2.0f);
                markerItem->setProperty("maximumX", (width() + page->width() ) / 2.0f);
            } else {
                markerItem->setProperty("minimumX", 0);
                markerItem->setProperty("maximumX", m_contentWidth);
            }
        } else {
            markerItem->setVisible(false);
        }
    };

    changePosMarker(m_startSelectionMarker, pageForStartMarker, selectionMarkers.first);
    changePosMarker(m_endSelectionMarker, pageForEndMarker, selectionMarkers.second);
}

void DocView::_updateAbsolutePosition()
{
    if ( m_contentWidth == 0 || m_contentHeight == 0) {
        qCWarning(lcDocviewer()) << "Width or height must not be null" << m_contentWidth << m_contentHeight;
        return;
    }
    m_absolutePosition.setX(m_contentX / m_contentWidth);
    m_absolutePosition.setY(m_contentY / m_contentHeight);
}

void DocView::_restoreAbsolutePosition()
{
    m_contentX = m_contentWidth * m_absolutePosition.x();
    m_contentY = m_contentHeight * m_absolutePosition.y();

    emit contentXChanged(m_contentX);
    emit contentYChanged(m_contentY);
}

void DocView::_calculateVisible()
{
    auto minPageIndex = 0;

    auto startVisibleArea = qMax(0.0f, float(m_orientation == Qt::Vertical ? m_contentY : m_contentX));
    auto endVisibleArea = startVisibleArea + (m_orientation == Qt::Vertical ? height() : width());
    auto startPaintedPage = -1;
    auto endPaintedPage = -1;

    if (m_moveDirection <= 0) {
        if (!m_paintedPages.isEmpty())
            minPageIndex = qMax(0, *std::min_element(m_paintedPages.begin(), m_paintedPages.end()));

        for (int i = minPageIndex; i < m_count; ++i) {
            auto pageCoordinates = m_mapper->actualPagePosition(i);

            if (pageCoordinates.end * m_itemScale >= startVisibleArea && pageCoordinates.start * m_itemScale <= startVisibleArea) {
                startPaintedPage = i;
                break;
            }

            if (pageCoordinates.start * m_itemScale >= startVisibleArea && pageCoordinates.end * m_itemScale <= endVisibleArea) {
                startPaintedPage = i;
                break;
            }
        }
    } else {
        if (!m_paintedPages.isEmpty())
            minPageIndex = qMax(m_count - 1, *std::max_element(m_paintedPages.begin(), m_paintedPages.end()));

        for (int i = minPageIndex; i >= 0; --i) {
            auto pageCoordinates = m_mapper->actualPagePosition(i);

            if (pageCoordinates.end * m_itemScale >= startVisibleArea && pageCoordinates.start * m_itemScale <= startVisibleArea) {
                startPaintedPage = i;
                break;
            }
        }
    }

    for (int i = startPaintedPage; i < m_count; ++i) {
        auto pageCoordinate = m_mapper->actualPagePosition(i);
        if (pageCoordinate.start * m_itemScale > startVisibleArea && pageCoordinate.end * m_itemScale <= endVisibleArea)
            endPaintedPage = i;

        if (pageCoordinate.end * m_itemScale >= endVisibleArea) {
            endPaintedPage = i;
            break;
        }
    }

    if (startPaintedPage == m_count - 1)
        endPaintedPage = startPaintedPage;

    if (startPaintedPage < 0)
        startPaintedPage = minPageIndex;

    endPaintedPage = qMin(endPaintedPage + 1, m_count - 1);

    m_paintedPages.clear();
    for (int i = qMax(0, startPaintedPage - 1); i <= endPaintedPage; ++i)
        m_paintedPages.append(i);

    std::sort(m_paintedPages.begin(), m_paintedPages.end());

    QMutableHashIterator<int, QQuickItem *> pagesIt(m_pages);
    while (pagesIt.hasNext()) {
        pagesIt.next();

        if (!pagesIt.value()) {
            pagesIt.remove();
            continue;
        }

        if (!m_paintedPages.contains(pagesIt.key())) {
            pagesIt.value()->deleteLater();
            pagesIt.remove();
        }
    }

    m_paintedItemsSize = 0;
    for (auto pageIndex : m_paintedPages) {
        auto pageGeometry = m_mapper->actualPagePosition(pageIndex);
        m_paintedItemsSize += (pageGeometry.end - pageGeometry.start) * m_itemScale;
    }

    emit loadPages();
}

void DocView::_updateCurrentIndex()
{
    auto newCurrentIndex = m_currentIndex;
    for (auto pageIndex : m_paintedPages) {
        if (!m_pages.contains(pageIndex))
            continue;

        auto page = m_pages.value(pageIndex);
        if (page == nullptr)
            continue;

        if (m_orientation == Qt::Vertical) {
            if (m_catchBound >= page->y() && m_catchBound < page->y() + page->height()) {
                if (m_currentIndex != pageIndex) {
                    newCurrentIndex = pageIndex;
                }
            }
        } else {
            if (m_catchBound >= page->x() && m_catchBound < page->x() + page->width()) {
                if (m_currentIndex != pageIndex)
                    newCurrentIndex = pageIndex;
            }
        }
    }

    if (newCurrentIndex != m_currentIndex) {
        m_currentIndex = newCurrentIndex;
        emit currentIndexChanged(m_currentIndex);
    }
}

void DocView::_processActivatedAnnotation(BaseAnnotation *annotation)
{
    if (annotation == nullptr)
        return;

    if (annotation->type == BaseAnnotation::AnnotationType::Url) {
        if (!annotation->content.isEmpty())
            emit clickedUrl(annotation->content);

        return;
    }

    if (annotation->type != BaseAnnotation::AnnotationType::Link)
        return;

    auto pageIndex = annotation->linkToPage;
    if (pageIndex < 0 && pageIndex >= m_count)
        return;


    auto pageCoordinate = annotation->pageCoordinate;

    _goToPosition(pageIndex, pageCoordinate);
}

void DocView::_processActivatedCell(BaseAnnotation *annotation)
{
    if (annotation == nullptr)
        return;

    m_documentProvider->goToCell(annotation->content);
}

void DocView::_processActivatedNote(BaseAnnotation *annotation)
{
    if (annotation == nullptr)
        return;

    if (annotation->type == BaseAnnotation::AnnotationType::Comment)
        emit noteActivated(annotation->content, annotation->author, annotation->dateTime);
}

void DocView::_loadDocument()
{
    if (m_path.isEmpty())
        return;

    if (m_documentProvider == nullptr)
        return;

    emit statusChanged(BaseDocument::DocumentStatus::Loading);

    m_documentProvider->setPath(m_path);
}

void DocView::_documentEdited()
{
    m_documentEdited = true;
    emit documentEditedChanged(true);
}

void DocView::_goToPosition(int pageIndex, QPointF pos)
{
    auto pageGeometry = m_mapper->originalPageGeometry(pageIndex);

    if (pos.y() <= 0)
        pos.setY(0);

    if (pos.x() <= 0)
        pos.setX(0);

    auto targetPageSize = PageContainer::pageCurrentSize(pageGeometry,
                                                         {width(), height()},
                                                         m_orientation,
                                                         m_itemScale);

    auto pageRate = targetPageSize.width() / pageGeometry.width();
    QPointF linkOnPagePosition(pos.x() * pageRate,
                               (pageGeometry.height() - pos.y()) * pageRate);

    auto pagePosition = m_mapper->actualPagePosition(pageIndex);
    if (m_orientation == Qt::Vertical) {
        if (pos.y() <= 0) {
            positionViewAtIndex(pageIndex);
        } else {
            setContentY(pagePosition.start * m_itemScale + pos.y() * pageRate - height() / 2);

            if (pos.x() * pageRate < contentWidth() - width())
                setContentX(qMax(pos.x() * pageRate - width() / 2, qreal(0.0f)));
            else
                setContentX(contentWidth() - width());
        }
    } else {
        setContentX(pagePosition.start * m_itemScale);
    }

    _positionPages();
    _positionSelectionMarkers();

    emit clickedGoToPage(pageIndex, pos);
}

BaseDocument::InternalPosition DocView::_posToInternalPosition(const QPointF &pos)
{
    BaseDocument::InternalPosition internalPos;
    auto actualPos = pos;
    actualPos.setY(pos.y() - m_topMargin);
    int pageIndex = m_mapper->pageIndexAtPosition(actualPos, m_itemScale);
    auto page = qobject_cast<PageContainer *>(m_pages[pageIndex]);

    if (!page)
        return internalPos;

    actualPos.setX(pos.x() - qMax(page->x(), qreal(0.0f)));
    auto actualPosOnPage = m_mapper->mapToPage(actualPos, m_itemScale);
    internalPos.pageIndex = pageIndex;
    internalPos.pos = actualPosOnPage;
    internalPos.zoom = page->imageScale();

    return internalPos;
}

void DocView::_positionCursor()
{
    if (!m_documentProvider)
        return;

    if (!m_cursorDelegate)
        return;

    auto cursor = m_documentProvider->cursor();
    auto page = qobject_cast<PageContainer *>(m_pages[cursor.first]);

    if (!page)
        return;

    m_cursorDelegate->setY(page->y() + cursor.second.y() * page->pageRate());
    m_cursorDelegate->setX(page->x() + cursor.second.x() * page->pageRate());
    m_cursorDelegate->setHeight(cursor.second.height() * page->pageRate());
}

QQuickItem *DocView::endSelectionMarker() const
{
    return m_endSelectionMarker;
}

void DocView::setEndSelectionMarker(QQuickItem *newEndSelectionMarker)
{
    if (m_endSelectionMarker == newEndSelectionMarker)
        return;
    m_endSelectionMarker = newEndSelectionMarker;
    m_endSelectionMarker->setParentItem(this);
    m_endSelectionMarker->setZ(10);
    emit endSelectionMarkerChanged();
}

QQuickItem *DocView::startSelectionMarker() const
{
    return m_startSelectionMarker;
}

void DocView::setStartSelectionMarker(QQuickItem *newStartSelectionMarker)
{
    if (m_startSelectionMarker == newStartSelectionMarker)
        return;
    m_startSelectionMarker = newStartSelectionMarker;
    m_startSelectionMarker->setParentItem(this);
    m_startSelectionMarker->setZ(10);
    emit startSelectionMarkerChanged();
}

void DocView::_preparePages()
{
    if (m_paintedPages.isEmpty())
        return;

    auto baseDocument = qobject_cast<BaseDocument *>(m_documentProvider);

    bool needPositioning = false;
    for (const auto &pageIndex : m_paintedPages) {
        if (m_pages.keys().contains(pageIndex)) {
            auto pageContainer = qobject_cast<PageContainer *>(m_pages.value(pageIndex));
            if (pageContainer && !pageContainer->source()) {
                auto pageSource = m_documentProvider->loadPage(pageContainer->index());
                if (pageSource) {
                    pageContainer->setPageSource(pageSource);
                    needPositioning = true;
                }
            }

            continue;
        }

        auto page = new PageContainer(this);

        connect(page, &PageContainer::pageReady, this, &DocView::_positionPages);
        connect(page, &PageContainer::pageReady, this, &DocView::_positionSelectionMarkers);
        connect(this, &DocView::typeScrollChanged, page, &PageContainer::setOrientation);
        connect(this, &DocView::itemScaleChanged, page, &PageContainer::setScale);
        connect(this, &DocView::annotationsPaintChanged, page, &PageContainer::setAnnotationsPaint);
        connect(page, &PageContainer::annotationActivate, this, &DocView::_processActivatedAnnotation);
        connect(page, &PageContainer::cellActivate, this, &DocView::_processActivatedCell);
        connect(page, &PageContainer::noteActivate, this, &DocView::_processActivatedNote);
        connect(this, &DocView::notesPaintChanged, page, &PageContainer::setNotesPaint);
        connect(page, &PageContainer::pageChanged, this, &DocView::_documentEdited);

        page->setVisible(false);
        page->setPageGeometry(m_mapper->originalPageGeometry(pageIndex));
        page->setOrientation(m_orientation);
        page->setRequestedSize({ width(), height() });
        page->setScale(m_itemScale);
        page->setMapper(m_mapper);
        page->setAnnotationsPaint(m_annotationsPaint);
        page->setNotesPaint(m_notesPaint);
        page->setHighLightColor(highLightColor());

        auto pageSource = baseDocument->loadPage(pageIndex);
        if (pageSource)
            page->setPageSource(pageSource);

        m_pages.insert(pageIndex, page);
        needPositioning = true;
    }

    if (needPositioning) {
        _positionPages();
        _positionSelectionMarkers();
    }
}

const QColor &DocView::highLightColor() const
{
    return m_highLightColor;
}

void DocView::setHighLightColor(const QColor &highLightColor)
{
    if (m_highLightColor == highLightColor)
        return;

    m_highLightColor = highLightColor;
    emit highLightColorChanged();

    for (auto pageItem : m_pages.values()) {
        auto page = qobject_cast<PageContainer *>(pageItem);
        if (page)
            page->setHighLightColor(m_highLightColor);
    }
}

QQuickItem *DocView::cursor() const
{
    return m_cursorDelegate;
}

void DocView::setCursor(QQuickItem *newCursor)
{
    if (m_cursorDelegate == newCursor)
        return;
    m_cursorDelegate = newCursor;
    m_cursorDelegate->setParentItem(this);
    m_cursorDelegate->setZ(10);
    emit cursorChanged();
}

bool DocView::selectionMode() const
{
    return m_selectionMode;
}

void DocView::setSelectionMode(bool newSelectionMode)
{
    if (m_selectionMode == newSelectionMode)
        return;
    m_selectionMode = newSelectionMode;
    emit selectionModeChanged();
}

const QString DocView::selectionText() const
{
    return m_documentProvider->slsectionText();
}
