/*******************************************************************************
**
** 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 "imagescache.h"

#include <QtConcurrent/QtConcurrent>
#include <QFileInfo>
#include <QDir>
#include <QUrl>
#include <QImage>

uint qHash(const ImagesCache::CacheKey &key)
{
    return qHash(QString("%1_%2_%3_%4_%5_%6_%7_%8_%9")
                     .arg(key.fileId)
                     .arg(key.pageNumber)
                     .arg(key.imageSize.width())
                     .arg(key.imageSize.height())
                     .arg(key.rect.x())
                     .arg(key.rect.y())
                     .arg(key.rect.width())
                     .arg(key.rect.height())
                     .arg(key.zoom)
                 );
}

namespace {
const qint64 LIMIT_CACHE = 5.243e+7; // 50 Mb

}

class WriteImageWorker : public QRunnable
{
public:
    WriteImageWorker(const QString &fileId, int pageNumber, const QRect &rectTile, qreal zoom, const QImage &image, ImagesCache *documentCache)
        : fileId(fileId), pageNumber(pageNumber), rect(rectTile), zoom(zoom), image(image), imagesCache(documentCache) {}

private:
    void run() override
    {
        bool status = image.save(imagesCache->makeCachePath(fileId, pageNumber, image.size(), rect, zoom), "PNG");

        if (status)
            imagesCache->saveFile(fileId, pageNumber, image.size(), rect, zoom);
    }

private:
    QString fileId;
    int pageNumber;
    QRect rect;
    qreal zoom;
    QImage image;
    ImagesCache *imagesCache;
};

class ReadImageWorker : public QRunnable
{
public:
    ReadImageWorker(const QString &fileId, int pageNumber, const QSize &canvasSize, const QRect &rectTile, qreal zoom, ImagesCache *documentCache)
        : fileId(fileId), pageNumber(pageNumber), canvasSize(canvasSize), rect(rectTile), zoom(zoom), imagesCache(documentCache) {}

private:
    void run() override
    {
        QImage image(imagesCache->makeCachePath(fileId, pageNumber, canvasSize, rect, zoom), "PNG");
        if (!image.isNull())
            emit imagesCache->imageLoaded(fileId, pageNumber, image, rect, zoom);
    }

private:
    QString fileId;
    int pageNumber;
    QSize canvasSize;
    QRect rect;
    qreal zoom;
    ImagesCache *imagesCache;
};

class LoadWorker : public QRunnable
{
public:
    LoadWorker(ImagesCache *documentCache) : imagesCache(documentCache) {}

private:
    void run() override
    {
        imagesCache->loadCache();
    }

    ImagesCache *imagesCache;
};

ImagesCache::ImagesCache(const QString &pathCache, QObject *parent)
    : QObject(parent)
    , m_path(pathCache)
    , m_limit(LIMIT_CACHE)
    , m_currentCacheSize(0)
    , m_loading(false)
{
    static int nCPUs = 0;

    if (nCPUs == 0)
        while (QDir().exists(QString("/sys/devices/system/cpu/cpu") + QString::number(nCPUs)))
            ++nCPUs;

    m_threadPool.setMaxThreadCount(nCPUs);
    QDir().mkpath(pathCache);
    m_threadPool.start(new LoadWorker(this), -1);
}

ImagesCache::~ImagesCache()
{
    m_threadPool.clear();
    m_threadPool.waitForDone();
}

bool ImagesCache::loadImage(const QString &fileId, int pageNumber, const QSize &canvasSize, const QRect &rectTile, qreal zoom)
{
    bool contains = m_cache.contains(qHash({fileId, pageNumber, canvasSize, rectTile, zoom}));

    if (contains)
        m_threadPool.start(new ReadImageWorker(fileId, pageNumber, canvasSize, rectTile, zoom, this));

    return contains;
}

void ImagesCache::insertImage(const QString &fileId, int pageNumber, const QImage &image, const QRect &rectTile, qreal zoom)
{
    if (m_loading) {
        m_writeQueue.insert(CacheKey(fileId, pageNumber, image.size(), rectTile, zoom), image);
        return;
    }

    if (m_cache.contains(qHash({fileId, pageNumber, image.size(), rectTile, zoom})))
        return;

    m_threadPool.start(new WriteImageWorker(fileId, pageNumber, rectTile, zoom, image, this), -1);
}

void ImagesCache::loadCache()
{
    m_loading = true;
    QDir dir(m_path);

    for (const auto &fileInfo : dir.entryInfoList({}, QDir::Files)) {
        auto fileId = fileInfo.fileName().toUInt(nullptr, 16);
        m_cache.insert(fileId, fileInfo);
        m_currentCacheSize += fileInfo.size();
        m_cacheSortedByCreated.insert(fileInfo.created(), fileId);
    }

    m_loading = false;

    for (auto it  = m_writeQueue.begin(); it != m_writeQueue.end(); ++it)
        insertImage(it.key().fileId, it.key().pageNumber, it.value(), it.key().rect, it.key().zoom);
}

void ImagesCache::saveFile(const QString &filePath, int pageNumber, const QSize &imageSize, const QRect &rectTile, qreal zoom)
{
    m_mutex.lock();

    auto fileId = qHash({filePath, pageNumber, imageSize, rectTile, zoom});
    auto fileInfo = QFileInfo(makeCachePath(filePath, pageNumber, imageSize, rectTile, zoom));
    m_cache.insert(fileId, fileInfo);
    m_cacheSortedByCreated.insert(fileInfo.created(), fileId);
    m_currentCacheSize += fileInfo.size();

    while (m_currentCacheSize > m_limit) {
        auto oldFileId = m_cacheSortedByCreated.first();
        m_cacheSortedByCreated.remove(m_cacheSortedByCreated.firstKey());
        auto oldFileInfo = m_cache.value(oldFileId);
        m_cache.remove(oldFileId);
        m_currentCacheSize -= oldFileInfo.size();
        QFile::remove(oldFileInfo.filePath());
    }

    m_mutex.unlock();
}

QString ImagesCache::makeCachePath(const QString &fileId, int pageNumber, const QSize &imageSize, const QRect &rectTile, qreal zoom)
{
    return QString("%1/%2").arg(m_path).arg(qHash({fileId, pageNumber, imageSize, rectTile, zoom}), 0, 16);
}

