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

#include "editorconfiguration.h"

#include "project.h"
#include "projectexplorertr.h"
#include "projectmanager.h"

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

#include <texteditor/behaviorsettings.h>
#include <texteditor/codestylepool.h>
#include <texteditor/texteditorconstants.h>
#include <texteditor/icodestylepreferences.h>
#include <texteditor/extraencodingsettings.h>
#include <texteditor/icodestylepreferencesfactory.h>
#include <texteditor/marginsettings.h>
#include <texteditor/storagesettings.h>
#include <texteditor/tabsettings.h>
#include <texteditor/textdocument.h>
#include <texteditor/texteditor.h>
#include <texteditor/typingsettings.h>


#include <QDebug>

using namespace TextEditor;
using namespace Utils;

namespace ProjectExplorer {

const Key kPrefix("EditorConfiguration.");
const Key kUseGlobal("EditorConfiguration.UseGlobal");
const Key kCodec("EditorConfiguration.Codec");
const Key kCodeStylePrefix("EditorConfiguration.CodeStyle.");
const Key kCodeStyleCount("EditorConfiguration.CodeStyle.Count");

struct EditorConfigurationPrivate
{
    EditorConfigurationPrivate() :
        m_textEncoding(Core::EditorManager::defaultTextEncoding())
    { }

    ICodeStylePreferences m_defaultCodeStyle;
    TextEncoding m_textEncoding;

    QMap<Id, ICodeStylePreferences *> m_languageCodeStylePreferences;
};

EditorConfiguration::EditorConfiguration()
    : marginSettings(kPrefix)
    , d(std::make_unique<EditorConfigurationPrivate>())
{
    useGlobalSettings.setSettingsPageId(TextEditor::Constants::TEXT_EDITOR_BEHAVIOR_SETTINGS);

    typingSettings.setAutoApply(true);
    behaviorSettings.setAutoApply(true);
    storageSettings.setAutoApply(true);
    extraEncodingSettings.setAutoApply(true);

    const QMap<Id, ICodeStylePreferences *> globalStyles = TextEditor::codeStyles();
    for (const auto &[languageId, originalPreferences] : globalStyles.asKeyValueRange()) {
        ICodeStylePreferencesFactory *factory = codeStyleFactory(languageId);
        // clone of global prefs for language - it will became project prefs for language
        ICodeStylePreferences *preferences = factory->createCodeStyle();
        // project prefs can point to the global language pool, which contains also the global language prefs
        preferences->setDelegatingPool(codeStylePool(languageId));
        preferences->setId(languageId.name() + "Project");
        preferences->setDisplayName(Tr::tr("Project %1", "Settings, %1 is a language (C++ or QML)").arg(factory->displayName()));
        // project prefs by default point to global prefs (which in turn can delegate to anything else or not)
        preferences->setCurrentDelegate(originalPreferences);
        d->m_languageCodeStylePreferences.insert(languageId, preferences);
    }

    // clone of global prefs (not language specific), for project scope
    d->m_defaultCodeStyle.setDelegatingPool(&globalCodeStylePool());
    d->m_defaultCodeStyle.setDisplayName(Tr::tr("Project", "Settings"));
    d->m_defaultCodeStyle.setId("Project");
    // if setCurrentDelegate is 0 values are read from *this prefs
    d->m_defaultCodeStyle.setCurrentDelegate(&globalCodeStyle());

    connect(ProjectManager::instance(), &ProjectManager::aboutToRemoveProject,
            this, &EditorConfiguration::slotAboutToRemoveProject);
    connect(&extraEncodingSettings.defaultEncoding, &BaseAspect::changed, this, [this] {
        setTextEncoding(extraEncodingSettings.defaultEncoding());
    });

    useGlobalSettings.addOnChanged(this, [this] {
        const bool use = useGlobalSettings();
        d->m_defaultCodeStyle.setCurrentDelegate(use ? &globalCodeStyle() : nullptr);
        const QList<Core::IEditor *> editors = Core::DocumentModel::editorsForOpenedDocuments();
        for (Core::IEditor *editor : editors) {
            if (auto widget = TextEditorWidget::fromEditor(editor)) {
                Project *project = ProjectManager::projectForFile(editor->document()->filePath());
                if (project && project->editorConfiguration() == this)
                    switchSettings(widget);
            }
        }
    });
}

EditorConfiguration::~EditorConfiguration()
{
    qDeleteAll(d->m_languageCodeStylePreferences);
}

void EditorConfiguration::cloneGlobalSettings()
{
    d->m_defaultCodeStyle.setTabSettings(globalCodeStyle().tabSettings());
    typingSettings.setData(globalTypingSettings().data());
    storageSettings.setData(globalStorageSettings().data());
    behaviorSettings.setData(globalBehaviorSettings().data());
    extraEncodingSettings.setData(globalExtraEncodingSettings().data());
    marginSettings.setData(TextEditor::marginSettings().data());
    d->m_textEncoding = Core::EditorManager::defaultTextEncoding();
}

TextEncoding EditorConfiguration::textEncoding() const
{
    return d->m_textEncoding;
}

ICodeStylePreferences *EditorConfiguration::codeStyle() const
{
    return &d->m_defaultCodeStyle;
}

ICodeStylePreferences *EditorConfiguration::codeStyle(Id languageId) const
{
    return d->m_languageCodeStylePreferences.value(languageId, codeStyle());
}

static void toMapWithPrefix(Store *map, const Store &source)
{
    for (auto it = source.constBegin(), end = source.constEnd(); it != end; ++it)
        map->insert(kPrefix + it.key(), it.value());
}

void EditorConfiguration::toMap(Store &map) const
{
    map.insert(kUseGlobal, useGlobalSettings());
    map.insert(kCodec, d->m_textEncoding.name());
    map.insert(kCodeStyleCount, d->m_languageCodeStylePreferences.count());

    int i = 0;
    for (auto itCodeStyle = d->m_languageCodeStylePreferences.cbegin(),
               end = d->m_languageCodeStylePreferences.cend();
            itCodeStyle != end; ++itCodeStyle) {
        Store inner;
        itCodeStyle.value()->toMap(inner);
        const Store settingsIdMap = {
            {"language", QVariant::fromValue(itCodeStyle.key().toSetting())},
            {"value", QVariant::fromValue(inner)}
        };
        map.insert(numberedKey(kCodeStylePrefix, i), variantFromStore(settingsIdMap));
        i++;
    }

    Store inner;
    d->m_defaultCodeStyle.tabSettings().toMap(inner);
    typingSettings.toMap(inner);
    storageSettings.toMap(inner);
    behaviorSettings.toMap(inner);
    extraEncodingSettings.toMap(inner);
    toMapWithPrefix(&map, inner);
    marginSettings.toMap(map);
}

void EditorConfiguration::fromMap(const Store &map)
{
    const QByteArray codecName = map.value(kCodec, d->m_textEncoding.name()).toByteArray();
    d->m_textEncoding = TextEncoding(codecName);
    if (!d->m_textEncoding.isValid())
        d->m_textEncoding = Core::EditorManager::defaultTextEncoding();

    const int codeStyleCount = map.value(kCodeStyleCount, 0).toInt();
    for (int i = 0; i < codeStyleCount; ++i) {
        Store settingsIdMap = storeFromVariant(map.value(numberedKey(kCodeStylePrefix, i)));
        if (settingsIdMap.isEmpty()) {
            qWarning() << "No data for code style settings list" << i << "found!";
            continue;
        }
        Id languageId = Id::fromSetting(settingsIdMap.value("language"));
        Store value = storeFromVariant(settingsIdMap.value("value"));
        ICodeStylePreferences *preferences = d->m_languageCodeStylePreferences.value(languageId);
        if (preferences)
             preferences->fromMap(value);
    }

    Store submap;
    for (auto it = map.constBegin(), end = map.constEnd(); it != end; ++it) {
        if (it.key().view().startsWith(kPrefix.view()))
            submap.insert(it.key().toByteArray().mid(kPrefix.view().size()), it.value());
    }
    d->m_defaultCodeStyle.fromMap(submap);
    typingSettings.fromMap(submap);
    storageSettings.fromMap(submap);
    behaviorSettings.fromMap(submap);
    extraEncodingSettings.fromMap(submap);
    marginSettings.fromMap(map);

    useGlobalSettings.setValue(map.value(kUseGlobal, true).toBool());
}

void EditorConfiguration::configureEditor(Core::IEditor *editor) const
{
    TextEditorWidget *widget = TextEditorWidget::fromEditor(editor);
    if (widget) {
        TextDocument *document = widget->textDocument();
        document->setCodeStyle(codeStyle(widget->languageSettingsId()));
        if (!useGlobalSettings()) {
            // On session restore, editors are reopened before the project has
            // finished loading, so a file may have been decoded with the global
            // default encoding instead of the project's. Re-read unmodified,
            // undecodable files now.
            const bool reloadWithProjectEncoding = document->hasDecodingError()
                    && !document->isModified()
                    && document->encoding() != d->m_textEncoding;
            document->setEncoding(d->m_textEncoding);
            switchSettings(widget);
            if (reloadWithProjectEncoding)
                document->reload(d->m_textEncoding);
        }
    }
}

void EditorConfiguration::switchSettings(TextEditorWidget *widget) const
{
    if (useGlobalSettings()) {
        widget->setMarginSettings(TextEditor::marginSettings().data());
        widget->setTypingSettings(globalTypingSettings().data());
        widget->setStorageSettings(globalStorageSettings().data());
        widget->setBehaviorSettings(globalBehaviorSettings().data());
        widget->setExtraEncodingSettings(globalExtraEncodingSettings().data());
        QObject::disconnect(&typingSettings, &AspectContainer::changed, widget, nullptr);
        QObject::disconnect(&storageSettings, &AspectContainer::changed, widget, nullptr);
        QObject::disconnect(&behaviorSettings, &AspectContainer::changed, widget, nullptr);
        QObject::disconnect(&extraEncodingSettings, &AspectContainer::changed, widget, nullptr);
        QObject::connect(&globalTypingSettings(), &AspectContainer::changed,
                         widget, [widget] { widget->setTypingSettings(globalTypingSettings().data()); });
        QObject::connect(&globalStorageSettings(), &AspectContainer::changed,
                         widget, [widget] { widget->setStorageSettings(globalStorageSettings().data()); });
        QObject::connect(&globalBehaviorSettings(), &AspectContainer::changed,
                         widget, [widget] { widget->setBehaviorSettings(globalBehaviorSettings().data()); });
        QObject::connect(&globalExtraEncodingSettings(), &AspectContainer::changed,
                         widget, [widget] { widget->setExtraEncodingSettings(globalExtraEncodingSettings().data()); });
    } else {
        widget->setMarginSettings(marginSettings.data());
        widget->setTypingSettings(typingSettings.data());
        widget->setStorageSettings(storageSettings.data());
        widget->setBehaviorSettings(behaviorSettings.data());
        widget->setExtraEncodingSettings(extraEncodingSettings.data());
        QObject::disconnect(&globalTypingSettings(), &AspectContainer::changed, widget, nullptr);
        QObject::disconnect(&globalStorageSettings(), &AspectContainer::changed, widget, nullptr);
        QObject::disconnect(&globalBehaviorSettings(), &AspectContainer::changed, widget, nullptr);
        QObject::disconnect(&globalExtraEncodingSettings(), &AspectContainer::changed, widget, nullptr);
        QObject::connect(&typingSettings, &AspectContainer::changed,
                         widget, [this, widget] { widget->setTypingSettings(typingSettings.data()); });
        QObject::connect(&storageSettings, &AspectContainer::changed,
                         widget, [this, widget] { widget->setStorageSettings(storageSettings.data()); });
        QObject::connect(&behaviorSettings, &AspectContainer::changed,
                         widget, [this, widget] { widget->setBehaviorSettings(behaviorSettings.data()); });
        QObject::connect(&extraEncodingSettings, &AspectContainer::changed,
                         widget, [this, widget] { widget->setExtraEncodingSettings(extraEncodingSettings.data()); });
    }
}

void EditorConfiguration::setTextEncoding(const TextEncoding &textEncoding)
{
    d->m_textEncoding = textEncoding;
}

void EditorConfiguration::slotAboutToRemoveProject(Project *project)
{
    if (project->editorConfiguration() != this)
        return;

    for (Core::IEditor *editor : Core::DocumentModel::editorsForOpenedDocuments()) {
        if (auto widget = TextEditorWidget::fromEditor(editor)) {
            const Utils::FilePath filePath = editor->document()->filePath();
            if (project->isKnownFile(filePath) || ProjectManager::isInProjectSourceDir(filePath, *project)) {
                widget->textDocument()->setCodeStyle(
                    codeStyleForLanguage(widget->languageSettingsId()));
            }
        }
    }
}

TabSettingsData actualTabSettings(const FilePath &file, const TextDocument *baseTextdocument)
{
    if (baseTextdocument)
        return baseTextdocument->tabSettings();
    if (Project *project = ProjectManager::projectForFile(file))
        return project->editorConfiguration()->codeStyle()->tabSettings();
    return globalCodeStyle().tabSettings();
}

} // ProjectExplorer
