// 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 <QDir>
#include <QFileInfo>
#include <QFormLayout>
#include <QGroupBox>
#include <QHeaderView>
#include <QJsonDocument>
#include <QLabel>
#include <QMenu>
#include <QMimeData>
#include <QPushButton>
#include <QSortFilterProxyModel>
#include <QToolButton>
#include <QTreeView>
#include <QTreeWidget>

constexpr char typeIdKey[] = "typeId";
constexpr char enabledKey[] = "enabled";
constexpr char startupBehaviorKey[] = "startupBehavior";
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,
        [](const QModelIndex &, const QModelIndex &, const QList<int> roles) {
            if (roles.contains(Qt::CheckStateRole))
                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"));
    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);
}

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->settingsTypeId.setValue(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->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->id()))
            result << setting;
    }
    return result;
}

void LanguageClientSettingsPage::addSettings(BaseSettings *settings)
{
    m_model.insertSettings(settings);
    m_changedSettings << settings->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->enabled() ? Qt::Checked : Qt::Unchecked;
    else if (role == idRole)
        return setting->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->enabled() != value.toBool()) {
        setting->enabled.setValue(value.toBool());
        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->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 = LanguageClientSettings::settingById(m_settings, id);
    if (!setting)
        return;
    if (setting->enabled() == enable)
        return;
    setting->enabled.setValue(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);

    filePattern.setSettingsKey("filePattern");
    filePattern.setLabelText(Tr::tr("File pattern:"));
    filePattern.setDisplayStyle(StringAspect::LineEditDisplay);
    filePattern.setPlaceHolderText(Tr::tr("File pattern"));
    filePattern.setToolTip(
        Tr::tr("List of file patterns.\nExample: *.cpp%1*.h").arg(filterSeparator));

    id.setSettingsKey("id");
    id.setDefaultValue(QUuid::createUuid().toString());

    settingsTypeId.setSettingsKey(typeIdKey);

    startBehavior.setSettingsKey(startupBehaviorKey);
    startBehavior.setDisplayStyle(SelectionAspect::DisplayStyle::ComboBox);
    startBehavior.setLabelText(Tr::tr("Startup behavior:"));
    startBehavior.addOption(Tr::tr("Always On"));
    startBehavior.addOption(Tr::tr("Requires an Open File"));
    startBehavior.addOption(Tr::tr("Start Server per Project"));
    startBehavior.setDefaultValue(RequiresFile);

    showInSettings.setDefaultValue(true);

    activatable.setDefaultValue(true);

    mimeTypes.setSettingsKey("mimeType");

    initializationOptions.setSettingsKey("initializationOptions");
    initializationOptions.setLabelText(Tr::tr("Initialization options:"));
    initializationOptions.setDisplayStyle(StringAspect::LineEditDisplay);
    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;
    });
    initializationOptions.setPlaceHolderText(
        Tr::tr(
            "Language server-specific JSON to pass via "
            "\"initializationOptions\" field of \"initialize\" "
            "request."));

    configuration.setSettingsKey("configuration");

    enabled.setSettingsKey(enabledKey);
    enabled.setDefaultValue(true);
    enabled.setLabelText(Tr::tr("Enabled:"));
}

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 *)
{
    bool changed = isDirty();
    AspectContainer::apply();
    return changed;
}

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

BaseSettings *BaseSettings::copy() const
{
    BaseSettings *other = create();
    Store store;
    toMap(store);
    other->fromMap(store);
    // some members are not stored in the map, copy them manually, reeavluate whether those settings
    // need to be an aspect at all after full aspectification of the lsp settings
    other->showInSettings.setValue(showInSettings());
    other->activatable.setValue(activatable());
    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(id()))
            return true;
        if (settings.disabledSettings().contains(id()))
            return false;
    }
    return enabled();
}

const LanguageFilter BaseSettings::languageFilter() const
{
    return LanguageFilter{mimeTypes(), filePattern().split(filterSeparator)};
}

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(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);
}

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();
}

BaseSettings *LanguageClientSettings::settingById(QList<BaseSettings *> settings, const QString &id)
{
    return Utils::findOrDefault(settings, [id](const BaseSettings *setting) {
        return setting->id() == id;
    });
}

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 = [](const BaseSettings *settings) {
        return settings->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("id").toString();
        if (typeId.isValid() && !clientTypes().contains(typeId) && settingById(typedSettings, 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)
{
    return new BaseSettingsWidget(this, parent, [this](Layouting::Layout *layout) {
        layout->addRow({&executable});
        layout->addRow({&arguments});
    });
}

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;
    }
};

BaseSettingsWidget::BaseSettingsWidget(
    const BaseSettings *settings, QWidget *parent, Layouting::LayoutModifier additionalItems)
    : QWidget(parent)
{
    using namespace Layouting;

    // clang-format off
    auto form = Form {
        settings->name, br,
        settings->mimeTypes, br,
        settings->filePattern, br,
        settings->startBehavior, br,
        settings->initializationOptions, br
    };

    if (additionalItems)
        additionalItems(&form);

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


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();
    auto regexps = Utils::transform(filePattern, [](const QString &pattern){
        return QRegularExpression(QRegularExpression::wildcardToRegularExpression(pattern),
                                  QRegularExpression::CaseInsensitiveOption);
    });
    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->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->id()))
                comboBox->setCurrentIndex(1);
            else if (m_settings.disabledSettings().contains(settings->id()))
                comboBox->setCurrentIndex(2);
            else
                comboBox->setCurrentIndex(0);
            connect(
                comboBox,
                &QComboBox::currentIndexChanged,
                this,
                [id = settings->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
