/*
 * SPDX-FileCopyrightText: 2018 Boudewijn Rempt <boud@valdyas.org>
 *
 * SPDX-License-Identifier: LGPL-2.0-or-later
 */

#include "KisMemoryStorage.h"

#include <optional>
#include <QVector>

#include <KisMimeDatabase.h>
#include <kis_debug.h>
#include <KisTag.h>
#include <KisResourceStorage.h>
#include <QBuffer>
#include <KisGlobalResourcesInterface.h>
#include <kis_pointer_utils.h>
#include <KoMD5Generator.h>
#include <kis_assert.h>
#include <KisMpl.h>


namespace detail {

    /**
     * TODO: move this function into KisStorageVersioningHelper with fixing the
     * versioning functions to handle subfolders as well
     */
    std::optional<std::pair<QString, QString>> splitResourceUrl(const QString &url)
    {
        if (!url.contains('/')) return std::nullopt;

        QStringList parts = url.split('/', Qt::SkipEmptyParts);

        if (parts.isEmpty()) return std::nullopt;

        const QString resourceType = parts[0];
        parts.removeFirst();
        const QString resourceFilename = parts.join('/');
        return std::make_pair(resourceType, resourceFilename);
    }

}

struct StoredResource
{
    QDateTime timestamp;
    QSharedPointer<QByteArray> data;
    KoResourceSP resource;
};

class MemoryTagIterator : public KisResourceStorage::TagIterator
{
public:
    MemoryTagIterator(const QVector<KisTagSP> &tags)
        : m_it(tags)
    {
    }

    bool hasNext() const override
    {
        return m_it.hasNext();
    }

    void next() override
    {
        m_it.next();
    }

    KisTagSP tag() const override
    {
        return m_it.peekPrevious();
    }

private:
    QVectorIterator<KisTagSP> m_it;
};


class MemoryItem : public KisResourceStorage::ResourceItem
{
public:
    ~MemoryItem() override {}
};


class KisMemoryStorage::Private {
public:
    QHash<QString, QHash<QString, StoredResource>> resourcesNew;
    QHash<QString, QVector<KisTagSP>> tags;
    QMap<QString, QVariant> metadata;
};


KisMemoryStorage::KisMemoryStorage(const QString &location)
    : KisStoragePlugin(location)
    , d(new Private)
{
}

KisMemoryStorage::~KisMemoryStorage()
{
}

KisMemoryStorage::KisMemoryStorage(const KisMemoryStorage &rhs)
    : KisStoragePlugin(rhs.location())
    , d(new Private)
{
    *this = rhs;
    d->resourcesNew = rhs.d->resourcesNew;
    d->tags = rhs.d->tags;
    d->metadata = rhs.d->metadata;
}

KisMemoryStorage &KisMemoryStorage::operator=(const KisMemoryStorage &rhs)
{
    if (this != &rhs) {
        d->resourcesNew = rhs.d->resourcesNew;

        Q_FOREACH(const QString &key, rhs.d->tags.keys()) {
            Q_FOREACH(const KisTagSP tag, rhs.d->tags[key]) {
                if (!d->tags.contains(key)) {
                    d->tags[key] = QVector<KisTagSP>();
                }
                d->tags[key] << tag->clone();
            }
        }
    }
    return *this;
}

bool KisMemoryStorage::saveAsNewVersion(const QString &resourceType, KoResourceSP resource)
{
    QHash<QString, StoredResource> &typedResources =
        d->resourcesNew[resourceType];

    auto checkExists =
        [&typedResources] (const QString &filename) {
            return typedResources.contains(filename);
        };

    const QString newFilename =
        KisStorageVersioningHelper::chooseUniqueName(resource, 0, checkExists);

    if (newFilename.isEmpty()) return false;

    resource->setFilename(newFilename);

    StoredResource storedResource;
    storedResource.timestamp = QDateTime::currentDateTime();
    storedResource.data.reset(new QByteArray());
    QBuffer buffer(storedResource.data.data());
    buffer.open(QIODevice::WriteOnly);
    bool result = resource->saveToDevice(&buffer);
    buffer.close();
    if (!result) {
        storedResource.resource = resource;
    }

    typedResources.insert(newFilename, storedResource);

    return true;
}

KisResourceStorage::ResourceItem KisMemoryStorage::resourceItem(const QString &url)
{
    MemoryItem item;
    item.url = url;
    item.folder = QString();
    item.lastModified = QDateTime::fromMSecsSinceEpoch(0);
    return item;
}

bool KisMemoryStorage::loadVersionedResource(KoResourceSP resource)
{
    const QString resourceType = resource->resourceType().first;
    const QString resourceFilename = resource->filename();

    bool retval = false;

    if (d->resourcesNew.contains(resourceType) &&
        d->resourcesNew[resourceType].contains(resourceFilename)) {

        const StoredResource &storedResource =
            d->resourcesNew[resourceType][resourceFilename];

        if (storedResource.data->size() > 0) {
            QBuffer buffer(storedResource.data.data());
            buffer.open(QIODevice::ReadOnly);
            resource->loadFromDevice(&buffer, KisGlobalResourcesInterface::instance());
        } else {
            KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(storedResource.data->size() > 0, false);
            qWarning() << "Cannot load resource from device in KisMemoryStorage::loadVersionedResource";
            return false;
        }
        retval = true;
    }

    return retval;
}

bool KisMemoryStorage::importResource(const QString &url, QIODevice *device)
{
    auto parsedUrl = detail::splitResourceUrl(url);
    if (!parsedUrl) return false;
    auto [resourceType, resourceFilename] = *parsedUrl;

    // we cannot overwrite existing file by API convention
    if (d->resourcesNew.contains(resourceType) &&
        d->resourcesNew[resourceType].contains(resourceFilename)) {
        return false;
    }

    StoredResource storedResource;
    storedResource.timestamp = QDateTime::currentDateTime();
    storedResource.data.reset(new QByteArray(device->readAll()));

    QHash<QString, StoredResource> &typedResources =
        d->resourcesNew[resourceType];
    typedResources.insert(resourceFilename, storedResource);

    return true;
}

bool KisMemoryStorage::exportResource(const QString &url, QIODevice *device)
{
    auto parsedUrl = detail::splitResourceUrl(url);
    if (!parsedUrl) return false;
    auto [resourceType, resourceFilename] = *parsedUrl;

    if (!d->resourcesNew.contains(resourceType) ||
        !d->resourcesNew[resourceType].contains(resourceFilename)) {
        return false;
    }

    const StoredResource &storedResource =
        d->resourcesNew[resourceType][resourceFilename];

    if (!storedResource.data) {
        qWarning() << "Stored resource doesn't have a serialized representation!";
        return false;
    }

    device->write(*storedResource.data);
    return true;
}

bool KisMemoryStorage::addResource(const QString &resourceType,  KoResourceSP resource)
{
    QHash<QString, StoredResource> &typedResources = d->resourcesNew[resourceType];

    if (typedResources.contains(resource->filename())) {
        /// here we silently overwrite the resource if the filename
        /// is the same; it is the job of higher-level code to actually
        /// resolve file name clashes
        return true;
    };

    StoredResource storedResource;
    storedResource.timestamp = QDateTime::currentDateTime();
    storedResource.data.reset(new QByteArray());
    if (resource->isSerializable()) {
        QBuffer buffer(storedResource.data.data());
        buffer.open(QIODevice::WriteOnly);
        if (!resource->saveToDevice(&buffer)) {
            storedResource.resource = resource;
        }
        buffer.close();
    } else {
        storedResource.resource = resource;
    }

    typedResources.insert(resource->filename(), storedResource);

    return true;
}

bool KisMemoryStorage::testingRemoveResource(const QString &url)
{
    auto parsedUrl = detail::splitResourceUrl(url);
    if (!parsedUrl) return false;
    auto [resourceType, resourceFilename] = *parsedUrl;

    if (d->resourcesNew.contains(resourceType)) {
        return d->resourcesNew[resourceType].remove(resourceFilename) > 0;
    }

    return false;
}

bool KisMemoryStorage::testingAddTag(const QString &resourceType, KisTagSP tag)
{
    KIS_SAFE_ASSERT_RECOVER_NOOP(resourceType == tag->resourceType());

    QVector<KisTagSP> &typedTags = d->tags[resourceType];

    auto existingIt = std::find_if(typedTags.begin(), typedTags.end(), kismpl::mem_equal_to(&KisTag::url, tag->url()));
    if (existingIt != typedTags.end()) {
        typedTags.erase(existingIt);
    }

    typedTags.append(tag);

    return true;
}

bool KisMemoryStorage::testingRemoveTag(const QString &resourceType, const QString &tagUrl)
{
    QVector<KisTagSP> &typedTags = d->tags[resourceType];

    auto existingIt = std::find_if(typedTags.begin(), typedTags.end(), kismpl::mem_equal_to(&KisTag::url, tagUrl));
    if (existingIt != typedTags.end()) {
        typedTags.erase(existingIt);
        return true;
    }

    return false;
}

QString KisMemoryStorage::resourceMd5(const QString &url)
{
    auto parsedUrl = detail::splitResourceUrl(url);
    if (!parsedUrl) return QString();
    auto [resourceType, resourceFilename] = *parsedUrl;

    QString result;

    if (d->resourcesNew.contains(resourceType) &&
        d->resourcesNew[resourceType].contains(resourceFilename)) {

        const StoredResource &storedResource =
            d->resourcesNew[resourceType][resourceFilename];

        if (storedResource.data->size() > 0 || storedResource.resource.isNull()) {
            result = KoMD5Generator::generateHash(*storedResource.data);
        } else {
            result = storedResource.resource->md5Sum();
        }
    }

    return result;
}

QSharedPointer<KisResourceStorage::ResourceIterator> KisMemoryStorage::resources(const QString &resourceType)
{
    QVector<VersionedResourceEntry> entries;


    QHash<QString, StoredResource> &typedResources =
        d->resourcesNew[resourceType];

    for (auto it = typedResources.begin(); it != typedResources.end(); ++it) {
        VersionedResourceEntry entry;
        entry.filename = it.key();
        entry.lastModified = it.value().timestamp;
        entry.tagList = {}; // TODO
        entry.resourceType = resourceType;
        entries.append(entry);

    }

    KisStorageVersioningHelper::detectFileVersions(entries);

    return toQShared(new KisVersionedStorageIterator(entries, this));
}

QSharedPointer<KisResourceStorage::TagIterator> KisMemoryStorage::tags(const QString &resourceType)
{
    return QSharedPointer<KisResourceStorage::TagIterator>(new MemoryTagIterator(d->tags[resourceType]));
}

void KisMemoryStorage::setMetaData(const QString &key, const QVariant &value)
{
    d->metadata[key] = value;
}

QStringList KisMemoryStorage::metaDataKeys() const
{
    QStringList keys = d->metadata.keys();

    if (!keys.contains(KisResourceStorage::s_meta_name)) {
        keys << KisResourceStorage::s_meta_name;
    }

    return keys;
}

QVariant KisMemoryStorage::metaData(const QString &key) const
{
    QVariant r;
    if (d->metadata.contains(key)) {
        r = d->metadata[key];
    }
    return r;
}
