// Copyright (C) Oleg Shparber, et al. <https://zealdocs.org>
// SPDX-License-Identifier: GPL-3.0-or-later

#include "settings.h"

#include "application.h"

#include <QApplication>
#include <QDir>
#include <QFileInfo>
#include <QLoggingCategory>
#include <QSettings>
#include <QStandardPaths>
#include <QUrl>
#include <QUuid>
#include <QWebEngineProfile>
#include <QWebEngineSettings>

#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
#include <QStyle>
#include <QStyleFactory>
#include <QStyleHints>
#else
#include <QPalette>
#endif

namespace Zeal::Core {

namespace {
Q_LOGGING_CATEGORY(log, "zeal.core.settings")

// Configuration file groups
constexpr char GroupUI[] = "ui";
constexpr char GroupContent[] = "content";
constexpr char GroupDocsets[] = "docsets";
constexpr char GroupGlobalShortcuts[] = "global_shortcuts";
constexpr char GroupSearch[] = "search";
constexpr char GroupTabs[] = "tabs";
constexpr char GroupInternal[] = "internal";
constexpr char GroupState[] = "state";
constexpr char GroupProxy[] = "proxy";
} // namespace

Settings::Settings(QObject *parent)
    : QObject(parent)
{
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
    qRegisterMetaTypeStreamOperators<ContentAppearance>("ContentAppearance");
    qRegisterMetaTypeStreamOperators<ExternalLinkPolicy>("ExternalLinkPolicy");
#else
    qRegisterMetaType<ContentAppearance>("ContentAppearance");
    qRegisterMetaType<ExternalLinkPolicy>("ExternalLinkPolicy");
#endif

#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
    // When the OS color scheme changes, reapply the color scheme.
    connect(qApp->styleHints(), &QStyleHints::colorSchemeChanged, this, [this]() {
        if (contentAppearance == ContentAppearance::Automatic) {
            applyColorScheme();
        }
    });
#endif

    load();
}

Settings::~Settings()
{
    save();
}

bool Settings::isDarkModeEnabled() const
{
    if (contentAppearance == ContentAppearance::Dark) {
        return true;
    }

    if (contentAppearance == ContentAppearance::Automatic && colorScheme() == ColorScheme::Dark) {
        return true;
    }

    return false;
}

#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
void Settings::applyColorScheme()
{
    Qt::ColorScheme scheme = Qt::ColorScheme::Unknown;
    switch (contentAppearance) {
    case ContentAppearance::Light:
        scheme = Qt::ColorScheme::Light;
        break;
    case ContentAppearance::Dark:
        scheme = Qt::ColorScheme::Dark;
        break;
    default:
        break;
    }

    qApp->styleHints()->setColorScheme(scheme);
    // setColorScheme() alone doesn't reliably update existing widgets:
    //  - Widgets with stylesheets (QStyleSheetStyle) only update on polish().
    //  - Direct palette consumers (QTreeView, QLineEdit) only update when the
    //    application palette changes.
    // Re-instantiating the style triggers the full unpolish-polish cycle for
    // QStyleSheetStyle widgets and also updates the application palette via
    // the new style's standardPalette().
    QStyle *newStyle = QStyleFactory::create(qApp->style()->name());
    if (newStyle != nullptr) {
        qApp->setStyle(newStyle);
    }
}
#endif

Settings::ColorScheme Settings::colorScheme()
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
    return QApplication::styleHints()->colorScheme();
#else
    // Pre Qt 6.5 detection from https://www.qt.io/blog/dark-mode-on-windows-11-with-qt-6.5.
    const QPalette p;
    if (p.color(QPalette::WindowText).lightness() > p.color(QPalette::Window).lightness()) {
        return ColorScheme::Dark;
    }

    return ColorScheme::Light;
#endif
}

void Settings::load()
{
    QScopedPointer<QSettings> settings(qsettings());
    qCDebug(log, "Using settings file: %s", qPrintable(settings->fileName()));
    migrate(settings.data());

    // TODO: Put everything in groups
    startMinimized = settings->value(QStringLiteral("start_minimized"), false).toBool();
#ifdef ZEAL_FEATURE_UPDATE_CHECK
    checkForUpdate = settings->value(QStringLiteral("check_for_update"), true).toBool();
#else
    checkForUpdate = settings->value(QStringLiteral("check_for_update"), false).toBool();
#endif

    showSystrayIcon = settings->value(QStringLiteral("show_systray_icon"), true).toBool();
    minimizeToSystray = settings->value(QStringLiteral("minimize_to_systray"), false).toBool();
    hideOnClose = settings->value(QStringLiteral("hide_on_close"), false).toBool();

    settings->beginGroup(GroupUI);
    hideMenuBar = settings->value(QStringLiteral("hide_menu_bar"), false).toBool();
    settings->endGroup();

    settings->beginGroup(GroupGlobalShortcuts);
    showShortcut = settings->value(QStringLiteral("show")).value<QKeySequence>();
    settings->endGroup();

    settings->beginGroup(GroupTabs);
    openNewTabAfterActive = settings->value(QStringLiteral("open_new_tab_after_active"), false).toBool();
    settings->endGroup();

    settings->beginGroup(GroupSearch);
    isFuzzySearchEnabled = settings->value(QStringLiteral("fuzzy_search_enabled"), false).toBool();
    settings->endGroup();

    settings->beginGroup(GroupContent);

    contentAppearance = settings->value(QStringLiteral("appearance"), QVariant::fromValue(ContentAppearance::Automatic))
                            .value<ContentAppearance>();

#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0)
    // Dark mode needs to be applied before Qt WebEngine is initialized.
    if (isDarkModeEnabled()) {
        QByteArray chromiumFlags = qgetenv("QTWEBENGINE_CHROMIUM_FLAGS");
        if (!chromiumFlags.isEmpty()) {
            chromiumFlags += ' ';
        }

        chromiumFlags += "--blink-settings=forceDarkModeEnabled=true,darkModeInversionAlgorithm=4";
        qputenv("QTWEBENGINE_CHROMIUM_FLAGS", chromiumFlags);
    }
#endif

#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
    applyColorScheme();
#endif

    // Fonts
    QWebEngineSettings *webSettings = QWebEngineProfile::defaultProfile()->settings();
    serifFontFamily = settings
                          ->value(QStringLiteral("serif_font_family"),
                                  webSettings->fontFamily(QWebEngineSettings::SerifFont))
                          .toString();
    sansSerifFontFamily = settings
                              ->value(QStringLiteral("sans_serif_font_family"),
                                      webSettings->fontFamily(QWebEngineSettings::SansSerifFont))
                              .toString();
    fixedFontFamily = settings
                          ->value(QStringLiteral("fixed_font_family"),
                                  webSettings->fontFamily(QWebEngineSettings::FixedFont))
                          .toString();

    static const QMap<QString, QWebEngineSettings::FontFamily> fontFamilies =
        {{QStringLiteral("sans-serif"), QWebEngineSettings::SansSerifFont},
         {QStringLiteral("serif"), QWebEngineSettings::SerifFont},
         {QStringLiteral("monospace"), QWebEngineSettings::FixedFont}};

    defaultFontFamily = settings->value(QStringLiteral("default_font_family"), QStringLiteral("serif")).toString();

    // Fallback to the serif font family.
    if (!fontFamilies.contains(defaultFontFamily)) {
        defaultFontFamily = QStringLiteral("serif");
    }

    webSettings->setFontFamily(QWebEngineSettings::SansSerifFont, sansSerifFontFamily);
    webSettings->setFontFamily(QWebEngineSettings::SerifFont, serifFontFamily);
    webSettings->setFontFamily(QWebEngineSettings::FixedFont, fixedFontFamily);

    const QString defaultFontFamilyResolved = webSettings->fontFamily(fontFamilies.value(defaultFontFamily));
    webSettings->setFontFamily(QWebEngineSettings::StandardFont, defaultFontFamilyResolved);

    defaultFontSize = settings
                          ->value(QStringLiteral("default_font_size"),
                                  webSettings->fontSize(QWebEngineSettings::DefaultFontSize))
                          .toInt();
    defaultFixedFontSize = settings
                               ->value(QStringLiteral("default_fixed_font_size"),
                                       webSettings->fontSize(QWebEngineSettings::DefaultFixedFontSize))
                               .toInt();
    minimumFontSize = settings
                          ->value(QStringLiteral("minimum_font_size"),
                                  webSettings->fontSize(QWebEngineSettings::MinimumFontSize))
                          .toInt();

    webSettings->setFontSize(QWebEngineSettings::DefaultFontSize, defaultFontSize);
    webSettings->setFontSize(QWebEngineSettings::DefaultFixedFontSize, defaultFixedFontSize);
    webSettings->setFontSize(QWebEngineSettings::MinimumFontSize, minimumFontSize);

    isHighlightOnNavigateEnabled = settings->value(QStringLiteral("highlight_on_navigate"), true).toBool();
    customCssFile = settings->value(QStringLiteral("custom_css_file")).toString();
    externalLinkPolicy = settings
                             ->value(QStringLiteral("external_link_policy"),
                                     QVariant::fromValue(ExternalLinkPolicy::Ask))
                             .value<ExternalLinkPolicy>();
    isSmoothScrollingEnabled = settings->value(QStringLiteral("smooth_scrolling"), true).toBool();
    settings->endGroup();

    settings->beginGroup(GroupProxy);
    proxyType = static_cast<ProxyType>(settings->value(QStringLiteral("type"), ProxyType::System).toUInt());
    proxyHost = settings->value(QStringLiteral("host")).toString();
    proxyPort = static_cast<quint16>(settings->value(QStringLiteral("port"), 0).toUInt());
    proxyAuthenticate = settings->value(QStringLiteral("authenticate"), false).toBool();
    proxyUserName = settings->value(QStringLiteral("username")).toString();
    proxyPassword = settings->value(QStringLiteral("password")).toString();
    isIgnoreSslErrorsEnabled = settings->value(QStringLiteral("ignore_ssl_errors"), false).toBool();
    settings->endGroup();

    settings->beginGroup(GroupDocsets);
    if (settings->contains(QStringLiteral("path"))) {
        docsetPath = settings->value(QStringLiteral("path")).toString();
    } else {
#ifndef PORTABLE_BUILD
        docsetPath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + QLatin1String("/docsets");
#else
        docsetPath = QStringLiteral("docsets");
#endif
    }
    settings->endGroup();

    // Create the docset storage directory if it doesn't exist.
    const QFileInfo fi(docsetPath);
    if (!fi.exists()) {
        const QString path = fi.isRelative() ? QCoreApplication::applicationDirPath() + QLatin1String("/") + docsetPath
                                             : docsetPath;
        if (!QDir().mkpath(path)) {
            qCWarning(log, "Failed to create docset storage directory '%s'.", qPrintable(path));
        }
    }

    settings->beginGroup(GroupState);
    windowGeometry = settings->value(QStringLiteral("window_geometry")).toByteArray();
    verticalSplitterGeometry = settings->value(QStringLiteral("splitter_geometry")).toByteArray();
    tocSplitterState = settings->value(QStringLiteral("toc_splitter_state")).toByteArray();
    settings->endGroup();

    settings->beginGroup(GroupInternal);
    installId = settings
                    ->value(QStringLiteral("install_id"),
                            // Avoid curly braces (QTBUG-885)
                            QUuid::createUuid().toString().mid(1, 36))
                    .toString();
    settings->endGroup();
}

void Settings::save()
{
    QScopedPointer<QSettings> settings(qsettings());

    // TODO: Put everything in groups
    settings->setValue(QStringLiteral("start_minimized"), startMinimized);
    settings->setValue(QStringLiteral("check_for_update"), checkForUpdate);

    settings->setValue(QStringLiteral("show_systray_icon"), showSystrayIcon);
    settings->setValue(QStringLiteral("minimize_to_systray"), minimizeToSystray);
    settings->setValue(QStringLiteral("hide_on_close"), hideOnClose);

    settings->beginGroup(GroupUI);
    settings->setValue(QStringLiteral("hide_menu_bar"), hideMenuBar);
    settings->endGroup();

    settings->beginGroup(GroupGlobalShortcuts);
    settings->setValue(QStringLiteral("show"), showShortcut);
    settings->endGroup();

    settings->beginGroup(GroupTabs);
    settings->setValue(QStringLiteral("open_new_tab_after_active"), openNewTabAfterActive);
    settings->endGroup();

    settings->beginGroup(GroupSearch);
    settings->setValue(QStringLiteral("fuzzy_search_enabled"), isFuzzySearchEnabled);
    settings->endGroup();

    settings->beginGroup(GroupContent);
    settings->setValue(QStringLiteral("default_font_family"), defaultFontFamily);
    settings->setValue(QStringLiteral("serif_font_family"), serifFontFamily);
    settings->setValue(QStringLiteral("sans_serif_font_family"), sansSerifFontFamily);
    settings->setValue(QStringLiteral("fixed_font_family"), fixedFontFamily);

    settings->setValue(QStringLiteral("default_font_size"), defaultFontSize);
    settings->setValue(QStringLiteral("default_fixed_font_size"), defaultFixedFontSize);
    settings->setValue(QStringLiteral("minimum_font_size"), minimumFontSize);

    settings->setValue(QStringLiteral("appearance"), QVariant::fromValue(contentAppearance));
    settings->setValue(QStringLiteral("highlight_on_navigate"), isHighlightOnNavigateEnabled);
    settings->setValue(QStringLiteral("custom_css_file"), customCssFile);
    settings->setValue(QStringLiteral("external_link_policy"), QVariant::fromValue(externalLinkPolicy));
    settings->setValue(QStringLiteral("smooth_scrolling"), isSmoothScrollingEnabled);
    settings->endGroup();

    settings->beginGroup(GroupProxy);
    settings->setValue(QStringLiteral("type"), proxyType);
    settings->setValue(QStringLiteral("host"), proxyHost);
    settings->setValue(QStringLiteral("port"), proxyPort);
    settings->setValue(QStringLiteral("authenticate"), proxyAuthenticate);
    settings->setValue(QStringLiteral("username"), proxyUserName);
    settings->setValue(QStringLiteral("password"), proxyPassword);
    settings->setValue(QStringLiteral("ignore_ssl_errors"), isIgnoreSslErrorsEnabled);
    settings->endGroup();

    settings->beginGroup(GroupDocsets);
    settings->setValue(QStringLiteral("path"), docsetPath);
    settings->endGroup();

    settings->beginGroup(GroupState);
    settings->setValue(QStringLiteral("window_geometry"), windowGeometry);
    settings->setValue(QStringLiteral("splitter_geometry"), verticalSplitterGeometry);
    settings->setValue(QStringLiteral("toc_splitter_state"), tocSplitterState);
    settings->endGroup();

    settings->beginGroup(GroupInternal);
    settings->setValue(QStringLiteral("install_id"), installId);
    // Version of configuration file format, should match Zeal version. Used for migration rules.
    settings->setValue(QStringLiteral("version"), Application::version().toString());
    settings->endGroup();

    settings->sync();

#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
    applyColorScheme();
#endif

    emit updated();
}

/*!
 * \internal
 * \brief Migrates settings from older application versions.
 * \param settings QSettings object to update.
 *
 * The settings migration process relies on 'internal/version' option, that was introduced in the
 * release 0.2.0, so a missing option indicates pre-0.2 release.
 */
void Settings::migrate(QSettings *settings) const
{
    settings->beginGroup(GroupInternal);
    const auto version = QVersionNumber::fromString(settings->value(QStringLiteral("version")).toString());
    settings->endGroup();

    //
    // 0.6.0
    //

    // Unset content.default_fixed_font_size.
    // The causing bug was 0.6.1 (#903), but the incorrect setting still comes to haunt us (#1054).
    if (version == QVersionNumber(0, 6, 0)) {
        settings->beginGroup(GroupContent);
        settings->remove(QStringLiteral("default_fixed_font_size"));
        settings->endGroup();
    }

    //
    // Pre 0.4
    //

    // Rename 'browser' group into 'content'.
    if (version < QVersionNumber(0, 4, 0)) {
        settings->beginGroup(QStringLiteral("browser"));
        const QVariant tmpMinimumFontSize = settings->value(QStringLiteral("minimum_font_size"));
        settings->endGroup();
        settings->remove(QStringLiteral("browser"));

        if (tmpMinimumFontSize.isValid()) {
            settings->beginGroup(GroupContent);
            settings->setValue(QStringLiteral("minimum_font_size"), tmpMinimumFontSize);
            settings->endGroup();
        }
    }

    //
    // Pre 0.3
    //

    // Unset 'state/splitter_geometry', because custom styles were removed.
    if (version < QVersionNumber(0, 3, 0)) {
        settings->beginGroup(GroupState);
        settings->remove(QStringLiteral("splitter_geometry"));
        settings->endGroup();
    }
}

/*!
 * \internal
 * \brief Returns an initialized QSettings object.
 * \param parent Optional parent object.
 * \return QSettings object.
 *
 * QSettings is initialized according to build options, e.g. standard vs portable.
 * Caller is responsible for deleting the returned object.
 */
QSettings *Settings::qsettings(QObject *parent)
{
#ifndef PORTABLE_BUILD
    return new QSettings(parent);
#else
    return new QSettings(QCoreApplication::applicationDirPath() + QLatin1String("/zeal.ini"),
                         QSettings::IniFormat,
                         parent);
#endif
}

} // namespace Zeal::Core

QDataStream &operator<<(QDataStream &out, Zeal::Core::Settings::ContentAppearance policy)
{
    out << static_cast<std::underlying_type_t<Zeal::Core::Settings::ContentAppearance>>(policy);
    return out;
}

QDataStream &operator>>(QDataStream &in, Zeal::Core::Settings::ContentAppearance &policy)
{
    std::underlying_type_t<Zeal::Core::Settings::ContentAppearance> value;
    in >> value;
    policy = static_cast<Zeal::Core::Settings::ContentAppearance>(value);
    return in;
}

QDataStream &operator<<(QDataStream &out, Zeal::Core::Settings::ExternalLinkPolicy policy)
{
    out << static_cast<std::underlying_type_t<Zeal::Core::Settings::ExternalLinkPolicy>>(policy);
    return out;
}

QDataStream &operator>>(QDataStream &in, Zeal::Core::Settings::ExternalLinkPolicy &policy)
{
    std::underlying_type_t<Zeal::Core::Settings::ExternalLinkPolicy> value;
    in >> value;
    policy = static_cast<Zeal::Core::Settings::ExternalLinkPolicy>(value);
    return in;
}
