/*******************************************************************************
**
** 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 <QSizeF>
#include <QThreadPool>
#include <QJsonObject>
#include <QJsonDocument>
#include <QJsonArray>
#include <QPair>
#include <QCoreApplication>
#include <QRegularExpression>
#include <QDateTime>
#include <utility>

#include <debug.h>
#include <basepage.h>
#include <basebookmark.h>
#define LOK_USE_UNSTABLE_API
#include <LibreOfficeKit/LibreOfficeKitEnums.h>

#include "pagepreloader.h"
#include "lodocumentprovider.h"
#include "lopageprovider.h"
#include "lodocument.h"
#include "lopage.h"

QRect parseRectangle(const QString &strRect) {
    QRect rect;
    auto elements = strRect.split(",");
    if (elements.count() == 4) {
        rect.setX(elements.at(0).trimmed().toInt());
        rect.setY(elements.at(1).trimmed().toInt());
        rect.setWidth(elements.at(2).trimmed().toInt());
        rect.setHeight(elements.at(3).trimmed().toInt());
    }

    return rect;
}

LoDocument::LoDocument(QObject *parent)
    : BaseDocument(parent)
    , m_innerThreadPool(new QThreadPool(this))
    , m_partIndex(0)
{
    qCDebug(lcDocviewer) << "Create LoDocument";
    qRegisterMetaType<QHash<int, QSizeF>>();

    m_status = DocumentStatus::Null;
    m_innerThreadPool->setMaxThreadCount(1);

    connect(this, &BaseDocument::pageIndexChanged, this, &LoDocument::setPartIndex);
}

LoDocument::~LoDocument()
{
    qCDebug(lcDocviewer) << "Delete LoDocument";
    disconnect(m_loProvider.data(), 0, 0, 0);

    QThreadPool::globalInstance()->clear();

    while (!QThreadPool::globalInstance()->waitForDone(10))
        QCoreApplication::processEvents();

    m_innerThreadPool->clear();

    while (!m_innerThreadPool->waitForDone(10))
        QCoreApplication::processEvents();
}

QThreadPool *LoDocument::threadPool() const
{
    return m_innerThreadPool;
}

QString LoDocument::path() const
{
    return m_loProvider ? m_loProvider->path() : QLatin1String("");
}

QString LoDocument::fileName() const
{
    return m_loProvider ? m_loProvider->fileName() : QLatin1String("");
}

QSizeF LoDocument::pageSize(int pageNumber) const
{
    if (m_status != DocumentStatus::Ready)
        return {};

    return m_pageSizes.value(pageNumber);
}

int LoDocument::count() const
{
    return m_loProvider ? m_loProvider->pageCount() : -1;
}

int LoDocument::error() const
{
    return m_loProvider ? m_loProvider->error() : -1;
}

BaseDocumentProvider::DocumentType LoDocument::documentType() const
{
    return m_loProvider ? m_loProvider->documentType() : BaseDocumentProvider::Other;
}

QList<QString> LoDocument::pageNames() const
{
    return m_loProvider ? m_loProvider->pageNames() : QList<QString>();
}

QSharedPointer<BasePage> LoDocument::loadPage(int pageIndex)
{
    if (pageIndex < 0 || pageIndex >= count())
        return {};

    if (m_loadedPages.contains(pageIndex))
        return m_loadedPages.value(pageIndex);

    if (m_pagesInProcess.contains(pageIndex))
        return {};

    auto pageLoader = new PagePreloader(m_loProvider, pageIndex);

    connect(pageLoader, &PagePreloader::done, this, [&] (int loadedPageIndex, PageLoadStatus loadStatus) {
        if (loadStatus == PageLoadStatus::Success)
            m_loadedPages.insert(loadedPageIndex, QSharedPointer<BasePage>(
                                                      new LoPage(QSharedPointer<LoPageProvider>(new LoPageProvider(loadedPageIndex, m_loProvider)), this)));

        emit pageLoaded(loadedPageIndex, loadStatus);
    });

    QThreadPool::globalInstance()->start(pageLoader);
    m_pagesInProcess.insert(pageIndex);

    return {};
}

void LoDocument::startLoadBookmarks() const
{
    // TODO: Will be used when adding a bookmark feature
}

QVector<BaseBookmark *> LoDocument::bookmarks() const
{
    return m_baseBookmarks;
}

int LoDocument::fileVersion() const
{
    // TODO: Add getting a version from a document
    return (m_loProvider.isNull() ? -1 : 1);
}

bool LoDocument::saveDocumentAs(const QString &path, const QString &format) const
{
    if (m_loProvider)
        m_loProvider->saveDocumentAs(path, format);

    return true;
}

void LoDocument::mousePress(const InternalPosition &internaPos)
{
    if (m_loProvider && internaPos.pageIndex != -1) {
        m_loProvider->sendMouseEvent(LOK_MOUSEEVENT_MOUSEBUTTONDOWN, internaPos.pos.x(), internaPos.pos.y(), 1, 1, 0, internaPos.pageIndex, internaPos.zoom);
        m_pageIndex = internaPos.pageIndex;
    }
}

void LoDocument::mouseRelease(const InternalPosition &internaPos)
{
    if (m_loProvider && internaPos.pageIndex != -1) {
        m_loProvider->sendMouseEvent(LOK_MOUSEEVENT_MOUSEBUTTONUP, internaPos.pos.x(), internaPos.pos.y(), 1, 1, 0, internaPos.pageIndex, internaPos.zoom);
        m_pageIndex = internaPos.pageIndex;
    }
}

void LoDocument::mouseDoubleClick(const InternalPosition &internaPos)
{
    if (m_loProvider && internaPos.pageIndex != -1) {
        m_loProvider->sendMouseEvent(LOK_MOUSEEVENT_MOUSEBUTTONDOWN, internaPos.pos.x(), internaPos.pos.y(), 2, 1, 0, internaPos.pageIndex, internaPos.zoom);
        m_pageIndex = internaPos.pageIndex;
        if (mode() == Mode::ModeSearch)
            stopSearch();
        setMode(Mode::ModeSelect);
    }
}

void LoDocument::startSelectText(const InternalPosition &internaPos)
{
    if (m_loProvider && internaPos.pageIndex != -1) {
        m_loProvider->sendSelectEvent(LOK_SETTEXTSELECTION_START, internaPos.pos.x(), internaPos.pos.y(), internaPos.pageIndex, internaPos.zoom);
        m_pageIndex = internaPos.pageIndex;
        if (mode() == Mode::ModeSearch)
            stopSearch();
        setMode(Mode::ModeSelect);
    }
}

void LoDocument::endSelectText(const InternalPosition &internaPos)
{
    if (m_loProvider && internaPos.pageIndex != -1) {
        m_loProvider->sendSelectEvent(LOK_SETTEXTSELECTION_END, internaPos.pos.x(), internaPos.pos.y(), internaPos.pageIndex, internaPos.zoom);
        m_pageIndex = internaPos.pageIndex;
        if (mode() == Mode::ModeSearch)
            stopSearch();
        setMode(Mode::ModeSelect);
    }
}

void LoDocument::resetSelectText()
{
    if (m_loProvider) {
        m_loProvider->sendSelectEvent(LOK_SETTEXTSELECTION_RESET, {}, {}, -1, 1);
    }
    m_selectionMarkers = {};
    emit selectionMarkerChanged();
    m_textSelected = {};
    emit textSelectedUpdated({}, {});
    setMode(Mode::ModeView);
}

void LoDocument::sendUnoCommand(const QString &command, const QString &arguments, bool notifyWhenFinished)
{
    if (m_loProvider)
        m_loProvider->sendUnoCommand(command.toUtf8(), arguments.toUtf8(), notifyWhenFinished);
}

void LoDocument::getUnoCommandValues(const QString &command)
{
    if (m_loProvider)
        m_loProvider->getUnoCommandValues(command.toUtf8());
}

void LoDocument::startSearch(const QString &string, SearchDirection direction, const QPointF &startPosition)
{
    if (mode() == Mode::ModeSelect) {
        resetSelectText();
    }

    Q_UNUSED(startPosition)
    auto executeSearch = [&] (bool highlightAll) {
        /* TODO: Add support for start point search
        QPoint startPosTwip;
        if (false) {
            startPosTwip = QPoint(LoDocumentProvider::pixelToTwip(startPosition.x()), LoDocumentProvider::pixelToTwip(startPosition.y()));

            if (!m_searchStartPoint.isNull())
                startPosTwip = m_searchStartPoint;

            if (string.isEmpty()) {
                m_searchStartPoint = QPoint();
                return;
            }

            m_searchStartPoint = startPosTwip;
        }
                             */

        QJsonObject searchString
            {
                { "type", "string" },
                { "value", string }
            };

        QJsonObject searchBackward
            {
                { "type", "boolean" },
                { "value", direction == SearchDirection::SearchBackward ? "true" : "false" }
            };
        /*
        QJsonObject searchStartPointX
            {
                { "type", "long" },
                { "value", qint64(startPosTwip.x()) }
            };

        QJsonObject searchStartPointY
            {
                { "type", "long" },
                { "value", qint64(startPosTwip.y()) }
            };
        */
        QJsonObject searchCmdType
            {
                { "type", "unsigned short" },
                { "value", static_cast<ushort>(highlightAll ? SearchCommand::CmdFindAll : SearchCommand::CmdFind) }
            };
        QJsonObject searchItem
            {
                { "SearchItem.SearchString", searchString },
                { "SearchItem.Backward", searchBackward },
                { "SearchItem.Command", searchCmdType }
                /*
                ,{ "SearchItem.SearchStartPointX", searchStartPointX },
                { "SearchItem.SearchStartPointY", searchStartPointY }
                */
            };
        QJsonDocument doc(searchItem);

        if (m_loProvider)
            m_loProvider->sendUnoCommand(".uno:ExecuteSearch", doc.toJson(), true);

    };

    if (m_loProvider->documentType() == BaseDocumentProvider::Text && m_searchString != string) {
        executeSearch(true);
        m_searchString = string;
    }

    executeSearch(false);
    setMode(Mode::ModeSearch);
}

void LoDocument::stopSearch()
{
    m_searchString = QString();
    m_allHighlighted.clear();
    m_currentHighlighted.clear();
    emit highlightedAllUpdated();
    setMode(Mode::ModeView);
}

void LoDocument::goToCell(const QString &adressCell)
{
    if (adressCell == m_currentCellAdress) {
        emit cellClicked(m_currentCellContent);
    } else {
        QJsonObject value
            {
                { "type", "string" },
                { "value", adressCell }
            };
        QJsonObject aExtraWidth
            {
                { "ToPoint", value }
            };

        if (m_loProvider)
            m_loProvider->sendUnoCommand(".uno:GoToCell", QJsonDocument(aExtraWidth).toJson(), true);
    }
}

QList<QRect> LoDocument::currentHighlightedSearch(int pageIndex) const
{
    return m_currentHighlighted.contains(pageIndex) ? m_currentHighlighted[pageIndex] : QList<QRect>() ;
}

QList<QRect> LoDocument::allHighlightedSearch(int pageIndex) const
{
    return m_allHighlighted.contains(pageIndex) ? m_allHighlighted[pageIndex] : QList<QRect>() ;
}

QList<QPair<QString, QRect>> LoDocument::cellSelected(int pageIndex) const
{
    return m_cellSelected.contains(pageIndex) ? m_cellSelected[pageIndex] : QList<QPair<QString, QRect>>() ;
}

QList<QRect> LoDocument::highlightedTextArea(int pageIndex) const
{
    return m_textSelected.contains(pageIndex) ? m_textSelected[pageIndex] : QList<QRect>() ;
}

QString LoDocument::slsectionText() const
{
    return m_selectionText;
}

QList<QPair<CommentData, QRect>> LoDocument::comments(int pageIndex) const
{
    return m_comments.contains(pageIndex) ? m_comments[pageIndex] : QList<QPair<CommentData, QRect>>();
}

QPair<BaseDocument::Marker, BaseDocument::Marker> LoDocument::selectionMarkers() const
{
    return m_selectionMarkers;
}

QPair<int, QRect> LoDocument::cursor() const
{
    return m_cursor;
}

void LoDocument::setPartIndex(int partIndex)
{
    if (partIndex < 0 || partIndex >= count()) {
        qCWarning(lcDocviewer) << "Not correct partIndex" << partIndex;
        return;
    }

    if (m_loProvider)
        m_loProvider->setPartIndex(partIndex);

    m_partIndex = partIndex;
}

void LoDocument::setPath(const QString &path)
{
    if (m_loProvider && (m_loProvider->path() == path))
        return;

    if (m_status != DocumentStatus::Loading) {
        m_status = DocumentStatus::Loading;
        emit statusChanged(m_status);
    }
    if (m_loProvider) {
        m_loProvider->close();
    }

    m_loProvider.reset(new LoDocumentProvider());

    if (m_loProvider == nullptr) {
        m_status = DocumentStatus::Error;
        emit statusChanged(m_status);
        return;
    }

    connect(m_loProvider.data(), &BaseDocumentProvider::loadedChanged, this, [&] {
        if (m_loProvider->loaded()) {
            QHash<int, QSizeF> sizes;
            for (int i = 0; i < m_loProvider->pageCount(); ++i)
                sizes.insert(i, m_loProvider->pageSize(i + 1));
            m_pageSizes.swap(sizes);

            getUnoCommandValues(".uno:ViewAnnotations");

            m_status = DocumentStatus::Ready;
            emit statusChanged(m_status);
        }
    });

    connect(m_loProvider.data(), &BaseDocumentProvider::loadProgressChanged, this, [&] {
        setLoadingProgress(m_loProvider->loadProgress());
    });

    connect(m_loProvider.data(), &BaseDocumentProvider::processingChanged, this, [&] {
        setProcessing(m_loProvider->processing());
    });

    connect(m_loProvider.data(), &BaseDocumentProvider::errorChanged, this, [&] {
        if (m_loProvider->error() != BaseDocumentProvider::NoError) {
            m_status = DocumentStatus::Error;
            emit statusChanged(m_status);
        }
        emit errorChanged();
    });

    connect(m_loProvider.data(), &BaseDocumentProvider::documentTypeChanged, this, &BaseDocument::documentTypeChanged);
    connect(m_loProvider.data(), &BaseDocumentProvider::pageNamesChanged, this, &BaseDocument::pageNamesChanged);
    connect(m_loProvider.data(), &BaseDocumentProvider::pageCountChanged, this, &BaseDocument::countChanged);
    connect(m_loProvider.data(), &BaseDocumentProvider::libreofficeAnswer, this, &LoDocument::_libreofficeAnswer);
    connect(m_loProvider.data(), &BaseDocumentProvider::unoAnswer, this, &LoDocument::_unoAnswer);
    connect(m_loProvider.data(), &BaseDocumentProvider::textSelectionAnswer, this, &LoDocument::_textSelectionAnswer);
    connect(m_loProvider.data(), &BaseDocumentProvider::saveAsStatusChanged, this, &BaseDocument::saveAsStatusChanged);

    m_loProvider->setPath(path);

    emit pathChanged(path);
    emit fileNameChanged();
}

void LoDocument::_libreofficeAnswer(int type, const QByteArray &data)
{
    switch (type) {
    case LOK_CALLBACK_INVALIDATE_VISIBLE_CURSOR:
        _invalidateVisibleCursor(data);
        break;
    case LOK_CALLBACK_INVALIDATE_TILES:
        _invalidateTiles(data);
        break;
    case LOK_CALLBACK_TEXT_SELECTION:
        _textSelection(data);
        break;
    case LOK_CALLBACK_TEXT_SELECTION_START:
        _textSelectionStart(data);
        break;
    case LOK_CALLBACK_TEXT_SELECTION_END:
        _textSelectionEnd(data);
        break;
    case LOK_CALLBACK_SEARCH_RESULT_SELECTION:
         _searchResult(data);
        break;
    case LOK_CALLBACK_SEARCH_NOT_FOUND:
        _searchNotFound(data);
        break;
    case LOK_CALLBACK_UNO_COMMAND_RESULT:
        _unoCommandResult(data);
        break;
    case LOK_CALLBACK_CELL_CURSOR:
        _cellCursor(data);
        break;
    case LOK_CALLBACK_CELL_FORMULA:
        _cellFormula(data);
        break;
    case LOK_CALLBACK_CELL_ADDRESS:
        _cellAdress(data);
        break;
    case LOK_CALLBACK_STATE_CHANGED:
        _callbackStateChanged(data);
        break;
    };
}

void LoDocument::_unoAnswer(const QByteArray &command, const QByteArray &result)
{
    if (command.toStdString() == ".uno:ViewAnnotations")
        _comments(result);
}

void LoDocument::_textSelectionAnswer(const QString &text)
{
    m_selectionText = text;
    emit selectionTextChanged();
    qCDebug(lcDocviewer) << "TEXT SELECTION" << text;
}

void LoDocument::_comments(const QByteArray &data)
{
    auto obj = QJsonDocument::fromJson(data).object();
    auto comments = obj["comments"].toArray();

    for (auto item : std::as_const(comments)) {
        auto str = item.toObject()["textRange"].toString();
        auto rects = str.split(';');

        for (const auto &rect : rects) {
            QRect rectTileTwip = parseRectangle(rect);

            if (!rectTileTwip.isNull()) {
                int pageIndex = _pageIndexFromTwipY(rectTileTwip.y());
                if (pageIndex != -1) {
                    auto rect = m_loProvider->twipRectToPixel(rectTileTwip, 1.0,
                                                              m_loProvider->shiftX(), m_loProvider->shiftY(pageIndex));

                    QString author = item.toObject()["author"].toString();
                    QDateTime dateTime = QDateTime::fromString(item.toObject()["dateTime"].toString(), Qt::ISODate);
                    QString text = item.toObject()["text"].toString();

                    m_comments[pageIndex].append({ { author, dateTime.toString("dd.MM.yyyy hh:mm"), text }, rect });
                    emit commentsUpdated(pageIndex);
                }
            }
        }
    }
}

void LoDocument::_searchResult(const QByteArray &data)
{
    if (mode() != Mode::ModeSearch)
        return;

    auto jObj = QJsonDocument::fromJson(data).object();
    auto searchResultSelection = jObj["searchResultSelection"].toArray();
    bool highlightAll = jObj["highlightAll"].toString() == "true";
    int highlightAllCount = 0;

    if (highlightAll)
        m_allHighlighted.clear();
    else
        m_currentHighlighted.clear();

    for (auto item : std::as_const(searchResultSelection)) {
        auto rectStr = item.toObject()["rectangles"].toString();
        QRect rectTileTwip = parseRectangle(rectStr);

        if (!rectTileTwip.isNull()) {
            if (m_loProvider->documentType() == BaseDocumentProvider::Spreadsheet || m_loProvider->documentType() == BaseDocumentProvider::Presentation) {
                auto rect = m_loProvider->twipRectToPixel(rectTileTwip, 1.0,  m_loProvider->shiftX(), m_loProvider->shiftY());
                int pageIndex = item.toObject()["part"].toString().toInt();

                if (pageIndex != -1) {
                    m_currentHighlighted[pageIndex].append(rect);
                    emit highlightedUpdated(m_currentHighlighted[pageIndex], pageIndex);
                }
            } else {
                int pageIndex = _pageIndexFromTwipY(rectTileTwip.y());

                if (pageIndex != -1) {
                    auto rect = m_loProvider->twipRectToPixel(rectTileTwip, 1.0,  m_loProvider->shiftX(), m_loProvider->shiftY(pageIndex));

                    if (highlightAll) {
                        m_allHighlighted[pageIndex].append(rect);
                        highlightAllCount++;
                    } else {
                        m_currentHighlighted[pageIndex].append(rect);
                        emit highlightedUpdated(m_currentHighlighted[pageIndex], pageIndex);
                    }
                } else {
                    qCWarning(lcDocviewer) << "Search result, not found page" << rectTileTwip;
                }
            }
        }
    }

    if (highlightAll)
        emit highlightedAllUpdated();

    if (highlightAllCount) {
        //% "Found %n matches"
        emit showInfo(qtTrId("docviewer-la-text_found_matches", highlightAllCount));
    }

}

void LoDocument::_searchNotFound(const QByteArray &data)
{
    //% "Text not found"
    emit showInfo(qtTrId("docviewer-la-text_not_found"));
}

void LoDocument::_unoCommandResult(const QByteArray &data)
{
    auto jObj = QJsonDocument::fromJson(data).object();
    QString commandName = jObj["commandName"].toString();
    bool success = jObj["success"].toBool();

    if (!success && commandName == ".uno:ExecuteSearch" && documentType() == BaseDocumentProvider::Text) {
        m_currentHighlighted.clear();
        m_allHighlighted.clear();
        emit highlightedAllUpdated();
    }
}

void LoDocument::_textSelection(const QByteArray &data)
{
    if (mode() != Mode::ModeSelect)
        return;

    QList<QByteArray> rectList = data.split(';');
    QHash<int, QList<QRect>> textSelected;
    int startPageIndexSelection = -1;
    int endPageIndexSelection = -1;
    QPoint posStartMarker;
    QPoint posEndMarker;

    for (const auto &rectStr : std::as_const(rectList)) {
        QRect rectTileTwip = parseRectangle(rectStr);

        if (!rectTileTwip.isNull()) {
            int pageIndex =  _pageIndexFromTwipY(rectTileTwip.y());
            if (documentType() == BaseDocumentProvider::Spreadsheet || documentType() == BaseDocumentProvider::Presentation)
                pageIndex = m_pageIndex;

            if (pageIndex != -1) {
                auto rect = m_loProvider->twipRectToPixel(rectTileTwip, 1.0,  m_loProvider->shiftX(), m_loProvider->shiftY(pageIndex));
                textSelected[pageIndex].append(rect);

                if ((posStartMarker.x() > rect.left() && posStartMarker.y() > rect.top()) || posStartMarker.isNull())
                    posStartMarker = rect.topLeft();

                if (posEndMarker.x() < rect.right() && posEndMarker.y() < rect.bottom())
                    posEndMarker = rect.bottomRight();

                if (pageIndex < startPageIndexSelection || startPageIndexSelection == -1)
                    startPageIndexSelection = pageIndex;

                if (pageIndex > endPageIndexSelection)
                    endPageIndexSelection = pageIndex;
            } else {
                qCWarning(lcDocviewer) << "Search result, not found page" << rectTileTwip;
            }
        }
    }

    m_textSelected = textSelected;
    for (auto it = textSelected.constBegin(); it != textSelected.constEnd(); ++it) {
        if (it.value().count())
            emit textSelectedUpdated(it.value(), it.key());
    }

    m_loProvider->getTextSelection();
}

void LoDocument::_textSelectionStart(const QByteArray &data)
{
    if (mode() != Mode::ModeSelect)
        return;

    QRect rectTileTwip = parseRectangle(data);
    int pageIndex = _pageIndexFromTwipY(rectTileTwip.y());
    if (documentType() == BaseDocumentProvider::Spreadsheet || documentType() == BaseDocumentProvider::Presentation)
        pageIndex = m_pageIndex;
    if (pageIndex != -1) {
        auto rect = m_loProvider->twipRectToPixel(rectTileTwip, 1.0,  m_loProvider->shiftX(), m_loProvider->shiftY(pageIndex));
        m_selectionMarkers = QPair<Marker, Marker>({ Marker::Start, pageIndex, rect.bottomLeft() }
                                                   , m_selectionMarkers.second);
        emit selectionMarkerChanged();
    }
}

void LoDocument::_textSelectionEnd(const QByteArray &data)
{
    if (mode() != Mode::ModeSelect)
        return;

    QRect rectTileTwip = parseRectangle(data);
    int pageIndex = _pageIndexFromTwipY(rectTileTwip.y());
    if (documentType() == BaseDocumentProvider::Spreadsheet || documentType() == BaseDocumentProvider::Presentation)
        pageIndex = m_pageIndex;
    if (pageIndex != -1) {
        auto rect = m_loProvider->twipRectToPixel(rectTileTwip, 1.0,  m_loProvider->shiftX(), m_loProvider->shiftY(pageIndex));
        m_selectionMarkers = QPair<Marker, Marker>(m_selectionMarkers.first
                                                   , { Marker::Start, pageIndex, rect.bottomRight() });
        emit selectionMarkerChanged();
    }
}

void LoDocument::_invalidateTiles(const QByteArray &data)
{
    if (m_loProvider->documentType() == BaseDocumentProvider::Spreadsheet)
        return;

    QList<QByteArray> elements = data.split(',');

    if (elements.count() < 4)
        return;

    QRect rectTileTwip;
    rectTileTwip.setX(elements.at(0).trimmed().toInt());
    rectTileTwip.setY(elements.at(1).trimmed().toInt());
    rectTileTwip.setWidth(elements.at(2).trimmed().toInt());
    rectTileTwip.setHeight(elements.at(3).trimmed().toInt());

    int pageIndex = _pageIndexFromTwipY(rectTileTwip.y());

    if (pageIndex != -1) {
        auto rect = m_loProvider->twipRectToPixel(rectTileTwip, 1.0,  m_loProvider->shiftX(), m_loProvider->shiftY(pageIndex));
        emit invalidateRect(rect, pageIndex);
    }
}

void LoDocument::_invalidateVisibleCursor(const QByteArray &data)
{
    {
        QJsonDocument jDoc = QJsonDocument::fromJson(data);
        auto jHyperlink = jDoc.object().value("hyperlink");

        if (!jHyperlink.isUndefined()) {
            auto url = jHyperlink.toObject().value("link").toString();

            if (!url.isEmpty() && url[0] != '#')
                emit hyperlinkClicked(url);
        }
        auto strRect = jDoc.object().value("rectangle").toString();
        int part = jDoc.object().value("part").toString("-1").toInt();
        auto rectTileTwip = parseRectangle(strRect);
        int pageIndex = part != -1 ? part : _pageIndexFromTwipY(rectTileTwip.y());
        if (pageIndex != -1) {
            auto rect = m_loProvider->twipRectToPixel(rectTileTwip, 1.0,  m_loProvider->shiftX(), m_loProvider->shiftY(pageIndex));
            m_cursor = { pageIndex, rect };
            emit cursorChanged(rect, pageIndex);
        }
    }
}

QString numToLetters(int num)
{
    QString address;
    const int COUNT_LETERS = 'Z' - 'A' + 1;

    if (num / COUNT_LETERS > 0)
        address += numToLetters(num / COUNT_LETERS - 1);

    if (num != -1)
        address += 'A' + (num % COUNT_LETERS);

    return address;
}

QString columnRowToAddress(int column, int row)
{
    return QString("%1%2").arg(numToLetters(column)).arg(row + 1);
}

void LoDocument::_cellCursor(const QByteArray &data)
{
    QList<QByteArray> elements = data.split(',');
    if (elements.count() != 6)
        return;

    QRect rectTileTwip;
    rectTileTwip.setX(elements.at(0).trimmed().toInt());
    rectTileTwip.setY(elements.at(1).trimmed().toInt());
    rectTileTwip.setWidth(elements.at(2).trimmed().toInt());
    rectTileTwip.setHeight(elements.at(3).trimmed().toInt());

    int column = elements.at(4).trimmed().toInt();
    int row = elements.at(5).trimmed().toInt();
    QString address = columnRowToAddress(column, row);
    int pageIndex = _pageIndexFromTwipY(rectTileTwip.y());

    if (m_loProvider->documentType() == BaseDocumentProvider::Spreadsheet)
        pageIndex = m_pageIndex;

    m_cellSelected.clear();

    if (pageIndex != -1) {
        auto rect = m_loProvider->twipRectToPixel(rectTileTwip, 1.0,  m_loProvider->shiftX(), m_loProvider->shiftY(pageIndex));

        m_cellSelected[pageIndex] = { QPair<QString, QRect>(address, rect) };
        emit cellSelectedUpdated({ rect }, pageIndex);
    }
}

void LoDocument::_cellFormula(const QByteArray &data)
{
    m_currentCellFormula = QString::fromUtf8(data);
    if (!m_currentCellFormula.isEmpty() && m_currentCellFormula[0] != QChar('=')) {
        m_currentCellContent = m_currentCellFormula;
    }
}

void LoDocument::_cellAdress(const QByteArray &data)
{
    m_currentCellAdress = QString::fromUtf8(data);

    if (!m_currentCellFormula.isEmpty() && m_currentCellFormula[0] != QChar('=')) {
        m_currentCellContent = m_currentCellFormula;
    } else {
        m_currentCellContent = "";
    }
}

void LoDocument::_callbackStateChanged(const QByteArray &data)
{
    QString str = QString::fromUtf8(data);
    if(str.contains(".uno:StateTableCell")) {
        QRegularExpression regExp("Average:\\s+(\\d*.?\\d*); Sum:\\s+(\\d*.?\\d*)");
        QRegularExpressionMatch match = regExp.match(str);

        if (match.hasMatch() && match.lastCapturedIndex() >= 2 && m_currentCellFormula[0] == QChar('=')) {
            m_currentCellContent = match.captured(2);
        }
    }
}

int LoDocument::_pageIndexFromTwipY(int y)
{
    for (int i = 0; i < count(); i++) {
        auto rect = m_loProvider->originalRect(i + 1);
        if (rect.y() <= y && y < rect.y() + rect.height())
            return i;
    }

    return -1;
}
