/* GCompris - ActivityInfoTree.cpp
 *
 * SPDX-FileCopyrightText: 2014 Bruno Coudoin <bruno.coudoin@gcompris.net>
 *
 * Authors:
 *   Bruno Coudoin <bruno.coudoin@gcompris.net>
 *
 *   SPDX-License-Identifier: GPL-3.0-or-later
 */
#include "ActivityInfoTree.h"
#include "ApplicationInfo.h"
#include "File.h"

#include <QDir>
#include <QFile>
#include <QtDebug>
#include <QQmlProperty>
#include <QQmlComponent>
#include <QResource>
#include <QCoreApplication>
#include <QTextStream>
#include <QLatin1StringView>

ActivityInfoTree *ActivityInfoTree::m_instance = nullptr;

ActivityInfoTree::ActivityInfoTree(QObject *parent) :
    QObject(parent),
    m_rootMenu(nullptr),
    m_currentActivity(nullptr)
{
}

void ActivityInfoTree::setRootMenu(ActivityInfo *rootMenu)
{
    m_rootMenu = rootMenu;
}

ActivityInfo *ActivityInfoTree::getRootMenu() const
{
    return m_rootMenu;
}

QQmlListProperty<ActivityInfo> ActivityInfoTree::menuTree()
{
    return { this, nullptr, &menuTreeCount, &menuTreeAt };
}

QList<ActivityInfo>::size_type ActivityInfoTree::menuTreeCount(QQmlListProperty<ActivityInfo> *property)
{
    ActivityInfoTree *obj = qobject_cast<ActivityInfoTree *>(property->object);
    if (obj != nullptr)
        return obj->m_menuTree.count();

    return 0;
}

ActivityInfo *ActivityInfoTree::menuTreeAt(QQmlListProperty<ActivityInfo> *property, QList<ActivityInfo>::size_type index)
{
    ActivityInfoTree *obj = qobject_cast<ActivityInfoTree *>(property->object);
    if (obj != nullptr)
        return obj->m_menuTree.at(index);

    return nullptr;
}

QQmlListProperty<ActivityInfo> ActivityInfoTree::getMenuTreeFull()
{
    return { this, nullptr, &menuTreeFullCount, &menuTreeFullAt };
}

QList<ActivityInfo>::size_type ActivityInfoTree::menuTreeFullCount(QQmlListProperty<ActivityInfo> *property)
{
    ActivityInfoTree *obj = qobject_cast<ActivityInfoTree *>(property->object);
    if (obj != nullptr)
        return obj->m_menuTreeFull.count();

    return 0;
}

ActivityInfo *ActivityInfoTree::menuTreeFullAt(QQmlListProperty<ActivityInfo> *property, QList<ActivityInfo>::size_type index)
{
    ActivityInfoTree *obj = qobject_cast<ActivityInfoTree *>(property->object);
    if (obj != nullptr)
        return obj->m_menuTreeFull.at(index);

    return nullptr;
}

ActivityInfo *ActivityInfoTree::menuTree(int index) const
{
    return m_menuTree.at(index);
}

void ActivityInfoTree::setCurrentActivityFromName(const QString &name)
{
    const auto &constMenuTreeFull = m_menuTreeFull;
    for (const auto &activity: constMenuTreeFull) {
        if (activity->name() == name) {
            m_currentActivity = activity;
            Q_EMIT currentActivityChanged();
            break;
        }
    }
}

void ActivityInfoTree::setCurrentActivity(ActivityInfo *currentActivity)
{
    m_currentActivity = currentActivity;
    Q_EMIT currentActivityChanged();
}

ActivityInfo *ActivityInfoTree::getCurrentActivity() const
{
    return m_currentActivity;
}

void ActivityInfoTree::menuTreeAppend(ActivityInfo *menu)
{
    m_menuTreeFull.append(menu);
}

void ActivityInfoTree::sortByDifficultyThenName(bool emitChanged)
{
    std::sort(m_menuTree.begin(), m_menuTree.end(),
              [](const ActivityInfo *a, const ActivityInfo *b) {
                  /* clang-format off */
                  return (a->minimalDifficulty() < b->minimalDifficulty()) ||
                         (a->minimalDifficulty() == b->minimalDifficulty() && (a->name() < b->name()));
                  /* clang-format on */
              });
    if (emitChanged)
        Q_EMIT menuTreeChanged();
}

// Filter the current activity list by the given tag
// the tag 'all' means no filter
// the tag 'favorite' means only marked as favorite
// The level is also filtered based on the global property
void ActivityInfoTree::filterByTag(const QString &tag, const QString &category, bool emitChanged)
{
    using namespace Qt::Literals::StringLiterals;

    m_menuTree.clear();
    // https://www.kdab.com/goodbye-q_foreach/, for loops on QList may cause detach
    const auto constMenuTreeFull = m_menuTreeFull;
    const bool isAllTag = (tag == "all"_L1);
    const bool isFavoriteTag = (tag == "favorite"_L1);
    for (const auto &activity: constMenuTreeFull) {
        // filter on category if given else on tag
        /* clang-format off */
        if((isAllTag ||
            (isFavoriteTag && activity->favorite()) ||
            (!category.isEmpty() && activity->section().indexOf(category) != -1) ||
            (category.isEmpty() && activity->section().indexOf(tag) != -1)
            ) &&
            (activity->maximalDifficulty() >= ApplicationSettings::getInstance()->filterLevelMin() &&
             activity->minimalDifficulty() <= ApplicationSettings::getInstance()->filterLevelMax())) {
            m_menuTree.push_back(activity);
        }
        /* clang-format on */
    }
    sortByDifficultyThenName(false);
    if (emitChanged)
        Q_EMIT menuTreeChanged();
}

bool ActivityInfoTree::launchedActivityMissGivenDifficulty() const
{
    bool activityMissDifficulty = true;
    const auto constMenuTreeFull = m_menuTreeFull;
    for (const auto *activity: constMenuTreeFull) {
        if (activity->name() == m_startingActivity) {
            if (activity->maximalDifficulty() >= ApplicationSettings::getInstance()->filterLevelMin() && activity->minimalDifficulty() <= ApplicationSettings::getInstance()->filterLevelMax()) {
                activityMissDifficulty = false;
            }
            break;
        }
    }
    return activityMissDifficulty;
}

void ActivityInfoTree::filterByDifficulty(quint32 levelMin, quint32 levelMax)
{
    m_menuTree.removeIf([&](const ActivityInfo *activity) {
        return activity->minimalDifficulty() < levelMin || activity->maximalDifficulty() > levelMax;
    });
}

void ActivityInfoTree::filterEnabledActivities(bool emitChanged)
{
    m_menuTree.removeIf([](const ActivityInfo *activity) { return !activity->enabled(); });
    if (emitChanged)
        Q_EMIT menuTreeChanged();
}

void ActivityInfoTree::filterCreatedWithinVersions(int firstVersion,
                                                   int lastVersion,
                                                   bool emitChanged)
{
    m_menuTree.clear();
    const auto constMenuTreeFull = m_menuTreeFull;
    for (const auto &activity: constMenuTreeFull) {
        if (firstVersion < activity->createdInVersion() && activity->createdInVersion() <= lastVersion) {
            m_menuTree.push_back(activity);
        }
    }
    if (emitChanged)
        Q_EMIT menuTreeChanged();
}

void ActivityInfoTree::resetLevels(const QString &activityName)
{
    auto activityIterator = std::find_if(m_menuTreeFull.begin(), m_menuTreeFull.end(), [&activityName](const ActivityInfo *value) {
        return activityName == value->name();
    });
    if (activityIterator == m_menuTreeFull.end()) {
        // We didn't find the activity
        return;
    }
    ActivityInfo *activity = *activityIterator;
    activity->resetLevels();
}

void ActivityInfoTree::exportAsSQL()
{
    QTextStream qtOut(stdout);

    ApplicationSettings::getInstance()->setFilterLevelMin(1);
    ApplicationSettings::getInstance()->setFilterLevelMax(6);
    filterByTag("all");

    qtOut << "CREATE TABLE activities ("
          << "id INT UNIQUE, "
          << "name TEXT,"
          << "section TEXT,"
          << "author TEXT,"
          << "difficulty INT,"
          << "icon TEXT,"
          << "title TEXT,"
          << "description TEXT,"
          << "prerequisite TEXT,"
          << "goal TEXT,"
          << "manual TEXT,"
          << "credit TEXT);\n";
    qtOut << "DELETE FROM activities\n";

    int i(0);
    const auto constMenuTree = m_menuTree;
    for (const auto *activity: constMenuTree) {
        qtOut << "INSERT INTO activities VALUES(" << i++ << ", "
              << "'" << activity->name() << "', "
              << "'" << activity->section() << "', "
              << "'" << activity->author() << "', " << activity->difficulty() << ", "
              << "'" << activity->icon() << "', "
              << "\"" << activity->title() << "\", "
              << "\"" << activity->description() << "\", "
              << "\"" << activity->prerequisite() << "\", "
              << "\"" << activity->goal().toHtmlEscaped() << "\", "
              << "\"" << activity->manual().toHtmlEscaped() << "\", "
              << "\"" << activity->credit() << ");\n";
    }
    qtOut.flush();
}

void ActivityInfoTree::listActivities()
{
    QTextStream qtOut(stdout);
    const QStringList list = getActivityList();
    for (const QString &activity: list) {
        qtOut << activity << '\n';
    }
    qtOut.flush();
}

QStringList ActivityInfoTree::getActivityList()
{
    QStringList list;
    QFile file(":/gcompris/src/activities/activities_out.txt");
    if (!file.open(QFile::ReadOnly)) {
        qDebug() << "Failed to load the activity list";
        return list;
    }
    QTextStream in(&file);
    while (!in.atEnd()) {
        QString line = in.readLine();
        if (!line.startsWith(QLatin1String("#"))) {
            list << line;
        }
    }
    file.close();
    return list;
}

void ActivityInfoTree::initialize(QQmlEngine *engine)
{
    QQmlComponent componentRoot(engine,
                                QUrl("qrc:/gcompris/src/activities/menu/ActivityInfo.qml"));
    QObject *objectRoot = componentRoot.create();
    setRootMenu(qobject_cast<ActivityInfo *>(objectRoot));

    const QStringList activities = getActivityList();
    QString startingActivity = m_startingActivity;
    for (const QString &line: activities) {
        QString url = QString("qrc:/gcompris/src/activities/%1/ActivityInfo.qml").arg(line);

#ifdef WITH_RCC
        if (!QResource::registerResource(
                ApplicationInfo::getFilePath(line + ".rcc")))
            qDebug() << "Failed to load the resource file " << line + ".rcc";
#endif

        QQmlComponent activityComponentRoot(engine, QUrl(url));
        QObject *activityObjectRoot = activityComponentRoot.create();
        if (activityObjectRoot != nullptr) {
            ActivityInfo *activityInfo = qobject_cast<ActivityInfo *>(activityObjectRoot);
            activityInfo->fillDatasets(engine);
            menuTreeAppend(activityInfo);

            // Check if the activity is the one we want to start in and set the full name
            if (!startingActivity.isEmpty() && startingActivity == line) {
                startingActivity = activityInfo->name();
            }
        }
        else {
            qDebug() << "ERROR: failed to load " << line << " " << activityComponentRoot.errors();
        }

#if !__ANDROID__
        // As we need to load the qml files within the main thread, we need to process the events so
        // the qml loading screen is active and do not only display a white screen
        QCoreApplication::processEvents();
#endif
    }

    if (ApplicationSettings::getInstance()->getUpdateToNewIgnoreLevels()) {
        ApplicationSettings::getInstance()->removeLevelsSection();
    }

    // In case we have asked for a specific activity to start but the activity does not exist, we reinitialise the value
    if (m_startingActivity == startingActivity) {
        m_startingActivity = "";
    }
    else {
        m_startingActivity = startingActivity;
    }

    filterByTag("favorite", "", false);
    filterEnabledActivities(true);
}

ActivityInfoTree *ActivityInfoTree::create(QQmlEngine *engine, QJSEngine *scriptEngine)
{
    Q_UNUSED(engine)
    Q_UNUSED(scriptEngine)

    ActivityInfoTree *tree = getInstance();
    return tree;
}

void ActivityInfoTree::registerResources()
{
#ifdef WITH_RCC
    if (!QResource::registerResource(ApplicationInfo::getFilePath("core.rcc")))
        qDebug() << "Failed to load the resource file " << ApplicationInfo::getFilePath("core.rcc");

    if (!QResource::registerResource(ApplicationInfo::getFilePath("menu.rcc")))
        qDebug() << "Failed to load the resource file menu.rcc";
#endif
    if (!QResource::registerResource(ApplicationInfo::getFilePath("activities.rcc")))
        qDebug() << "Failed to load the resource file activities.rcc";
}

void ActivityInfoTree::filterBySearch(const QString &text)
{
    m_menuTree.clear();
    if (!text.trimmed().isEmpty()) {
        // perform search on each word entered in the searchField
        const QStringList wordsList = text.split(' ', Qt::SkipEmptyParts);
        for (const QString &searchTerm: wordsList) {
            const QString trimmedText = searchTerm.trimmed();
            const auto &constMenuTreeFull = m_menuTreeFull;
            for (const auto &activity: constMenuTreeFull) {
                /* clang-format off */
                if (activity->title().remove(QChar::SoftHyphen).contains(trimmedText, Qt::CaseInsensitive) ||
                    activity->name().remove(QChar::SoftHyphen).contains(trimmedText, Qt::CaseInsensitive) ||
                    activity->description().remove(QChar::SoftHyphen).contains(trimmedText, Qt::CaseInsensitive)) {
                    /* clang-format on */
                    // add the activity only if it's not added
                    if (m_menuTree.indexOf(activity) == -1)
                        m_menuTree.push_back(activity);
                }
            }
        }
    }
    else {
        m_menuTree = m_menuTreeFull;
    }

    filterEnabledActivities(false);
    filterByDifficulty(ApplicationSettings::getInstance()->filterLevelMin(), ApplicationSettings::getInstance()->filterLevelMax());
    sortByDifficultyThenName(false);
    Q_EMIT menuTreeChanged();
}

void ActivityInfoTree::minMaxFiltersChanged(quint32 levelMin, quint32 levelMax, bool doSynchronize)
{
    for (ActivityInfo *activity: std::as_const(m_menuTreeFull)) {
        activity->enableDatasetsBetweenDifficulties(levelMin, levelMax);
    }
    if (doSynchronize) {
        ApplicationSettings::getInstance()->sync();
    }
}

QVariantList ActivityInfoTree::allCharacters()
{
    QSet<QChar> keyboardChars;
    const auto constMenuTreeFull = m_menuTreeFull;
    for (const auto *tree: constMenuTreeFull) {
        const QString &title = tree->title();
        for (const QChar &letter: title) {
            if (letter.isLetterOrNumber() || letter == QLatin1Char('\'') || letter == QLatin1Char('-')) {
                keyboardChars.insert(letter.toLower());
            }
        }
    }
    QVariantList keyboardCharacters;
    for (const QChar &letters: keyboardChars) {
        keyboardCharacters.push_back(letters);
    }
    std::sort(keyboardCharacters.begin(), keyboardCharacters.end(), [](const QVariant &v1, const QVariant &v2) {
        return ApplicationInfo::getInstance()->localeCompare(v1.toString(), v2.toString()) < 0;
    });

    return keyboardCharacters;
}

void ActivityInfoTree::createDataset(const QJsonObject &dataset)
{
    // Prefix dataset filename with "c-" to ensure it's not a restricted name on some platform
    const QString datasetName("c-" + dataset["dataset_name"].toString());
    const QString activityName(dataset["activity_name"].toString());
    const QString datasetFolder(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/" + activityName + '/' + datasetName);

    removeDataset(dataset);

    QDir datasetDir(datasetFolder);
    if (!datasetDir.exists()) {
        datasetDir.mkpath(datasetFolder);
    }
    const QString filename = datasetFolder + "/Data.qml";
    QString qmlFile = QString("import core 1.0\nData {\n    "
                              "objective: \"%1\"\n    difficulty: %2\n    "
                              "data: %3\n}\n")
                          .arg(dataset["dataset_objective"].toString())
                          .arg(dataset["dataset_difficulty"].toInt())
                          .arg(dataset["dataset_content"].toString());

    File datasetFile;
    qDebug() << "Write" << qmlFile << "in" << filename;
    datasetFile.write(qmlFile, filename);

    const auto &constMenuTreeFull = m_menuTreeFull;
    QQmlEngine *engine = qmlEngine(this);

    for (const auto &activity: constMenuTreeFull) {
        if (activity->name().split("/").at(0) == activityName) {
            activity->fillDatasets(engine);
            break;
        }
    }
}

void ActivityInfoTree::removeDataset(const QJsonObject &dataset, bool clearCache)
{
    qDebug() << "ActivityInfoTree::removeDataset" << dataset;
    const QString datasetName("c-" + dataset["dataset_name"].toString());
    const QString activityName(dataset["activity_name"].toString());
    const QString datasetFolder(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/" + activityName + '/' + datasetName);
    QDir datasetDir(datasetFolder);
    if (datasetDir.exists()) {
        datasetDir.removeRecursively();
    }

    const auto &constMenuTreeFull = m_menuTreeFull;
    QQmlEngine *engine = qmlEngine(this);

    for (const auto &activity: constMenuTreeFull) {
        if (activity->name().split("/").at(0) == activityName) {
            activity->removeDataset(datasetName);
            activity->fillDatasets(engine);

            if (QStringList ignoredLevels = ApplicationSettings::getInstance()->ignoredLevels(activityName); ignoredLevels.contains(datasetName)) {
                ignoredLevels.removeOne(datasetName);
                ApplicationSettings::getInstance()->setIgnoredLevels(activityName, ignoredLevels);
            }
            break;
        }
    }
    if (clearCache) {
        engine->clearComponentCache();
    }
}

void ActivityInfoTree::removeAllLocalDatasets()
{
    const QString datasetRootFolder(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
    QDir datasetsDir(datasetRootFolder);
    for (const QFileInfo &activityFolder: datasetsDir.entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot)) {
        QJsonObject dataset;
        dataset["activity_name"] = activityFolder.fileName();
        QDir activityDir(activityFolder.absoluteFilePath());
        for (const QFileInfo &datasetFolder: activityDir.entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot)) {
            dataset["dataset_name"] = datasetFolder.fileName();
            removeDataset(dataset, false);
        }
    }
    QQmlEngine *engine = qmlEngine(this);
    engine->clearComponentCache();
}

#include "moc_ActivityInfoTree.cpp"
