/*
    SPDX-FileCopyrightText: 2012 Aurélien Gâteau <agateau@kde.org>
    SPDX-FileCopyrightText: 2014 Eike Hein <hein@kde.org>

    SPDX-License-Identifier: GPL-2.0-or-later
*/

#include "runnermodel.h"
#include "runnermatchesmodel.h"

#include <QSet>

#include <KLocalizedString>
#include <KRunner/AbstractRunner>
#include <KRunner/RunnerManager>
#include <chrono>

using namespace std::chrono_literals;

RunnerModel::RunnerModel(QObject *parent)
    : QAbstractListModel(parent)
    , m_favoritesModel(nullptr)
    , m_appletInterface(nullptr)
    , m_runnerManager(nullptr)
    , m_mergeResults(false)
    , m_deleteWhenEmpty(false)
{
    m_queryTimer.setSingleShot(true);
    m_queryTimer.setInterval(10ms);
    connect(&m_queryTimer, &QTimer::timeout, this, &RunnerModel::startQuery);
}

RunnerModel::~RunnerModel()
{
}

QHash<int, QByteArray> RunnerModel::roleNames() const
{
    return {{Qt::DisplayRole, "display"}};
}

AbstractModel *RunnerModel::favoritesModel() const
{
    return m_favoritesModel;
}

void RunnerModel::setFavoritesModel(AbstractModel *model)
{
    if (m_favoritesModel != model) {
        m_favoritesModel = model;

        clear();

        if (!m_query.isEmpty()) {
            m_queryTimer.start();
        }

        Q_EMIT favoritesModelChanged();
    }
}

QObject *RunnerModel::appletInterface() const
{
    return m_appletInterface;
}

void RunnerModel::setAppletInterface(QObject *appletInterface)
{
    if (m_appletInterface != appletInterface) {
        m_appletInterface = appletInterface;

        clear();

        if (!m_query.isEmpty()) {
            m_queryTimer.start();
        }

        Q_EMIT appletInterfaceChanged();
    }
}

bool RunnerModel::deleteWhenEmpty() const
{
    return m_deleteWhenEmpty;
}

void RunnerModel::setDeleteWhenEmpty(bool deleteWhenEmpty)
{
    if (m_deleteWhenEmpty != deleteWhenEmpty) {
        m_deleteWhenEmpty = deleteWhenEmpty;

        clear();

        if (!m_query.isEmpty()) {
            m_queryTimer.start();
        }

        Q_EMIT deleteWhenEmptyChanged();
    }
}

bool RunnerModel::mergeResults() const
{
    return m_mergeResults;
}

void RunnerModel::setMergeResults(bool merge)
{
    if (m_mergeResults != merge) {
        m_mergeResults = merge;

        clear();

        if (!m_query.isEmpty()) {
            m_queryTimer.start();
        }

        Q_EMIT mergeResultsChanged();
    }
}

QVariant RunnerModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid() || index.row() >= m_models.count()) {
        return QVariant();
    }

    if (role == Qt::DisplayRole) {
        return m_models.at(index.row())->name();
    }

    return QVariant();
}

int RunnerModel::rowCount(const QModelIndex &parent) const
{
    return parent.isValid() ? 0 : m_models.count();
}

int RunnerModel::count() const
{
    return rowCount();
}

QObject *RunnerModel::modelForRow(int row)
{
    if (row < 0 || row >= m_models.count()) {
        return nullptr;
    }

    return m_models.at(row);
}

QStringList RunnerModel::runners() const
{
    return m_runners;
}

void RunnerModel::setRunners(const QStringList &runners)
{
    if (QSet<QString>(runners.cbegin(), runners.cend()) != QSet<QString>(m_runners.cbegin(), m_runners.cend())) {
        m_runners = runners;

        if (m_runnerManager) {
            m_runnerManager->setAllowedRunners(runners);
        }

        Q_EMIT runnersChanged();
    }
}

QString RunnerModel::query() const
{
    return m_query;
}

void RunnerModel::setQuery(const QString &query)
{
    if (m_query != query) {
        m_query = query;

        m_queryTimer.start();

        Q_EMIT queryChanged();
    }
}

void RunnerModel::startQuery()
{
    if (m_query.isEmpty()) {
        clear();
    }

    if (m_query.isEmpty() && m_runnerManager) {
        return;
    }

    createManager();

    m_runnerManager->launchQuery(m_query);
}

void RunnerModel::matchesChanged(const QList<Plasma::QueryMatch> &matches)
{
    // Group matches by runner.
    // We do not use a QMultiHash here because it keeps values in LIFO order, while we want FIFO.
    QHash<QString, QList<Plasma::QueryMatch>> matchesForRunner;

    for (const Plasma::QueryMatch &match : matches) {
        auto it = matchesForRunner.find(match.runner()->id());

        if (it == matchesForRunner.end()) {
            it = matchesForRunner.insert(match.runner()->id(), QList<Plasma::QueryMatch>());
        }

        it.value().append(match);
    }

    // Sort matches for all runners in descending order, note the reverse iterators. This allows the best
    // match to win whilest preserving order between runners.
    for (auto &list : matchesForRunner) {
        std::sort(list.rbegin(), list.rend());
    }

    bool countHasChanged = false;

    if (m_mergeResults) {
        RunnerMatchesModel *matchesModel = nullptr;

        if (m_models.isEmpty()) {
            matchesModel = new RunnerMatchesModel(QString(), i18n("Search results"), m_runnerManager, this);
            connect(matchesModel, &RunnerMatchesModel::requestUpdateQueryString, this, &RunnerModel::requestUpdateQuery);

            beginInsertRows(QModelIndex(), 0, 0);
            m_models.append(matchesModel);
            endInsertRows();
            countHasChanged = true;
        } else {
            matchesModel = m_models.at(0);
        }

        QList<Plasma::QueryMatch> matches;
        // To preserve the old behavior when allowing all runners we use static sorting
        const static QStringList runnerIds = {
            QStringLiteral("desktopsessions"),
            QStringLiteral("services"),
            QStringLiteral("krunner_systemsettings"),
            QStringLiteral("places"),
            QStringLiteral("PowerDevil"),
            QStringLiteral("calculator"),
            QStringLiteral("unitconverter"),
            QStringLiteral("shell"),
            QStringLiteral("bookmarks"),
            QStringLiteral("recentdocuments"),
            QStringLiteral("locations"),
        };
        if (m_runners.isEmpty()) {
            const auto baloo = matchesForRunner.take(QStringLiteral("baloosearch"));
            const auto appstream = matchesForRunner.take(QStringLiteral("krunner_appstream"));
            for (const QString &runnerId : runnerIds) {
                matches.append(matchesForRunner.take(runnerId));
            }
            for (const auto &match : matchesForRunner) {
                matches.append(match);
            }
            matches.append(baloo);
            matches.append(appstream);
        } else {
            for (const QString &runnerId : qAsConst(m_runners)) {
                matches.append(matchesForRunner.take(runnerId));
            }
        }

        matchesModel->setMatches(matches);

        if (countHasChanged) {
            Q_EMIT countChanged();
        }

        return;
    }

    // Assign matches to existing models. If there is no match for a model, delete it.
    for (int row = m_models.count() - 1; row >= 0; --row) {
        RunnerMatchesModel *matchesModel = m_models.at(row);
        QList<Plasma::QueryMatch> matches = matchesForRunner.take(matchesModel->runnerId());

        if (m_deleteWhenEmpty && matches.isEmpty()) {
            beginRemoveRows(QModelIndex(), row, row);
            m_models.removeAt(row);
            delete matchesModel;
            endRemoveRows();
            countHasChanged = true;
        } else {
            matchesModel->setMatches(matches);
        }
    }

    // At this point, matchesForRunner contains only matches for runners which
    // do not have a model yet. Create new models for them.
    if (!matchesForRunner.isEmpty()) {
        auto it = matchesForRunner.constBegin();
        auto end = matchesForRunner.constEnd();

        QList<RunnerMatchesModel *> toPrepend;
        QList<RunnerMatchesModel *> toAppend;

        for (; it != end; ++it) {
            QList<Plasma::QueryMatch> matches = it.value();
            Q_ASSERT(!matches.isEmpty());
            RunnerMatchesModel *matchesModel = new RunnerMatchesModel(it.key(), matches.first().runner()->name(), m_runnerManager, this);
            matchesModel->setMatches(matches);

            if (it.key() == QLatin1String("services")) {
                toPrepend.append(matchesModel);
            } else {
                toAppend.append(matchesModel);
            }
        }

        if (!toPrepend.isEmpty()) {
            beginInsertRows(QModelIndex(), 0, toPrepend.count() - 1);
            for (auto match : std::as_const(toPrepend)) {
                m_models.prepend(match);
            }
            endInsertRows();
            countHasChanged = true;
        }

        if (!toAppend.isEmpty()) {
            beginInsertRows(QModelIndex(), m_models.count(), m_models.count() + toAppend.count() - 1);
            m_models.append(toAppend);
            endInsertRows();
            countHasChanged = true;
        }
    }

    if (countHasChanged) {
        Q_EMIT countChanged();
    }
}

void RunnerModel::createManager()
{
    if (!m_runnerManager) {
        m_runnerManager = new Plasma::RunnerManager(QStringLiteral("krunnerrc"), this);
        if (m_runners.isEmpty()) {
            m_runnerManager->enableKNotifyPluginWatcher();
        } else {
            m_runnerManager->setAllowedRunners(m_runners);
        }
        connect(m_runnerManager, &Plasma::RunnerManager::matchesChanged, this, &RunnerModel::matchesChanged);
        connect(m_runnerManager, &Plasma::RunnerManager::queryFinished, this, &RunnerModel::queryFinished);
    }
}

void RunnerModel::clear()
{
    if (m_runnerManager) {
        m_runnerManager->reset();
        m_runnerManager->matchSessionComplete();
    }

    if (m_models.isEmpty()) {
        return;
    }

    beginResetModel();

    qDeleteAll(m_models);
    m_models.clear();

    endResetModel();

    Q_EMIT countChanged();
}
