/*
    directoryserviceswidget.cpp

    This file is part of Kleopatra, the KDE keymanager
    SPDX-FileCopyrightText: 2001, 2002, 2004 Klarälvdalens Datakonsult AB
    SPDX-FileCopyrightText: 2017 Bundesamnt für Sicherheit in der Informationstechnik

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

#include "directoryserviceswidget.h"

#include "ui_directoryserviceswidget.h"

#include "kleo_ui_debug.h"

#include <KLocalizedString>

#include <QItemDelegate>
#include <QAbstractTableModel>
#include <QSpinBox>
#include <QHeaderView>

#include <vector>

#include <climits>
#include <algorithm>
#include <functional>

#include <gpgme++/engineinfo.h>

using namespace Kleo;

namespace
{

static QUrl defaultX509Service()
{
    QUrl url;
    url.setScheme(QStringLiteral("ldap"));
    url.setHost(i18nc("default server name, keep it a valid domain name, ie. no spaces", "server"));
    return url;
}
static QUrl defaultOpenPGPService()
{
    QUrl url;
    if (GpgME::engineInfo(GpgME::GpgEngine).engineVersion() < "2.1.16") {
        url.setScheme(QStringLiteral("hkp"));
        url.setHost(QStringLiteral("keys.gnupg.net"));
    } else {
        url.setScheme(QStringLiteral("hkps"));
        url.setHost(QStringLiteral("hkps.pool.sks-keyservers.net"));
    }
   return url;
}

static bool is_ldap_scheme(const QUrl &url)
{
    const QString scheme = url.scheme();
    return QString::compare(scheme, QStringLiteral("ldap"),  Qt::CaseInsensitive) == 0
           || QString::compare(scheme, QStringLiteral("ldaps"), Qt::CaseInsensitive) == 0;
}

static const struct {
    const char label[6];
    unsigned short port;
    DirectoryServicesWidget::Scheme base;
} protocols[] = {
    { I18N_NOOP("hkp"), 11371, DirectoryServicesWidget::HKP  },
    { I18N_NOOP("http"),   80, DirectoryServicesWidget::HTTP },
    { I18N_NOOP("https"), 443, DirectoryServicesWidget::HTTP },
    { I18N_NOOP("ftp"),    21, DirectoryServicesWidget::FTP  },
    { I18N_NOOP("ftps"),  990, DirectoryServicesWidget::FTP  },
    { I18N_NOOP("ldap"),  389, DirectoryServicesWidget::LDAP },
    { I18N_NOOP("ldaps"), 636, DirectoryServicesWidget::LDAP },
};
static const unsigned int numProtocols = sizeof protocols / sizeof * protocols;

static unsigned short default_port(const QString &scheme)
{
    for (unsigned int i = 0; i < numProtocols; ++i)
        if (QString::compare(scheme, QLatin1String(protocols[i].label), Qt::CaseInsensitive) == 0) {
            return protocols[i].port;
        }
    return 0;
}

static QString display_scheme(const QUrl &url)
{
    if (url.scheme().isEmpty()) {
        return QStringLiteral("hkp");
    } else {
        return url.scheme();
    }
}

static QString display_host(const QUrl &url)
{
    // work around "subkeys.pgp.net" being interpreted as a path, not host
    if (url.host().isEmpty()) {
        return url.path();
    } else {
        return url.host();
    }
}

static unsigned short display_port(const QUrl &url)
{
    if (url.port() > 0) {
        return url.port();
    } else {
        return default_port(display_scheme(url));
    }
}

static QRect calculate_geometry(const QRect &cell, const QSize &sizeHint)
{
    const int height = qMax(cell.height(), sizeHint.height());
    return {cell.left(), cell.top() - (height - cell.height()) / 2,
                 cell.width(), height};
}

/* The Model contains a bit historic cruft because in the past it was
 * thought to be a good idea to combine openPGP and X509 in a single
 * table although while you can have multiple X509 Keyservers there can
 * only be one OpenPGP Keyserver. So the OpenPGP Keyserver is now a
 * single lineedit. */
class Model : public QAbstractTableModel
{
    Q_OBJECT
public:
    explicit Model(QObject *parent = nullptr)
        : QAbstractTableModel(parent),
          m_items(),
          m_x509ReadOnly(false),
          m_schemes(DirectoryServicesWidget::LDAP)
    {

    }

    void setX509ReadOnly(bool ro)
    {
        if (ro == m_x509ReadOnly) {
            return;
        }
        m_x509ReadOnly = ro;
        for (int row = 0, end = rowCount(); row != end; ++row) {
            Q_EMIT dataChanged(index(row, 0), index(row, NumColumns));
        }
    }

    QModelIndex addX509Service(const QUrl &url, bool force = false)
    {
        const auto it = force ? m_items.end() : findExistingUrl(url);
        unsigned int row;
        if (it != m_items.end()) {
            // existing item:
            row = it - m_items.begin();
            Q_EMIT dataChanged(index(row, 0), index(row, NumColumns));
        } else {
            // append new item
            row = m_items.size();
            beginInsertRows(QModelIndex(), row, row);
            m_items.push_back(url);
            endInsertRows();
        }
        return index(row, firstEditableColumn(row));
    }

    unsigned int numServices() const
    {
        return m_items.size();
    }
    QUrl          service(unsigned int row) const
    {
        return row < m_items.size() ?  m_items[row] : QUrl();
    }

    enum Columns {
        Host,
        Port,
        BaseDN,
        UserName,
        Password,

        NumColumns
    };

    QModelIndex duplicateRow(unsigned int row)
    {
        if (row >= m_items.size()) {
            return {};
        }

        beginInsertRows(QModelIndex(), row + 1, row + 1);
        m_items.insert(m_items.begin() + row + 1, m_items[row]);
        endInsertRows();
        return index(row + 1, 0);
    }

    void deleteRow(unsigned int row)
    {
        if (row >= m_items.size()) {
            return;
        }

        beginRemoveRows(QModelIndex(), row, row);
        m_items.erase(m_items.begin() + row);
        endInsertRows();
    }

    void clear()
    {
        if (m_items.empty()) {
            return;
        }
        beginRemoveRows(QModelIndex(), 0, m_items.size() - 1);
        m_items.clear();
        endRemoveRows();
    }

    int columnCount(const QModelIndex & = QModelIndex()) const override
    {
        return NumColumns;
    }
    int rowCount(const QModelIndex & = QModelIndex()) const override
    {
        return m_items.size();
    }

    QVariant data(const QModelIndex &idx, int role) const override;
    QVariant headerData(int section, Qt::Orientation o, int role) const override;

    Qt::ItemFlags flags(const QModelIndex &idx) const override;
    bool setData(const QModelIndex &idx, const QVariant &value, int role) override;

private:
    bool doSetData(unsigned int row, unsigned int column, const QVariant &value, int role);

    static QString toolTipForColumn(int column);
    bool isLdapRow(unsigned int row) const;
    int firstEditableColumn(unsigned int) const
    {
        return Host;
    }

private:
    std::vector<QUrl> m_items;
    bool m_x509ReadOnly    : 1;
    DirectoryServicesWidget::Schemes m_schemes;

private:
    std::vector<QUrl>::iterator findExistingUrl(const QUrl &url)
    {
        return std::find_if(m_items.begin(), m_items.end(),
                            [&url](const QUrl &item) {
                                const QUrl &lhs = url;
                                const QUrl &rhs = item;
                                return QString::compare(display_scheme(lhs), display_scheme(rhs), Qt::CaseInsensitive) == 0
                                    && QString::compare(display_host(lhs), display_host(rhs), Qt::CaseInsensitive) == 0
                                    && lhs.port() == rhs.port()
                                    && lhs.userName() == rhs.userName()
                                    // ... ignore password...
                                    && (!is_ldap_scheme(lhs)
                                        || lhs.query() == rhs.query());
                            });
    }
};

class Delegate : public QItemDelegate
{
    Q_OBJECT
public:
    explicit Delegate(QObject *parent = nullptr)
        : QItemDelegate(parent),
          m_schemes(DirectoryServicesWidget::LDAP)
    {

    }

    void setAllowedSchemes(const DirectoryServicesWidget::Schemes schemes)
    {
        m_schemes = schemes;
    }
    DirectoryServicesWidget::Schemes allowedSchemes() const
    {
        return m_schemes;
    }

    QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &idx) const override
    {
        switch (idx.column()) {
        case Model::Port:
            return createPortWidget(parent);
        }
        return QItemDelegate::createEditor(parent, option, idx);
    }

    void setEditorData(QWidget *editor, const QModelIndex &idx) const override
    {
        switch (idx.column()) {
        case Model::Port:
            setPortEditorData(qobject_cast<QSpinBox *>(editor), idx.data(Qt::EditRole).toInt());
            break;
        default:
            QItemDelegate::setEditorData(editor, idx);
            break;
        }
    }

    void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &idx) const override
    {
        switch (idx.column()) {
        case Model::Port:
            setPortModelData(qobject_cast<QSpinBox *>(editor), model, idx);
            break;
        default:
            QItemDelegate::setModelData(editor, model, idx);
            break;
        }
    }

    void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override
    {
        if (index.column() == Model::Port) {
            editor->setGeometry(calculate_geometry(option.rect, editor->sizeHint()));
        } else {
            QItemDelegate::updateEditorGeometry(editor, option, index);
        }
    }

private:
    QWidget *createPortWidget(QWidget *parent) const
    {
        auto *sb = new QSpinBox(parent);
        sb->setRange(1, USHRT_MAX);   // valid port numbers
        return sb;
    }
    void setPortEditorData(QSpinBox *sb, unsigned short port) const
    {
        Q_ASSERT(sb);
        sb->setValue(port);
    }
    void setPortModelData(const QSpinBox *sb, QAbstractItemModel *model, const QModelIndex &idx) const
    {
        Q_ASSERT(sb);
        Q_ASSERT(model);
        model->setData(idx, sb->value());
    }

private:
    DirectoryServicesWidget::Schemes m_schemes;
};

}

class DirectoryServicesWidget::Private
{
    friend class ::Kleo::DirectoryServicesWidget;
    DirectoryServicesWidget *const q;
public:
    explicit Private(DirectoryServicesWidget *qq)
        : q(qq),
          protocols(AllProtocols),
          readOnlyProtocols(NoProtocol),
          model(),
          delegate(),
          ui(q)
    {
        ui.treeView->setModel(&model);
        ui.treeView->setItemDelegate(&delegate);

        ui.pgpKeyserver->setPlaceholderText(defaultOpenPGPService().toString());

        connect(&model, &QAbstractItemModel::dataChanged, q, &DirectoryServicesWidget::changed);
        connect(&model, &QAbstractItemModel::rowsInserted, q, &DirectoryServicesWidget::changed);
        connect(&model, &QAbstractItemModel::rowsRemoved, q, &DirectoryServicesWidget::changed);
        connect(ui.treeView->selectionModel(), &QItemSelectionModel::selectionChanged,
                q, [this]() { slotSelectionChanged(); });
        connect(ui.pgpKeyserver, &QLineEdit::textChanged, q, &DirectoryServicesWidget::changed);

        slotShowUserAndPasswordToggled(false);
    }

private:
    void edit(const QModelIndex &index)
    {
        if (index.isValid()) {
            ui.treeView->clearSelection();
            ui.treeView->selectionModel()->setCurrentIndex(index, QItemSelectionModel::Select | QItemSelectionModel::Rows);
            ui.treeView->edit(index);
        }
    }
    void slotNewX509Clicked()
    {
        edit(model.addX509Service(defaultX509Service(), true));
    }
    void slotDeleteClicked()
    {
        model.deleteRow(selectedRow());
    }
    void slotSelectionChanged()
    {
        enableDisableActions();
    }
    void slotShowUserAndPasswordToggled(bool on)
    {
        QHeaderView *const hv = ui.treeView->header();
        Q_ASSERT(hv);
        hv->setSectionHidden(Model::UserName, !on);
        hv->setSectionHidden(Model::Password, !on);
    }

    int selectedRow() const
    {
        const QModelIndexList mil = ui.treeView->selectionModel()->selectedRows();
        return mil.empty() ? -1 : mil.front().row();
    }
    int currentRow() const
    {
        const QModelIndex idx = ui.treeView->selectionModel()->currentIndex();
        return idx.isValid() ? idx.row() : -1;
    }

    void enableDisableActions()
    {
        const bool x509 = (protocols & X509Protocol) && !(readOnlyProtocols & X509Protocol);
        const bool pgp  = (protocols & OpenPGPProtocol) && !(readOnlyProtocols & OpenPGPProtocol);
        ui.newTB->setEnabled(x509);
        ui.pgpKeyserver->setEnabled(pgp);
        const int row = selectedRow();
        ui.deleteTB->setEnabled(row >= 0 && !(readOnlyProtocols & X509Protocol));
    }

private:
    Protocols protocols;
    Protocols readOnlyProtocols;
    Model model;
    Delegate delegate;
    struct UI : Ui_DirectoryServicesWidget {

        explicit UI(DirectoryServicesWidget *q)
            : Ui_DirectoryServicesWidget()
        {
            setupUi(q);
        }

    } ui;
};

DirectoryServicesWidget::DirectoryServicesWidget(QWidget *p, Qt::WindowFlags f)
    : QWidget(p, f), d(new Private(this))
{

}

DirectoryServicesWidget::~DirectoryServicesWidget()
{
    delete d;
}

void DirectoryServicesWidget::setAllowedSchemes(Schemes schemes)
{
    d->delegate.setAllowedSchemes(schemes);
}

DirectoryServicesWidget::Schemes DirectoryServicesWidget::allowedSchemes() const
{
    return d->delegate.allowedSchemes();
}

void DirectoryServicesWidget::setAllowedProtocols(Protocols protocols)
{
    if (d->protocols == protocols) {
        return;
    }
    d->protocols = protocols;
    d->enableDisableActions();
}

DirectoryServicesWidget::Protocols DirectoryServicesWidget::allowedProtocols() const
{
    return d->protocols;
}

void DirectoryServicesWidget::setReadOnlyProtocols(Protocols protocols)
{
    if (d->readOnlyProtocols == protocols) {
        return;
    }
    d->readOnlyProtocols = protocols;
    d->model.setX509ReadOnly(protocols & X509Protocol);
    d->enableDisableActions();
}

DirectoryServicesWidget::Protocols DirectoryServicesWidget::readOnlyProtocols() const
{
    return d->readOnlyProtocols;
}

void DirectoryServicesWidget::addOpenPGPServices(const QList<QUrl> &urls)
{
    if (urls.size() > 1) {
        qCWarning(KLEO_UI_LOG) << "More then one PGP Server, Ignoring all others.";
    }
    if (urls.size()) {
        d->ui.pgpKeyserver->setText(urls[0].toString());
    }
}

QList<QUrl> DirectoryServicesWidget::openPGPServices() const
{
    QList<QUrl> result;
    const QString pgpStr = d->ui.pgpKeyserver->text();
    if (pgpStr.contains(QLatin1String("://"))) {
        // Maybe validate here? Otoh maybe gnupg adds support for more schemes
        // then we know about in the future.
        result.push_back(QUrl::fromUserInput(pgpStr));
    } else if (!pgpStr.isEmpty()) {
        result.push_back(QUrl::fromUserInput(QStringLiteral("hkp://") + pgpStr));
    }
    return result;
}

void DirectoryServicesWidget::addX509Services(const QList<QUrl> &urls)
{
    for (const QUrl &url : urls) {
        d->model.addX509Service(url);
    }
}

QList<QUrl> DirectoryServicesWidget::x509Services() const
{
    QList<QUrl> result;
    for (unsigned int i = 0, end = d->model.numServices(); i != end; ++i) {
        result.push_back(d->model.service(i));
    }
    return result;
}

void DirectoryServicesWidget::clear()
{
    if (!d->model.numServices()) {
        return;
    }
    d->model.clear();
    d->ui.pgpKeyserver->setText(QString());
    Q_EMIT changed();
}

//
// Model
//

QVariant Model::headerData(int section, Qt::Orientation orientation, int role) const
{
    if (orientation == Qt::Horizontal)
        if (role == Qt::ToolTipRole) {
            return toolTipForColumn(section);
        } else if (role == Qt::DisplayRole)
            switch (section) {
            case Host:     return i18n("Server Name");
            case Port:     return i18n("Server Port");
            case BaseDN:   return i18n("Base DN");
            case UserName: return i18n("User Name");
            case Password: return i18n("Password");
            default:       return QVariant();
            }
        else {
            return QVariant();
        }
    else {
        return QAbstractTableModel::headerData(section, orientation, role);
    }
}

QVariant Model::data(const QModelIndex &index, int role) const
{
    const unsigned int row = index.row();
    if (index.isValid() && row < m_items.size())
        switch (role) {
        case Qt::ToolTipRole: {
            const QString tt = toolTipForColumn(index.column());
            if (!m_x509ReadOnly) {
                return tt;
            } else
                return tt.isEmpty()
                       ? i18n("(read-only)")
                       : i18nc("amended tooltip; %1: original tooltip",
                               "%1 (read-only)", tt);
        }
        case Qt::DisplayRole:
        case Qt::EditRole:
            switch (index.column()) {
            case Host:
                return display_host(m_items[row]);
            case Port:
                return display_port(m_items[row]);
            case BaseDN:
                if (isLdapRow(row)) {
                    return m_items[row].query();
                } else {
                    return QVariant();
                }
            case UserName:
                return m_items[row].userName();
            case Password:
                return m_items[row].password();
            default:
                return QVariant();
            }
        }
    return QVariant();
}

bool Model::isLdapRow(unsigned int row) const
{
    if (row >= m_items.size()) {
        return false;
    }
    return is_ldap_scheme(m_items[row]);
}

Qt::ItemFlags Model::flags(const QModelIndex &index) const
{
    const unsigned int row = index.row();
    Qt::ItemFlags flags = QAbstractTableModel::flags(index);
    if (m_x509ReadOnly) {
        flags &= ~Qt::ItemIsSelectable;
    }
    if (index.isValid() && row < m_items.size())
        switch (index.column()) {
        case Host:
        case Port:
            if (m_x509ReadOnly) {
                return flags & ~(Qt::ItemIsEditable | Qt::ItemIsEnabled);
            } else {
                return flags | Qt::ItemIsEditable;
            }
        case BaseDN:
            if (isLdapRow(row) && !m_x509ReadOnly) {
                return flags | Qt::ItemIsEditable;
            } else {
                return flags & ~(Qt::ItemIsEditable | Qt::ItemIsEnabled);
            }
        case UserName:
        case Password:
            if (m_x509ReadOnly) {
                return flags & ~(Qt::ItemIsEditable | Qt::ItemIsEnabled);
            } else {
                return flags | Qt::ItemIsEditable;
            }
        }
    return flags;
}

bool Model::setData(const QModelIndex &idx, const QVariant &value, int role)
{
    const unsigned int row = idx.row();
    if (!idx.isValid() || row >= m_items.size()) {
        return false;
    }
    if (m_x509ReadOnly) {
        return false;
    }
    if (!doSetData(row, idx.column(), value, role)) {
        return false;
    }
    Q_EMIT dataChanged(idx, idx);
    return true;
}

bool Model::doSetData(unsigned int row, unsigned int column, const QVariant &value, int role)
{
    if (role == Qt::EditRole)
        switch (column) {
        case Host:
            if (display_host(m_items[row]) != m_items[row].host()) {
                m_items[row].setScheme(display_scheme(m_items[row]));
            }
            m_items[row].setHost(value.toString());
            return true;
        case Port:
            if (value.toUInt() == default_port(display_scheme(m_items[row]))) {
                m_items[row].setPort(-1);
            } else {
                m_items[row].setPort(value.toUInt());
            }
            return true;
        case BaseDN:
            if (value.toString().isEmpty()) {
                m_items[row].setPath(QString());
                m_items[row].setQuery(QString());
            } else {
                m_items[row].setQuery(value.toString());
            }
            return true;
        case UserName:
            m_items[row].setUserName(value.toString());
            return true;
        case Password:
            m_items[row].setPassword(value.toString());
            return true;
        }
    return false;
}


// static
QString Model::toolTipForColumn(int column)
{
    switch (column) {
    case Host:     return i18n("Enter the name or IP address of the server "
                                   "hosting the directory service.");
    case Port:     return i18n("<b>(Optional, the default is fine in most cases)</b> "
                                   "Pick the port number the directory service is "
                                   "listening on.");
    case BaseDN:   return i18n("<b>(Only for LDAP)</b> "
                                   "Enter the base DN for this LDAP server to "
                                   "limit searches to only that subtree of the directory.");
    case UserName: return i18n("<b>(Optional)</b> "
                                   "Enter your user name here, if needed.");
    case Password: return i18n("<b>(Optional, not recommended)</b> "
                                   "Enter your password here, if needed. "
                                   "Note that the password will be saved in the clear "
                                   "in a config file in your home directory.");
    default:
        return QString();
    }
}

#include "directoryserviceswidget.moc"
#include "moc_directoryserviceswidget.cpp"
