/*******************************************************************************
**
** 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 <QtMath>
#include <QFutureWatcher>
#include <utility>

#include <debug.h>

#include "multipagetile.h"
#include "docitemannotation.h"
#include "docitemcomment.h"
#include "docitemsearchhighlight.h"
#include "docitemcellselected.h"
#include "baseannotation.h"
#include "basepage.h"
#include "documentmapper.h"
#include "backgroundpage.h"

#include "pagecontainer.h"

PageContainer::PageContainer(QQuickItem *parent) : QQuickItem(parent),
    m_scale(1.0),
    m_mapper(nullptr),
    m_backgroundPage(nullptr),
    m_maxTileZ(0),
    m_tileSize(350),
    m_annotationsPaint(false),
    m_notesPaint(false),
    m_cellSelectedPaint(true),
    m_allTilesReady(false),
    m_cursor(nullptr)
{
    QByteArray(320*2, 0);
    setFlag(QQuickItem::ItemHasContents, true);

    auto itemWindow = window();
    if (itemWindow != nullptr)
        connect(itemWindow, &QQuickWindow::beforeSynchronizing, this, &PageContainer::_updateVisible, Qt::DirectConnection);
}

PageContainer::~PageContainer()
{
    if (m_pageSource)
        m_pageSource->stopAllBitmapPart();
}

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

void PageContainer::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry)
{
    if (qFuzzyCompare(newGeometry.width(), oldGeometry.width()) && qFuzzyCompare(newGeometry.height(), oldGeometry.height()))
        return;

    if (m_pageSource)
        m_pageSource->stopAllBitmapPart();

    QQuickItem::geometryChanged(newGeometry, oldGeometry);

    auto pageRate = width() / m_pageGeometry.width();

    if (m_backgroundPage) {
        m_backgroundPage->setWidth(width());
        m_backgroundPage->setHeight(height());
        m_backgroundPage->setImageScale(pageRate);
        m_backgroundPage->setPageScale(m_scale);
    }

    auto geometryChangedannotations = [&](QList<BaseDocItem *> items) {
        QMutableListIterator<BaseDocItem *> iter(items);
        while (iter.hasNext()) {
            iter.next();
            if (iter.value() == nullptr) {
                iter.remove();
                continue;
            }
            auto annotation = iter.value()->source();
            iter.value()->setX(annotation->rect.x() * pageRate);
            iter.value()->setY(annotation->rect.y() * pageRate);
            iter.value()->setWidth(annotation->rect.width() * pageRate);
            iter.value()->setHeight(annotation->rect.height() * pageRate);
        }
    };

    geometryChangedannotations(m_annotationsItems);
    geometryChangedannotations(m_commentsItems);
    geometryChangedannotations(m_highlightedItems);
    geometryChangedannotations(m_currentCellItems);
}

QSizeF PageContainer::requestedSize() const
{
    return m_requestedSize;
}

Qt::Orientation PageContainer::orientation() const
{
    return m_orientation;
}

qreal PageContainer::scale() const
{
    return m_scale;
}

void PageContainer::setPageSource(QSharedPointer<BasePage> pageSource)
{
    if (!pageSource)
        return;

    if (m_pageSource == pageSource) {
        _correctSize();
        return;
    }

    m_pageSource = pageSource;

    m_pageSource->originalSize();
    auto sizeWatcher = new QFutureWatcher<QSizeF>(this);
    connect(sizeWatcher, &QFutureWatcher<QSizeF>::finished, this, [this, sizeWatcher]() {
        if (m_pageSource) {
            for(auto &tile : m_tilesMap.values())
                tile->setPageSource(m_pageSource);

            update();
        }

        sizeWatcher->deleteLater();
    });
    sizeWatcher->setFuture(m_pageSource->originalSize());

    _prepareBackgroundPage();

    _correctSize();

    connect(m_pageSource.data(), &BasePage::invalidateRect, this, &PageContainer::forceUpdateRect);

    if (!m_pageSource->isAnnotationsSupport())
        return;

    connect(m_pageSource.data(), &BasePage::annotationsLoaded, this, &PageContainer::_loadAnnotations);
    connect(m_pageSource.data(), &BasePage::annotationsChanged, this, [this] {
        m_pageSource->loadAnnotations();
        emit pageChanged();
    });
    connect(m_pageSource.data(), &BasePage::annotationAdded, this, [this](bool added) {
        if (m_pageSource && added) {
            m_pageSource->loadAnnotations();
            emit pageChanged();
        }
    });



    if (m_pageSource->annotations().isEmpty())
        m_pageSource->loadAnnotations();
    else
        _loadAnnotations();
}

void PageContainer::setPageGeometry(const PageGeometry &pg)
{
    m_pageGeometry = pg;
    _correctSize();
}

int PageContainer::index() const
{
    return m_pageSource ? m_pageSource->pageIndex() : -1;
}

void PageContainer::setMapper(DocumentMapper *mapper)
{
    if (mapper == m_mapper || mapper == nullptr)
        return;

    m_mapper = mapper;
}

QSizeF PageContainer::pageCurrentSize(const PageGeometry &pageGeometry,
                                         const QSizeF &requestedSize,
                                         Qt::Orientation orientation,
                                         qreal scale)
{
    auto heightToWidthRatio = pageGeometry.height() / pageGeometry.width();
    auto fitHeight = qMin(static_cast<qreal>(requestedSize.height()), requestedSize.width() * heightToWidthRatio);
    return {
        static_cast<qreal>((orientation == Qt::Vertical ? requestedSize.width() : fitHeight / heightToWidthRatio) * scale),
        static_cast<qreal>((orientation == Qt::Vertical ? requestedSize.width() * heightToWidthRatio : fitHeight) * scale)
    };
}

void PageContainer::setVisibleArea(const QRectF &visibleArea)
{
    if (visibleArea == m_visibleArea)
        return;

    m_visibleArea = visibleArea;
}

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

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

QSharedPointer<BasePage> PageContainer::source() const {
    return m_pageSource;
}

void PageContainer::addAnnotation(const QRectF &rect, const QColor &color, const QString &author, const QString &content)
{
    if (!m_pageSource)
        return;

    auto pageRate = width() / m_pageGeometry.width();
    auto correctRect = QRectF();
    correctRect.setX(qMax(0.0f, float(rect.x() / pageRate)));
    correctRect.setY(qMax(0.0f, float((height() - rect.y() - rect.height()) / pageRate)));

    correctRect.setWidth(rect.width() / pageRate);
    if (correctRect.x() + correctRect.width() > m_pageGeometry.width())
        correctRect.setWidth(m_pageGeometry.width() - correctRect.x() - 1);

    correctRect.setHeight(rect.height() / pageRate);
    if (correctRect.y() + correctRect.height() > m_pageGeometry.height())
        correctRect.setHeight(m_pageGeometry.height() - correctRect.y() - 1);

    m_pageSource->addAnnotation(correctRect, color, author, content);
}

qreal PageContainer::imageScale() const
{
    return m_imageScale;
}

void PageContainer::setRequestedSize(QSizeF requestedSize)
{
    if (m_requestedSize == requestedSize)
        return;

    m_requestedSize = requestedSize;
    emit requestedSizeChanged(m_requestedSize);
    _correctSize();
}

void PageContainer::setOrientation(Qt::Orientation orientation)
{
    if (m_orientation == orientation)
        return;

    m_orientation = orientation;
    emit orientationChanged(m_orientation);

    _correctSize();
}

void PageContainer::setScale(qreal scale)
{
    if (scale == 0) {
        qCWarning(lcDocviewer()) << "Scale must not be zero";
        return;
    }

    if (qFuzzyCompare(m_scale, scale))
        return;

    m_scale = scale;
    emit scaleChanged(m_scale);

    _correctSize();
}

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

    m_annotationsPaint = annotationsPaint;
    emit annotationsPaintChanged(m_annotationsPaint);

    if (m_annotationsItems.isEmpty())
        _loadAnnotations();
    else
        for (auto annotation : std::as_const(m_annotationsItems))
            annotation->setOpacity(m_annotationsPaint ? 1.0 : 0.0);
}

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

    m_notesPaint = notesPaint;
    emit notesPaintChanged(m_notesPaint);

    if (m_commentsItems.isEmpty())
        _loadAnnotations();
    else
        for (auto note : m_commentsItems)
            note->setOpacity(m_notesPaint ? 1.0 : 0.0);
}

void PageContainer::forceUpdateRect(const QRectF &visibleArea)
{
    Q_UNUSED(visibleArea)
    for (auto &tile : m_tilesMap.values()) {
        tile->render(true);
    }
}

void PageContainer::setColorSelected(const QColor &color)
{
    for (auto &item : m_highlightedItems)
        item->setColor(color);

    for (auto &item : m_currentCellItems)
        item->setColor(color);
}

void PageContainer::_correctSize()
{
    auto heightToWidthRatio = m_pageGeometry.height() / m_pageGeometry.width();
    auto fitHeight = qMin(static_cast<qreal>(m_requestedSize.height()), m_requestedSize.width() * heightToWidthRatio);
    m_pageImageSize.setWidth((m_orientation == Qt::Vertical ? m_requestedSize.width() : fitHeight / heightToWidthRatio) * m_scale);
    m_pageImageSize.setHeight((m_orientation == Qt::Vertical ? m_requestedSize.width() * heightToWidthRatio : fitHeight) * m_scale);

    setWidth(m_pageImageSize.width());
    setHeight(m_pageImageSize.height());

    auto pageScale = qMin(m_pageImageSize.width() / m_pageGeometry.width(), m_pageImageSize.height() / m_pageGeometry.height());

    if (qFuzzyCompare(m_imageScale, pageScale) && !m_tilesMap.isEmpty())
        return;

    m_imageScale = pageScale;

    _tailorise();
}

void PageContainer::_tailorise()
{
    auto actualTileWidth = m_tileSize * m_scale;
    auto actualTileHeight = m_tileSize * m_scale;
    auto tileCountX = qCeil(m_pageImageSize.width() / actualTileWidth);
    auto tileCountY = qCeil(m_pageImageSize.height() / actualTileHeight);
    tileCountX = tileCountX > 0 ? tileCountX : 1;
    tileCountY = tileCountY > 0 ? tileCountY : 1;

    if (m_tilesMap.size() != tileCountX * tileCountY) {
        for (auto &tile : m_tilesMap.values()) {
            tile->setEnabled(false);
            tile->setVisible(false);
            tile->deleteLater();
        }

        m_tilesMap.clear();
    }

    actualTileWidth = m_pageImageSize.width() / tileCountX;
    actualTileHeight = m_pageImageSize.height() / tileCountY;
    auto tileIndex = -1;
    auto oldMaxZ = m_maxTileZ;
    for (int tileX = 0; tileX < tileCountX; ++tileX) {
        for (int tileY = 0; tileY < tileCountY; ++tileY) {
            ++tileIndex;

            if (!m_tilesMap.contains(tileIndex)) {
                auto tile = new MultiPageTile(this);
                tile->setDebugIndex(tileIndex);
                tile->setRenderable(false);
                m_tilesMap.insert(tileIndex, tile);
            }

            auto tile = m_tilesMap.value(tileIndex);
            tile->setImageScale(m_imageScale);

            if (m_pageSource)
                tile->setPageSource(m_pageSource);

            tile->setTileSize(m_tileSize);
            QPointF tilePosition(tileX * actualTileWidth, tileY * actualTileHeight);
            tile->setX(tilePosition.x());
            tile->setY(tilePosition.y());

            auto currentWidth = tilePosition.x() + actualTileWidth;
            if (currentWidth <= m_pageImageSize.width())
                tile->setWidth(actualTileWidth);
            else
                tile->setWidth(actualTileWidth - qAbs(std::remainder(m_pageImageSize.width(), actualTileWidth)));

            auto currentHeight = tilePosition.y() + actualTileHeight;
            if (currentHeight <= m_pageImageSize.height())
                tile->setHeight(actualTileHeight);
            else
                tile->setHeight(actualTileHeight - qAbs(std::remainder(m_pageImageSize.height(), actualTileHeight)));

            m_maxTileZ = qMax(m_maxTileZ, tile->z());
        }
    }

    if (qFuzzyCompare(m_maxTileZ, oldMaxZ)) {
        for (auto annotationItem : std::as_const(m_annotationsItems))
            annotationItem->setZ(m_maxTileZ + 1);

        for (auto noteItem : std::as_const(m_commentsItems))
            noteItem->setZ(m_maxTileZ + 1);

        for (auto item : std::as_const(m_highlightedItems))
            item->setZ(m_maxTileZ + 1);

        for (auto item : std::as_const(m_currentCellItems))
            item->setZ(m_maxTileZ + 1);
    }

    emit pageReady();

    m_allTilesReady = false;
}

void PageContainer::_updateVisible()
{
    if (!isEnabled())
        return;

    if (!m_pageSource)
        return;

    if (m_mapper == nullptr)
        return;

    if (m_orientation == Qt::Vertical) {
        if (y() > m_visibleArea.height() || height() < -y()) {
            for (auto &tile : m_tilesMap.values())
                tile->setRenderable(false);

            return;
        }
    } else {
        if (x() > m_visibleArea.width() || width() < -x()) {
            for (auto &tile : m_tilesMap.values())
                tile->setRenderable(false);
            return;
        }
    }

    auto hVisible = 0.0f;
    auto wVisible = 0.0f;
    auto yVisible = 0.0f;
    auto xVisible = 0.0f;

    if (m_orientation == Qt::Vertical) {
        if (m_visibleArea.height() > height())
            hVisible = y() > 0 ? (m_visibleArea.height() - y()) : (height() + y());
        else
            hVisible = m_visibleArea.height();

        wVisible = m_visibleArea.width();
        yVisible = y() > 0 ? 0.0f : -y();
        xVisible = x() > 0 ? 0.0f : -x();
    } else {
        hVisible = m_visibleArea.height();

        if (m_visibleArea.width() > width())
            wVisible = x() > 0 ? (m_visibleArea.width() - x()) : (width() + x());
        else
            wVisible = m_visibleArea.width();

        yVisible = y() > 0 ? 0.0f : -y();
        xVisible = x() > 0 ? 0.0f : -x();
    }

    QRectF localVisibleArea(xVisible, yVisible, wVisible, hVisible);

    bool allTilesAreBitmap = true;
    for (auto &tile : m_tilesMap.values()) {
        QRectF tileRect(tile->x(), tile->y(), tile->width(), tile->height());
        auto tileVisible = localVisibleArea.intersects(tileRect);
        tile->setRenderable(tileVisible);

        if (!tile->isBitmap() && tileVisible)
            allTilesAreBitmap = false;
    }

    if (m_allTilesReady)
        return;

    if (allTilesAreBitmap) {
        for (auto &annotation : m_annotationsItems)
            annotation->setVisible(true);

        for (auto &note : m_commentsItems)
            note->setVisible(true);

        m_allTilesReady = true;
    }
}

void PageContainer::_loadAnnotations()
{
    if (!m_pageSource)
        return;

    QList<BaseDocItem *> oldAnnotationItems;
    oldAnnotationItems.append(m_annotationsItems);
    oldAnnotationItems.append(m_commentsItems);
    oldAnnotationItems.append(m_highlightedItems);
    oldAnnotationItems.append(m_currentCellItems);

    m_annotationsItems.clear();
    m_commentsItems.clear();
    m_highlightedItems.clear();
    m_currentCellItems.clear();

    auto pageRate = width() / m_pageGeometry.width();
    auto annotations = m_pageSource->annotations();
    for (const auto &annotation : std::as_const(annotations)) {
        if (annotation == nullptr)
            continue;

        BaseDocItem *item = nullptr;
        if (annotation->type == BaseAnnotation::AnnotationType::Link || annotation->type == BaseAnnotation::AnnotationType::Url) {
            item = new DocItemAnnotation(this, annotation);

            connect(item, &BaseDocItem::triggered, this, &PageContainer::annotationActivate);
            connect(this, &PageContainer::yChanged, item, &BaseDocItem::clearHighlight);

            item->setVisible(false);
            m_annotationsItems.append(item);
        } else if (annotation->type == BaseAnnotation::AnnotationType::HighLight
                   || annotation->type == BaseAnnotation::AnnotationType::Comment) {
            item = new DocItemComment(this, annotation);
            connect(item, &BaseDocItem::triggered, this, &PageContainer::noteActivate);
            connect(this, &PageContainer::yChanged, item, &BaseDocItem::clearHighlight);

            item->setVisible(false);
            m_commentsItems.append(item);
        } else if (annotation->type == BaseAnnotation::AnnotationType::CurrentSearch
            || annotation->type == BaseAnnotation::AnnotationType::AllSearch
            || annotation->type == BaseAnnotation::AnnotationType::TextSelected) {

            item = new DocItemSearchHighlight(highLightColor(), this, annotation);
            item->setVisible(false);
            m_highlightedItems.append(item);
        } else if (annotation->type == BaseAnnotation::AnnotationType::CellSelected) {
            item = new DocItemCellSelected(highLightColor(), this, annotation);
            connect(item, &BaseDocItem::triggered, this, &PageContainer::cellActivate);
            item->setVisible(m_cellSelectedPaint);
            m_currentCellItems.append(item);
        }

        if (item) {
            item->setOpacity(m_notesPaint ? 1.0 : 0.0);
            item->setX(annotation->rect.x() * pageRate);
            item->setY(annotation->rect.y() * pageRate);
            item->setWidth(annotation->rect.width() * pageRate);
            item->setHeight(annotation->rect.height() * pageRate);
            item->setZ(m_maxTileZ);
        }
    }

    for (auto &item : m_highlightedItems)
        item->setVisible(true);

    for (auto &item : m_commentsItems)
        item->setVisible(true);

    for (auto &item : oldAnnotationItems) {
        item->setVisible(false);
        item->deleteLater();
    }
}

void PageContainer::_prepareBackgroundPage()
{
    if (!m_pageSource)
        return;

    if (m_backgroundPage) {
        m_backgroundPage->setRenderable(false);
        m_backgroundPage->deleteLater();
    }

    m_backgroundPage = new BackgroundPage(this);

    if (m_pageSource->documentType() != BaseDocumentProvider::Spreadsheet)
        m_backgroundPage->setPageSource(m_pageSource);

    m_backgroundPage->setImageScale(m_imageScale);
    m_backgroundPage->setPageScale(m_scale);
    m_backgroundPage->setWidth(width());
    m_backgroundPage->setHeight(height());
    m_backgroundPage->setRenderable(true);
    m_backgroundPage->setZ(-1);
}

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

void PageContainer::setHighLightColor(const QColor &newHighLightColor)
{
    if (m_highLightColor == newHighLightColor)
        return;
    m_highLightColor = newHighLightColor;
    emit highLightColorChanged();

    for (auto &item : m_highlightedItems)
        item->setColor(newHighLightColor);

    for (auto &item : m_currentCellItems)
        item->setColor(newHighLightColor);
}

qreal PageContainer::pageRate() const
{
    return width() / m_pageGeometry.width();
}
