/*******************************************************************************
**
** 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 "officeworker.h"
#include "documentprovider.h"

#include <atomic>
#include <QCoreApplication>
#include <QDataStream>
#include <QDir>
#include <QFileInfo>
#include <QJsonDocument>
#include <QJsonObject>
#include <QLocalServer>
#include <QLocalSocket>
#include <QPointer>
#include <QSharedMemory>
#include <QThreadPool>
#include <QTime>
#include <QUrl>
#include <QWaitCondition>
#include <QMutex>
#include <QStack>
#include <QMetaObject>

#define LIBREOFFICE_PATH "/usr/lib/libreoffice/program"

uint qHash(const OfficeWorker::Task &task)
{
    return qHashBits(&task, sizeof(task.pageNumber) + sizeof (task.rect)) + qRound(task.zoom * 100);
}

class WorkerThread : public QThread
{
public:
    WorkerThread(QObject *parent) : QThread(parent), m_stoped(false) {
        m_officeWroker = qobject_cast<OfficeWorker *>(parent);
        setObjectName(QStringLiteral("WorkerThread"));
    }

    ~WorkerThread() {
        m_stoped = true;
        wake();
        wait();
    }

    void run() override {
        m_officeWroker->processing();
    }

    QMutex &mutex() {
        return m_mutex;
    }

    QWaitCondition &waitCondition() {
        return m_condition;
    }

    void wake() {
        m_condition.wakeOne();
    }

    bool isStoped() const {
        return m_stoped;
    }

private:
    QMutex m_mutex;
    QWaitCondition m_condition;
    OfficeWorker *m_officeWroker;
    std::atomic<bool> m_stoped;
};

LoadingWorkerLok::LoadingWorkerLok(lok::Office *office, const QByteArray &url)
    : m_office(office)
    , m_url(url)
{
    setAutoDelete(true);
}

LoadingWorkerLok::~LoadingWorkerLok() {}

void LoadingWorkerLok::run()
{
    m_office->registerCallback(progressCallback, this);
    const int flags = LOK_FEATURE_DOCUMENT_PASSWORD | LOK_FEATURE_DOCUMENT_PASSWORD_TO_MODIFY
                       | LOK_FEATURE_RANGE_HEADERS | LOK_FEATURE_VIEWID_IN_VISCURSOR_INVALIDATION_CALLBACK
                       | LOK_FEATURE_NO_TILED_ANNOTATIONS;
    m_office->setOptionalFeatures(flags);
    QString options;

    emit documentReady(m_office->documentLoad(m_url.data(), options.toStdString().c_str()));
    m_office->registerCallback(NULL, NULL);
}

void LoadingWorkerLok::progressCallback(int nType, const char *pPayload, void *pData)
{
    LoadingWorkerLok *worker = reinterpret_cast<LoadingWorkerLok *>(pData);
    if (nType == LOK_CALLBACK_STATUS_INDICATOR_SET_VALUE)
        emit worker->progress(QByteArray(pPayload).toInt());
    else
        qInfo() << "Loading call back type:" << lokCallbackTypeToString(nType) << "data:" << QByteArray(pPayload);
}

OfficeWorker::OfficeWorker(const QString &socketName, QObject *parent)
    : QObject(parent)
    , m_office(nullptr)
    , m_document(nullptr)
    , m_server(nullptr)
    , m_socket(nullptr)
    , m_innnerThreadPool(new QThreadPool(this))
    , m_processing(false)
    , m_workerThread(new WorkerThread(this))
{
    qRegisterMetaType<RenderCommand>("RenderCommand");
    qRegisterMetaType<LoEvent::Message>("LoEvent::Message");
    m_office = lok::lok_cpp_init(LIBREOFFICE_PATH);
    if (!m_office) {
        sendError(BaseDocumentProvider::UnknownError);
        qFatal("Worker error: can't init the office instance");
    }

    m_innnerThreadPool->setExpiryTimeout(-1);

    /// TODO: remove when fixed
    // In Sailfish, CPUs could be switched off one by one. As a result,
    // "ideal thread count" set by Qt could be off.
    // In other systems, this procedure is not needed and the defaults can be used
    //
    int nCPUs = 0;
    QDir dir;
    while (dir.exists(QString("/sys/devices/system/cpu/cpu") + QString::number(nCPUs)))
        ++nCPUs;
    m_innnerThreadPool->setMaxThreadCount(nCPUs);

    initServer(socketName);

    m_workerThread->start();
    LoEvent::Message msg(LoEvent::OfficeReady, true);
    sendMessage(msg);
}

OfficeWorker::~OfficeWorker()
{
    delete m_workerThread;
    m_innnerThreadPool->clear();
    m_innnerThreadPool->waitForDone();
    qDeleteAll(m_sharedMemoryList);
}

void OfficeWorker::loadDocument(char *url)
{
    qInfo() << "Load documend" << url;

    if (!QFileInfo::exists(QUrl(QString(url)).path())) {
        sendError(BaseDocumentProvider::FileNotFound);
        return;
    }

    m_urlDocument = QString::fromUtf8(url);
    LoadingWorkerLok *worker = new LoadingWorkerLok(m_office, url);

    connect(worker, &LoadingWorkerLok::progress, this, &OfficeWorker::progressReceived);
    connect(worker, &LoadingWorkerLok::documentReady, this, &OfficeWorker::documentLoaded);

    m_innnerThreadPool->start(worker);
}

void OfficeWorker::processing()
{
    while (true) {
        if (m_workerThread->isStoped())
            break;

        m_workerThread->mutex().lock();
        if (m_lowRenderCommands.isEmpty() && m_highRenderCommands.isEmpty() && m_queueCommands.isEmpty())
            m_workerThread->waitCondition().wait(&m_workerThread->mutex());

        RenderCommand lowRenderCmd;
        RenderCommand highRenderCmd;
        LoEvent::Message cmd;

        if (!m_queueCommands.isEmpty())
            cmd = m_queueCommands.dequeue();
        else if (!m_highRenderCommands.isEmpty())
            highRenderCmd = m_highRenderCommands.dequeue();
        else if (!m_lowRenderCommands.isEmpty())
            lowRenderCmd = m_lowRenderCommands.pop();

        m_workerThread->mutex().unlock();

        if (cmd.header.commandType != LoEvent::CommandType::UnknownCommand)
            processingLoCmd(cmd);

        if (highRenderCmd.pageIndex != -1)
            processingRenderCmd(highRenderCmd);

        if (lowRenderCmd.pageIndex != -1)
            processingRenderCmd(lowRenderCmd);
    }
}

void OfficeWorker::globalCallback(int nType, const char *pPayload, void *pData)
{
    QMetaObject::invokeMethod(static_cast<OfficeWorker *>(pData), "callbackImpl", Q_ARG(int, nType), Q_ARG(QByteArray, QByteArray(pPayload)));
}

void OfficeWorker::callbackImpl(int type, const QByteArray &payload)
{
    LoEvent::PayloadCallbackEvent payloadCallback;
    payloadCallback.type = LibreOfficeKitCallbackType(type);
    payloadCallback.payload = payload;
    LoEvent::Message msg(LoEvent::CallbakEvent, payloadCallback);
    sendMessage(msg);
}

void OfficeWorker::initServer(const QString &socketName)
{
    m_server = new QLocalServer(this);
    m_server->setMaxPendingConnections(1);
    connect(m_server, &QLocalServer::newConnection, this, &OfficeWorker::newConnection);
    if (!m_server->listen(socketName)) {
        if (m_server->serverError() == QAbstractSocket::AddressInUseError) {
            QLocalServer::removeServer(socketName);
            if (!m_server->listen(socketName))
                qFatal("Server error: %s", m_server->errorString().toUtf8().data());
        }
    }
}

void OfficeWorker::newConnection()
{
    m_socket = m_server->nextPendingConnection();
    if (m_socket) {
        connect(m_socket, &QLocalSocket::readyRead, this, &OfficeWorker::newCommand);
        connect(m_socket, &QLocalSocket::disconnected, [] { abort(); });

        // Sending a message is required to start sending the queue
        sendDebug("Socket created");
    }
}

void OfficeWorker::newCommand()
{
    bool readedPacket = false;

    do {
        LoEvent::Message msg;
        readedPacket = false;

        static auto sizeHeader = sizeof(LoEvent::Header);
        if (m_socket->bytesAvailable() >= sizeHeader) {
            QByteArray headerBuf = m_socket->peek(sizeHeader);
            QDataStream inHeader(headerBuf);
            inHeader >> msg.header;

            if (m_socket->bytesAvailable() >= (msg.header.sizePayload + sizeHeader)) {
                readedPacket = true;
                QDataStream in(m_socket);
                in >> msg;
                runCommand(msg);
            }
        }
    } while (readedPacket && m_socket->bytesAvailable() >= sizeof(LoEvent::Header));
}

void OfficeWorker::sendMessage(const LoEvent::Message &message)
{
    if (QThread::currentThread() != thread()) {
        QMetaObject::invokeMethod(this, "sendMessage", Q_ARG(LoEvent::Message, message));
        return;
    }

    if (m_socket && m_socket->state() == QLocalSocket::ConnectedState) {
        QDataStream out(m_socket);
        while (!m_sendQueue.isEmpty()) {
            out << m_sendQueue.takeFirst();
        }

        out << message;
    } else {
        m_sendQueue.append(message);
    }
}

void OfficeWorker::runCommand(const LoEvent::Message &message)
{
    QDataStream in(message.payload);
    switch (message.header.commandType) {
    case LoEvent::MakePage: {
        LoEvent::PayloadMakePage msgMakePage;
        in >> msgMakePage;

        Task task = { msgMakePage.pageIndex, msgMakePage.rect, msgMakePage.zoom };
        if (m_pendingTasks.contains(task))
            break;

        m_pendingTasks += task;
        RenderCommand cmd(msgMakePage, true, makeKey());
        bool high = msgMakePage.rect.size() != msgMakePage.canvasSize;
        addRenderCmd(cmd, high);
        setProcessingStatus(true);
    } break;
    case LoEvent::MakeTile: {
        LoEvent::PayloadMakeTile msgMakeTile;
        in >> msgMakeTile;

        Task task = { msgMakeTile.pageIndex, msgMakeTile.rect, msgMakeTile.zoom };
        if (m_pendingTasks.contains(task))
            break;

        m_pendingTasks += task;
        RenderCommand cmd(msgMakeTile, true, makeKey());
        bool high = msgMakeTile.rect.size() != msgMakeTile.canvasSize;
        addRenderCmd(cmd, high);
        setProcessingStatus(true);
    } break;
    case LoEvent::ClearTasks:
        clearRenderCmd();
        m_pendingTasks.clear();

        if (m_pendingTasks.isEmpty())
            setProcessingStatus(false);
        break;
    case LoEvent::StopRender: {
        LoEvent::PayloadStopRender msgStopRender;
        in >> msgStopRender;

        Task task = { msgStopRender.pageIndex, msgStopRender.rect, msgStopRender.zoom };
        // Render backgroundpage
        bool high = msgStopRender.rect.size() != msgStopRender.canvasSize;
        removeRenderCmd(task, high);
        m_pendingTasks.remove(task);

        if (m_pendingTasks.isEmpty())
            setProcessingStatus(false);
    } break;
    case LoEvent::RemoveSharedMem: {
        QString key;
        in >> key;
        delete m_sharedMemoryList[key];
        m_sharedMemoryList.remove(key);
        m_availableKeyList.append(key);
    } break;
    case LoEvent::PostUnoCommand:
    case LoEvent::PostKeyEvent:
    case LoEvent::PostMouseEvent:
    case LoEvent::PostSelectEvent:
    case LoEvent::SetPart:
    case LoEvent::SaveAs:
    case LoEvent::GetUnoCommandValues:
    case LoEvent::GetTextSelection:
        addLoCmd(message);
        break;
    default:
        break;
    }
}

void OfficeWorker::sendError(int error)
{
    LoEvent::Message msg(LoEvent::Error, error);
    sendMessage(msg);
}

QString OfficeWorker::makeKey()
{
    static int countKey = 0;
    if (m_availableKeyList.count())
        return m_availableKeyList.takeFirst();
    else
        return QString("LOK_%1").arg(++countKey);
}

void OfficeWorker::addRenderCmd(const RenderCommand &cmd, bool high)
{
    m_workerThread->mutex().lock();

    if (high)
        m_highRenderCommands.enqueue(cmd);
    else
        m_lowRenderCommands.push(cmd);

    m_workerThread->mutex().unlock();
    m_workerThread->wake();
}

void OfficeWorker::removeRenderCmd(const Task &task, bool high)
{
    m_workerThread->mutex().lock();

    auto removeItem = [](const Task &task, auto &stack) {
        for (int i = 0; i < stack.count(); i++) {
            auto &cmd = stack[i];
            if (cmd.pageIndex == task.pageNumber && qFuzzyCompare(cmd.zoom, task.zoom) && cmd.rect == task.rect) {
                stack.removeAt(i);
                break;
            }
        }
    };

    if (high)
        removeItem(task, m_highRenderCommands);
    else
        removeItem(task, m_lowRenderCommands);

    m_workerThread->mutex().unlock();
}

void OfficeWorker::clearRenderCmd()
{
    m_workerThread->mutex().lock();
    m_highRenderCommands.clear();
    m_lowRenderCommands.clear();
    m_workerThread->mutex().unlock();
}

void OfficeWorker::processingRenderCmd(const RenderCommand &cmd)
{
    QSharedMemory *mem = new QSharedMemory(cmd.keySharedMem);

    if (mem->attach()) // Releasing stuck shared memory
        mem->detach();

    uchar *udata = nullptr;
    if (static_cast<bool>(cmd.renderRequest & LoEvent::RenderRequestFlags::Visible)) {
        // TODO: Add status processing
        bool status = mem->create(cmd.canvasSize.width() * cmd.canvasSize.height() * 4);
        if (!status)
            qDebug() << "Not make shared memory" << cmd.keySharedMem << cmd.canvasSize;
        udata = reinterpret_cast<uchar *>(mem->data());
    } else {
        udata = new uchar[cmd.canvasSize.width() * cmd.canvasSize.height() * 4];
    }
    int docType = m_document->getDocumentType();

    if (docType == LOK_DOCTYPE_SPREADSHEET || docType == LOK_DOCTYPE_PRESENTATION) {
        if (docType == LOK_DOCTYPE_SPREADSHEET)
            m_document->setClientZoom(cmd.canvasSize.width(), cmd.canvasSize.height(), cmd.rect.width(), cmd.rect.height());

        m_document->paintPartTile(udata, cmd.pageIndex, cmd.canvasSize.width(), cmd.canvasSize.height(), cmd.rect.x(), cmd.rect.y(), cmd.rect.width(), cmd.rect.height());
    } else {
        m_document->paintTile(udata, cmd.canvasSize.width(), cmd.canvasSize.height(), cmd.rect.x(), cmd.rect.y(), cmd.rect.width(), cmd.rect.height());
    }

    if (static_cast<bool>(cmd.renderRequest & LoEvent::RenderRequestFlags::Visible)) {
        RenderCommand cmdWithMem = cmd;
        cmdWithMem.mem = mem;

        if (cmd.renderPart)
            QMetaObject::invokeMethod(this, "partRendered", Q_ARG(RenderCommand, cmdWithMem));
        else
            QMetaObject::invokeMethod(this, "fullRendered", Q_ARG(RenderCommand, cmdWithMem));
    } else {
        delete[] udata;
        QMetaObject::invokeMethod(this, "renderedNotVisible", Q_ARG(RenderCommand, cmd));
    }
}

void OfficeWorker::addLoCmd(const LoEvent::Message &cmd)
{
    m_workerThread->mutex().lock();
    m_queueCommands.append(cmd);
    m_workerThread->mutex().unlock();
    m_workerThread->wake();
}

void OfficeWorker::processingLoCmd(const LoEvent::Message &cmd)
{
    QDataStream in(cmd.payload);
    switch (cmd.header.commandType) {
    case LoEvent::PostUnoCommand: {
        LoEvent::PayloadUnoEvent unoEvent;
        in >> unoEvent;
        m_document->postUnoCommand(unoEvent.command.toStdString().c_str(), unoEvent.arguments.toStdString().c_str(), unoEvent.notifyWhenFinished);
    } break;
    case LoEvent::PostKeyEvent: {
        LoEvent::PayloadKeyEvent keyEvent;
        in >> keyEvent;
        m_document->postKeyEvent(keyEvent.type, keyEvent.charCode, keyEvent.keyCode);
    } break;
    case LoEvent::PostMouseEvent: {
        LoEvent::PayloadMouseEvent mouseEvent;
        in >> mouseEvent;

        if (mouseEvent.pageIndex != m_document->getPart())
            m_document->setPart(mouseEvent.pageIndex);

        m_document->postMouseEvent(mouseEvent.type, mouseEvent.x, mouseEvent.y, mouseEvent.count, mouseEvent.buttons, mouseEvent.modifier);
    } break;
    case LoEvent::PostSelectEvent: {
        LoEvent::PayloadSelectEvent selectEvent;
        in >> selectEvent;

        m_document->setTextSelection(selectEvent.type, selectEvent.x, selectEvent.y);
    } break;
    case LoEvent::SetPart: {
        int partIndex = 0;
        in >> partIndex;
        if (partIndex != m_document->getPart())
            m_document->setPart(partIndex);
    } break;
    case LoEvent::SaveAs: {
        LoEvent::PayloadSaveAs payloadSaveAs;
        in >> payloadSaveAs;
        const char* format = payloadSaveAs.format.isEmpty() ? nullptr : payloadSaveAs.format.toUtf8().data();
        const char* filterOptions = payloadSaveAs.filterOptions.isEmpty() ? nullptr : payloadSaveAs.filterOptions.toUtf8().data();

        bool status = m_document->saveAs(payloadSaveAs.path.toUtf8().data(), format, filterOptions);

        LoEvent::PayloadSaveAsStatus payloadSaveAsStatus;
        payloadSaveAsStatus.path = payloadSaveAs.path;
        payloadSaveAsStatus.format = payloadSaveAs.format;
        payloadSaveAsStatus.status = status;

        sendMessage(LoEvent::Message(LoEvent::SaveAsStatus, payloadSaveAsStatus));
    } break;
    case LoEvent::GetUnoCommandValues: {
        LoEvent::PayloadUnoEvent unoEvent;
        in >> unoEvent;
        char *result = m_document->getCommandValues(unoEvent.command.data());
        unoEvent.arguments = QByteArray(result);

        sendMessage(LoEvent::Message(LoEvent::CallbakUnoCommand, unoEvent));
    } break;

    case LoEvent::GetTextSelection: {
        QString mimeType;
        in >> mimeType;
        char *result = m_document->getTextSelection(mimeType.toUtf8());

        sendMessage(LoEvent::Message(LoEvent::CallbackTextSelection, QString::fromUtf8(result)));
    } break;
    }
}

void OfficeWorker::sendDebug(const QString &errorString)
{
    LoEvent::Message msg(LoEvent::Debug, errorString);
    sendMessage(msg);
}

void OfficeWorker::setProcessingStatus(bool processing)
{
    if (m_processing != processing) {
        m_processing = processing;
        sendMessage(LoEvent::Message(LoEvent::ProcessingStatus, processing));
    }
}

void OfficeWorker::progressReceived(int progress)
{
    LoEvent::Message msg(LoEvent::LoadProgress, progress);
    sendMessage(msg);
}

void OfficeWorker::documentLoaded(void *pointer)
{
    m_document = static_cast<lok::Document *>(pointer);
    m_document->registerCallback(&OfficeWorker::globalCallback, this);
    //m_document->setPartMode(LOK_PARTMODE_NOTES);
    if (m_document) {
        int type = m_document->getDocumentType();
        LoEvent::Message msg(LoEvent::DocumentType, type);
        sendMessage(msg);
        switch (type) {
        case LOK_DOCTYPE_TEXT:
            m_document->initializeForRendering("{\".uno:ShowAnnotations\":{\"type\":\"void\"}}");

        case LOK_DOCTYPE_OTHER: {
            QByteArray rectangles(m_document->getPartPageRectangles());
            QList<QByteArray> rectList = rectangles.split(';');
            QList<QRect> pageRectangles;

            for (int i(0); i < rectList.count(); i++) {
                QList<QByteArray> elements = rectList.at(i).split(',');
                QRect rectPage;
                rectPage.setX(elements.at(0).trimmed().toInt());
                rectPage.setY(elements.at(1).trimmed().toInt());
                rectPage.setWidth(elements.at(2).trimmed().toInt());
                rectPage.setHeight(elements.at(3).trimmed().toInt());

                if (rectPage.isValid())
                    pageRectangles.append(rectPage);
            }

            LoEvent::Message msg(LoEvent::PageRectangles, pageRectangles);
            sendMessage(msg);
        } break;
        case LOK_DOCTYPE_DRAWING:
        case LOK_DOCTYPE_PRESENTATION:
        case LOK_DOCTYPE_SPREADSHEET: {
            m_document->initializeForRendering("{\".uno:Author\":{\"type\":\"string\",\"value\":\"Local Host #0\"}}");

            int parts = m_document->getParts();
            LoEvent::Message msg(LoEvent::PageCount, parts);
            sendMessage(msg);

            for (int i = 0; i < parts; i++) {
                long width(0);
                long height(0);
                m_document->setPart(i);
                m_document->getDocumentSize(&width, &height);

                LoEvent::Message msgSize(LoEvent::PageSize, LoEvent::PayloadPageSize(i, QSize(width, height)));
                sendMessage(msgSize);

                char *pageName = m_document->getPartName(i);
                LoEvent::Message msgName(LoEvent::PageName, LoEvent::PayloadPageName(i, QString::fromUtf8(pageName)));
                sendMessage(msgName);
            }
        } break;
        }
        {
            LoEvent::Message msg(LoEvent::LoadStatus, true);
            sendMessage(msg);
        }
    } else {
        QString errorString(m_office->getError());
        m_office->freeError(m_office->getError());

        // Processing messages for LibreOffice desktop component version 7.0.2.2
        // Checked messages from function lo_documentLoadWithOptions() in
        // upstream/desktop/source/lib/init.cxx line 2111
        static const QMap<QString, int> errors{{"Filename to load was not provided.", BaseDocumentProvider::EmptyUrl},
                                               {"ComponentContext is not available", BaseDocumentProvider::ProviderContextError},
                                               {"ComponentLoader is not available", BaseDocumentProvider::ProviderLoaderError},
                                               {"loadComponentFromURL returned an empty reference", BaseDocumentProvider::UnsupportedFileType},
                                               {"", BaseDocumentProvider::CorruptedDocument}};

        sendError(errors.value(errorString, BaseDocumentProvider::UnknownError));
    }
}

void OfficeWorker::fullRendered(const RenderCommand &cmd)
{
    m_sharedMemoryList.insert(cmd.keySharedMem, cmd.mem);

    m_pendingTasks.remove({cmd.pageIndex, cmd.rect, cmd.zoom});
    LoEvent::PayloadNewPage newTile;
    newTile.pageIndex = cmd.pageIndex;
    newTile.size = cmd.canvasSize;
    newTile.zoom = cmd.zoom;
    newTile.sharedMemKey = cmd.keySharedMem;
    newTile.renderRequest = cmd.renderRequest;
    LoEvent::Message msg(LoEvent::NewPage, newTile);

    sendMessage(msg);

    if (m_pendingTasks.isEmpty())
        setProcessingStatus(false);
}

void OfficeWorker::partRendered(const RenderCommand &cmd)
{
    m_sharedMemoryList.insert(cmd.keySharedMem, cmd.mem);

    m_pendingTasks.remove({cmd.pageIndex, cmd.rect, cmd.zoom});
    LoEvent::PayloadNewTile newTile;
    newTile.pageIndex = cmd.pageIndex;
    newTile.rect = cmd.rect;
    newTile.canvasSize = cmd.canvasSize;
    newTile.zoom = cmd.zoom;
    newTile.sharedMemKey = cmd.keySharedMem;
    newTile.renderRequest = cmd.renderRequest;
    LoEvent::Message msg(LoEvent::NewTile, newTile);

    sendMessage(msg);

    if (m_pendingTasks.isEmpty())
        setProcessingStatus(false);
}

void OfficeWorker::renderedNotVisible(const RenderCommand &cmd)
{
    m_pendingTasks.remove({cmd.pageIndex, cmd.rect, cmd.zoom});

    if (m_pendingTasks.isEmpty())
        setProcessingStatus(false);
}

QDebug operator<< (QDebug d, const OfficeWorker::Task &task)
{
    d << "{" << task.pageNumber << task.rect << "zoom(" << task.zoom << ") }";
    return d;
}

