/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/

#include "clangdiagnosticconfigswidget.h"

#include "cppcodemodelsettings.h"
#include "ui_clangdiagnosticconfigswidget.h"
#include "ui_clangbasechecks.h"

#include <utils/executeondestruction.h>
#include <utils/treemodel.h>

#include <QInputDialog>
#include <QPushButton>

namespace CppTools {

class ConfigNode : public Utils::TreeItem
{
public:
    ConfigNode(const ClangDiagnosticConfig &config)
        : config(config)
    {}

    QVariant data(int, int role) const override
    {
        if (role == Qt::DisplayRole)
            return config.displayName();
        return QVariant();
    }

    ClangDiagnosticConfig config;
};

class GroupNode : public Utils::StaticTreeItem
{
public:
    GroupNode(const QString &text)
        : Utils::StaticTreeItem(text)
    {}

    Qt::ItemFlags flags(int) const { return {}; }
    QVariant data(int column, int role) const
    {
        if (role == Qt::ForegroundRole) {
            // Avoid disabled color.
            return QApplication::palette().color(QPalette::ColorGroup::Normal,
                                                 QPalette::ColorRole::Text);
        }
        return Utils::StaticTreeItem::data(column, role);
    }
};

class ConfigsModel : public Utils::TreeModel<Utils::TreeItem, GroupNode, ConfigNode>
{
    Q_OBJECT

public:
    ConfigsModel(const ClangDiagnosticConfigs &configs)
    {
        m_builtinRoot = new GroupNode(tr("Built-in"));
        m_customRoot = new GroupNode(tr("Custom"));
        rootItem()->appendChild(m_builtinRoot);
        rootItem()->appendChild(m_customRoot);

        for (const ClangDiagnosticConfig &config : configs) {
            Utils::TreeItem *parent = config.isReadOnly() ? m_builtinRoot : m_customRoot;
            parent->appendChild(new ConfigNode(config));
        }
    }

    int customConfigsCount() const { return m_customRoot->childCount(); }
    QModelIndex fallbackConfigIndex() const { return m_builtinRoot->lastChild()->index(); }

    ClangDiagnosticConfigs configs() const
    {
        ClangDiagnosticConfigs configs;
        forItemsAtLevel<2>([&configs](const ConfigNode *node) {
            configs << node->config;
        });
        return configs;
    }

    void appendCustomConfig(const ClangDiagnosticConfig &config)
    {
        m_customRoot->appendChild(new ConfigNode(config));
    }

    void removeConfig(const Core::Id &id)
    {
       ConfigNode *node = itemForConfigId(id);
       node->parent()->removeChildAt(node->indexInParent());
    }

    ConfigNode *itemForConfigId(const Core::Id &id) const
    {
        return findItemAtLevel<2>([id](const ConfigNode *node) {
            return node->config.id() == id;
        });
    }

private:
    Utils::TreeItem *m_builtinRoot = nullptr;
    Utils::TreeItem *m_customRoot = nullptr;
};

ClangDiagnosticConfigsWidget::ClangDiagnosticConfigsWidget(const ClangDiagnosticConfigs &configs,
                                                           const Core::Id &configToSelect,
                                                           QWidget *parent)
    : QWidget(parent)
    , m_ui(new Ui::ClangDiagnosticConfigsWidget)
    , m_configsModel(new ConfigsModel(configs))
{
    m_ui->setupUi(this);
    m_ui->configsView->setHeaderHidden(true);
    m_ui->configsView->setUniformRowHeights(true);
    m_ui->configsView->setRootIsDecorated(false);
    m_ui->configsView->setModel(m_configsModel);
    m_ui->configsView->setCurrentIndex(m_configsModel->itemForConfigId(configToSelect)->index());
    m_ui->configsView->setItemsExpandable(false);
    m_ui->configsView->expandAll();
    connect(m_ui->configsView->selectionModel(),
            &QItemSelectionModel::currentChanged,
            this,
            &ClangDiagnosticConfigsWidget::sync);

    m_clangBaseChecks = std::make_unique<CppTools::Ui::ClangBaseChecks>();
    m_clangBaseChecksWidget = new QWidget();
    m_clangBaseChecks->setupUi(m_clangBaseChecksWidget);

    m_ui->tabWidget->addTab(m_clangBaseChecksWidget, tr("Clang Warnings"));
    m_ui->tabWidget->setCurrentIndex(0);

    connect(m_ui->copyButton, &QPushButton::clicked,
            this, &ClangDiagnosticConfigsWidget::onCopyButtonClicked);
    connect(m_ui->renameButton, &QPushButton::clicked,
            this, &ClangDiagnosticConfigsWidget::onRenameButtonClicked);
    connect(m_ui->removeButton, &QPushButton::clicked,
            this, &ClangDiagnosticConfigsWidget::onRemoveButtonClicked);
    connectClangOnlyOptionsChanged();
}

ClangDiagnosticConfigsWidget::~ClangDiagnosticConfigsWidget()
{
    delete m_ui;
}

void ClangDiagnosticConfigsWidget::onCopyButtonClicked()
{
    const ClangDiagnosticConfig &config = currentConfig();
    bool dialogAccepted = false;
    const QString newName = QInputDialog::getText(this,
                                                  tr("Copy Diagnostic Configuration"),
                                                  tr("Diagnostic configuration name:"),
                                                  QLineEdit::Normal,
                                                  tr("%1 (Copy)").arg(config.displayName()),
                                                  &dialogAccepted);
    if (dialogAccepted) {
        const ClangDiagnosticConfig customConfig
            = ClangDiagnosticConfigsModel::createCustomConfig(config, newName);

        m_configsModel->appendCustomConfig(customConfig);
        m_ui->configsView->setCurrentIndex(
            m_configsModel->itemForConfigId(customConfig.id())->index());
        sync();
        m_clangBaseChecks->diagnosticOptionsTextEdit->setFocus();
    }
}

void ClangDiagnosticConfigsWidget::onRenameButtonClicked()
{
    const ClangDiagnosticConfig &config = currentConfig();

    bool dialogAccepted = false;
    const QString newName = QInputDialog::getText(this,
                                                  tr("Rename Diagnostic Configuration"),
                                                  tr("New name:"),
                                                  QLineEdit::Normal,
                                                  config.displayName(),
                                                  &dialogAccepted);
    if (dialogAccepted) {
        ConfigNode *configNode = m_configsModel->itemForConfigId(config.id());
        configNode->config.setDisplayName(newName);
    }
}

const ClangDiagnosticConfig ClangDiagnosticConfigsWidget::currentConfig() const
{
    Utils::TreeItem *item = m_configsModel->itemForIndex(m_ui->configsView->currentIndex());
    return static_cast<ConfigNode *>(item)->config;
}

void ClangDiagnosticConfigsWidget::onRemoveButtonClicked()
{
    const Core::Id configToRemove = currentConfig().id();
    if (m_configsModel->customConfigsCount() == 1)
        m_ui->configsView->setCurrentIndex(m_configsModel->fallbackConfigIndex());
    m_configsModel->removeConfig(configToRemove);
    sync();
}

static bool isAcceptedWarningOption(const QString &option)
{
    return option == "-w"
        || option == "-pedantic"
        || option == "-pedantic-errors";
}

// Reference:
// https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html
// https://clang.llvm.org/docs/DiagnosticsReference.html
static bool isValidOption(const QString &option)
{
    if (option == "-Werror")
        return false; // Avoid errors due to unknown or misspelled warnings.
    return option.startsWith("-W") || isAcceptedWarningOption(option);
}

static QString validateDiagnosticOptions(const QStringList &options)
{
    // This is handy for testing, allow disabling validation.
    if (qEnvironmentVariableIntValue("QTC_CLANG_NO_DIAGNOSTIC_CHECK"))
        return QString();

    for (const QString &option : options) {
        if (!isValidOption(option))
            return ClangDiagnosticConfigsWidget::tr("Option \"%1\" is invalid.").arg(option);
    }

    return QString();
}

static QStringList normalizeDiagnosticInputOptions(const QString &options)
{
    return options.simplified().split(QLatin1Char(' '), QString::SkipEmptyParts);
}

void ClangDiagnosticConfigsWidget::onClangOnlyOptionsChanged()
{
    const bool useBuildSystemWarnings = m_clangBaseChecks->useFlagsFromBuildSystemCheckBox
                                            ->isChecked();

    // Clean up options input
    const QString diagnosticOptions = m_clangBaseChecks->diagnosticOptionsTextEdit->document()
                                          ->toPlainText();
    const QStringList normalizedOptions = normalizeDiagnosticInputOptions(diagnosticOptions);

    // Validate options input
    const QString errorMessage = validateDiagnosticOptions(normalizedOptions);
    updateValidityWidgets(errorMessage);
    if (!errorMessage.isEmpty()) {
        // Remember the entered options in case the user will switch back.
        m_notAcceptedOptions.insert(currentConfig().id(), diagnosticOptions);
        return;
    }
    m_notAcceptedOptions.remove(currentConfig().id());

    // Commit valid changes
    ClangDiagnosticConfig updatedConfig = currentConfig();
    updatedConfig.setClangOptions(normalizedOptions);
    updatedConfig.setUseBuildSystemWarnings(useBuildSystemWarnings);
    updateConfig(updatedConfig);
}

void ClangDiagnosticConfigsWidget::sync()
{
    if (!m_ui->configsView->currentIndex().isValid())
        return;

    disconnectClangOnlyOptionsChanged();
    Utils::ExecuteOnDestruction e([this]() { connectClangOnlyOptionsChanged(); });

    // Update main button row
    const ClangDiagnosticConfig &config = currentConfig();
    m_ui->removeButton->setEnabled(!config.isReadOnly());
    m_ui->renameButton->setEnabled(!config.isReadOnly());

    // Update check box
    m_clangBaseChecks->useFlagsFromBuildSystemCheckBox->setChecked(config.useBuildSystemWarnings());

    // Update Text Edit
    const QString options = m_notAcceptedOptions.contains(config.id())
            ? m_notAcceptedOptions.value(config.id())
            : config.clangOptions().join(QLatin1Char(' '));
    setDiagnosticOptions(options);
    m_clangBaseChecksWidget->setEnabled(!config.isReadOnly());

    if (config.isReadOnly()) {
        m_ui->infoLabel->setType(Utils::InfoLabel::Information);
        m_ui->infoLabel->setText(tr("Copy this configuration to customize it."));
        m_ui->infoLabel->setFilled(false);
    }

    syncExtraWidgets(config);
}

void ClangDiagnosticConfigsWidget::updateConfig(const ClangDiagnosticConfig &config)
{
    m_configsModel->itemForConfigId(config.id())->config = config;
}

void ClangDiagnosticConfigsWidget::setDiagnosticOptions(const QString &options)
{
    if (options != m_clangBaseChecks->diagnosticOptionsTextEdit->document()->toPlainText())
        m_clangBaseChecks->diagnosticOptionsTextEdit->document()->setPlainText(options);

    const QString errorMessage
            = validateDiagnosticOptions(normalizeDiagnosticInputOptions(options));
    updateValidityWidgets(errorMessage);
}

void ClangDiagnosticConfigsWidget::updateValidityWidgets(const QString &errorMessage)
{
    if (errorMessage.isEmpty()) {
        m_ui->infoLabel->setType(Utils::InfoLabel::Information);
        m_ui->infoLabel->setText(tr("Configuration passes sanity checks."));
        m_ui->infoLabel->setFilled(false);
    } else {
        m_ui->infoLabel->setType(Utils::InfoLabel::Error);
        m_ui->infoLabel->setText(tr("%1").arg(errorMessage));
        m_ui->infoLabel->setFilled(true);
    }
}

void ClangDiagnosticConfigsWidget::connectClangOnlyOptionsChanged()
{
    connect(m_clangBaseChecks->useFlagsFromBuildSystemCheckBox,
            &QCheckBox::stateChanged,
            this,
            &ClangDiagnosticConfigsWidget::onClangOnlyOptionsChanged);
    connect(m_clangBaseChecks->diagnosticOptionsTextEdit->document(),
            &QTextDocument::contentsChanged,
            this,
            &ClangDiagnosticConfigsWidget::onClangOnlyOptionsChanged);
}

void ClangDiagnosticConfigsWidget::disconnectClangOnlyOptionsChanged()
{
    disconnect(m_clangBaseChecks->useFlagsFromBuildSystemCheckBox,
               &QCheckBox::stateChanged,
               this,
               &ClangDiagnosticConfigsWidget::onClangOnlyOptionsChanged);
    disconnect(m_clangBaseChecks->diagnosticOptionsTextEdit->document(),
               &QTextDocument::contentsChanged,
               this,
               &ClangDiagnosticConfigsWidget::onClangOnlyOptionsChanged);
}

ClangDiagnosticConfigs ClangDiagnosticConfigsWidget::configs() const
{
    return m_configsModel->configs();
}

QTabWidget *ClangDiagnosticConfigsWidget::tabWidget() const
{
    return m_ui->tabWidget;
}

} // CppTools namespace

#include "clangdiagnosticconfigswidget.moc"
