// Copyright (C) 2018 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0

#include "languageclientsettings.h"

#include "client.h"
#include "languageclient_global.h"
#include "languageclientinterface.h"
#include "languageclientmanager.h"
#include "languageclienttr.h"

#include <coreplugin/editormanager/documentmodel.h>
#include <coreplugin/icore.h>
#include <coreplugin/idocument.h>

#include <projectexplorer/buildconfiguration.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include <projectexplorer/projectpanelfactory.h>
#include <projectexplorer/projectsettingswidget.h>

#include <texteditor/plaintexteditorfactory.h>
#include <texteditor/textmark.h>

#include <utils/algorithm.h>
#include <utils/delegates.h>
#include <utils/fancylineedit.h>
#include <utils/jsontreeitem.h>
#include <utils/layoutbuilder.h>
#include <utils/macroexpander.h>
#include <utils/mimeconstants.h>
#include <utils/stringutils.h>
#include <utils/utilsicons.h>
#include <utils/variablechooser.h>

#include <QBoxLayout>
#include <QComboBox>
#include <QCompleter>
#include <QCoreApplication>
#include <QDialog>
#include <QDialogButtonBox>
#include <QDir>
#include <QFileInfo>
#include <QFormLayout>
#include <QGroupBox>
#include <QHeaderView>
#include <QJsonDocument>
#include <QLabel>
#include <QListView>
#include <QMenu>
#include <QMimeData>
#include <QPushButton>
#include <QSortFilterProxyModel>
#include <QStringListModel>
#include <QToolButton>
#include <QTreeView>
#include <QTreeWidget>

constexpr char typeIdKey[] = "typeId";
constexpr char idKey[] = "id";
constexpr char enabledKey[] = "enabled";
constexpr char startupBehaviorKey[] = "startupBehavior";
constexpr char mimeTypeKey[] = "mimeType";
constexpr char filePatternKey[] = "filePattern";
constexpr char settingsGroupKey[] = "LanguageClient";
constexpr char clientsKey[] = "clients";
constexpr char typedClientsKey[] = "typedClients";
constexpr char outlineSortedKey[] = "outlineSorted";
constexpr char mimeType[] = "application/language.client.setting";

using namespace ProjectExplorer;
using namespace Utils;
using namespace TextEditor;

namespace LanguageClient {

class LanguageClientSettingsModel : public QAbstractListModel
{
public:
    LanguageClientSettingsModel() = default;
    ~LanguageClientSettingsModel() override;

    // QAbstractItemModel interface
    int rowCount(const QModelIndex &/*parent*/ = QModelIndex()) const final { return m_settings.count(); }
    QVariant data(const QModelIndex &index, int role) const final;
    bool removeRows(int row, int count = 1, const QModelIndex &parent = QModelIndex()) final;
    bool insertRows(int row, int count = 1, const QModelIndex &parent = QModelIndex()) final;
    bool setData(const QModelIndex &index, const QVariant &value, int role) final;
    Qt::ItemFlags flags(const QModelIndex &index) const final;
    Qt::DropActions supportedDropActions() const override { return Qt::MoveAction; }
    QStringList mimeTypes() const override { return {mimeType}; }
    QMimeData *mimeData(const QModelIndexList &indexes) const override;
    bool dropMimeData(const QMimeData *data,
                      Qt::DropAction action,
                      int row,
                      int column,
                      const QModelIndex &parent) override;

    void reset(const QList<BaseSettings *> &settings);
    QList<BaseSettings *> settings() const { return m_settings; }
    QModelIndex insertSettings(BaseSettings *settings);
    void enableSetting(const QString &id, bool enable = true);
    QList<BaseSettings *> removed() const { return m_removed; }
    BaseSettings *settingForIndex(const QModelIndex &index) const;
    QModelIndex indexForSetting(BaseSettings *setting) const;

private:
    static constexpr int idRole = Qt::UserRole + 1;
    QList<BaseSettings *> m_settings; // owned
    QList<BaseSettings *> m_removed;
};

class FilterProxy final : public QSortFilterProxyModel
{
public:
    FilterProxy(LanguageClientSettingsModel &sourceModel)
        : m_settings(sourceModel)
    {
        setSourceModel(&sourceModel);
    }

    bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const final
    {
        const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
        const BaseSettings *setting
            = static_cast<LanguageClientSettingsModel *>(sourceModel())->settingForIndex(index);
        return setting && setting->showInSettings();
    }

    void reset(QList<BaseSettings *> settings)
    {
        m_settings.reset(settings);
        invalidateFilter();
    }

    QModelIndex insertSettings(BaseSettings *settings)
    {
        const auto idx = m_settings.insertSettings(settings);
        invalidateFilter();
        return mapFromSource(idx);
    }

    BaseSettings *settingForIndex(const QModelIndex &index) const
    {
        return m_settings.settingForIndex(mapToSource(index));
    }

    QModelIndex indexForSetting(BaseSettings *setting) const
    {
        return mapFromSource(m_settings.indexForSetting(setting));
    }

    QList<BaseSettings *> removed() const { return m_settings.removed(); }

private:
    LanguageClientSettingsModel &m_settings;
};

class LanguageClientSettingsPageWidget : public Core::IOptionsPageWidget
{
public:
    LanguageClientSettingsPageWidget(LanguageClientSettingsModel &settings,
                                     QSet<QString> &changedSettings);

    void currentChanged(const QModelIndex &index);
    int currentRow() const;
    void resetCurrentSettings(int row);
    void applyCurrentSettings();

    void apply() final
    {
        applyCurrentSettings();
        LanguageClientManager::applySettings();

        for (BaseSettings *setting : m_settings.removed()) {
            for (Client *client : LanguageClientManager::clientsForSetting(setting))
                LanguageClientManager::shutdownClient(client);
        }

        int row = currentRow();
        m_settings.reset(LanguageClientManager::currentSettings());
        resetCurrentSettings(row);
    }
    void cancel() override
    {
        m_settings.reset(LanguageClientManager::currentSettings());
        m_changedSettings.clear();
    }

private:
    QTreeView *m_view = nullptr;
    struct CurrentSettings {
        BaseSettings *setting = nullptr;
        QWidget *widget = nullptr;
    } m_currentSettings;

    void addItem(const Id &clientTypeId);
    void deleteItem();

    FilterProxy m_settings;
    QSet<QString> &m_changedSettings;
};

QMap<Id, ClientType> &clientTypes()
{
    static QMap<Id, ClientType> types;
    return types;
}

LanguageClientSettingsPageWidget::LanguageClientSettingsPageWidget(LanguageClientSettingsModel &settings,
                                                                   QSet<QString> &changedSettings)
    : m_view(new QTreeView())
    , m_settings(settings)
    , m_changedSettings(changedSettings)
{
    QObject::connect(
        &m_settings,
        &LanguageClientSettingsModel::dataChanged,
        this,
        [] { markSettingsDirty(); });

    auto mainLayout = new QVBoxLayout();
    auto layout = new QHBoxLayout();

    m_view->setModel(&m_settings);
    m_view->setHeaderHidden(true);
    m_view->setSelectionMode(QAbstractItemView::SingleSelection);
    m_view->setSelectionBehavior(QAbstractItemView::SelectItems);
    m_view->setDragEnabled(true);
    m_view->viewport()->setAcceptDrops(true);
    m_view->setDropIndicatorShown(true);
    m_view->setDragDropMode(QAbstractItemView::InternalMove);
    connect(m_view->selectionModel(), &QItemSelectionModel::currentChanged,
            this, &LanguageClientSettingsPageWidget::currentChanged);
    auto buttonLayout = new QVBoxLayout();
    auto addButton = new QPushButton(Tr::tr("&Add"));
    setIgnoreForDirtyHook(addButton);
    auto addMenu = new QMenu(this);
    addMenu->clear();
    for (const ClientType &type : clientTypes()) {
        if (!type.userAddable)
            continue;
        auto action = new QAction(type.name, this);
        connect(action, &QAction::triggered, this, [this, id = type.id]() { addItem(id); });
        addMenu->addAction(action);
    }
    addButton->setMenu(addMenu);
    auto deleteButton = new QPushButton(Tr::tr("&Delete"));
    connect(deleteButton, &QPushButton::pressed, this, &LanguageClientSettingsPageWidget::deleteItem);
    mainLayout->addLayout(layout);
    setLayout(mainLayout);
    layout->addWidget(m_view);
    layout->addLayout(buttonLayout);
    buttonLayout->addWidget(addButton);
    buttonLayout->addWidget(deleteButton);
    buttonLayout->addStretch(10);

    installMarkSettingsDirtyTriggerRecursively(this);
}

void LanguageClientSettingsPageWidget::currentChanged(const QModelIndex &index)
{
    if (m_currentSettings.widget) {
        applyCurrentSettings();
        layout()->removeWidget(m_currentSettings.widget);
        delete m_currentSettings.widget;
    }

    if (index.isValid()) {
        m_currentSettings.setting = m_settings.settingForIndex(index);
        m_currentSettings.widget = m_currentSettings.setting->createSettingsWidget(this);
        layout()->addWidget(m_currentSettings.widget);
        installMarkSettingsDirtyTriggerRecursively(m_currentSettings.widget);
    } else {
        m_currentSettings.setting = nullptr;
        m_currentSettings.widget = nullptr;
    }
}

int LanguageClientSettingsPageWidget::currentRow() const
{
    return m_settings.indexForSetting(m_currentSettings.setting).row();
}

void LanguageClientSettingsPageWidget::resetCurrentSettings(int row)
{
    if (m_currentSettings.widget) {
        layout()->removeWidget(m_currentSettings.widget);
        delete m_currentSettings.widget;
    }

    m_currentSettings.setting = nullptr;
    m_currentSettings.widget = nullptr;
    m_view->setCurrentIndex(m_settings.index(row, 0));
}

void LanguageClientSettingsPageWidget::applyCurrentSettings()
{
    if (!m_currentSettings.setting)
        return;

    if (m_currentSettings.setting->applyFromSettingsWidget(m_currentSettings.widget)) {
        auto index = m_settings.indexForSetting(m_currentSettings.setting);
        emit m_settings.sourceModel()->dataChanged(index, index);
    }
}

BaseSettings *generateSettings(const Id &clientTypeId)
{
    if (auto generator = clientTypes().value(clientTypeId).generator) {
        auto settings = generator();
        settings->m_settingsTypeId = clientTypeId;
        return settings;
    }
    return nullptr;
}

void LanguageClientSettingsPageWidget::addItem(const Id &clientTypeId)
{
    auto newSettings = generateSettings(clientTypeId);
    QTC_ASSERT(newSettings, return);
    markSettingsDirty();
    m_view->setCurrentIndex(m_settings.insertSettings(newSettings));
}

void LanguageClientSettingsPageWidget::deleteItem()
{
    auto index = m_view->currentIndex();
    if (!index.isValid())
        return;

    m_settings.removeRow(index.row());
    markSettingsDirty();
}

class LanguageClientSettingsPage : public Core::IOptionsPage
{
public:
    LanguageClientSettingsPage();

    void init();
    bool initialized() const { return m_initialized; }

    QList<BaseSettings *> settings() const;
    QList<BaseSettings *> changedSettings() const;
    void addSettings(BaseSettings *settings);
    void enableSettings(const QString &id, bool enable = true);

private:
    bool m_initialized = false;
    LanguageClientSettingsModel m_model;
    QSet<QString> m_changedSettings;
};

LanguageClientSettingsPage::LanguageClientSettingsPage()
{
    setId(Constants::LANGUAGECLIENT_SETTINGS_PAGE);
    setDisplayName(Tr::tr("General"));
    setCategory(Constants::LANGUAGECLIENT_SETTINGS_CATEGORY);
    setWidgetCreator([this] { return new LanguageClientSettingsPageWidget(m_model, m_changedSettings); });
    QObject::connect(&m_model, &LanguageClientSettingsModel::dataChanged, [this](const QModelIndex &index) {
        if (BaseSettings *setting = m_model.settingForIndex(index))
            m_changedSettings << setting->m_id;
    });
}

void LanguageClientSettingsPage::init()
{
    m_initialized = true;
    QList<BaseSettings *> newList = LanguageClientSettings::fromSettings(Core::ICore::settings());
    m_model.reset(newList);
    qDeleteAll(newList);
}

QList<BaseSettings *> LanguageClientSettingsPage::settings() const
{
    return m_model.settings();
}

QList<BaseSettings *> LanguageClientSettingsPage::changedSettings() const
{
    QList<BaseSettings *> result;
    const QList<BaseSettings *> &all = settings();
    for (BaseSettings *setting : all) {
        if (m_changedSettings.contains(setting->m_id))
            result << setting;
    }
    return result;
}

void LanguageClientSettingsPage::addSettings(BaseSettings *settings)
{
    m_model.insertSettings(settings);
    m_changedSettings << settings->m_id;
}

void LanguageClientSettingsPage::enableSettings(const QString &id, bool enable)
{
    m_model.enableSetting(id, enable);
}

LanguageClientSettingsModel::~LanguageClientSettingsModel()
{
    qDeleteAll(m_settings);
}

QVariant LanguageClientSettingsModel::data(const QModelIndex &index, int role) const
{
    BaseSettings *setting = settingForIndex(index);
    if (!setting)
        return QVariant();
    if (role == Qt::DisplayRole)
        return setting->name();
    else if (role == Qt::CheckStateRole)
        return setting->m_enabled ? Qt::Checked : Qt::Unchecked;
    else if (role == idRole)
        return setting->m_id;
    return QVariant();
}

bool LanguageClientSettingsModel::removeRows(int row, int count, const QModelIndex &parent)
{
    if (row >= int(m_settings.size()))
        return false;
    const int end = qMin(row + count - 1, int(m_settings.size()) - 1);
    beginRemoveRows(parent, row, end);
    for (auto i = end; i >= row; --i)
        m_removed << m_settings.takeAt(i);
    endRemoveRows();
    return true;
}

bool LanguageClientSettingsModel::insertRows(int row, int count, const QModelIndex &parent)
{
    if (row > m_settings.size() || row < 0)
        return false;
    beginInsertRows(parent, row, row + count - 1);
    for (int i = 0; i < count; ++i)
        m_settings.insert(row + i, new StdIOSettings());
    endInsertRows();
    return true;
}

bool LanguageClientSettingsModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    BaseSettings *setting = settingForIndex(index);
    if (!setting || role != Qt::CheckStateRole)
        return false;

    if (setting->m_enabled != value.toBool()) {
        setting->m_enabled = !setting->m_enabled;
        emit dataChanged(index, index, { Qt::CheckStateRole });
    }
    return true;
}

Qt::ItemFlags LanguageClientSettingsModel::flags(const QModelIndex &index) const
{
    const Qt::ItemFlags dragndropFlags = index.isValid() ? Qt::ItemIsDragEnabled
                                                         : Qt::ItemIsDropEnabled;
    return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsUserCheckable | dragndropFlags;
}

QMimeData *LanguageClientSettingsModel::mimeData(const QModelIndexList &indexes) const
{
    QTC_ASSERT(indexes.count() == 1, return nullptr);

    QMimeData *mimeData = new QMimeData;
    QByteArray encodedData;

    QDataStream stream(&encodedData, QIODevice::WriteOnly);

    for (const QModelIndex &index : indexes) {
        if (index.isValid())
            stream << data(index, idRole).toString();
    }

    mimeData->setData(mimeType, indexes.first().data(idRole).toString().toUtf8());
    return mimeData;
}

bool LanguageClientSettingsModel::dropMimeData(
    const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
{
    if (!canDropMimeData(data, action, row, column, parent))
        return false;

    if (action == Qt::IgnoreAction)
        return true;

    const QString id = QString::fromUtf8(data->data(mimeType));
    auto setting = findOrDefault(m_settings, [id](const BaseSettings *setting) {
        return setting->m_id == id;
    });
    if (!setting)
        return false;

    if (row == -1)
        row = parent.isValid() ? parent.row() : rowCount(QModelIndex());

    beginInsertRows(parent, row, row);
    m_settings.insert(row, setting->copy());
    endInsertRows();

    return true;
}

void LanguageClientSettingsModel::reset(const QList<BaseSettings *> &settings)
{
    beginResetModel();
    qDeleteAll(m_settings);
    qDeleteAll(m_removed);
    m_removed.clear();
    m_settings = Utils::transform(settings, [](const BaseSettings *other) { return other->copy(); });
    endResetModel();
}

QModelIndex LanguageClientSettingsModel::insertSettings(BaseSettings *settings)
{
    int row = rowCount();
    beginInsertRows(QModelIndex(), row, row);
    m_settings.insert(row, settings);
    endInsertRows();
    return createIndex(row, 0, settings);
}

void LanguageClientSettingsModel::enableSetting(const QString &id, bool enable)
{
    BaseSettings *setting = Utils::findOrDefault(m_settings, Utils::equal(&BaseSettings::m_id, id));
    if (!setting)
        return;
    if (setting->m_enabled == enable)
        return;
    setting->m_enabled = enable;
    const QModelIndex &index = indexForSetting(setting);
    if (index.isValid())
        emit dataChanged(index, index, {Qt::CheckStateRole});
}

BaseSettings *LanguageClientSettingsModel::settingForIndex(const QModelIndex &index) const
{
    if (!index.isValid() || index.row() >= m_settings.size())
        return nullptr;
    return m_settings[index.row()];
}

QModelIndex LanguageClientSettingsModel::indexForSetting(BaseSettings *setting) const
{
    const int index = m_settings.indexOf(setting);
    return index < 0 ? QModelIndex() : createIndex(index, 0, setting);
}

// BaseSettings

BaseSettings::BaseSettings()
{
    name.setSettingsKey("name");
    name.setDefaultValue("New Language Server");
    name.setLabelText(Tr::tr("Name:"));
    name.setDisplayStyle(StringAspect::LineEditDisplay);

    showInSettings.setDefaultValue(true);

    activatable.setDefaultValue(true);

    initializationOptions.setSettingsKey("initializationOptions");

    configuration.setSettingsKey("configuration");
}

QJsonObject BaseSettings::initializationOptionsAsJson() const
{
    return QJsonDocument::fromJson(initializationOptions().toUtf8()).object();
}

QJsonValue BaseSettings::configurationAsJson() const
{
    const QJsonDocument document = QJsonDocument::fromJson(configuration().toUtf8());
    if (document.isArray())
        return document.array();
    if (document.isObject())
        return document.object();
    return {};
}

bool BaseSettings::applyFromSettingsWidget(QWidget *widget)
{
    bool changed = isDirty();
    AspectContainer::apply();

    if (auto settingsWidget = qobject_cast<BaseSettingsWidget *>(widget)) {
        if (m_languageFilter != settingsWidget->filter()) {
            m_languageFilter = settingsWidget->filter();
            changed = true;
        }
        if (m_startBehavior != settingsWidget->startupBehavior()) {
            m_startBehavior = settingsWidget->startupBehavior();
            changed = true;
        }
        if (initializationOptions.isDirty()) {
            initializationOptions.apply();
            changed = true;
        }
    }
    return changed;
}

QWidget *BaseSettings::createSettingsWidget(QWidget *parent) const
{
    return new BaseSettingsWidget(this, parent);
}

BaseSettings *BaseSettings::copy() const
{
    BaseSettings *other = create();
    Store store;
    toMap(store);
    other->fromMap(store);
    return other;
}

bool BaseSettings::isValid() const
{
    return !name().isEmpty();
}

bool BaseSettings::isValidOnBuildConfiguration(BuildConfiguration *) const
{
    return isValid();
}

Client *BaseSettings::createClient() const
{
    return createClient(static_cast<BuildConfiguration *>(nullptr));
}

bool BaseSettings::isEnabledOnProject(Project *project) const
{
    if (project) {
        LanguageClient::ProjectSettings settings(project);
        if (settings.enabledSettings().contains(m_id))
            return true;
        if (settings.disabledSettings().contains(m_id))
            return false;
    }
    return m_enabled;
}

Client *BaseSettings::createClient(BuildConfiguration *bc) const
{
    if (!isValidOnBuildConfiguration(bc))
        return nullptr;
    if (bc && !isEnabledOnProject(bc->project()))
        return nullptr;
    BaseClientInterface *interface = createInterface(bc);
    QTC_ASSERT(interface, return nullptr);
    auto *client = createClient(interface);
    QTC_ASSERT(client, return nullptr);

    if (client->name().isEmpty())
        client->setName(name());

    client->setSupportedLanguage(m_languageFilter);
    client->setInitializationOptions(initializationOptionsAsJson());
    client->setActivatable(activatable());
    client->setCurrentBuildConfiguration(bc);
    client->updateConfiguration(configurationAsJson());
    return client;
}

Client *BaseSettings::createClient(BaseClientInterface *interface) const
{
    return new Client(interface);
}

void BaseSettings::toMap(Store &map) const
{
    AspectContainer::toMap(map);
    map.insert(typeIdKey, m_settingsTypeId.toSetting());
    map.insert(idKey, m_id);
    map.insert(enabledKey, m_enabled);
    map.insert(startupBehaviorKey, m_startBehavior);
    map.insert(mimeTypeKey, m_languageFilter.mimeTypes);
    map.insert(filePatternKey, m_languageFilter.filePattern);
}

void BaseSettings::fromMap(const Store &map)
{
    AspectContainer::fromMap(map);
    m_id = map.value(idKey, QUuid::createUuid().toString()).toString();
    m_enabled = map[enabledKey].toBool();
    m_startBehavior = BaseSettings::StartBehavior(
        map.value(startupBehaviorKey, BaseSettings::RequiresFile).toInt());
    m_languageFilter.mimeTypes = map[mimeTypeKey].toStringList();
    m_languageFilter.filePattern = map[filePatternKey].toStringList();
    m_languageFilter.filePattern.removeAll(QString()); // remove empty entries
    m_settingsTypeId = Id::fromSetting(map[typeIdKey]);
}

static LanguageClientSettingsPage &settingsPage()
{
    static LanguageClientSettingsPage settingsPage;
    return settingsPage;
}

void LanguageClientSettings::init()
{
    settingsPage().init();
    LanguageClientManager::applySettings();
}

bool LanguageClientSettings::initialized()
{
    return settingsPage().initialized();
}

QList<Store> LanguageClientSettings::storesBySettingsType(Id settingsTypeId)
{
    QList<Store> result;

    QtcSettings *settingsIn = Core::ICore::settings();
    settingsIn->beginGroup(settingsGroupKey);

    for (const QVariantList &varList :
         {settingsIn->value(clientsKey).toList(), settingsIn->value(typedClientsKey).toList()}) {
        for (const QVariant &var : varList) {
            const Store store = storeFromVariant(var);
            if (settingsTypeId == Id::fromSetting(store.value(typeIdKey)))
                result << store;
        }
    }

    settingsIn->endGroup();

    return result;
}

QList<BaseSettings *> LanguageClientSettings::fromSettings(QtcSettings *settingsIn)
{
    settingsIn->beginGroup(settingsGroupKey);
    QList<BaseSettings *> result;

    for (const QVariantList &varList :
         {settingsIn->value(clientsKey).toList(), settingsIn->value(typedClientsKey).toList()}) {
        for (const QVariant &var : varList) {
            const Store map = storeFromVariant(var);
            Id typeId = Id::fromSetting(map.value(typeIdKey));
            if (!typeId.isValid())
                typeId = Constants::LANGUAGECLIENT_STDIO_SETTINGS_ID;
            if (BaseSettings *settings = generateSettings(typeId)) {
                settings->fromMap(map);
                result << settings;
            }
        }
    }

    settingsIn->endGroup();
    return result;
}

QList<BaseSettings *> LanguageClientSettings::pageSettings()
{
    return settingsPage().settings();
}

QList<BaseSettings *> LanguageClientSettings::changedSettings()
{
    return settingsPage().changedSettings();
}

void LanguageClientSettings::registerClientType(const ClientType &type)
{
    QTC_ASSERT(!clientTypes().contains(type.id), return);
    clientTypes()[type.id] = type;
}

void LanguageClientSettings::addSettings(BaseSettings *settings)
{
    settingsPage().addSettings(settings);
}

void LanguageClientSettings::enableSettings(const QString &id, bool enable)
{
    settingsPage().enableSettings(id, enable);
}

void LanguageClientSettings::toSettings(QtcSettings *settings,
                                        const QList<BaseSettings *> &languageClientSettings)
{
    settings->beginGroup(settingsGroupKey);
    auto transform = [](const QList<BaseSettings *> &settings) {
        return Utils::transform(settings, [](const BaseSettings *setting) {
            Store store;
            setting->toMap(store);
            return variantFromStore(store);
        });
    };
    auto isStdioSetting = Utils::equal(
        &BaseSettings::m_settingsTypeId, Id(Constants::LANGUAGECLIENT_STDIO_SETTINGS_ID));
    auto [stdioSettings, typedSettings] = Utils::partition(languageClientSettings, isStdioSetting);
    settings->setValue(clientsKey, transform(stdioSettings));

    // write back typed settings for unregistered client types
    QVariantList typedSettingsVariant;
    for (const QVariant &var : settings->value(typedClientsKey).toList()) {
        const Store map = storeFromVariant(var);
        const Id typeId = Id::fromSetting(map.value(typeIdKey));
        const QString id = map.value(idKey).toString();
        if (typeId.isValid() && !clientTypes().contains(typeId)
            && !Utils::anyOf(typedSettings, Utils::equal(&BaseSettings::m_id, id)))
            typedSettingsVariant << var;
    }

    typedSettingsVariant << transform(typedSettings);
    settings->setValue(typedClientsKey, typedSettingsVariant);
    settings->endGroup();
}

bool LanguageClientSettings::outlineComboBoxIsSorted()
{
    auto settings = Core::ICore::settings();
    settings->beginGroup(settingsGroupKey);
    bool sorted = settings->value(outlineSortedKey).toBool();
    settings->endGroup();
    return sorted;
}

void LanguageClientSettings::setOutlineComboBoxSorted(bool sorted)
{
    auto settings = Core::ICore::settings();
    settings->beginGroup(settingsGroupKey);
    settings->setValue(outlineSortedKey, sorted);
    settings->endGroup();
}

// StdIOSettings

StdIOSettings::StdIOSettings()
{
    executable.setSettingsKey("executable");
    executable.setExpectedKind(PathChooser::ExistingCommand);
    executable.setLabelText(Tr::tr("Executable:"));

    arguments.setSettingsKey("arguments");
    arguments.setDisplayStyle(StringAspect::LineEditDisplay);
    arguments.setLabelText(Tr::tr("Arguments:"));
}

StdIOSettings::~StdIOSettings() = default;

QWidget *StdIOSettings::createSettingsWidget(QWidget *parent) const
{
    return new StdIOSettingsWidget(this, parent);
}

bool StdIOSettings::isValid() const
{
    return BaseSettings::isValid() && !executable().isEmpty();
}

CommandLine StdIOSettings::command() const
{
    return CommandLine(executable(), arguments(), CommandLine::Raw);
}

BaseClientInterface *StdIOSettings::createInterface(BuildConfiguration *bc) const
{
    auto interface = new StdIOClientInterface;
    interface->setCommandLine(command());
    if (bc)
        interface->setWorkingDirectory(bc->project()->projectDirectory());
    return interface;
}

class JsonTreeItemDelegate : public QStyledItemDelegate
{
public:
    QString displayText(const QVariant &value, const QLocale &) const override
    {
        QString result = value.toString();
        if (result.size() == 1) {
            switch (result.at(0).toLatin1()) {
            case '\n':
                return QString("\\n");
            case '\t':
                return QString("\\t");
            case '\r':
                return QString("\\r");
            }
        }
        return result;
    }
};

static QString startupBehaviorString(BaseSettings::StartBehavior behavior)
{
    switch (behavior) {
    case BaseSettings::AlwaysOn:
        return Tr::tr("Always On");
    case BaseSettings::RequiresFile:
        return Tr::tr("Requires an Open File");
    case BaseSettings::RequiresProject:
        return Tr::tr("Start Server per Project");
    default:
        break;
    }
    return {};
}

BaseSettingsWidget::BaseSettingsWidget(
    const BaseSettings *settings, QWidget *parent, Layouting::LayoutModifier additionalItems)
    : QWidget(parent)
    , m_mimeTypes(new QLabel(settings->m_languageFilter.mimeTypes.join(filterSeparator), this))
    , m_filePattern(
          new QLineEdit(settings->m_languageFilter.filePattern.join(filterSeparator), this))
    , m_startupBehavior(new QComboBox)
    , m_initializationOptions(new FancyLineEdit(this))
{
    using namespace Layouting;

    auto chooser = new VariableChooser(this);
    chooser->addSupportedWidget(m_initializationOptions);

    auto addMimeTypeButton = new QPushButton(Tr::tr("Set MIME Types..."), this);
    connect(
        addMimeTypeButton, &QPushButton::pressed, this, &BaseSettingsWidget::showAddMimeTypeDialog);

    m_filePattern->setPlaceholderText(Tr::tr("File pattern"));
    m_filePattern->setToolTip(
        Tr::tr("List of file patterns.\nExample: *.cpp%1*.h").arg(filterSeparator));

    for (int behavior = 0; behavior < BaseSettings::LastSentinel ; ++behavior)
        m_startupBehavior->addItem(startupBehaviorString(BaseSettings::StartBehavior(behavior)));
    m_startupBehavior->setCurrentIndex(settings->m_startBehavior);

    m_initializationOptions->setValidationFunction([](const QString &text) -> Result<> {
            const QString value = globalMacroExpander()->expand(text);

            if (value.isEmpty())
                return ResultOk;

            QJsonParseError parseInfo;
            const QJsonDocument json = QJsonDocument::fromJson(value.toUtf8(), &parseInfo);

            if (json.isNull()) {
                return ResultError(Tr::tr("Failed to parse JSON at %1: %2")
                                        .arg(parseInfo.offset)
                                        .arg(parseInfo.errorString()));
            }
            return ResultOk;
        });
    m_initializationOptions->setText(settings->initializationOptions());
    m_initializationOptions->setPlaceholderText(Tr::tr("Language server-specific JSON to pass via "
                                                   "\"initializationOptions\" field of \"initialize\" "
                                                   "request."));

    // clang-format off
    auto form = Form {
        settings->name, br,
        Tr::tr("Language:"), Row { m_mimeTypes, st, addMimeTypeButton }, br,
        Tr::tr("File pattern:"), m_filePattern, br,
        Tr::tr("Startup behavior:"), m_startupBehavior, br,
        Tr::tr("Initialization options:"), m_initializationOptions, br

    };

    if (additionalItems)
        additionalItems(&form);

    form.attachTo(this);
    // clang-format on
}

LanguageFilter BaseSettingsWidget::filter() const
{
    return {m_mimeTypes->text().split(filterSeparator, Qt::SkipEmptyParts),
                m_filePattern->text().split(filterSeparator, Qt::SkipEmptyParts)};
}

BaseSettings::StartBehavior BaseSettingsWidget::startupBehavior() const
{
    return BaseSettings::StartBehavior(m_startupBehavior->currentIndex());
}

QString BaseSettingsWidget::initializationOptions() const
{
    return m_initializationOptions->text();
}

class MimeTypeModel : public QStringListModel
{
public:
    using QStringListModel::QStringListModel;
    QVariant data(const QModelIndex &index, int role) const final
    {
        if (index.isValid() && role == Qt::CheckStateRole)
            return m_selectedMimeTypes.contains(index.data().toString()) ? Qt::Checked : Qt::Unchecked;
        return QStringListModel::data(index, role);
    }
    bool setData(const QModelIndex &index, const QVariant &value, int role) final
    {
        if (index.isValid() && role == Qt::CheckStateRole) {
            QString mimeType = index.data().toString();
            if (value.toInt() == Qt::Checked) {
                if (!m_selectedMimeTypes.contains(mimeType))
                    m_selectedMimeTypes.append(index.data().toString());
            } else {
                m_selectedMimeTypes.removeAll(index.data().toString());
            }
            return true;
        }
        return QStringListModel::setData(index, value, role);
    }

    Qt::ItemFlags flags(const QModelIndex &index) const final
    {
        if (!index.isValid())
            return Qt::NoItemFlags;
        return (QStringListModel::flags(index)
                & ~(Qt::ItemIsEditable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled))
                | Qt::ItemIsUserCheckable;
    }
    QStringList m_selectedMimeTypes;
};

class MimeTypeDialog : public QDialog
{
public:
    explicit MimeTypeDialog(const QStringList &selectedMimeTypes, QWidget *parent = nullptr)
        : QDialog(parent)
    {
        setWindowTitle(Tr::tr("Select MIME Types"));
        auto mainLayout = new QVBoxLayout;
        auto filter = new FancyLineEdit(this);
        filter->setFiltering(true);
        mainLayout->addWidget(filter);
        auto listView = new QListView(this);
        mainLayout->addWidget(listView);
        auto buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
        mainLayout->addWidget(buttons);
        setLayout(mainLayout);

        filter->setPlaceholderText(Tr::tr("Filter"));
        connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
        connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
        auto proxy = new QSortFilterProxyModel(this);
        m_mimeTypeModel
            = new MimeTypeModel(Utils::transform(Utils::allMimeTypes(), &MimeType::name), this);
        m_mimeTypeModel->m_selectedMimeTypes = selectedMimeTypes;
        proxy->setSourceModel(m_mimeTypeModel);
        proxy->sort(0);
        connect(filter, &QLineEdit::textChanged, proxy, &QSortFilterProxyModel::setFilterWildcard);
        listView->setModel(proxy);

        setModal(true);
    }

    MimeTypeDialog(const MimeTypeDialog &other) = delete;
    MimeTypeDialog(MimeTypeDialog &&other) = delete;

    MimeTypeDialog operator=(const MimeTypeDialog &other) = delete;
    MimeTypeDialog operator=(MimeTypeDialog &&other) = delete;


    QStringList mimeTypes() const
    {
        return m_mimeTypeModel->m_selectedMimeTypes;
    }
private:
    MimeTypeModel *m_mimeTypeModel = nullptr;
};

void BaseSettingsWidget::showAddMimeTypeDialog()
{
    MimeTypeDialog dialog(m_mimeTypes->text().split(filterSeparator, Qt::SkipEmptyParts),
                          Core::ICore::dialogParent());
    if (dialog.exec() == QDialog::Rejected)
        return;
    if (m_mimeTypes->text() != dialog.mimeTypes().join(filterSeparator))
        markSettingsDirty();
    m_mimeTypes->setText(dialog.mimeTypes().join(filterSeparator));
}

StdIOSettingsWidget::StdIOSettingsWidget(const StdIOSettings *settings, QWidget *parent)
    : BaseSettingsWidget(settings, parent)
{
    Layouting::Layout l(layout());
    settings->executable.addToLayout(l);
    l.flush();
    settings->arguments.addToLayout(l);
    l.flush();
}

bool LanguageFilter::isSupported(const FilePath &filePath, const QString &mimeTypeName) const
{
    if (!mimeTypes.isEmpty()) {
        const MimeType mimeType = Utils::mimeTypeForName(mimeTypeName);
        if (Utils::anyOf(mimeTypes, [mimeType](const QString &supported) {
                return mimeType.inherits(supported);
            })) {
            return true;
        }
    }
    if (filePattern.isEmpty() && filePath.isEmpty())
        return mimeTypes.isEmpty();
    const QRegularExpression::PatternOptions options
        = HostOsInfo::fileNameCaseSensitivity() == Qt::CaseInsensitive
              ? QRegularExpression::CaseInsensitiveOption
              : QRegularExpression::NoPatternOption;
    auto regexps = Utils::transform(filePattern, [&options](const QString &pattern){
        return QRegularExpression(QRegularExpression::wildcardToRegularExpression(pattern),
                                  options);
    });
    return Utils::anyOf(regexps, [filePath](const QRegularExpression &reg){
        return reg.match(filePath.toUrlishString()).hasMatch()
                || reg.match(filePath.fileName()).hasMatch();
    });
}

bool LanguageFilter::isSupported(const Core::IDocument *document) const
{
    return isSupported(document->filePath(), document->mimeType());
}

bool LanguageFilter::operator==(const LanguageFilter &other) const
{
    return this->filePattern == other.filePattern && this->mimeTypes == other.mimeTypes;
}

bool LanguageFilter::operator!=(const LanguageFilter &other) const
{
    return this->filePattern != other.filePattern || this->mimeTypes != other.mimeTypes;
}

BaseTextEditor *createJsonEditor(QObject *parent)
{
    using namespace Text;
    BaseTextEditor *textEditor = nullptr;
    for (Core::IEditorFactory *factory : Core::IEditorFactory::preferredEditorFactories("foo.json")) {
        Core::IEditor *editor = factory->createEditor();
        if (textEditor = qobject_cast<BaseTextEditor *>(editor); textEditor)
            break;
        delete editor;
    }
    QTC_ASSERT(textEditor, textEditor = createPlainTextEditor());
    textEditor->setParent(parent);

    TextDocument *document = textEditor->textDocument();
    TextEditorWidget *widget = textEditor->editorWidget();
    widget->configureGenericHighlighter(mimeTypeForName(Utils::Constants::JSON_MIMETYPE));
    widget->setLineNumbersVisible(false);
    widget->setRevisionsVisible(false);
    widget->setCodeFoldingSupported(false);
    QObject::connect(document, &TextDocument::contentsChanged, widget, [document]() {
        const Id jsonMarkId("LanguageClient.JsonTextMarkId");
        const TextMarks marks = document->marks();
        for (TextMark *mark : marks) {
            if (mark->category().id == jsonMarkId)
                delete mark;
        }
        const QString content = document->plainText().trimmed();
        if (content.isEmpty())
            return;
        QJsonParseError error;
        QJsonDocument::fromJson(content.toUtf8(), &error);
        if (error.error == QJsonParseError::NoError)
            return;
        const Position pos = Position::fromPositionInDocument(document->document(), error.offset);
        if (!pos.isValid())
            return;
        auto mark = new TextMark(
            FilePath(), pos.line, {::LanguageClient::Tr::tr("JSON Error"), jsonMarkId});
        mark->setLineAnnotation(error.errorString());
        mark->setColor(Theme::CodeModel_Error_TextMarkColor);
        mark->setIcon(Icons::CODEMODEL_ERROR.icon());
        document->addMark(mark);
    });
    return textEditor;
}

constexpr const char projectSettingsId[] = "LanguageClient.ProjectSettings";
constexpr const char enabledSettingsId[] = "LanguageClient.EnabledSettings";
constexpr const char disabledSettingsId[] = "LanguageClient.DisabledSettings";

ProjectSettings::ProjectSettings(ProjectExplorer::Project *project)
    : m_project(project)
{
    QTC_ASSERT(project, return);
    m_json = m_project->namedSettings(projectSettingsId).toByteArray();
    m_enabledSettings = m_project->namedSettings(enabledSettingsId).toStringList();
    m_disabledSettings = m_project->namedSettings(disabledSettingsId).toStringList();
}

QJsonValue ProjectSettings::workspaceConfiguration() const
{
    const auto doc = QJsonDocument::fromJson(m_json);
    if (doc.isObject())
        return doc.object();
    if (doc.isArray())
        return doc.array();
    return {};
}

QByteArray ProjectSettings::json() const
{
    return m_json;
}

void ProjectSettings::setJson(const QByteArray &json)
{
    QTC_ASSERT(m_project, return);
    const QJsonValue oldConfig = workspaceConfiguration();
    m_json = json;
    m_project->setNamedSettings(projectSettingsId, m_json);
    const QJsonValue newConfig = workspaceConfiguration();
    if (oldConfig != newConfig)
        LanguageClientManager::updateWorkspaceConfiguration(m_project, newConfig);
}

void ProjectSettings::enableSetting(const QString &id)
{
    QTC_ASSERT(m_project, return);
    if (m_disabledSettings.removeAll(id) > 0)
        m_project->setNamedSettings(disabledSettingsId, m_disabledSettings);
    if (m_enabledSettings.contains(id))
        return;
    m_enabledSettings << id;
    m_project->setNamedSettings(enabledSettingsId, m_enabledSettings);
    LanguageClientManager::applySettings(id);
}

void ProjectSettings::disableSetting(const QString &id)
{
    QTC_ASSERT(m_project, return);
    if (m_enabledSettings.removeAll(id) > 0)
        m_project->setNamedSettings(enabledSettingsId, m_enabledSettings);
    if (m_disabledSettings.contains(id))
        return;
    m_disabledSettings << id;
    m_project->setNamedSettings(disabledSettingsId, m_disabledSettings);
    LanguageClientManager::applySettings(id);
}

void ProjectSettings::clearOverride(const QString &id)
{
    QTC_ASSERT(m_project, return);
    const bool changedEnabled = m_enabledSettings.removeAll(id) > 0;
    if (changedEnabled)
        m_project->setNamedSettings(enabledSettingsId, m_enabledSettings);
    const bool changedDisabled = m_disabledSettings.removeAll(id) > 0;
    if (changedDisabled)
        m_project->setNamedSettings(disabledSettingsId, m_disabledSettings);
    if (changedEnabled || changedDisabled)
        LanguageClientManager::applySettings(id);
}

QStringList ProjectSettings::enabledSettings()
{
    return m_enabledSettings;
}

QStringList ProjectSettings::disabledSettings()
{
    return m_disabledSettings;
}

class LanguageClientProjectSettingsWidget : public ProjectSettingsWidget
{
public:
    explicit LanguageClientProjectSettingsWidget(Project *project)
        : m_settings(project)
    {
        setUseGlobalSettingsCheckBoxVisible(false);
        setGlobalSettingsId(Constants::LANGUAGECLIENT_SETTINGS_PAGE);
        setExpanding(true);

        BaseTextEditor *editor = createJsonEditor(this);
        editor->document()->setContents(m_settings.json());

        auto layout = new QVBoxLayout(this);
        layout->setContentsMargins(0, 0, 0, 0);

        QFormLayout *settingsLayout = nullptr;
        for (auto settings : LanguageClientSettings::pageSettings()) {

            if (settings->m_startBehavior != BaseSettings::RequiresProject)
                continue;
            if (!settingsLayout) {
                auto group = new QGroupBox(Tr::tr("Project Specific Language Servers"));
                settingsLayout = new QFormLayout;
                settingsLayout->setFormAlignment(Qt::AlignLeft);
                settingsLayout->setFieldGrowthPolicy(QFormLayout::FieldsStayAtSizeHint);
                group->setLayout(settingsLayout);
                layout->addWidget(group);
            }
            QComboBox *comboBox = new QComboBox;
            comboBox->addItem(Tr::tr("Use Global Settings"));
            comboBox->addItem(Tr::tr("Enabled"));
            comboBox->addItem(Tr::tr("Disabled"));
            if (m_settings.enabledSettings().contains(settings->m_id))
                comboBox->setCurrentIndex(1);
            else if (m_settings.disabledSettings().contains(settings->m_id))
                comboBox->setCurrentIndex(2);
            else
                comboBox->setCurrentIndex(0);
            connect(
                comboBox,
                &QComboBox::currentIndexChanged,
                this,
                [id = settings->m_id, this](int index) {
                    if (index == 0)
                        m_settings.clearOverride(id);
                    else if (index == 1)
                        m_settings.enableSetting(id);
                    else if (index == 2)
                        m_settings.disableSetting(id);
                });
            settingsLayout->addRow(settings->name(), comboBox);
        }

        auto group = new QGroupBox(Tr::tr("Workspace Configuration"));
        group->setLayout(new QVBoxLayout);
        group->layout()->addWidget(new QLabel(Tr::tr(
            "Additional JSON configuration sent to all running language servers for this project.\n"
            "See the documentation of the specific language server for valid settings.")));
        group->layout()->addWidget(editor->widget());
        layout->addWidget(group);

        connect(
            editor->editorWidget()->textDocument(),
            &TextDocument::contentsChanged,
            this,
            [this, editor] { m_settings.setJson(editor->document()->contents()); });
    }

private:
    ProjectSettings m_settings;
};

class LanguageClientProjectPanelFactory : public ProjectPanelFactory
{
public:
    LanguageClientProjectPanelFactory()
    {
        setPriority(35);
        setDisplayName(Tr::tr("Language Server"));
        setId(Constants::LANGUAGECLIENT_SETTINGS_PANEL);
        setCreateWidgetFunction([](Project *project) {
            return new LanguageClientProjectSettingsWidget(project);
        });
    }
};

void setupLanguageClientProjectPanel()
{
    static LanguageClientProjectPanelFactory theLanguageClientProjectPanelFactory;
}

} // namespace LanguageClient
