// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include "qsettings.h"
#ifndef QT_NO_SETTINGS

#include "qsettings_p.h"
#ifndef QT_NO_QOBJECT
#include "qcoreapplication.h"
#include <QFile>
#endif // QT_NO_QOBJECT
#include <QDebug>
#include <QtCore/private/qstdweb_p.h>

#include <QFileInfo>
#include <QDir>
#include <QList>
#include <QSet>

#include <emscripten.h>
#  include <emscripten/proxying.h>
#  include <emscripten/threading.h>
#  include <emscripten/val.h>

QT_BEGIN_NAMESPACE

using emscripten::val;
using namespace Qt::StringLiterals;

namespace {
QStringView keyNameFromPrefixedStorageName(QStringView prefix, QStringView prefixedStorageName)
{
    // Return the key slice after m_keyPrefix, or an empty string view if no match
    if (!prefixedStorageName.startsWith(prefix))
        return QStringView();
    return prefixedStorageName.sliced(prefix.length());
}
} // namespace

//
// Native settings implementation for WebAssembly using window.localStorage
// as the storage backend. localStorage is a key-value store with a synchronous
// API and a 5MB storage limit.
//
class QWasmLocalStorageSettingsPrivate final : public QSettingsPrivate
{
public:
    QWasmLocalStorageSettingsPrivate(QSettings::Scope scope, const QString &organization,
                                     const QString &application);
    ~QWasmLocalStorageSettingsPrivate() final = default;

    void remove(const QString &key) final;
    void set(const QString &key, const QVariant &value) final;
    std::optional<QVariant> get(const QString &key) const final;
    QStringList children(const QString &prefix, ChildSpec spec) const final;
    void clear() final;
    void sync() final;
    void flush() final;
    bool isWritable() const final;
    QString fileName() const final;

private:
    QStringList m_keyPrefixes;
};

QWasmLocalStorageSettingsPrivate::QWasmLocalStorageSettingsPrivate(QSettings::Scope scope,
                                                                   const QString &organization,
                                                                   const QString &application)
    : QSettingsPrivate(QSettings::NativeFormat, scope, organization, application)
{
    if (organization.isEmpty()) {
        setStatus(QSettings::AccessError);
        return;
    }

    // The key prefix contians "qt" to separate Qt keys from other keys on localStorage, a
    // version tag to allow for making changes to the key format in the future, the org
    // and app names.
    //
    // User code could could create separate settings object with different org and app names,
    // and would expect them to have separate settings. Also, different webassembly instances
    // on the page could write to the same window.localStorage. Add the org and app name
    // to the key prefix to differentiate, even if that leads to keys with redundant sections
    // for the common case of a single org and app name.
    //
    // Also, the common Qt mechanism for user/system scope and all-application settings are
    // implemented, using different prefixes.
    const QString allAppsSetting = QStringLiteral("all-apps");
    const QString systemSetting = QStringLiteral("sys-tem");

    const QLatin1String separator("-");
    const QLatin1String doubleSeparator("--");
    const QString escapedOrganization = QString(organization).replace(separator, doubleSeparator);
    const QString escapedApplication = QString(application).replace(separator, doubleSeparator);
    const QString prefix = "qt-v0-" + escapedOrganization + separator;
    if (scope == QSettings::Scope::UserScope) {
        if (!escapedApplication.isEmpty())
            m_keyPrefixes.push_back(prefix + escapedApplication + separator);
        m_keyPrefixes.push_back(prefix + allAppsSetting + separator);
    }
    if (!escapedApplication.isEmpty()) {
        m_keyPrefixes.push_back(prefix + escapedApplication + separator + systemSetting
                                + separator);
    }
    m_keyPrefixes.push_back(prefix + allAppsSetting + separator + systemSetting + separator);
}

void QWasmLocalStorageSettingsPrivate::remove(const QString &key)
{
    const std::string removed = QString(m_keyPrefixes.first() + key).toStdString();

    qstdweb::runTaskOnMainThread<void>([this, &removed, &key]() {
        std::vector<std::string> children = { removed };
        const int length = val::global("window")["localStorage"]["length"].as<int>();
        for (int i = 0; i < length; ++i) {
            const QString storedKeyWithPrefix = QString::fromStdString(
                    val::global("window")["localStorage"].call<val>("key", i).as<std::string>());

            const QStringView storedKey = keyNameFromPrefixedStorageName(
                    m_keyPrefixes.first(), QStringView(storedKeyWithPrefix));
            if (storedKey.isEmpty() || !storedKey.startsWith(key))
                continue;

            children.push_back(storedKeyWithPrefix.toStdString());
        }

        for (const auto &child : children)
            val::global("window")["localStorage"].call<val>("removeItem", child);
    });
}

void QWasmLocalStorageSettingsPrivate::set(const QString &key, const QVariant &value)
{
    qstdweb::runTaskOnMainThread<void>([this, &key, &value]() {
        const std::string keyString = QString(m_keyPrefixes.first() + key).toStdString();
        const std::string valueString = QSettingsPrivate::variantToString(value).toStdString();
        val::global("window")["localStorage"].call<void>("setItem", keyString, valueString);
    });
}

std::optional<QVariant> QWasmLocalStorageSettingsPrivate::get(const QString &key) const
{
    return qstdweb::runTaskOnMainThread<std::optional<QVariant>>(
            [this, &key]() -> std::optional<QVariant> {
                for (const auto &prefix : m_keyPrefixes) {
                    const std::string keyString = QString(prefix + key).toStdString();
                    const emscripten::val value =
                            val::global("window")["localStorage"].call<val>("getItem", keyString);
                    if (!value.isNull()) {
                        return QSettingsPrivate::stringToVariant(
                                QString::fromStdString(value.as<std::string>()));
                    }
                    if (!fallbacks) {
                        return std::nullopt;
                    }
                }
                return std::nullopt;
            });
}

QStringList QWasmLocalStorageSettingsPrivate::children(const QString &prefix, ChildSpec spec) const
{
    return qstdweb::runTaskOnMainThread<QStringList>([this, &prefix, &spec]() -> QStringList {
        QSet<QString> nodes;
        // Loop through all keys on window.localStorage, return Qt keys belonging to
        // this application, with the correct prefix, and according to ChildSpec.
        QStringList children;
        const int length = val::global("window")["localStorage"]["length"].as<int>();
        for (int i = 0; i < length; ++i) {
            for (const auto &storagePrefix : m_keyPrefixes) {
                const QString keyString =
                        QString::fromStdString(val::global("window")["localStorage"]
                                                       .call<val>("key", i)
                                                       .as<std::string>());

                const QStringView key =
                        keyNameFromPrefixedStorageName(storagePrefix, QStringView(keyString));
                if (!key.isEmpty() && key.startsWith(prefix)) {
                    QStringList children;
                    QSettingsPrivate::processChild(key.sliced(prefix.length()), spec, children);
                    if (!children.isEmpty())
                        nodes.insert(children.first());
                }
                if (!fallbacks)
                    break;
            }
        }

        return QStringList(nodes.begin(), nodes.end());
    });
}

void QWasmLocalStorageSettingsPrivate::clear()
{
    qstdweb::runTaskOnMainThread<void>([this]() {
        // Get all Qt keys from window.localStorage
        const int length = val::global("window")["localStorage"]["length"].as<int>();
        QStringList keys;
        keys.reserve(length);
        for (int i = 0; i < length; ++i)
            keys.append(QString::fromStdString(
                    (val::global("window")["localStorage"].call<val>("key", i).as<std::string>())));

        // Remove all Qt keys. Note that localStorage does not guarantee a stable
        // iteration order when the storage is mutated, which is why removal is done
        // in a second step after getting all keys.
        for (const QString &key : keys) {
            if (!keyNameFromPrefixedStorageName(m_keyPrefixes.first(), key).isEmpty())
                val::global("window")["localStorage"].call<val>("removeItem", key.toStdString());
        }
    });
}

void QWasmLocalStorageSettingsPrivate::sync() { }

void QWasmLocalStorageSettingsPrivate::flush() { }

bool QWasmLocalStorageSettingsPrivate::isWritable() const
{
    return true;
}

QString QWasmLocalStorageSettingsPrivate::fileName() const
{
    return QString();
}

//
// Native settings implementation for WebAssembly using the indexed database as
// the storage backend
//
class QWasmIDBSettingsPrivate : public QConfFileSettingsPrivate
{
public:
    QWasmIDBSettingsPrivate(QSettings::Scope scope, const QString &organization,
                            const QString &application);
    ~QWasmIDBSettingsPrivate();

    std::optional<QVariant> get(const QString &key) const override;
    QStringList children(const QString &prefix, ChildSpec spec) const override;
    void clear() override;
    void sync() override;
    void flush() override;
    bool isWritable() const override;
    void initAccess() override;

    void loadLocal();
    void setReady();

private:
    bool writeSettingsToTemporaryFile(void *dataPtr, int size);

    QString databaseName;
    QString id;
};

static bool isReadReady = false;

constexpr char DbName[] = "/home/web_user";

QWasmIDBSettingsPrivate::QWasmIDBSettingsPrivate(QSettings::Scope scope,
                                                 const QString &organization,
                                                 const QString &application)
    : QConfFileSettingsPrivate(QSettings::WebIndexedDBFormat, scope, organization, application)
{
    Q_ASSERT_X(qstdweb::haveJspi(), Q_FUNC_INFO, "QWasmIDBSettingsPrivate needs JSPI to work");

    if (organization.isEmpty()) {
        setStatus(QSettings::AccessError);
        return;
    }

    setStatus(QSettings::AccessError); // access error until sandbox gets loaded
    databaseName = organization;
    id = application;

    int exists = 0;
    int error = 0;
    emscripten_idb_exists(DbName, fileName().toLocal8Bit(), &exists, &error);
    if (error) {
        setStatus(QSettings::AccessError);
        return;
    }
    if (exists) {
        void *contents;
        int size;
        emscripten_idb_load(DbName, fileName().toLocal8Bit(), &contents, &size, &error);
        if (error || !writeSettingsToTemporaryFile(contents, size)) {
            setStatus(QSettings::AccessError);
            return;
        }
    }

    setReady();
}

QWasmIDBSettingsPrivate::~QWasmIDBSettingsPrivate() = default;

void QWasmIDBSettingsPrivate::initAccess()
{
     if (isReadReady)
         QConfFileSettingsPrivate::initAccess();
}

std::optional<QVariant> QWasmIDBSettingsPrivate::get(const QString &key) const
{
    if (isReadReady)
        return QConfFileSettingsPrivate::get(key);

    return std::nullopt;
}

bool QWasmIDBSettingsPrivate::writeSettingsToTemporaryFile(void *dataPtr, int size)
{
    QFile file(fileName());
    QFileInfo fileInfo(fileName());
    QDir dir(fileInfo.path());
    if (!dir.exists())
        dir.mkpath(fileInfo.path());

    if (!file.open(QFile::WriteOnly))
        return false;

    return size == file.write(reinterpret_cast<char *>(dataPtr), size);
}

QStringList QWasmIDBSettingsPrivate::children(const QString &prefix, ChildSpec spec) const
{
    return QConfFileSettingsPrivate::children(prefix, spec);
}

void QWasmIDBSettingsPrivate::clear()
{
    QConfFileSettingsPrivate::clear();

    int error = 0;
    emscripten_idb_delete(DbName, fileName().toLocal8Bit(), &error);
    setStatus(!!error ? QSettings::AccessError : QSettings::NoError);
}

void QWasmIDBSettingsPrivate::sync()
{
    QConfFileSettingsPrivate::sync();

    QFile file(fileName());
    if (file.open(QFile::ReadOnly)) {
        QByteArray dataPointer = file.readAll();

        int error = 0;
        emscripten_idb_store(DbName, fileName().toLocal8Bit(),
                             reinterpret_cast<void *>(dataPointer.data()), dataPointer.length(),
                             &error);
        setStatus(!!error ? QSettings::AccessError : QSettings::NoError);
    }
}

void QWasmIDBSettingsPrivate::flush()
{
    sync();
}

bool QWasmIDBSettingsPrivate::isWritable() const
{
    return isReadReady && QConfFileSettingsPrivate::isWritable();
}

void QWasmIDBSettingsPrivate::setReady()
{
    isReadReady = true;
    setStatus(QSettings::NoError);
    QConfFileSettingsPrivate::initAccess();
}

QSettingsPrivate *QSettingsPrivate::create(QSettings::Format format, QSettings::Scope scope,
                                           const QString &organization, const QString &application)
{
    // Make WebLocalStorageFormat the default native format
    if (format == QSettings::NativeFormat)
        format = QSettings::WebLocalStorageFormat;

    // Check if cookies are enabled (required for using persistent storage)

    const bool cookiesEnabled = qstdweb::runTaskOnMainThread<bool>(
            []() { return val::global("navigator")["cookieEnabled"].as<bool>(); });

    constexpr QLatin1StringView cookiesWarningMessage(
            "QSettings::%1 requires cookies, falling back to IniFormat with temporary file");
    if (!cookiesEnabled) {
        if (format == QSettings::WebLocalStorageFormat) {
            qWarning() << cookiesWarningMessage.arg("WebLocalStorageFormat");
            format = QSettings::IniFormat;
        } else if (format == QSettings::WebIndexedDBFormat) {
            qWarning() << cookiesWarningMessage.arg("WebIndexedDBFormat");
            format = QSettings::IniFormat;
        }
    }
    if (format == QSettings::WebIndexedDBFormat && !qstdweb::haveJspi()) {
        qWarning() << "QSettings::WebIndexedDBFormat requires JSPI, falling back to IniFormat with "
                      "temporary file";
        format = QSettings::IniFormat;
    }

    // Create settings backend according to selected format
    switch (format) {
    case QSettings::Format::WebLocalStorageFormat:
        return new QWasmLocalStorageSettingsPrivate(scope, organization, application);
    case QSettings::Format::WebIndexedDBFormat:
        return new QWasmIDBSettingsPrivate(scope, organization, application);
    case QSettings::Format::IniFormat:
    case QSettings::Format::CustomFormat1:
    case QSettings::Format::CustomFormat2:
    case QSettings::Format::CustomFormat3:
    case QSettings::Format::CustomFormat4:
    case QSettings::Format::CustomFormat5:
    case QSettings::Format::CustomFormat6:
    case QSettings::Format::CustomFormat7:
    case QSettings::Format::CustomFormat8:
    case QSettings::Format::CustomFormat9:
    case QSettings::Format::CustomFormat10:
    case QSettings::Format::CustomFormat11:
    case QSettings::Format::CustomFormat12:
    case QSettings::Format::CustomFormat13:
    case QSettings::Format::CustomFormat14:
    case QSettings::Format::CustomFormat15:
    case QSettings::Format::CustomFormat16:
        return new QConfFileSettingsPrivate(format, scope, organization, application);
    case QSettings::Format::InvalidFormat:
        return nullptr;
    case QSettings::Format::NativeFormat:
        Q_UNREACHABLE();
        break;
    }
}

QT_END_NAMESPACE
#endif // QT_NO_SETTINGS
