/* -*- mode: c++; c-basic-offset:4 -*-
    dialogs/certificateselectiondialog.cpp

    This file is part of Kleopatra, the KDE keymanager
    SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB

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

#include <config-kleopatra.h>

#include "certificateselectiondialog.h"

#include <view/searchbar.h>
#include <view/tabwidget.h>

#include <Libkleo/KeyListModel>
#include <Libkleo/KeyCache>

#include <commands/reloadkeyscommand.h>
#include <commands/lookupcertificatescommand.h>
#include <commands/newcertificatecommand.h>
#include <commands/importcertificatefromfilecommand.h>

#include <gpgme++/key.h>

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

#include <QLabel>
#include <QPushButton>
#include <QDialogButtonBox>
#include <QItemSelectionModel>
#include <QAbstractItemView>
#include <QPointer>
#include <QVBoxLayout>

#include <algorithm>

using namespace Kleo;
using namespace Kleo::Dialogs;
using namespace Kleo::Commands;
using namespace GpgME;

class CertificateSelectionDialog::Private
{
    friend class ::Kleo::Dialogs::CertificateSelectionDialog;
    CertificateSelectionDialog *const q;
public:
    explicit Private(CertificateSelectionDialog *qq);

private:
    void reload()
    {
        Command *const cmd = new ReloadKeysCommand(nullptr);
        cmd->setParentWidget(q);
        cmd->start();
    }
    void create()
    {
        NewCertificateCommand *cmd = new NewCertificateCommand(nullptr);
        cmd->setParentWidget(q);
        if ((options & AnyFormat) != AnyFormat) {
            cmd->setProtocol((options & OpenPGPFormat) ? OpenPGP : CMS);
        }
        cmd->start();
    }
    void lookup()
    {
        Command *const cmd = new LookupCertificatesCommand(nullptr);
        cmd->setParentWidget(q);
        cmd->start();
    }
    void slotKeysMayHaveChanged();
    void slotCurrentViewChanged(QAbstractItemView *newView);
    void slotSelectionChanged();
    void slotDoubleClicked(const QModelIndex &idx);

private:
    bool acceptable(const std::vector<Key> &keys)
    {
        return !keys.empty();
    }
    void updateLabelText()
    {
        ui.label.setText(!customLabelText.isEmpty() ? customLabelText :
                         (options & MultiSelection)
                         ? i18n("Please select one or more of the following certificates:")
                         : i18n("Please select one of the following certificates:"));
    }

private:
    QPointer<QAbstractItemView> lastView;
    QString customLabelText;
    Options options;

    struct UI {
        QLabel label;
        SearchBar searchBar;
        TabWidget tabWidget;
        QDialogButtonBox buttonBox;
        QVBoxLayout vlay;

        explicit UI(CertificateSelectionDialog *q)
            : label(q),
              searchBar(q),
              tabWidget(q),
              buttonBox(q),
              vlay(q)
        {
            KDAB_SET_OBJECT_NAME(label);
            KDAB_SET_OBJECT_NAME(searchBar);
            KDAB_SET_OBJECT_NAME(tabWidget);
            KDAB_SET_OBJECT_NAME(buttonBox);
            KDAB_SET_OBJECT_NAME(vlay);

            vlay.addWidget(&label);
            vlay.addWidget(&searchBar);
            vlay.addWidget(&tabWidget, 1);
            vlay.addWidget(&buttonBox);

            QPushButton *const ok = buttonBox.addButton(QDialogButtonBox::Ok);
            ok->setEnabled(false);
            QPushButton *const cancel = buttonBox.addButton(QDialogButtonBox::Close);
            Q_UNUSED(cancel);
            QPushButton *const reload = buttonBox.addButton(i18n("Reload"),    QDialogButtonBox::ActionRole);
            QPushButton *const import = buttonBox.addButton(i18n("Import..."),    QDialogButtonBox::ActionRole);
            QPushButton *const lookup = buttonBox.addButton(i18n("Lookup..."), QDialogButtonBox::ActionRole);
            QPushButton *const create = buttonBox.addButton(i18n("New..."),    QDialogButtonBox::ActionRole);

            import->setToolTip(i18nc("@info:tooltip", "Import certificate from file"));
            lookup->setToolTip(i18nc("@info:tooltip", "Lookup certificates on server"));
            reload->setToolTip(i18nc("@info:tooltip", "Refresh certificate list"));
            create->setToolTip(i18nc("@info:tooltip", "Create a new certificate"));

            connect(&buttonBox, &QDialogButtonBox::accepted, q, &CertificateSelectionDialog::accept);
            connect(&buttonBox, &QDialogButtonBox::rejected, q, &CertificateSelectionDialog::reject);
            connect(reload,     SIGNAL(clicked()),  q, SLOT(reload()));
            connect(lookup,     SIGNAL(clicked()),  q, SLOT(lookup()));
            connect(create,     SIGNAL(clicked()),  q, SLOT(create()));
            connect(KeyCache::instance().get(), SIGNAL(keysMayHaveChanged()),
                    q, SLOT(slotKeysMayHaveChanged()));

            connect(import, &QPushButton::clicked, q, [import, q] () {
                import->setEnabled(false);
                auto cmd = new Kleo::ImportCertificateFromFileCommand();
                connect(cmd, &Kleo::ImportCertificateFromFileCommand::finished,
                        q, [import]() {
                    import->setEnabled(true);
                });
                cmd->setParentWidget(q);
                cmd->start();
            });
        }
    } ui;
};

CertificateSelectionDialog::Private::Private(CertificateSelectionDialog *qq)
    : q(qq),
      ui(q)
{
    ui.tabWidget.setFlatModel(AbstractKeyListModel::createFlatKeyListModel(q));
    ui.tabWidget.setHierarchicalModel(AbstractKeyListModel::createHierarchicalKeyListModel(q));
    ui.tabWidget.connectSearchBar(&ui.searchBar);

    connect(&ui.tabWidget, SIGNAL(currentViewChanged(QAbstractItemView*)),
            q, SLOT(slotCurrentViewChanged(QAbstractItemView*)));

    updateLabelText();
    q->setWindowTitle(i18nc("@title:window", "Certificate Selection"));
}

CertificateSelectionDialog::CertificateSelectionDialog(QWidget *parent)
    : QDialog(parent), d(new Private(this))
{
    const KSharedConfig::Ptr config = KSharedConfig::openConfig(QStringLiteral("kleopatracertificateselectiondialogrc"));
    d->ui.tabWidget.loadViews(config.data());
    const KConfigGroup geometry(config, "Geometry");
    resize(geometry.readEntry("size", size()));
    d->slotKeysMayHaveChanged();
}

CertificateSelectionDialog::~CertificateSelectionDialog() {}

void CertificateSelectionDialog::setCustomLabelText(const QString &txt)
{
    if (txt == d->customLabelText) {
        return;
    }
    d->customLabelText = txt;
    d->updateLabelText();
}

QString CertificateSelectionDialog::customLabelText() const
{
    return d->customLabelText;
}

void CertificateSelectionDialog::setOptions(Options options)
{
    if (d->options == options) {
        return;
    }
    d->options = options;

    d->ui.tabWidget.setMultiSelection(options & MultiSelection);

    d->slotKeysMayHaveChanged();
}

CertificateSelectionDialog::Options CertificateSelectionDialog::options() const
{
    return d->options;
}

void CertificateSelectionDialog::setStringFilter(const QString &filter)
{
    d->ui.tabWidget.setStringFilter(filter);
}

void CertificateSelectionDialog::setKeyFilter(const std::shared_ptr<KeyFilter> &filter)
{
    d->ui.tabWidget.setKeyFilter(filter);
}

void CertificateSelectionDialog::selectCertificates(const std::vector<Key> &keys)
{
    const QAbstractItemView *const view = d->ui.tabWidget.currentView();
    if (!view) {
        return;
    }
    const auto *const model = d->ui.tabWidget.currentModel();
    Q_ASSERT(model);
    QItemSelectionModel *const sm = view->selectionModel();
    Q_ASSERT(sm);

    Q_FOREACH (const QModelIndex &idx, model->indexes(keys))
        if (idx.isValid()) {
            sm->select(idx, QItemSelectionModel::Select | QItemSelectionModel::Rows);
        }
}

void CertificateSelectionDialog::selectCertificate(const Key &key)
{
    selectCertificates(std::vector<Key>(1, key));
}

std::vector<Key> CertificateSelectionDialog::selectedCertificates() const
{
    const QAbstractItemView *const view = d->ui.tabWidget.currentView();
    if (!view) {
        return std::vector<Key>();
    }
    const auto *const model = d->ui.tabWidget.currentModel();
    Q_ASSERT(model);
    const QItemSelectionModel *const sm = view->selectionModel();
    Q_ASSERT(sm);
    return model->keys(sm->selectedRows());
}

Key CertificateSelectionDialog::selectedCertificate() const
{
    const std::vector<Key> keys = selectedCertificates();
    return keys.empty() ? Key() : keys.front();
}

void CertificateSelectionDialog::hideEvent(QHideEvent *e)
{
    KSharedConfig::Ptr config = KSharedConfig::openConfig(QStringLiteral("kleopatracertificateselectiondialogrc"));
    d->ui.tabWidget.saveViews(config.data());
    KConfigGroup geometry(config, "Geometry");
    geometry.writeEntry("size", size());
    QDialog::hideEvent(e);
}

void CertificateSelectionDialog::Private::slotKeysMayHaveChanged()
{
    q->setEnabled(true);
    std::vector<Key> keys = (options & SecretKeys) ? KeyCache::instance()->secretKeys() : KeyCache::instance()->keys();
    q->filterAllowedKeys(keys, options);
    const std::vector<Key> selected = q->selectedCertificates();
    if (AbstractKeyListModel *const model = ui.tabWidget.flatModel()) {
        model->setKeys(keys);
    }
    if (AbstractKeyListModel *const model = ui.tabWidget.hierarchicalModel()) {
        model->setKeys(keys);
    }
    q->selectCertificates(selected);
}

void CertificateSelectionDialog::filterAllowedKeys(std::vector<Key> &keys, int options)
{
    std::vector<Key>::iterator end = keys.end();

    switch (options & AnyFormat) {
    case OpenPGPFormat:
        end = std::remove_if(keys.begin(), end, [](const Key &key) { return key.protocol() != OpenPGP; });
        break;
    case CMSFormat:
        end = std::remove_if(keys.begin(), end, [](const Key &key) { return key.protocol() != CMS; });
        break;
    default:
    case AnyFormat:
        ;
    }

    switch (options & AnyCertificate) {
    case SignOnly:
        end = std::remove_if(keys.begin(), end, [](const Key &key) { return !key.canReallySign(); });
        break;
    case EncryptOnly:
        end = std::remove_if(keys.begin(), end, [](const Key &key) { return !key.canEncrypt(); });
        break;
    default:
    case AnyCertificate:
        ;
    }

    if (options & SecretKeys) {
        end = std::remove_if(keys.begin(), end, [](const Key &key) { return !key.hasSecret(); });
    }

    keys.erase(end, keys.end());
}

void CertificateSelectionDialog::Private::slotCurrentViewChanged(QAbstractItemView *newView)
{
    if (lastView) {
        disconnect(lastView, SIGNAL(doubleClicked(QModelIndex)),
                   q, SLOT(slotDoubleClicked(QModelIndex)));
        Q_ASSERT(lastView->selectionModel());
        disconnect(lastView->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)),
                   q, SLOT(slotSelectionChanged()));
    }
    lastView = newView;
    if (newView) {
        connect(newView, SIGNAL(doubleClicked(QModelIndex)),
                q, SLOT(slotDoubleClicked(QModelIndex)));
        Q_ASSERT(newView->selectionModel());
        connect(newView->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)),
                q, SLOT(slotSelectionChanged()));
    }
    slotSelectionChanged();
}

void CertificateSelectionDialog::Private::slotSelectionChanged()
{
    if (QPushButton *const pb = ui.buttonBox.button(QDialogButtonBox::Ok)) {
        pb->setEnabled(acceptable(q->selectedCertificates()));
    }
}

void CertificateSelectionDialog::Private::slotDoubleClicked(const QModelIndex &idx)
{
    QAbstractItemView *const view = ui.tabWidget.currentView();
    Q_ASSERT(view);
    const auto *const model = ui.tabWidget.currentModel();
    Q_ASSERT(model);
    Q_UNUSED(model);
    QItemSelectionModel *const sm = view->selectionModel();
    Q_ASSERT(sm);
    sm->select(idx, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
    QMetaObject::invokeMethod(q, [this]() {q->accept();}, Qt::QueuedConnection);
}

void CertificateSelectionDialog::accept()
{
    if (d->acceptable(selectedCertificates())) {
        QDialog::accept();
    }
}

#include "moc_certificateselectiondialog.cpp"
