/*
    SPDX-FileCopyrightText: 2000 Yves Arrouye <yves@realnames.com>
    SPDX-FileCopyrightText: 2001, 2002 Dawit Alemayehu <adawit@kde.org>
    SPDX-FileCopyrightText: 2009 Nick Shaforostoff <shaforostoff@kde.ru>

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

#include "ikwsopts.h"
#include "ikwsopts_p.h"

#include "searchprovider.h"
#include "searchproviderdlg.h"

#include <KConfigGroup>
#include <KLocalizedString>
#include <KPluginFactory>
#include <KSharedConfig>

#ifdef WITH_DBUS
#include <QDBusConnection>
#include <QDBusMessage>
#endif

#include <QFile>
#include <QPointer>
#include <QSortFilterProxyModel>
#include <QToolTip>

// BEGIN ProvidersModel

ProvidersModel::~ProvidersModel()
{
}

QVariant ProvidersModel::headerData(int section, Qt::Orientation orientation, int role) const
{
    Q_UNUSED(orientation);
    if (role == Qt::DisplayRole) {
        switch (section) {
        case Name:
            return i18nc("@title:column Name label from web search keyword column", "Name");
        case Shortcuts:
            return i18nc("@title:column", "Keywords");
        case Preferred:
            return i18nc("@title:column", "Preferred");
        default:
            break;
        }
    }
    return QVariant();
}

Qt::ItemFlags ProvidersModel::flags(const QModelIndex &index) const
{
    if (!index.isValid()) {
        return Qt::ItemIsEnabled;
    }
    if (index.column() == Preferred) {
        return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable;
    }
    return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
}

bool ProvidersModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    if (role == Qt::CheckStateRole) {
        if (value.toInt() == Qt::Checked) {
            m_favoriteEngines.insert(m_providers.at(index.row())->desktopEntryName());
        } else {
            m_favoriteEngines.remove(m_providers.at(index.row())->desktopEntryName());
        }
        Q_EMIT dataModified();
        return true;
    }
    return false;
}

QVariant ProvidersModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid()) {
        return QVariant();
    }

    if (role == Qt::CheckStateRole && index.column() == Preferred) {
        return m_favoriteEngines.contains(m_providers.at(index.row())->desktopEntryName()) ? Qt::Checked : Qt::Unchecked;
    }

    if (role == Qt::DecorationRole && index.column() == Name) {
        return QIcon::fromTheme(m_providers.at(index.row())->iconName());
    }

    if (role == Qt::DisplayRole) {
        if (index.column() == Name) {
            return m_providers.at(index.row())->name();
        }
        if (index.column() == Shortcuts) {
            return m_providers.at(index.row())->keys().join(QLatin1Char(','));
        }
    }

    if (role == Qt::ToolTipRole || role == Qt::WhatsThisRole) {
        if (index.column() == Preferred) {
            return xi18nc("@info:tooltip", "Check to select this web search keyword as preferred.");
        }
        else if (index.column() == Name) {
            // Show the top level domain used for the query.  Use string
            // functions instead of trying to construct a URL, because the
            // query substitutions may not be valid in a URL path.
            QString q(m_providers.at(index.row())->query());
            int idx1 = q.indexOf(QLatin1String("://"));
            if (idx1 != -1) {
                idx1 += 3;
                int idx2 = q.indexOf(QLatin1Char('/'), idx1);
                if (idx2 != -1) {
                    return (q.mid(idx1, idx2 - idx1));
                }
            }
        }
    }

    if (role == Qt::UserRole + 1) { // for sorting
        if (index.column() == Preferred) {
            return (data(index, Qt::CheckStateRole));
        } else {
            return (data(index, Qt::DisplayRole));
        }
    }

    if (role == Qt::UserRole) {
        return index.row(); // a nice way to bypass proxymodel
    }

    return QVariant();
}

void ProvidersModel::setProviders(const QList<SearchProvider *> &providers, const QStringList &favoriteEngines)
{
    m_providers = providers;
    setFavoriteProviders(favoriteEngines);
}

void ProvidersModel::setFavoriteProviders(const QStringList &favoriteEngines)
{
    beginResetModel();

    m_favoriteEngines = QSet<QString>(favoriteEngines.begin(), favoriteEngines.end());

    endResetModel();
}

int ProvidersModel::rowCount(const QModelIndex &parent) const
{
    if (parent.isValid()) {
        return 0;
    }
    return m_providers.size();
}

QAbstractListModel *ProvidersModel::createListModel()
{
    ProvidersListModel *pListModel = new ProvidersListModel(m_providers, this);
    connect(this, &QAbstractItemModel::modelAboutToBeReset, pListModel, &QAbstractItemModel::modelAboutToBeReset);
    connect(this, &QAbstractItemModel::modelReset, pListModel, &QAbstractItemModel::modelReset);
    connect(this, &QAbstractItemModel::dataChanged, pListModel, &ProvidersListModel::emitDataChanged);
    connect(this, &QAbstractItemModel::rowsAboutToBeInserted, pListModel, &ProvidersListModel::emitRowsAboutToBeInserted);
    connect(this, &QAbstractItemModel::rowsAboutToBeRemoved, pListModel, &ProvidersListModel::emitRowsAboutToBeRemoved);
    connect(this, &QAbstractItemModel::rowsInserted, pListModel, &ProvidersListModel::emitRowsInserted);
    connect(this, &QAbstractItemModel::rowsRemoved, pListModel, &ProvidersListModel::emitRowsRemoved);

    return pListModel;
}

void ProvidersModel::deleteProvider(SearchProvider *p)
{
    const int row = m_providers.indexOf(p);
    beginRemoveRows(QModelIndex(), row, row);
    m_favoriteEngines.remove(m_providers.takeAt(row)->desktopEntryName());
    endRemoveRows();
    Q_EMIT dataModified();
}

void ProvidersModel::addProvider(SearchProvider *p)
{
    beginInsertRows(QModelIndex(), m_providers.size(), m_providers.size());
    m_providers.append(p);
    endInsertRows();
    Q_EMIT dataModified();
}

void ProvidersModel::changeProvider(SearchProvider *p)
{
    const int row = m_providers.indexOf(p);
    Q_EMIT dataChanged(index(row, 0), index(row, ColumnCount - 1));
    Q_EMIT dataModified();
}

QStringList ProvidersModel::favoriteEngines() const
{
    return QStringList(m_favoriteEngines.cbegin(), m_favoriteEngines.cend());
}

// END ProvidersModel

// BEGIN ProvidersListModel
ProvidersListModel::ProvidersListModel(QList<SearchProvider *> &providers, QObject *parent)
    : QAbstractListModel(parent)
    , m_providers(providers)
{
}

QVariant ProvidersListModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid()) {
        return QVariant();
    }

    const int row = index.row();
    const bool noProvider = (row == m_providers.size()); // `None` is the last item

    switch (role) {
    case Qt::DisplayRole:
        if (noProvider) {
            return i18nc("@item:inlistbox No default web search keyword", "None");
        }
        return m_providers.at(index.row())->name();
    case ShortNameRole:
        if (noProvider) {
            return QString();
        }
        return m_providers.at(index.row())->desktopEntryName();
    case Qt::DecorationRole:
        if (noProvider) {
            return QIcon::fromTheme(QStringLiteral("empty"));
        }
        return QIcon::fromTheme(m_providers.at(index.row())->iconName());
    }

    return QVariant();
}

int ProvidersListModel::rowCount(const QModelIndex &parent) const
{
    if (parent.isValid()) {
        return 0;
    }
    return m_providers.size() + 1;
}

// END ProvidersListModel

static QSortFilterProxyModel *wrapInProxyModel(QAbstractItemModel *model)
{
    QSortFilterProxyModel *proxyModel = new QSortFilterProxyModel(model);
    proxyModel->setSourceModel(model);
    proxyModel->setDynamicSortFilter(true);
    proxyModel->setSortCaseSensitivity(Qt::CaseInsensitive);
    proxyModel->setSortRole(Qt::UserRole + 1);
    proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
    proxyModel->setFilterKeyColumn(-1);
    return proxyModel;
}

FilterOptions::FilterOptions(QObject *parent, const KPluginMetaData &data)
    : KCModule(parent, data)
    , m_providersModel(new ProvidersModel(this))
{
    // Used for tab text in the KCM
    widget()->setWindowTitle(i18n("Search F&ilters"));

    m_dlg.setupUi(widget());
    m_dlg.pbNew->setIcon(QIcon::fromTheme(QStringLiteral("list-add")));
    m_dlg.pbChange->setIcon(QIcon::fromTheme(QStringLiteral("document-edit")));
    m_dlg.pbDelete->setIcon(QIcon::fromTheme(QStringLiteral("list-remove")));

    QSortFilterProxyModel *searchProviderModel = wrapInProxyModel(m_providersModel);
    m_dlg.lvSearchProviders->setModel(searchProviderModel);
    m_dlg.cmbDefaultEngine->setModel(wrapInProxyModel(m_providersModel->createListModel()));

    // Connect all the signals/slots...
    connect(m_dlg.cbEnableShortcuts, &QAbstractButton::toggled, this, &FilterOptions::markAsChanged);
    connect(m_dlg.cbEnableShortcuts, &QAbstractButton::toggled, this, &FilterOptions::updateSearchProviderEditingButons);
    connect(m_dlg.cbUseSelectedShortcutsOnly, &QAbstractButton::toggled, this, &FilterOptions::markAsChanged);

    connect(m_providersModel, &ProvidersModel::dataModified, this, &FilterOptions::markAsChanged);
    connect(m_dlg.cmbDefaultEngine, qOverload<int>(&QComboBox::currentIndexChanged), this, &FilterOptions::markAsChanged);
    connect(m_dlg.cmbDelimiter, qOverload<int>(&QComboBox::currentIndexChanged), this, &FilterOptions::markAsChanged);

    connect(m_dlg.pbNew, &QAbstractButton::clicked, this, &FilterOptions::addSearchProvider);
    connect(m_dlg.pbDelete, &QAbstractButton::clicked, this, &FilterOptions::deleteSearchProvider);
    connect(m_dlg.pbChange, &QAbstractButton::clicked, this, &FilterOptions::changeSearchProvider);
    connect(m_dlg.lvSearchProviders->selectionModel(), &QItemSelectionModel::currentChanged, this, &FilterOptions::updateSearchProviderEditingButons);
    connect(m_dlg.lvSearchProviders, &QAbstractItemView::doubleClicked, this, &FilterOptions::changeSearchProvider);
    connect(m_dlg.searchLineEdit, &QLineEdit::textEdited, searchProviderModel, &QSortFilterProxyModel::setFilterFixedString);

    connect(m_dlg.lbQuickHelp, &QLabel::linkActivated, this, [this](const QString &link) {
        QToolTip::showText(QCursor::pos(),
                           xi18nc("@info:whatsthis",
                                  "<para>Web search keywords allow you to quickly search or look up words on the Internet. "
                                  "They work in Plasma's <application>KRunner</application> search, "
                                  "<application>Konqueror</application>'s address bar, and in any other "
                                  "applications that support them. For example, to search for information "
                                  "about the KDE project using the Google search engine, you can simply type "
                                  "<icode>gg:KDE</icode> or <icode>google:KDE</icode>.</para>"));
    });
}

void FilterOptions::setDefaultEngine(int index)
{
    QSortFilterProxyModel *proxy = qobject_cast<QSortFilterProxyModel *>(m_dlg.cmbDefaultEngine->model());
    if (index == -1) {
        index = proxy->rowCount() - 1; //"None" is the last
    }
    const QModelIndex modelIndex = proxy->mapFromSource(proxy->sourceModel()->index(index, 0));
    m_dlg.cmbDefaultEngine->setCurrentIndex(modelIndex.row());
    m_dlg.cmbDefaultEngine->view()->setCurrentIndex(modelIndex); // TODO: remove this when Qt bug is fixed
}

void FilterOptions::load()
{
    KConfig config(QStringLiteral("kuriikwsfilterrc"), KConfig::NoGlobals);
    KConfigGroup group = config.group(QStringLiteral("General"));

    const QString defaultSearchEngine = group.readEntry("DefaultWebShortcut", "duckduckgo");
    const QStringList favoriteEngines = group.readEntry("PreferredWebShortcuts", m_defaultProviders);

    const QList<SearchProvider *> allProviders = m_registry.findAll();
    QList<SearchProvider *> providers;
    for (auto *provider : allProviders) {
        if (!provider->isHidden()) {
            providers << provider;
        }
    }

    int defaultProviderIndex = providers.size(); // default is "None", it is last in the list

    for (SearchProvider *provider : std::as_const(providers)) {
        if (defaultSearchEngine == provider->desktopEntryName()) {
            defaultProviderIndex = providers.indexOf(provider);
            break;
        }
    }

    m_providersModel->setProviders(providers, favoriteEngines);
    m_dlg.lvSearchProviders->setColumnWidth(0, 200);
    m_dlg.lvSearchProviders->resizeColumnToContents(1);
    m_dlg.lvSearchProviders->sortByColumn(0, Qt::AscendingOrder);
    m_dlg.cmbDefaultEngine->model()->sort(0, Qt::AscendingOrder);
    setDefaultEngine(defaultProviderIndex);

    m_dlg.cbEnableShortcuts->setChecked(group.readEntry("EnableWebShortcuts", true));
    m_dlg.cbUseSelectedShortcutsOnly->setChecked(group.readEntry("UsePreferredWebShortcutsOnly", false));

    const QString delimiter = group.readEntry("KeywordDelimiter", ":");
    setDelimiter(delimiter.at(0).toLatin1());
}

char FilterOptions::delimiter()
{
    const char delimiters[] = {':', ' '};
    return delimiters[m_dlg.cmbDelimiter->currentIndex()];
}

void FilterOptions::setDelimiter(char sep)
{
    m_dlg.cmbDelimiter->setCurrentIndex(sep == ' ');
}

void FilterOptions::save()
{
    KConfig config(QStringLiteral("kuriikwsfilterrc"), KConfig::NoGlobals);

    KConfigGroup group = config.group(QStringLiteral("General"));
    group.writeEntry("EnableWebShortcuts", m_dlg.cbEnableShortcuts->isChecked());
    group.writeEntry("KeywordDelimiter", QString(QLatin1Char(delimiter())));
    group.writeEntry("DefaultWebShortcut", m_dlg.cmbDefaultEngine->view()->currentIndex().data(ProvidersListModel::ShortNameRole));
    group.writeEntry("PreferredWebShortcuts", m_providersModel->favoriteEngines());
    group.writeEntry("UsePreferredWebShortcutsOnly", m_dlg.cbUseSelectedShortcutsOnly->isChecked());

    const QList<SearchProvider *> providers = m_providersModel->providers();
    const QString path = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kf6/searchproviders/");

    for (SearchProvider *provider : providers) {
        if (!provider->isDirty()) {
            continue;
        }

        KConfig _service(path + provider->desktopEntryName() + QLatin1String(".desktop"), KConfig::SimpleConfig);
        KConfigGroup service(&_service, QStringLiteral("Desktop Entry"));
        service.writeEntry("Type", "Service");
        service.writeEntry("Name", provider->name());
        service.writeEntry("Query", provider->query());
        service.writeEntry("Keys", provider->keys());
        service.writeEntry("Charset", provider->charset());
        service.writeEntry("Hidden", false); // we might be overwriting a hidden entry
    }

    const QStringList servicesDirs =
        QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("kf6/searchproviders/"), QStandardPaths::LocateDirectory);
    for (const QString &providerName : std::as_const(m_deletedProviders)) {
        QStringList matches;
        for (const QString &dir : servicesDirs) {
            QString current = dir + QLatin1Char('/') + providerName + QLatin1String(".desktop");
            if (QFile::exists(current)) {
                matches += current;
            }
        }

        // Shouldn't happen
        if (matches.isEmpty()) {
            continue;
        }

        if (matches.size() == 1 && matches.first().startsWith(path)) {
            // If only the local copy existed, unlink it
            // TODO: error handling
            QFile::remove(matches.first());
            continue;
        }

        KConfig _service(path + providerName + QLatin1String(".desktop"), KConfig::SimpleConfig);
        KConfigGroup service(&_service, QStringLiteral("Desktop Entry"));
        service.writeEntry("Type", "Service");
        service.writeEntry("Hidden", true);
    }

    config.sync();

    setNeedsSave(false);

#ifdef WITH_DBUS
    // Update filters in running applications...
    QDBusMessage msg = QDBusMessage::createSignal(QStringLiteral("/"), QStringLiteral("org.kde.KUriFilterPlugin"), QStringLiteral("configure"));
    QDBusConnection::sessionBus().send(msg);
#endif
}

void FilterOptions::defaults()
{
    m_dlg.cbEnableShortcuts->setChecked(true);
    m_dlg.cbUseSelectedShortcutsOnly->setChecked(false);
    m_providersModel->setFavoriteProviders(m_defaultProviders);
    setDelimiter(':');

    const QList<SearchProvider *> providers = m_providersModel->providers();
    int defaultProviderIndex = providers.size(); // default is "None", it is last in the list
    for (SearchProvider *provider : std::as_const(providers)) {
        if (QLatin1String("duckduckgo") == provider->desktopEntryName()) {
            defaultProviderIndex = providers.indexOf(provider);
            break;
        }
    }
    setDefaultEngine(defaultProviderIndex);
}

void FilterOptions::addSearchProvider()
{
    QList<SearchProvider *> providers = m_providersModel->providers();
    QPointer<SearchProviderDialog> dlg = new SearchProviderDialog(nullptr, providers, widget());

    if (dlg->exec()) {
        m_providersModel->addProvider(dlg->provider());
        m_providersModel->changeProvider(dlg->provider());
    }
    delete dlg;
}

void FilterOptions::changeSearchProvider()
{
    QList<SearchProvider *> providers = m_providersModel->providers();
    SearchProvider *provider = providers.at(m_dlg.lvSearchProviders->currentIndex().data(Qt::UserRole).toInt());
    QPointer<SearchProviderDialog> dlg = new SearchProviderDialog(provider, providers, widget());

    if (dlg->exec()) {
        m_providersModel->changeProvider(dlg->provider());
    }

    delete dlg;
}

void FilterOptions::deleteSearchProvider()
{
    SearchProvider *provider = m_providersModel->providers().at(m_dlg.lvSearchProviders->currentIndex().data(Qt::UserRole).toInt());
    m_deletedProviders.append(provider->desktopEntryName());
    m_providersModel->deleteProvider(provider);
}

void FilterOptions::updateSearchProviderEditingButons()
{
    const bool enable = (m_dlg.cbEnableShortcuts->isChecked() && m_dlg.lvSearchProviders->currentIndex().isValid());
    m_dlg.pbChange->setEnabled(enable);
    m_dlg.pbDelete->setEnabled(enable);
}

K_PLUGIN_CLASS_WITH_JSON(FilterOptions, "kcm_webshortcuts.json")

#include "ikwsopts.moc"

#include "moc_ikwsopts.cpp"

#include "moc_ikwsopts_p.cpp"
