#include "streamingpreferences.h"
#include "utils.h"

#include <QSettings>
#include <QTranslator>
#include <QCoreApplication>
#include <QLocale>
#include <QReadWriteLock>
#include <QtMath>

#include <QtDebug>

#define SER_STREAMSETTINGS "streamsettings"
#define SER_WIDTH "width"
#define SER_HEIGHT "height"
#define SER_FPS "fps"
#define SER_BITRATE "bitrate"

#define SER_AUTOADJUSTBITRATE "autoadjustbitrate"
#define SER_SUNSHINEABR "sunshineabr"
#define SER_FULLSCREEN "fullscreen"
#define SER_IGNORE_ASPECT_RATIO "ignoreaspectratio"
#define SER_VSYNC "vsync"
#define SER_GAMEOPTS "gameopts"
#define SER_HOSTAUDIO "hostaudio"
#define SER_MULTICONT "multicontroller"
#define SER_AUDIOCFG "audiocfg"
#define SER_VIDEOCFG "videocfg"
#define SER_HDR "hdr"
#define SER_YUV444 "yuv444"
#define SER_VIDEODEC "videodec"
#define SER_WINDOWMODE "windowmode"
#define SER_MDNS "mdns"
#define SER_QUITAPPAFTER "quitAppAfter"
#define SER_ABSMOUSEMODE "mouseacceleration"
#define SER_ABSTOUCHMODE "abstouchmode"
#define SER_NATIVETOUCHPAD "nativeTouchpad"
#define SER_DUALSENSEHAPTICSMODE "dualSenseHapticsMode"
#define SER_STARTWINDOWED "startwindowed"
#define SER_FRAMEPACING "framepacing"
#define SER_VIDEOENHANCEMENT "videoenhancement"
#define SER_STREAMRESOLUTIONSCALE "streamresolutionscale"
#define SER_STREAMRESOLUTIONSCALERATIO "streamresolutionscaleratio"
#define SER_REMOTERESOLUTION "remoteresolution"
#define SER_REMOTEWIDTH "remotewidth"
#define SER_REMOTEHEIGHT "remoteheight"
#define SER_REMOTEFPS "remotefps"
#define SER_REMOTEFPSRATE "remotefpsrate"
#define SER_CONNWARNINGS "connwarnings"
#define SER_CONFWARNINGS "confwarnings"
#define SER_UIDISPLAYMODE "uidisplaymode"
#define SER_REMEMBERWINDOWPOSITION "rememberwindowposition"
#define SER_RICHPRESENCE "richpresence"
#define SER_GAMEPADMOUSE "gamepadmouse"
#define SER_DEFAULTVER "defaultver"
#define SER_PACKETSIZE "packetsize"
#define SER_DETECTNETBLOCKING "detectnetblocking"
#define SER_SHOWPERFOVERLAY "showperfoverlay"
#define SER_SWAPMOUSEBUTTONS "swapmousebuttons"
#define SER_SWAPWINALTKEYS "swapwinaltkeys"
#define SER_MUTEONFOCUSLOSS "muteonfocusloss"
#define SER_BACKGROUNDGAMEPAD "backgroundgamepad"
#define SER_GAMEPADQUITCOMBO "gamepadquitcombo"
#define SER_REVERSESCROLL "reversescroll"
#define SER_SWAPFACEBUTTONS "swapfacebuttons"
#define SER_CAPTURESYSKEYS "capturesyskeys"
#define SER_KEEPAWAKE "keepawake"
#define SER_LANGUAGE "language"
#define SER_SCREENCOMBINATIONMODE "screencombinationmode"
#define SER_LEGACY_CUSTOMSCREENMODE "customscreenmode"
#define SER_LEGACY_CUSTOMVDDSCREENMODE "customvddscreenmode"
#define SER_SHOWLOCALCURSOR "showLocalCursor"
#define SER_MICROPHONE "microphone"
#define SER_OVERLAYMENUPLACEMENT "overlaymenuplacement"
#define SER_LEGACY_OVERLAYMENUPOS "overlaymenuposition"
#define SER_HDRMODE "hdrmode"
#define SER_HDRBRIGHTNESSMODE "hdrbrightnessmode"
#define SER_HDRMAXBRIGHTNESS "hdrmaxbrightness"
#define SER_HDRMINBRIGHTNESS "hdrminbrightness"
#define SER_HDRMAXAVERAGEBRIGHTNESS "hdrmaxaveragebrightness"
#define SER_AUTOUPDATECHECK "autoupdatecheck"
#define SER_RENDERER "renderer"
#define SER_BACKGROUNDSOURCE "backgroundsource"
#define SER_BACKGROUNDIMAGEAPI "backgroundimageapi"
#define SER_BACKGROUNDIMAGELOCALPATH "backgroundimagelocalpath"
#define SER_BACKGROUNDOVERLAYOPACITY "backgroundoverlayopacity"
#define SER_BACKGROUNDSETUPCOMPLETED "backgroundsetupcompleted"

#define CURRENT_DEFAULT_VER 2

static constexpr int DEFAULT_BACKGROUND_OVERLAY_OPACITY = 72;

static StreamingPreferences* s_GlobalPrefs;

Q_GLOBAL_STATIC(QReadWriteLock, s_GlobalPrefsLock)

static StreamingPreferences::OverlayMenuPosition decodeOverlayMenuPlacement(int value)
{
    switch (value) {
    case StreamingPreferences::OMP_TOP_EDGE:
    case StreamingPreferences::OMP_RIGHT_EDGE:
    case StreamingPreferences::OMP_LEFT_EDGE:
    case StreamingPreferences::OMP_BUTTON:
    case StreamingPreferences::OMP_DISABLED:
        return static_cast<StreamingPreferences::OverlayMenuPosition>(value);
    default:
        return StreamingPreferences::OMP_DISABLED;
    }
}

static StreamingPreferences::BackgroundSource decodeBackgroundSource(int value)
{
    switch (value) {
    case StreamingPreferences::BGS_PHOTOGRAPHY:
    case StreamingPreferences::BGS_ANIME:
    case StreamingPreferences::BGS_API:
    case StreamingPreferences::BGS_LOCAL:
    case StreamingPreferences::BGS_NONE:
        return static_cast<StreamingPreferences::BackgroundSource>(value);
    default:
        return StreamingPreferences::BGS_PHOTOGRAPHY;
    }
}

static StreamingPreferences::OverlayMenuPosition migrateLegacyOverlayMenuPosition(int value)
{
    switch (value) {
    case 0: // Right edge
        return StreamingPreferences::OMP_RIGHT_EDGE;
    case 1: // Left edge
        return StreamingPreferences::OMP_LEFT_EDGE;
    case 3: // Disabled
        return StreamingPreferences::OMP_DISABLED;
    case 4: // Floating button
        return StreamingPreferences::OMP_BUTTON;
    case 2: // Removed at-cursor mode already fell back to the right edge
    default:
        return StreamingPreferences::OMP_RIGHT_EDGE;
    }
}

static StreamingPreferences::OverlayMenuPosition loadOverlayMenuPlacement(QSettings& settings)
{
    if (settings.contains(SER_OVERLAYMENUPLACEMENT)) {
        settings.remove(SER_LEGACY_OVERLAYMENUPOS);
        return decodeOverlayMenuPlacement(
                settings.value(SER_OVERLAYMENUPLACEMENT).toInt());
    }

    if (!settings.contains(SER_LEGACY_OVERLAYMENUPOS)) {
        return StreamingPreferences::OMP_DISABLED;
    }

    // Temporary compatibility bridge for versions that stored the old enum
    // under overlaymenuposition. Remove this migration and the legacy key
    // constant after support for upgrading from those versions is retired.
    const auto migratedPosition = migrateLegacyOverlayMenuPosition(
            settings.value(SER_LEGACY_OVERLAYMENUPOS).toInt());
    settings.setValue(SER_OVERLAYMENUPLACEMENT,
                      static_cast<int>(migratedPosition));
    settings.remove(SER_LEGACY_OVERLAYMENUPOS);
    return migratedPosition;
}

StreamingPreferences::StreamingPreferences(QQmlEngine *qmlEngine)
    : m_QmlEngine(qmlEngine)
{
    reload();
}

StreamingPreferences* StreamingPreferences::get(QQmlEngine *qmlEngine)
{
    {
        QReadLocker readGuard(s_GlobalPrefsLock);

        // If we have a preference object and it's associated with a QML engine or
        // if the caller didn't specify a QML engine, return the existing object.
        if (s_GlobalPrefs && (s_GlobalPrefs->m_QmlEngine || !qmlEngine)) {
            // The lifetime logic here relies on the QML engine also being a singleton.
            Q_ASSERT(!qmlEngine || s_GlobalPrefs->m_QmlEngine == qmlEngine);
            return s_GlobalPrefs;
        }
    }

    {
        QWriteLocker writeGuard(s_GlobalPrefsLock);

        // If we already have an preference object but the QML engine is now available,
        // associate the QML engine with the preferences.
        if (s_GlobalPrefs) {
            if (!s_GlobalPrefs->m_QmlEngine) {
                s_GlobalPrefs->m_QmlEngine = qmlEngine;
            }
            else {
                // We could reach this codepath if another thread raced with us
                // and created the object while we were outside the pref lock.
                Q_ASSERT(!qmlEngine || s_GlobalPrefs->m_QmlEngine == qmlEngine);
            }
        }
        else {
            s_GlobalPrefs = new StreamingPreferences(qmlEngine);
        }

        return s_GlobalPrefs;
    }
}

void StreamingPreferences::reload()
{
    QSettings settings;

    int defaultVer = settings.value(SER_DEFAULTVER, 0).toInt();

#ifdef Q_OS_DARWIN
    recommendedFullScreenMode = WindowMode::WM_FULLSCREEN_DESKTOP;
#else
    // Wayland doesn't support modesetting, so use fullscreen desktop mode
    // unless we have a slow GPU (which can take advantage of wp_viewporter
    // to reduce GPU load with lower resolution video streams).
    if (WMUtils::isRunningWayland() && !WMUtils::isGpuSlow()) {
        recommendedFullScreenMode = WindowMode::WM_FULLSCREEN_DESKTOP;
    }
    else {
        recommendedFullScreenMode = WindowMode::WM_FULLSCREEN;
    }
#endif

    width = settings.value(SER_WIDTH, 1280).toInt();
    height = settings.value(SER_HEIGHT, 720).toInt();
    fps = settings.value(SER_FPS, 60).toInt();
    enableYUV444 = settings.value(SER_YUV444, false).toBool();
    bitrateKbps = settings.value(SER_BITRATE, getDefaultBitrate(width, height, fps, enableYUV444)).toInt();
    autoAdjustBitrate = settings.value(SER_AUTOADJUSTBITRATE, true).toBool();
    enableSunshineAbr = settings.value(SER_SUNSHINEABR, false).toBool();
    ignoreAspectRatio = settings.value(SER_IGNORE_ASPECT_RATIO, true).toBool();
    enableVsync = settings.value(SER_VSYNC, true).toBool();
    gameOptimizations = settings.value(SER_GAMEOPTS, true).toBool();
    playAudioOnHost = settings.value(SER_HOSTAUDIO, false).toBool();
    multiController = settings.value(SER_MULTICONT, true).toBool();
    enableMdns = settings.value(SER_MDNS, true).toBool();
    quitAppAfter = settings.value(SER_QUITAPPAFTER, false).toBool();
    absoluteMouseMode = settings.value(SER_ABSMOUSEMODE, false).toBool();
    showLocalCursor = settings.value(SER_SHOWLOCALCURSOR, false).toBool();
    absoluteTouchMode = settings.value(SER_ABSTOUCHMODE, true).toBool();
    enableNativeTouchpad = settings.value(SER_NATIVETOUCHPAD, false).toBool();
#ifdef Q_OS_WIN32
    constexpr auto defaultDualSenseHapticsMode = DSHM_PHYSICAL;
#else
    constexpr auto defaultDualSenseHapticsMode = DSHM_EMULATED;
#endif
    dualSenseHapticsMode = static_cast<DualSenseHapticsMode>(
        settings.value(SER_DUALSENSEHAPTICSMODE, defaultDualSenseHapticsMode).toInt());
    if (dualSenseHapticsMode != DSHM_PHYSICAL && dualSenseHapticsMode != DSHM_EMULATED) {
        dualSenseHapticsMode = defaultDualSenseHapticsMode;
    }
#ifndef Q_OS_WIN32
    // Native authored PCM currently requires the Windows WASAPI renderer.
    // Do not retain a value that this build can neither negotiate nor render.
    if (dualSenseHapticsMode == DSHM_PHYSICAL) {
        dualSenseHapticsMode = DSHM_EMULATED;
    }
#endif
    framePacing = settings.value(SER_FRAMEPACING, false).toBool();
    videoEnhancement = settings.value(SER_VIDEOENHANCEMENT, false).toBool();
    enableMicrophone = settings.value(SER_MICROPHONE, false).toBool();
    overlayMenuPosition = loadOverlayMenuPlacement(settings);
    autoUpdateCheck = settings.value(SER_AUTOUPDATECHECK, true).toBool();

    streamResolutionScale = settings.value(SER_STREAMRESOLUTIONSCALE, false).toBool();
    streamResolutionScaleRatio = settings.value(SER_STREAMRESOLUTIONSCALERATIO, 100).toInt();
    remoteResolution = settings.value(SER_REMOTERESOLUTION, false).toBool();
    remoteResolutionWidth = settings.value(SER_REMOTEWIDTH, 1280).toInt();
    remoteResolutionHeight = settings.value(SER_REMOTEHEIGHT, 720).toInt();
    remoteFps = settings.value(SER_REMOTEFPS, false).toBool();
    remoteFpsRate = settings.value(SER_REMOTEFPSRATE, 60).toInt();

    connectionWarnings = settings.value(SER_CONNWARNINGS, true).toBool();
    configurationWarnings = settings.value(SER_CONFWARNINGS, true).toBool();
    richPresence = settings.value(SER_RICHPRESENCE, true).toBool();
    gamepadMouse = settings.value(SER_GAMEPADMOUSE, true).toBool();
    detectNetworkBlocking = settings.value(SER_DETECTNETBLOCKING, true).toBool();
    showPerformanceOverlay = settings.value(SER_SHOWPERFOVERLAY, false).toBool();
    packetSize = settings.value(SER_PACKETSIZE, 0).toInt();
    swapMouseButtons = settings.value(SER_SWAPMOUSEBUTTONS, false).toBool();
    swapWinAltKeys = settings.value(SER_SWAPWINALTKEYS, false).toBool();
    muteOnFocusLoss = settings.value(SER_MUTEONFOCUSLOSS, false).toBool();
    backgroundGamepad = settings.value(SER_BACKGROUNDGAMEPAD, false).toBool();
    gamepadQuitCombo = static_cast<GamepadQuitCombo>(settings.value(SER_GAMEPADQUITCOMBO,
                                                     static_cast<int>(GamepadQuitCombo::GQC_DEFAULT)).toInt());
    reverseScrollDirection = settings.value(SER_REVERSESCROLL, false).toBool();
    swapFaceButtons = settings.value(SER_SWAPFACEBUTTONS, false).toBool();
    keepAwake = settings.value(SER_KEEPAWAKE, true).toBool();
    enableHdr = settings.value(SER_HDR, false).toBool();
    hdrMode = static_cast<HdrMode>(settings.value(SER_HDRMODE,
                                                   static_cast<int>(HdrMode::HDR_PQ)).toInt());
#ifdef Q_OS_WIN32
    const auto defaultHdrBrightnessMode = HdrBrightnessMode::HBM_AUTO;
#else
    const auto defaultHdrBrightnessMode = HdrBrightnessMode::HBM_HOST_DEFAULT;
#endif
    hdrBrightnessMode = static_cast<HdrBrightnessMode>(
        settings.value(SER_HDRBRIGHTNESSMODE, static_cast<int>(defaultHdrBrightnessMode)).toInt());
    if (hdrBrightnessMode < HdrBrightnessMode::HBM_HOST_DEFAULT ||
            hdrBrightnessMode > HdrBrightnessMode::HBM_MANUAL) {
        hdrBrightnessMode = defaultHdrBrightnessMode;
    }
    hdrMaxBrightness = settings.value(SER_HDRMAXBRIGHTNESS, 1000.0).toDouble();
    hdrMinBrightness = settings.value(SER_HDRMINBRIGHTNESS, 0.001).toDouble();
    hdrMaxAverageBrightness = settings.value(SER_HDRMAXAVERAGEBRIGHTNESS, 1000.0).toDouble();
    captureSysKeysMode = static_cast<CaptureSysKeysMode>(settings.value(SER_CAPTURESYSKEYS,
                                                         static_cast<int>(CaptureSysKeysMode::CSK_OFF)).toInt());
    audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
                                                  static_cast<int>(AudioConfig::AC_STEREO)).toInt());
    videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
                                                  static_cast<int>(VideoCodecConfig::VCC_AUTO)).toInt());
    videoDecoderSelection = static_cast<VideoDecoderSelection>(settings.value(SER_VIDEODEC,
                                                  static_cast<int>(VideoDecoderSelection::VDS_AUTO)).toInt());
    rendererSelection = static_cast<RendererSelection>(settings.value(SER_RENDERER,
                                                  static_cast<int>(RendererSelection::RS_AUTO)).toInt());
    windowMode = static_cast<WindowMode>(settings.value(SER_WINDOWMODE,
                                                        // Try to load from the old preference value too
                                                        static_cast<int>(settings.value(SER_FULLSCREEN, true).toBool() ?
                                                                             recommendedFullScreenMode : WindowMode::WM_WINDOWED)).toInt());
    uiDisplayMode = static_cast<UIDisplayMode>(settings.value(SER_UIDISPLAYMODE,
                                               static_cast<int>(settings.value(SER_STARTWINDOWED, true).toBool() ? UIDisplayMode::UI_WINDOWED
                                                                                                                 : UIDisplayMode::UI_MAXIMIZED)).toInt());
    rememberWindowPosition = settings.value(SER_REMEMBERWINDOWPOSITION, true).toBool();
    language = static_cast<Language>(settings.value(SER_LANGUAGE,
                                                    static_cast<int>(Language::LANG_AUTO)).toInt());
    const auto defaultBackgroundSource = defaultVer > 0 ? BGS_ANIME : BGS_PHOTOGRAPHY;
    m_BackgroundSource = decodeBackgroundSource(settings.value(
                                                    SER_BACKGROUNDSOURCE,
                                                    static_cast<int>(defaultBackgroundSource)).toInt());
    m_BackgroundImageApi = settings.value(SER_BACKGROUNDIMAGEAPI).toString().trimmed();
    m_BackgroundImageLocalPath = settings.value(SER_BACKGROUNDIMAGELOCALPATH).toString();
    m_BackgroundOverlayOpacity = qBound(0,
                                        settings.value(SER_BACKGROUNDOVERLAYOPACITY,
                                                       DEFAULT_BACKGROUND_OVERLAY_OPACITY).toInt(),
                                        100);
    // This marker records an explicit background choice. The general settings
    // version cannot be used here because upgrades may never have configured a
    // background and must receive the same one-time choice as new installs.
    m_BackgroundSetupCompleted = settings.value(SER_BACKGROUNDSETUPCOMPLETED, false).toBool();
    int storedScreenCombinationMode;
    if (settings.contains(SER_SCREENCOMBINATIONMODE)) {
        storedScreenCombinationMode = settings.value(SER_SCREENCOMBINATIONMODE, SCM_FOLLOW_HOST).toInt();
    }
    else {
        const int legacyScreenMode = settings.value(SER_LEGACY_CUSTOMSCREENMODE, -1).toInt();
        const int legacyVddMode = settings.value(SER_LEGACY_CUSTOMVDDSCREENMODE, -1).toInt();

        if (legacyScreenMode != -1) {
            storedScreenCombinationMode = legacyScreenMode;
        }
        else {
            // The old VDD modes used 1 and 2 for primary and secondary layouts.
            // The unified Sunshine modes use 2 and 4 for those states.
            switch (legacyVddMode) {
            case 1:
                storedScreenCombinationMode = SCM_ENSURE_PRIMARY;
                break;
            case 2:
                storedScreenCombinationMode = SCM_ENSURE_SECONDARY;
                break;
            default:
                storedScreenCombinationMode = legacyVddMode;
                break;
            }
        }
    }

    if (storedScreenCombinationMode < SCM_FOLLOW_HOST ||
            storedScreenCombinationMode > SCM_ENSURE_SECONDARY) {
        storedScreenCombinationMode = SCM_FOLLOW_HOST;
    }
    screenCombinationMode = static_cast<ScreenCombinationMode>(storedScreenCombinationMode);

    // Perform default settings updates as required based on last default version
    if (defaultVer < 1) {
#ifdef Q_OS_DARWIN
        // Update window mode setting on macOS from full-screen (old default) to borderless windowed (new default)
        if (windowMode == WindowMode::WM_FULLSCREEN) {
            windowMode = WindowMode::WM_FULLSCREEN_DESKTOP;
        }
#endif
    }
    if (defaultVer < 2) {
        if (windowMode == WindowMode::WM_FULLSCREEN && WMUtils::isRunningWayland()) {
            windowMode = WindowMode::WM_FULLSCREEN_DESKTOP;
        }
    }

    // Fixup VCC value to the new settings format with codec and HDR separate
    if (videoCodecConfig == VCC_FORCE_HEVC_HDR_DEPRECATED) {
        videoCodecConfig = VCC_AUTO;
        enableHdr = true;
    }
}

void StreamingPreferences::setOverlayMenuPosition(OverlayMenuPosition position)
{
    if (overlayMenuPosition == position) {
        return;
    }

    overlayMenuPosition = position;
    emit overlayMenuPositionChanged();
}

StreamingPreferences::BackgroundSource StreamingPreferences::backgroundSource() const
{
    return m_BackgroundSource;
}

void StreamingPreferences::setBackgroundSource(BackgroundSource source)
{
    source = decodeBackgroundSource(static_cast<int>(source));

    const bool setupWasCompleted = m_BackgroundSetupCompleted;
    const bool changed = m_BackgroundSource != source;
    m_BackgroundSource = source;

    setBackgroundSetupCompleted(true);
    if (changed || !setupWasCompleted) {
        emit backgroundConfigurationChanged();
    }
}

QString StreamingPreferences::backgroundImageApi() const
{
    return m_BackgroundImageApi;
}

void StreamingPreferences::setBackgroundImageApi(const QString &apiUrl)
{
    const QString normalizedUrl = apiUrl.trimmed();
    const BackgroundSource source = normalizedUrl.isEmpty() ? BGS_PHOTOGRAPHY : BGS_API;
    const bool setupWasCompleted = m_BackgroundSetupCompleted;
    const bool changed = m_BackgroundImageApi != normalizedUrl ||
                         m_BackgroundSource != source;

    m_BackgroundImageApi = normalizedUrl;
    m_BackgroundSource = source;

    setBackgroundSetupCompleted(true);
    if (changed || !setupWasCompleted) {
        emit backgroundConfigurationChanged();
    }
}

QString StreamingPreferences::backgroundImageLocalPath() const
{
    return m_BackgroundImageLocalPath;
}

void StreamingPreferences::setBackgroundImageLocalPath(const QString &path)
{
    const QString normalizedPath = path.trimmed();
    const BackgroundSource source = normalizedPath.isEmpty() ? BGS_PHOTOGRAPHY : BGS_LOCAL;
    const bool setupWasCompleted = m_BackgroundSetupCompleted;
    const bool changed = m_BackgroundImageLocalPath != normalizedPath ||
                         m_BackgroundSource != source;

    m_BackgroundImageLocalPath = normalizedPath;
    m_BackgroundSource = source;

    setBackgroundSetupCompleted(true);
    if (changed || !setupWasCompleted) {
        emit backgroundConfigurationChanged();
    }
}

int StreamingPreferences::backgroundOverlayOpacity() const
{
    return m_BackgroundOverlayOpacity;
}

void StreamingPreferences::setBackgroundOverlayOpacity(int opacity)
{
    opacity = qBound(0, opacity, 100);
    if (m_BackgroundOverlayOpacity == opacity) {
        return;
    }

    m_BackgroundOverlayOpacity = opacity;
    emit backgroundOverlayOpacityChanged();
}

bool StreamingPreferences::backgroundSetupCompleted() const
{
    return m_BackgroundSetupCompleted;
}

void StreamingPreferences::setBackgroundSetupCompleted(bool completed)
{
    if (m_BackgroundSetupCompleted == completed) {
        return;
    }

    m_BackgroundSetupCompleted = completed;
    emit backgroundSetupCompletedChanged();
}

void StreamingPreferences::resetBackgroundConfiguration()
{
    const bool setupWasCompleted = m_BackgroundSetupCompleted;
    const bool overlayOpacityChanged =
            m_BackgroundOverlayOpacity != DEFAULT_BACKGROUND_OVERLAY_OPACITY;
    const bool changed = m_BackgroundSource != BGS_PHOTOGRAPHY ||
                         !m_BackgroundImageApi.isEmpty() ||
                         !m_BackgroundImageLocalPath.isEmpty();

    m_BackgroundSource = BGS_PHOTOGRAPHY;
    m_BackgroundImageApi.clear();
    m_BackgroundImageLocalPath.clear();
    m_BackgroundOverlayOpacity = DEFAULT_BACKGROUND_OVERLAY_OPACITY;

    setBackgroundSetupCompleted(true);
    if (overlayOpacityChanged) {
        emit backgroundOverlayOpacityChanged();
    }
    if (changed || !setupWasCompleted) {
        emit backgroundConfigurationChanged();
    }
}

bool StreamingPreferences::retranslate()
{
    static QTranslator* translator = nullptr;

#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0)
    if (m_QmlEngine != nullptr) {
        // Dynamic retranslation is not supported until Qt 5.10
        return false;
    }
#endif

    QTranslator* newTranslator = new QTranslator();
    QString languageSuffix = getSuffixFromLanguage(language);

    // Remove the old translator, even if we can't load a new one.
    // Otherwise we'll be stuck with the old translated values instead
    // of defaulting to English.
    if (translator != nullptr) {
        QCoreApplication::removeTranslator(translator);
        delete translator;
        translator = nullptr;
    }

    if (newTranslator->load(QString(":/languages/qml_") + languageSuffix)) {
        qInfo() << "Successfully loaded translation for" << languageSuffix;

        translator = newTranslator;
        QCoreApplication::installTranslator(translator);
    }
    else {
        qInfo() << "No translation available for" << languageSuffix;
        delete newTranslator;
    }

    if (m_QmlEngine != nullptr) {
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
        // This is a dynamic retranslation from the settings page.
        // We have to kick the QML engine into reloading our text.
        m_QmlEngine->retranslate();
#else
        // Unreachable below Qt 5.10 due to the check above
        Q_ASSERT(false);
#endif
    }
    else {
        // This is a translation from a non-QML context, which means
        // it is probably app startup. There's nothing to refresh.
    }

    return true;
}

QString StreamingPreferences::getSuffixFromLanguage(StreamingPreferences::Language lang)
{
    switch (lang)
    {
    case LANG_DE:
        return "de";
    case LANG_EN:
        return "en";
    case LANG_FR:
        return "fr";
    case LANG_ZH_CN:
        return "zh_CN";
    case LANG_NB_NO:
        return "nb_NO";
    case LANG_RU:
        return "ru";
    case LANG_ES:
        return "es";
    case LANG_JA:
        return "ja";
    case LANG_VI:
        return "vi";
    case LANG_TH:
        return "th";
    case LANG_KO:
        return "ko";
    case LANG_HU:
        return "hu";
    case LANG_NL:
        return "nl";
    case LANG_SV:
        return "sv";
    case LANG_TR:
        return "tr";
    case LANG_UK:
        return "uk";
    case LANG_ZH_TW:
        return "zh_TW";
    case LANG_PT:
        return "pt";
    case LANG_PT_BR:
        return "pt_BR";
    case LANG_EL:
        return "el";
    case LANG_IT:
        return "it";
    case LANG_HI:
        return "hi";
    case LANG_PL:
        return "pl";
    case LANG_CS:
        return "cs";
    case LANG_HE:
        return "he";
    case LANG_CKB:
        return "ckb";
    case LANG_LT:
        return "lt";
    case LANG_ET:
        return "et";
    case LANG_BG:
        return "bg";
    case LANG_EO:
        return "eo";
    case LANG_TA:
        return "ta";
    case LANG_AUTO:
    default:
        return QLocale::system().name();
    }
}

void StreamingPreferences::save()
{
    QSettings settings;

    settings.setValue(SER_WIDTH, width);
    settings.setValue(SER_HEIGHT, height);
    settings.setValue(SER_FPS, fps);
    settings.setValue(SER_BITRATE, bitrateKbps);
    settings.setValue(SER_AUTOADJUSTBITRATE, autoAdjustBitrate);
    settings.setValue(SER_SUNSHINEABR, enableSunshineAbr);
    settings.setValue(SER_IGNORE_ASPECT_RATIO, ignoreAspectRatio);
    settings.setValue(SER_VSYNC, enableVsync);
    settings.setValue(SER_GAMEOPTS, gameOptimizations);
    settings.setValue(SER_HOSTAUDIO, playAudioOnHost);
    settings.setValue(SER_MULTICONT, multiController);
    settings.setValue(SER_MDNS, enableMdns);
    settings.setValue(SER_QUITAPPAFTER, quitAppAfter);
    settings.setValue(SER_ABSMOUSEMODE, absoluteMouseMode);
    settings.setValue(SER_SHOWLOCALCURSOR, showLocalCursor);
    settings.setValue(SER_ABSTOUCHMODE, absoluteTouchMode);
    settings.setValue(SER_NATIVETOUCHPAD, enableNativeTouchpad);
    settings.setValue(SER_DUALSENSEHAPTICSMODE, dualSenseHapticsMode);
    settings.setValue(SER_FRAMEPACING, framePacing);
    settings.setValue(SER_VIDEOENHANCEMENT, videoEnhancement);
    settings.setValue(SER_STREAMRESOLUTIONSCALE, streamResolutionScale);
    settings.setValue(SER_STREAMRESOLUTIONSCALERATIO, streamResolutionScaleRatio);
    settings.setValue(SER_REMOTERESOLUTION, remoteResolution);
    settings.setValue(SER_REMOTEWIDTH, remoteResolutionWidth);
    settings.setValue(SER_REMOTEHEIGHT, remoteResolutionHeight);
    settings.setValue(SER_REMOTEFPS, remoteFps);
    settings.setValue(SER_REMOTEFPSRATE, remoteFpsRate);
    settings.setValue(SER_CONNWARNINGS, connectionWarnings);
    settings.setValue(SER_CONFWARNINGS, configurationWarnings);
    settings.setValue(SER_RICHPRESENCE, richPresence);
    settings.setValue(SER_GAMEPADMOUSE, gamepadMouse);
    settings.setValue(SER_PACKETSIZE, packetSize);
    settings.setValue(SER_DETECTNETBLOCKING, detectNetworkBlocking);
    settings.setValue(SER_SHOWPERFOVERLAY, showPerformanceOverlay);
    settings.setValue(SER_AUDIOCFG, static_cast<int>(audioConfig));
    settings.setValue(SER_HDR, enableHdr);
    settings.setValue(SER_HDRMODE, static_cast<int>(hdrMode));
    settings.setValue(SER_HDRBRIGHTNESSMODE, static_cast<int>(hdrBrightnessMode));
    settings.setValue(SER_HDRMAXBRIGHTNESS, hdrMaxBrightness);
    settings.setValue(SER_HDRMINBRIGHTNESS, hdrMinBrightness);
    settings.setValue(SER_HDRMAXAVERAGEBRIGHTNESS, hdrMaxAverageBrightness);
    settings.setValue(SER_YUV444, enableYUV444);
    settings.setValue(SER_VIDEOCFG, static_cast<int>(videoCodecConfig));
    settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
    settings.setValue(SER_RENDERER, static_cast<int>(rendererSelection));
    settings.setValue(SER_WINDOWMODE, static_cast<int>(windowMode));
    settings.setValue(SER_UIDISPLAYMODE, static_cast<int>(uiDisplayMode));
    settings.setValue(SER_REMEMBERWINDOWPOSITION, rememberWindowPosition);
    settings.setValue(SER_LANGUAGE, static_cast<int>(language));
    settings.setValue(SER_BACKGROUNDSOURCE, static_cast<int>(m_BackgroundSource));
    settings.setValue(SER_BACKGROUNDIMAGEAPI, m_BackgroundImageApi);
    settings.setValue(SER_BACKGROUNDIMAGELOCALPATH, m_BackgroundImageLocalPath);
    settings.setValue(SER_BACKGROUNDOVERLAYOPACITY, m_BackgroundOverlayOpacity);
    settings.setValue(SER_BACKGROUNDSETUPCOMPLETED, m_BackgroundSetupCompleted);
    settings.setValue(SER_DEFAULTVER, CURRENT_DEFAULT_VER);
    settings.setValue(SER_SWAPMOUSEBUTTONS, swapMouseButtons);
    settings.setValue(SER_SWAPWINALTKEYS, swapWinAltKeys);
    settings.setValue(SER_MUTEONFOCUSLOSS, muteOnFocusLoss);
    settings.setValue(SER_BACKGROUNDGAMEPAD, backgroundGamepad);
    settings.setValue(SER_GAMEPADQUITCOMBO, static_cast<int>(gamepadQuitCombo));
    settings.setValue(SER_REVERSESCROLL, reverseScrollDirection);
    settings.setValue(SER_SWAPFACEBUTTONS, swapFaceButtons);
    settings.setValue(SER_CAPTURESYSKEYS, captureSysKeysMode);
    settings.setValue(SER_KEEPAWAKE, keepAwake);
    settings.setValue(SER_SCREENCOMBINATIONMODE, static_cast<int>(screenCombinationMode));
    settings.remove(SER_LEGACY_CUSTOMSCREENMODE);
    settings.remove(SER_LEGACY_CUSTOMVDDSCREENMODE);
    settings.setValue(SER_MICROPHONE, enableMicrophone);
    settings.setValue(SER_OVERLAYMENUPLACEMENT, static_cast<int>(overlayMenuPosition));
    settings.setValue(SER_AUTOUPDATECHECK, autoUpdateCheck);
}

int StreamingPreferences::getDefaultBitrate(int width, int height, int fps, bool yuv444)
{
    // Don't scale bitrate linearly beyond 60 FPS. It's definitely not a linear
    // bitrate increase for frame rate once we get to values that high.
    float frameRateFactor = (fps <= 60 ? fps : (qSqrt(fps / 60.f) * 60.f)) / 30.f;

    // TODO: Collect some empirical data to see if these defaults make sense.
    // We're just using the values that the Shield used, as we have for years.
    static const struct resTable {
        int pixels;
        int factor;
    } resTable[] {
        { 640 * 360, 1 },
        { 854 * 480, 2 },
        { 1280 * 720, 5 },
        { 1920 * 1080, 10 },
        { 2560 * 1440, 20 },
        { 3840 * 2160, 40 },
        { -1, -1 },
    };

    // Calculate the resolution factor by linear interpolation of the resolution table
    float resolutionFactor;
    int pixels = width * height;
    for (int i = 0;; i++) {
        if (pixels == resTable[i].pixels) {
            // We can bail immediately for exact matches
            resolutionFactor = resTable[i].factor;
            break;
        }
        else if (pixels < resTable[i].pixels) {
            if (i == 0) {
                // Never go below the lowest resolution entry
                resolutionFactor = resTable[i].factor;
            }
            else {
                // Interpolate between the entry greater than the chosen resolution (i) and the entry less than the chosen resolution (i-1)
                resolutionFactor = ((float)(pixels - resTable[i-1].pixels) / (resTable[i].pixels - resTable[i-1].pixels)) * (resTable[i].factor - resTable[i-1].factor) + resTable[i-1].factor;
            }
            break;
        }
        else if (resTable[i].pixels == -1) {
            // Never go above the highest resolution entry
            resolutionFactor = resTable[i-1].factor;
            break;
        }
    }

    if (yuv444) {
        // This is rough estimation based on the fact that 4:4:4 doubles the amount of raw YUV data compared to 4:2:0
        resolutionFactor *= 2;
    }

    return qRound(resolutionFactor * frameRateFactor) * 1000;
}
