// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0

#include "qthost.h"
#include "autoupdaterdialog.h"
#include "displaywidget.h"
#include "logwindow.h"
#include "mainwindow.h"
#include "qtprogresscallback.h"
#include "qtutils.h"
#include "qtwindowinfo.h"
#include "settingswindow.h"
#include "setupwizarddialog.h"
#include "svgwidget.h"

#include "core/achievements.h"
#include "core/bus.h"
#include "core/cdrom.h"
#include "core/cheats.h"
#include "core/core.h"
#include "core/core_private.h"
#include "core/fullscreenui.h"
#include "core/fullscreenui_widgets.h"
#include "core/game_database.h"
#include "core/game_list.h"
#include "core/gdb_server.h"
#include "core/gpu.h"
#include "core/gpu_backend.h"
#include "core/gpu_hw_texture_cache.h"
#include "core/host.h"
#include "core/performance_counters.h"
#include "core/system.h"
#include "core/system_private.h"
#include "core/video_presenter.h"
#include "core/video_thread.h"
#include "core/video_thread_private.h"

#include "common/assert.h"
#include "common/crash_handler.h"
#include "common/error.h"
#include "common/file_system.h"
#include "common/log.h"
#include "common/path.h"
#include "common/scoped_guard.h"
#include "common/string_util.h"
#include "common/threading.h"

#include "util/http_cache.h"
#include "util/http_downloader.h"
#include "util/imgui_manager.h"
#include "util/ini_settings_interface.h"
#include "util/input_manager.h"
#include "util/postprocessing.h"
#include "util/translation.h"

#include "scmversion/scmversion.h"

#include "fmt/format.h"

#include <QtCore/QCoreApplication>
#include <QtCore/QDateTime>
#include <QtCore/QDebug>
#include <QtCore/QEventLoop>
#include <QtCore/QFile>
#include <QtCore/QTimer>
#include <QtCore/QTranslator>
#include <QtCore/QtPlugin>
#include <QtGui/QClipboard>
#include <QtGui/QKeyEvent>
#include <algorithm>
#include <cmath>
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <memory>

#if defined(_WIN32)
#include "common/windows_headers.h"
#include <objbase.h> // CoInitializeEx
#elif defined(__APPLE__)
#include <unistd.h>
#endif

#include "moc_qthost.cpp"

using namespace Qt::Literals::StringLiterals;

LOG_CHANNEL(Host);

#if 0
// Mac application menu strings
QT_TRANSLATE_NOOP("MAC_APPLICATION_MENU", "Services")
QT_TRANSLATE_NOOP("MAC_APPLICATION_MENU", "Hide %1")
QT_TRANSLATE_NOOP("MAC_APPLICATION_MENU", "Hide Others")
QT_TRANSLATE_NOOP("MAC_APPLICATION_MENU", "Show All")
QT_TRANSLATE_NOOP("MAC_APPLICATION_MENU", "Preferences...")
QT_TRANSLATE_NOOP("MAC_APPLICATION_MENU", "Quit %1")
QT_TRANSLATE_NOOP("MAC_APPLICATION_MENU", "About %1")
#endif

Q_IMPORT_PLUGIN(SVGIconEnginePlugin);
Q_IMPORT_PLUGIN(SVGImageHandlerPlugin);

static constexpr u32 SETTINGS_SAVE_DELAY = 1000;

/// Interval at which the controllers are polled when the system is not active.
static constexpr int BACKGROUND_CONTROLLER_POLLING_INTERVAL_WITH_DEVICES = 100;
static constexpr int BACKGROUND_CONTROLLER_POLLING_INTERVAL_WITHOUT_DEVICES = 1000;

/// Poll at half the vsync rate for FSUI to reduce the chance of getting a press+release in the same frame.
static constexpr int FULLSCREEN_UI_CONTROLLER_POLLING_INTERVAL = 8;

/// Poll at 10ms when downloads are active to ensure the speed is not impacted.
static constexpr int DOWNLOAD_CONTROLLER_POLLING_INTERVAL = 10;

/// Poll at 1ms when running GDB server. We can get rid of this once we move networking to its own thread.
static constexpr int GDB_SERVER_POLLING_INTERVAL = 1;

//////////////////////////////////////////////////////////////////////////
// Local function declarations
//////////////////////////////////////////////////////////////////////////
namespace QtHost {
static bool VeryEarlyProcessStartup();
static bool PerformEarlyHardwareChecks();
static bool EarlyProcessStartup();
static void ProcessShutdown();
static void MessageOutputHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg);
static void RegisterTypes();
static bool InitializeFoldersAndConfig(Error* error);
static QString GetAppIconPath();
static bool LoadResources(Error* error);
static void SaveSettings();
static bool RunSetupWizard();
static void ApplyMigrations();
static void UpdateFontOrder(std::string_view language);
static void UpdateApplicationLocale(std::string_view language);
static std::string_view GetSystemLanguage();
static void InitializeEarlyConsole();
static void HookSignals();
static void PrintCommandLineVersion();
static void PrintCommandLineHelp(const char* progname);
static bool ParseCommandLineParametersAndInitializeConfig(QApplication& app,
                                                          std::shared_ptr<SystemBootParameters>& boot_params);

#ifdef __linux__
static void AdjustQtEnvironmentVariables();
static bool IsRunningOnWayland();
static void ApplyWaylandWorkarounds();
static bool ParseDesktopFileExecPath(const std::string& desktop_file_path, std::string* out_exec_path);
static bool CreateDesktopFile(const std::string& app_path, const std::string& desktop_file_path, Error* error);
static void CheckDesktopFile();
#endif

namespace {
struct State
{
  std::unique_ptr<QTimer> settings_save_timer;
  std::vector<QTranslator*> translators;
  QIcon app_icon;
  QLocale app_locale;
  std::once_flag roboto_font_once_flag;
  QStringList roboto_font_families;
  std::once_flag fixed_font_once_flag;
  QStringList fixed_font_families;
  QFont fixed_font;
  bool batch_mode = false;
  bool nogui_mode = false;
  bool start_fullscreen_ui = false;
  bool start_fullscreen_ui_fullscreen = false;
  bool run_setup_wizard = false;
  bool cleanup_after_update = false;

#ifdef __linux__
  bool wayland_workarounds = false;
#endif
};
} // namespace

ALIGN_TO_CACHE_LINE static State s_state;

} // namespace QtHost

CoreThread* g_core_thread;

CoreThread::CoreThread()
  : QThread(), m_ui_thread(QThread::currentThread()),
    m_input_device_list_model(std::make_unique<InputDeviceListModel>())
{
  // owned by itself
  moveToThread(this);
}

CoreThread::~CoreThread() = default;

void QtHost::RegisterTypes()
{
  // Register any standard types we need elsewhere
  qRegisterMetaType<std::optional<WindowInfo>>("std::optional<WindowInfo>()");
  qRegisterMetaType<std::optional<bool>>();
  qRegisterMetaType<std::function<void()>>("std::function<void()>");
  qRegisterMetaType<std::shared_ptr<SystemBootParameters>>();
  qRegisterMetaType<const GameList::Entry*>();
  qRegisterMetaType<RenderAPI>("RenderAPI");
  qRegisterMetaType<GPURenderer>("GPURenderer");
  qRegisterMetaType<InputBindingKey>("InputBindingKey");
  qRegisterMetaType<InputDeviceListModel::Device>("InputDeviceListModel::Device");
  qRegisterMetaType<std::string>("std::string");
  qRegisterMetaType<std::vector<std::pair<std::string, std::string>>>(
    "std::vector<std::pair<std::string, std::string>>");
}

bool QtHost::PerformEarlyHardwareChecks()
{
  Error error;
  const bool okay = Core::PerformEarlyHardwareChecks(&error);
  if (okay && !error.IsValid()) [[likely]]
    return true;

  if (okay)
  {
    QMessageBox::warning(nullptr, QStringLiteral("Hardware Check Warning"),
                         QString::fromStdString(error.GetDescription()));
  }
  else
  {
    QMessageBox::critical(nullptr, QStringLiteral("Hardware Check Failed"),
                          QString::fromStdString(error.GetDescription()));
  }

  return okay;
}

bool QtHost::VeryEarlyProcessStartup()
{
  CrashHandler::Install(&Bus::CleanupMemoryMap);

#if defined(_WIN32)
  // Ensure COM is initialized before Qt gets a chance to do it, since this could change in the future.
  HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
  if (FAILED(hr)) [[unlikely]]
  {
    MessageBoxA(nullptr, fmt::format("CoInitializeEx failed: 0x{:08X}", hr).c_str(), "Error", MB_ICONERROR);
    return false;
  }
#elif defined(__linux__)
  AdjustQtEnvironmentVariables();
#endif

  QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
  return true;
}

bool QtHost::EarlyProcessStartup()
{
  // Redirect Qt errors.
  qInstallMessageHandler(MessageOutputHandler);

  // Set application details.
  // This is critical for Linux to show the correct application name in the task switcher, since it appears
  // to uses the application name to search for desktop files with the corresponding StartupWMClass.
  QApplication::setApplicationName(u"duckstation-qt"_s);
  QApplication::setApplicationVersion(QString::fromUtf8(g_scm_version_str));
  QApplication::setOrganizationName(u"Stenzek"_s);
  QApplication::setOrganizationDomain(u"duckstation.org"_s);
  QApplication::setDesktopFileName(u"org.duckstation.DuckStation"_s);

  Error error;
  if (!Core::ProcessStartup(&error)) [[unlikely]]
  {
    QMessageBox::critical(nullptr, QStringLiteral("Process Startup Failed"),
                          QString::fromStdString(error.GetDescription()));
    return false;
  }

  // allow us to override standard qt icons as well
  QStringList icon_theme_search_paths = QIcon::themeSearchPaths();
  if (!icon_theme_search_paths.contains(u":/icons"_s))
    icon_theme_search_paths.emplace_back(u":/icons"_s);
  icon_theme_search_paths.emplace_back(u":/standard-icons"_s);
  QIcon::setThemeSearchPaths(icon_theme_search_paths);
  QIcon::setThemeName("monochrome");
  return true;
}

void QtHost::ProcessShutdown()
{
  Core::ProcessShutdown();

  // Ensure log is flushed.
  Log::SetFileOutputParams(false, nullptr);

  // Clean up CoInitializeEx() from VeryEarlyProcessStartup().
#ifdef _WIN32
  CoUninitialize();
#endif
}

void QtHost::MessageOutputHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg)
{
  const char* function = context.function ? context.function : "<unknown>";
  const std::string smsg = msg.toStdString();
  switch (type)
  {
    case QtDebugMsg:
      GENERIC_LOG(Log::Channel::Host, Log::Level::Debug, Log::Color::Default, "qDebug({}): {}", function, smsg);
      break;
    case QtInfoMsg:
      GENERIC_LOG(Log::Channel::Host, Log::Level::Info, Log::Color::Default, "qInfo({}): {}", function, smsg);
      break;
    case QtWarningMsg:
      GENERIC_LOG(Log::Channel::Host, Log::Level::Warning, Log::Color::Default, "qWarning({}): {}", function, smsg);
      break;
    case QtCriticalMsg:
      GENERIC_LOG(Log::Channel::Host, Log::Level::Error, Log::Color::Default, "qCritical({}): {}", function, smsg);
      break;
    case QtFatalMsg:
      GENERIC_LOG(Log::Channel::Host, Log::Level::Error, Log::Color::Default, "qFatal({}): {}", function, smsg);
      Y_OnPanicReached(smsg.c_str(), function, context.file ? context.file : "<unknown>", context.line);
    default:
      GENERIC_LOG(Log::Channel::Host, Log::Level::Error, Log::Color::Default, "<unknown>({}): {}", function, smsg);
      break;
  }
}

bool QtHost::InBatchMode()
{
  return s_state.batch_mode;
}

bool QtHost::InNoGUIMode()
{
  return s_state.nogui_mode;
}

bool QtHost::IsDisplayWidgetContainerNeeded()
{
#ifdef __linux__
  return s_state.wayland_workarounds;
#else
  return false;
#endif
}

#ifdef __linux__

void QtHost::AdjustQtEnvironmentVariables()
{
  // Disable screensaver inhibit if running on gamescope.
  const char* desktop = std::getenv("XDG_CURRENT_DESKTOP");
  if (!desktop)
    desktop = std::getenv("XDG_SESSION_DESKTOP");

  std::fprintf(stderr, "XDG_SESSION_DESKTOP=%s\n", desktop ? desktop : "null");

  if (!desktop || std::strstr(desktop, "gamescope"))
  {
    INFO_LOG("Missing XDG_CURRENT_DESKTOP ({}) or running under gamescope, disabling screensaver inhibit.",
             desktop ? desktop : "null");
    QtHost::DisableScreensaverInhibit();
  }

  if (desktop && (std::strstr(desktop, "KDE") == 0 || std::strstr(desktop, "GNOME") == 0))
  {
    const char* platform_theme = std::getenv("QT_QPA_PLATFORMTHEME");
    if (platform_theme)
    {
      std::fprintf(stderr, "QT_QPA_PLATFORMTHEME=%s, not overridding\n", platform_theme);
    }
    else
    {
      std::fputs("Enabling xdg-desktop-portal platform theme.\n", stderr);
      setenv("QT_QPA_PLATFORMTHEME", "xdgdesktopportal", true);
    }
  }
}

bool QtHost::IsRunningOnWayland()
{
  const QString platform_name = QGuiApplication::platformName();
  return (platform_name == "wayland"_L1);
}

void QtHost::ApplyWaylandWorkarounds()
{
  if (!IsRunningOnWayland())
  {
    std::fputs("Wayland not detected, not applying workarounds.\n", stderr);
    return;
  }

  if (const char* desktop = std::getenv("XDG_SESSION_DESKTOP"); desktop && std::strcmp(desktop, "KDE") == 0)
  {
    std::fputs("Wayland with KDE detected, not applying Wayland workarounds.\n", stderr);
  }
  else
  {
    std::fputs("Wayland with non-KDE desktop detected, applying Wayland workarounds.\n"
               "Don't complain when things break.\n",
               stderr);

    // When rendering fullscreen or to a separate window on Wayland, because we take over the surface we need
    // to wrap the widget in a container because GNOME is stupid and refuses to ever support server-side
    // decorations. There's no sign of this ever changing. Fuck Wayland.
    s_state.wayland_workarounds = true;

    // On Wayland, turning any window into a native window causes DPI scaling to break, as well as window
    // updates, creating a complete mess of a window. Setting this attribute isn't ideal, since you'd think
    // that setting WA_DontCreateNativeAncestors on the widget would be sufficient, but apparently not.
    QGuiApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings, true);
  }
}

bool QtHost::ParseDesktopFileExecPath(const std::string& desktop_file_path, std::string* out_exec_path)
{
  std::optional<std::string> contents = FileSystem::ReadFileToString(desktop_file_path.c_str(), nullptr);
  if (!contents.has_value())
    return false;

  // Parse line by line looking for Exec=
  std::istringstream stream(contents.value());
  std::string line_str;

  while (std::getline(stream, line_str))
  {
    const std::string_view line = StringUtil::StripWhitespace(line_str);
    if (StringUtil::StartsWithNoCase(line, "Exec="))
    {
      std::string_view exec_value = line.substr(5); // Skip "Exec="

      // The Exec line may have arguments after the path, extract just the path.
      // Handle quoted paths.
      if (!exec_value.empty() && exec_value[0] == '"')
      {
        const size_t end_quote = exec_value.find('"', 1);
        if (end_quote != std::string::npos)
          *out_exec_path = exec_value.substr(1, end_quote - 1);
        else
          *out_exec_path = exec_value.substr(1);
      }
      else
      {
        // Unquoted path - take until first space or end
        const size_t space_pos = exec_value.find(' ');
        if (space_pos != std::string::npos)
          *out_exec_path = exec_value.substr(0, space_pos);
        else
          *out_exec_path = exec_value;
      }

      return true;
    }
  }

  return false;
}

bool QtHost::CreateDesktopFile(const std::string& app_path, const std::string& desktop_file_path, Error* error)
{
  // Ensure the applications directory exists.
  const std::string applications_directory = std::string(Path::GetDirectory(desktop_file_path));
  if (!FileSystem::EnsureDirectoryExists(applications_directory.c_str(), false, error))
    return false;

  // Because the icon is inside the AppImage, we have to copy it as well.
  // This should resolve to $XDG_DATA_DIR/icons or ~/.local/share/icons.
  const std::string icon_file_name = Path::ReplaceExtension(Path::GetFileName(desktop_file_path), "png");
  const std::string icons_directory = Path::Canonicalize(Path::BuildRelativePath(desktop_file_path, "../icons"));
  const std::string icon_path = Path::Combine(icons_directory, icon_file_name);
  if (!FileSystem::FileExists(icon_path.c_str()))
  {
    if (!FileSystem::EnsureDirectoryExists(icons_directory.c_str(), false, error) ||
        !FileSystem::CopyFilePath(GetAppIconPath().toUtf8().constData(), icon_path.c_str(), false, error))
    {
      Error::AddPrefix(error, "Failed to copy application icon: ");
      return false;
    }
  }

  // Create the .desktop file content.
  std::string desktop_content = fmt::format(R"([Desktop Entry]
Type=Application
Name=DuckStation
GenericName=PlayStation 1 Emulator
Comment=Fast PlayStation 1 emulator
Icon={}
Exec="{}" %f
Terminal=false
Categories=Game;Emulator;Qt
StartupNotify=true
StartupWMClass=duckstation-qt
)",
                                            icon_path, app_path, desktop_file_path);

  if (!FileSystem::WriteStringToFile(desktop_file_path.c_str(), desktop_content, error))
    return false;

  // Make the desktop file executable (required by some desktop environments).
  if (!FileSystem::SetPathExecutable(desktop_file_path.c_str(), true, error))
  {
    FileSystem::DeleteFile(desktop_file_path.c_str());
    return false;
  }

  INFO_LOG("Created desktop file at: {}", desktop_file_path);
  return true;
}

void QtHost::CheckDesktopFile()
{
  static constexpr const char* DESKTOP_FILE_NAME = "applications/org.duckstation.DuckStation.desktop";
  static constexpr const char* CONFIG_SECTION = "Main";
  static constexpr const char* CONFIG_KEY = "NoDesktopFile";

  // AppImage sets the APPIMAGE environment variable to the actual executable path.
  // Without this, we'd get the path to the mounted filesystem, which changes on each run.
  const char* appimage_path = std::getenv("APPIMAGE");
  if (!appimage_path || !FileSystem::FileExists(appimage_path))
  {
    ERROR_LOG("Not running from AppImage, skipping desktop file check.");
    return;
  }

  std::string application_path = Path::RealPath(appimage_path);
  if (application_path.empty())
    return;

  std::string desktop_file_path;
  if (const char* xdg_data_home = std::getenv("XDG_DATA_HOME"); xdg_data_home && xdg_data_home[0] != '\0')
  {
    desktop_file_path = Path::Combine(xdg_data_home, DESKTOP_FILE_NAME);
  }
  else
  {
    const char* home = std::getenv("HOME");
    if (!home)
      return;

    desktop_file_path = fmt::format("{}/.local/share/{}", home, DESKTOP_FILE_NAME);
  }

  const auto msgbox_title = u"DuckStation"_s;

  if (!FileSystem::FileExists(desktop_file_path.c_str()))
  {
    // Desktop file doesn't exist - ask if user wants to create it.
    // Only prompt once per installation by storing a flag in settings.
    if (Core::GetBaseBoolSettingValue(CONFIG_SECTION, CONFIG_KEY, false))
      return;

    bool accepted, ignore_future;
    {
      const std::unique_ptr<QMessageBox> msgbox(QtUtils::NewMessageBox(
        nullptr, QMessageBox::Question, msgbox_title,
        QCoreApplication::translate(
          "QtHost", "Would you like to create a launcher shortcut for DuckStation?\n\n"
                    "This will add DuckStation to your application menu, allowing you to launch it more easily.\n\n"
                    "The shortcut will be created at:\n%1")
          .arg(QString::fromStdString(desktop_file_path)),
        QMessageBox::Yes | QMessageBox::No, QMessageBox::NoButton, false));
      QCheckBox* const ignore_cb =
        new QCheckBox(QCoreApplication::translate("QtHost", "Don't ask again"), msgbox.get());
      msgbox->setCheckBox(ignore_cb);
      msgbox->setWindowIcon(GetAppIcon());

      accepted = (msgbox->exec() == QMessageBox::Yes);
      ignore_future = ignore_cb->isChecked();
    }

    if (!accepted)
    {
      if (ignore_future)
      {
        Core::SetBaseBoolSettingValue(CONFIG_SECTION, CONFIG_KEY, true);
        QtHost::SaveSettings();
      }

      return;
    }

    Error error;
    if (!CreateDesktopFile(application_path, desktop_file_path, &error))
    {
      QMessageBox::critical(g_main_window, msgbox_title,
                            QCoreApplication::translate("QtHost", "Failed to create launcher shortcut shortcut:\n%1")
                              .arg(QString::fromStdString(error.GetDescription())));
    }
    else
    {
      QMessageBox::information(g_main_window, msgbox_title,
                               QCoreApplication::translate("QtHost",
                                                           "Launcher shortcut created successfully.\n\n"
                                                           "You can find DuckStation in your application menu."));
    }
  }
  else
  {
    // Desktop file exists - check if the path matches.
    std::string existing_exec_path;
    if (!ParseDesktopFileExecPath(desktop_file_path, &existing_exec_path))
    {
      WARNING_LOG("Failed to parse existing desktop file: {}", desktop_file_path);
      return;
    }

    // Normalize paths for comparison.
    const std::string normalized_existing = Path::RealPath(existing_exec_path);
    if (application_path == normalized_existing)
      return;

    INFO_LOG("Desktop file path mismatch: current='{}', existing='{}'", application_path, normalized_existing);

    const QMessageBox::StandardButton result = QMessageBox::question(
      g_main_window, u"DuckStation"_s,
      QCoreApplication::translate("QtHost", "The existing launcher shortcut points to a different location:\n\n"
                                            "Current: %1\n"
                                            "Shortcut: %2\n\n"
                                            "Would you like to update the shortcut to point to the current location?")
        .arg(QString::fromStdString(application_path))
        .arg(QString::fromStdString(existing_exec_path)),
      QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);

    if (result == QMessageBox::Yes)
    {
      Error error;

      // Delete the old desktop file first.
      if (!FileSystem::DeleteFile(desktop_file_path.c_str(), &error))
      {
        QMessageBox::critical(g_main_window, msgbox_title,
                              QCoreApplication::translate("QtHost", "Failed to remove old launcher shortcut:\n%1")
                                .arg(QString::fromStdString(error.GetDescription())));
        return;
      }

      // Create the new desktop file.
      if (!CreateDesktopFile(application_path, desktop_file_path, &error))
      {
        QMessageBox::critical(g_main_window, msgbox_title,
                              QCoreApplication::translate("QtHost", "Failed to create updated launcher shortcut:\n%1")
                                .arg(QString::fromStdString(error.GetDescription())));
      }
      else
      {
        QMessageBox::information(g_main_window, msgbox_title,
                                 QCoreApplication::translate("QtHost", "Launcher shortcut updated successfully."));
      }
    }
  }
}

#endif // __linux__

QString QtHost::GetAppNameAndVersion()
{
  return QStringLiteral("DuckStation %1").arg(QLatin1StringView(g_scm_version_str));
}

QString QtHost::GetAppConfigSuffix()
{
#if defined(_DEVEL)
  return QStringLiteral(" [Devel]");
#elif defined(_DEBUGFAST)
  return QStringLiteral(" [DebugFast]");
#elif defined(_DEBUG)
  return QStringLiteral(" [Debug]");
#else
  return QString();
#endif
}

QString QtHost::GetResourcesBasePath()
{
  return QString::fromStdString(EmuFolders::Resources);
}

bool QtHost::SaveGameSettings(SettingsInterface* sif, bool delete_if_empty)
{
  INISettingsInterface* ini = static_cast<INISettingsInterface*>(sif);
  Error error;

  // if there's no keys, just toss the whole thing out
  if (delete_if_empty && ini->IsEmpty())
  {
    INFO_LOG("Removing empty gamesettings ini {}", Path::GetFileName(ini->GetPath()));

    // prevent empty ini from being saved if it was modified and transitioned back to empty
    ini->SetDirty(false);

    // grab the settings lock while we're writing the file, that way the CPU thread doesn't try
    // to read it at the same time.
    const auto lock = Core::GetSettingsLock();

    if (FileSystem::FileExists(ini->GetPath().c_str()) && !FileSystem::DeleteFile(ini->GetPath().c_str(), &error))
    {
      Host::ReportErrorAsync(
        TRANSLATE_SV("QtHost", "Error"),
        fmt::format(TRANSLATE_FS("QtHost", "An error occurred while deleting empty game settings:\n{}"),
                    error.GetDescription()));
      return false;
    }

    return true;
  }

  // clean unused sections, stops the file being bloated
  sif->RemoveEmptySections();

  // see above
  const auto lock = Core::GetSettingsLock();

  if (!ini->Save(&error, Settings::GetSectionSaveOrder()))
  {
    Host::ReportErrorAsync(
      TRANSLATE_SV("QtHost", "Error"),
      fmt::format(TRANSLATE_FS("QtHost", "An error occurred while saving game settings:\n{}"), error.GetDescription()));
    return false;
  }

  return true;
}

const QIcon& QtHost::GetAppIcon()
{
  return s_state.app_icon;
}

QString QtHost::GetAppIconPath()
{
  return GetResourceQPath("images/duck.png", true);
}

QPixmap QtHost::GetAppLogo()
{
  QPixmap pm(GetAppIconPath());
  pm.setDevicePixelRatio(qApp->devicePixelRatio());
  return pm;
}

void QtHost::DownloadFile(QWidget* parent, std::string url, std::string path,
                          std::function<void(bool result, std::string path, const Error& error)> completion_callback)
{
  INFO_LOG("Download from {}, saving to {}.", url, path);

  const std::string::size_type url_file_part_pos = url.rfind('/');
  const std::string status_text =
    fmt::format(TRANSLATE_FS("QtHost", "Downloading {}..."),
                std::string_view(url).substr((url_file_part_pos >= 0) ? (url_file_part_pos + 1) : 0));

  QtAsyncTaskWithProgressDialog::create(
    parent, TRANSLATE_SV("QtHost", "File Download"), status_text, false, true, 0, 0, 0.0f, true,
    [parent, url = std::move(url), path = std::move(path),
     completion_callback = std::move(completion_callback)](ProgressCallback* const progress) mutable {
      Error error;
      bool result = false;
      HTTPDownloader::CreateRequest(
        std::move(url), parent,
        [&result, &error, &path](s32 status_code, Error& http_error, std::string&, std::vector<u8>& hdata) {
          if (status_code != HTTPDownloader::HTTP_STATUS_OK)
          {
            error.SetString(http_error.GetDescription());
            return;
          }
          else if (hdata.empty())
          {
            error.SetStringView(TRANSLATE_SV("QtHost", "Download failed: Data is empty."));
            return;
          }

          result = FileSystem::WriteBinaryFile(path.c_str(), hdata, &error);
        },
        progress);

      // Block until completion.
      HTTPDownloader::WaitForAllRequestsFromOwner(parent);

      QtAsyncTaskWithProgressDialog::CompletionCallback ret;
      if (completion_callback)
      {
        ret = [path = std::move(path), completion_callback = std::move(completion_callback), error = std::move(error),
               result]() mutable { completion_callback(result, std::move(path), error); };
      }
      else
      {
        ret = []() {};
      }

      return ret;
    });
}

bool QtHost::InitializeFoldersAndConfig(Error* error)
{
  // Path to the resources directory relative to the application binary.
// On Windows/Linux, these are in the binary directory.
// On macOS, this is in the bundle resources directory.
#ifndef __APPLE__
  static constexpr const char* RESOURCES_RELATIVE_PATH = "resources";
#else
  static constexpr const char* RESOURCES_RELATIVE_PATH = "../Resources";
#endif

  if (!Core::SetCriticalFolders(RESOURCES_RELATIVE_PATH, error))
    return false;

  Error config_error;
  if (!Core::InitializeBaseSettingsLayer(Core::GetBaseSettingsPath(), &config_error))
  {
    if (QMessageBox::question(
          nullptr, u"DuckStation"_s,
          u"Failed to load configuration. The error was:\n\n%1\n\nThe settings file may be corrupted. Do you want to "
          "delete the settings file and try again? Note that any currently-configured settings will be lost."_s.arg(
            QString::fromStdString(config_error.GetDescription()))) == QMessageBox::Yes)
    {
      if (!FileSystem::DeleteFile(Core::GetBaseSettingsPath().c_str(), &config_error))
      {
        QMessageBox::critical(nullptr, QStringLiteral("DuckStation"),
                              QStringLiteral("Failed to delete settings file:\n\n%1")
                                .arg(QString::fromStdString(config_error.GetDescription())));
      }
    }

    // Try again after deleting.
    if (!Core::InitializeBaseSettingsLayer(Core::GetBaseSettingsPath(), &config_error))
    {
      Error::SetStringFmt(error,
                          "Failed to load configuration. The error was:\n\n{}\n\nPlease ensure that the data directory "
                          "is writable. The data directory is located at:\n\n{}\n\nYou can also try portable mode by "
                          "creating portable.txt in the same directory you installed DuckStation into.",
                          config_error.GetDescription(), EmuFolders::DataRoot);
      return false;
    }
  }

  // Very old installations pre-setup-wizard won't have the "SetupWizardIncomplete" key.
  // Instead, we rely on "SettingsVersion" there as a signal that setup has been completed.
  if (!Core::ContainsBaseSettingValue("Main", "SetupWizardIncomplete") &&
      !Core::ContainsBaseSettingValue("Main", "SettingsVersion"))
  {
    // Flag for running the setup wizard if this is our first run. We want to run it next time if they don't finish it.
    Core::SetBaseBoolSettingValue("Main", "SetupWizardIncomplete", true);
  }

  // Setup wizard was incomplete last time?
  s_state.run_setup_wizard =
    s_state.run_setup_wizard || Core::GetBaseBoolSettingValue("Main", "SetupWizardIncomplete", false);

  UpdateApplicationLanguage(nullptr);
  return true;
}

bool QtHost::LoadResources(Error* error)
{
  // the resources directory should exist, bail out if not
  const std::string rcc_path = Path::Combine(EmuFolders::Resources, "duckstation-qt.rcc");
  if (!FileSystem::FileExists(rcc_path.c_str()) || !QResource::registerResource(QString::fromStdString(rcc_path)))
  {
    Error::SetStringFmt(error,
                        "{} could not be loaded. Your installation is not complete. Please delete and re-download the "
                        "application from https://www.duckstation.org/.",
                        Path::GetFileName(rcc_path));
    return false;
  }

  s_state.app_icon = QIcon(GetAppIconPath());
  return true;
}

void Host::OnSettingsReloaded()
{
  emit g_core_thread->settingsReloaded();
}

void CoreThread::setDefaultSettings(bool host, bool system, bool controller)
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::setDefaultSettings, Qt::QueuedConnection, host, system, controller);
    return;
  }

  Core::SetDefaultSettings(host, system, controller, true);
}

void Host::SetDefaultSettings(SettingsInterface& si)
{
#if defined(_WIN32)
  si.SetBoolValue("Main", "DisableWindowRoundedCorners", false);
#elif defined(__APPLE__)
  si.SetBoolValue("Main", "UseFractionalWindowScale", false);
#endif

  si.SetBoolValue("Main", "DisableWindowResize", false);
  si.SetBoolValue("Main", "HideCursorInFullscreen", false);
  si.SetBoolValue("Main", "RenderToSeparateWindow", false);
  si.SetBoolValue("Main", "HideMainWindowWhenRunning", false);

  // TODO: We could include stuff like game list here, but meh...
}

void Host::OnSettingsResetToDefault(bool host, bool system, bool controller)
{
  g_core_thread->applySettings(false);

  if (system)
    emit g_core_thread->settingsResetToDefault(host, system, controller);
}

void Host::RequestResizeHostDisplay(s32 new_window_width, s32 new_window_height)
{
  emit g_core_thread->onResizeRenderWindowRequested(new_window_width, new_window_height);
}

void QtHost::ApplyMigrations()
{
  const std::string achievement_icons_directory = Path::Combine(EmuFolders::Cache, "achievement_images");
  if (FileSystem::DirectoryExists(achievement_icons_directory.c_str()))
  {
    // If it's empty, just delete it.
    if (!FileSystem::IsDirectoryEmpty(achievement_icons_directory.c_str()))
    {
      QMessageBox mb(
        QMessageBox::Question, u"DuckStation"_s,
        u"DuckStation has migrated to using an archive for caching achievement icons, which is significantly more "
        "efficient and does not create thousands of files.\n\n"
        "The old directory of achievement icons is no longer needed and can be deleted.\n\n"
        "Do you want to delete the old achievement icons cache now?"
        "\n\nNo save data or achievement progress will be lost."_s,
        QMessageBox::Yes | QMessageBox::No);
      mb.setWindowIcon(GetAppIcon());
      if (mb.exec() == QMessageBox::Yes)
      {
        Error error;
        if (!FileSystem::RecursiveDeleteDirectory(achievement_icons_directory.c_str(), &error))
        {
          QMessageBox::critical(nullptr, u"Error"_s,
                                QString::fromStdString(fmt::format(
                                  "Failed to delete old achievement icons directory: {}", error.GetDescription())));
        }
      }
    }
  }
}

void CoreThread::applySettings(bool display_osd_messages /* = false */)
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::applySettings, Qt::QueuedConnection, display_osd_messages);
    return;
  }

  System::ApplySettings(display_osd_messages);
}

void CoreThread::reloadGameSettings(bool display_osd_messages /* = false */)
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::reloadGameSettings, Qt::QueuedConnection, display_osd_messages);
    return;
  }

  System::ReloadGameSettings(display_osd_messages);
}

void CoreThread::reloadInputProfile(bool display_osd_messages /*= false*/)
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::reloadInputProfile, Qt::QueuedConnection, display_osd_messages);
    return;
  }

  System::ReloadInputProfile(display_osd_messages);
}

void CoreThread::updateEmuFolders()
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::updateEmuFolders, Qt::QueuedConnection);
    return;
  }

  EmuFolders::Update();
}

void CoreThread::updateControllerSettings()
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::updateControllerSettings, Qt::QueuedConnection);
    return;
  }

  if (!System::IsValid())
    return;

  System::UpdateControllerSettings();
}

void CoreThread::startFullscreenUI()
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::startFullscreenUI, Qt::QueuedConnection);
    return;
  }

  if (System::IsValid() || VideoThread::IsFullscreenUIRequested())
    return;

  // borrow the game start fullscreen flag
  const bool start_fullscreen =
    (QtHost::s_state.start_fullscreen_ui_fullscreen || Core::GetBaseBoolSettingValue("Main", "StartFullscreen", false));

  m_is_fullscreen_ui_started = true;
  emit fullscreenUIStartedOrStopped(true);

  Error error;
  if (!VideoThread::StartFullscreenUI(start_fullscreen, &error))
  {
    Host::ReportErrorAsync("Error", error.GetDescription());
    m_is_fullscreen_ui_started = false;
    emit fullscreenUIStartedOrStopped(false);
    return;
  }
}

void CoreThread::stopFullscreenUI()
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::stopFullscreenUI, Qt::QueuedConnection);

    // if we still have a system, don't wait
    if (QtHost::IsSystemValid())
      return;

    // wait until the host display is gone
    QtUtils::ProcessEventsWithSleep(QEventLoop::ExcludeUserInputEvents, []() {
      return QtHost::IsFullscreenUIStarted() || g_main_window->hasDisplayWidget();
    });
    return;
  }

  if (m_is_fullscreen_ui_started)
  {
    m_is_fullscreen_ui_started = false;
    emit fullscreenUIStartedOrStopped(false);
    VideoThread::StopFullscreenUI();
  }
}

void CoreThread::exitFullscreenUI()
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::exitFullscreenUI, Qt::QueuedConnection);
    return;
  }

  const bool was_in_nogui_mode = std::exchange(QtHost::s_state.nogui_mode, false);

  stopFullscreenUI();

  if (was_in_nogui_mode)
  {
    Host::RunOnUIThread([]() {
      // Restore the geometry of the main window, since the display window may have been moved.
      QtUtils::RestoreWindowGeometry(g_main_window);

      // if we were in nogui mode, the game list won't have been populated yet. do it now.
      g_main_window->refreshGameList(false);
    });
  }
}

void CoreThread::bootSystem(std::shared_ptr<SystemBootParameters> params)
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::bootSystem, Qt::QueuedConnection, std::move(params));
    return;
  }

  // Just in case of rapid clicking games before it gets the chance to start.
  if (System::IsValidOrInitializing())
    return;

  Error error;
  if (!System::BootSystem(std::move(*params), &error))
  {
    emit errorReported(tr("Error"),
                       tr("Failed to boot system: %1").arg(QString::fromStdString(error.GetDescription())));
  }
}

void CoreThread::bootOrSwitchNonDisc(const QString& path)
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::bootOrSwitchNonDisc, Qt::QueuedConnection, path);
    return;
  }

  std::string path_str = path.toStdString();

  // No system -> just boot it.
  if (!System::IsValid())
  {
    bootSystem(std::make_shared<SystemBootParameters>(std::move(path_str)));
    return;
  }

  const bool is_gpu_dump = System::IsGPUDumpPath(path_str);
  if (is_gpu_dump != System::IsReplayingGPUDump())
  {
    emit errorReported(tr("Error"), tr("Cannot change GPU dump state without restarting the system."));
    return;
  }

  if (is_gpu_dump)
  {
    System::ChangeGPUDump(std::move(path_str));
  }
  else
  {
    // Change override and reset.
    System::ChangeExeOverrideAndReset(std::move(path_str));
    System::ResetSystem();
  }
}

void CoreThread::bootOrLoadState(std::string path)
{
  DebugAssert(isCurrentThread());

  if (System::IsValid())
  {
    Error error;
    if (!System::LoadState(path.c_str(), &error, true, false).value_or(true))
    {
      emit errorReported(tr("Error"),
                         tr("Failed to load state: %1").arg(QString::fromStdString(error.GetDescription())));
    }
  }
  else
  {
    std::shared_ptr<SystemBootParameters> params = std::make_shared<SystemBootParameters>();
    params->save_state = std::move(path);
    bootSystem(std::move(params));
  }
}

void CoreThread::onRenderWindowKeyEvent(int key, bool pressed)
{
  DebugAssert(isCurrentThread());

  InputManager::InvokeEvents(InputManager::MakeHostKeyboardKey(key), static_cast<float>(pressed),
                             GenericInputBinding::Unknown);
}

void CoreThread::onRenderWindowTextEntered(const QString& text)
{
  DebugAssert(isCurrentThread());

  ImGuiManager::AddTextInput(text.toStdString());
}

void CoreThread::onRenderWindowMouseMoveAbsoluteEvent(float x, float y)
{
  InputManager::UpdatePointerAbsolutePosition(0, x, y);
}

void CoreThread::onRenderWindowMouseMoveRelativeEvent(float dx, float dy)
{
  if (dx != 0.0f)
    InputManager::UpdatePointerPositionRelativeDelta(0, InputPointerAxis::X, dx);
  if (dy != 0.0f)
    InputManager::UpdatePointerPositionRelativeDelta(0, InputPointerAxis::Y, dy);
}

void CoreThread::onRenderWindowMouseButtonEvent(int button, bool pressed)
{
  DebugAssert(isCurrentThread());

  InputManager::InvokeEvents(InputManager::MakePointerButtonKey(0, button), static_cast<float>(pressed),
                             GenericInputBinding::Unknown);
}

void CoreThread::onRenderWindowMouseWheelEvent(float dx, float dy)
{
  DebugAssert(isCurrentThread());

  if (dx != 0.0f)
    InputManager::UpdatePointerWheelRelativeDelta(0, InputPointerAxis::WheelX, dx);

  if (dy != 0.0f)
    InputManager::UpdatePointerWheelRelativeDelta(0, InputPointerAxis::WheelY, dy);
}

void CoreThread::onRenderWindowResized(int width, int height, float scale, float refresh_rate)
{
  VideoThread::ResizeRenderWindow(width, height, scale, refresh_rate);
}

void CoreThread::redrawRenderWindow()
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::redrawRenderWindow, Qt::QueuedConnection);
    return;
  }

  if (System::IsShutdown())
    return;

  VideoThread::PresentCurrentFrame();
}

void CoreThread::toggleFullscreen()
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::toggleFullscreen, Qt::QueuedConnection);
    return;
  }

  VideoThread::SetFullscreen(!VideoThread::IsFullscreen());
}

void CoreThread::setFullscreen(bool fullscreen)
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::setFullscreen, Qt::QueuedConnection, fullscreen);
    return;
  }

  VideoThread::SetFullscreen(fullscreen);
}

void CoreThread::setFullscreenWithCompletionHandler(bool fullscreen, std::function<void()> completion_handler)
{
  if (!isCurrentThread())
  {
    DebugAssert(QThread::isMainThread());
    QMetaObject::invokeMethod(this, &CoreThread::setFullscreenWithCompletionHandler, Qt::QueuedConnection, fullscreen,
                              [completion_handler = std::move(completion_handler)]() mutable {
                                Host::RunOnUIThread(std::move(completion_handler));
                              });
    return;
  }

  VideoThread::SetFullscreenWithCompletionHandler(fullscreen, std::move(completion_handler));
}

void CoreThread::recreateRenderWindow()
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::recreateRenderWindow, Qt::QueuedConnection);
    return;
  }

  VideoThread::RecreateRenderWindow();
}

void CoreThread::requestDisplaySize(float scale)
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::requestDisplaySize, Qt::QueuedConnection, scale);
    return;
  }

  if (!System::IsValid())
    return;

  System::RequestDisplaySize(scale);
}

std::optional<WindowInfo> CoreThread::acquireRenderWindow(RenderAPI render_api, bool fullscreen,
                                                          bool exclusive_fullscreen, Error* error)
{
  return emit onAcquireRenderWindowRequested(render_api, fullscreen, exclusive_fullscreen, error);
}

void CoreThread::releaseRenderWindow()
{
  emit onReleaseRenderWindowRequested();
}

void CoreThread::connectRenderWindowSignals(DisplayWidget* widget)
{
  widget->disconnect(this);

  connect(widget, &DisplayWidget::windowResizedEvent, this, &CoreThread::onRenderWindowResized);
  connect(widget, &DisplayWidget::windowRestoredEvent, this, &CoreThread::redrawRenderWindow);
  connect(widget, &DisplayWidget::windowKeyEvent, this, &CoreThread::onRenderWindowKeyEvent);
  connect(widget, &DisplayWidget::windowTextEntered, this, &CoreThread::onRenderWindowTextEntered);
  connect(widget, &DisplayWidget::windowMouseMoveAbsoluteEvent, this,
          &CoreThread::onRenderWindowMouseMoveAbsoluteEvent);
  connect(widget, &DisplayWidget::windowMouseMoveRelativeEvent, this,
          &CoreThread::onRenderWindowMouseMoveRelativeEvent);
  connect(widget, &DisplayWidget::windowMouseButtonEvent, this, &CoreThread::onRenderWindowMouseButtonEvent);
  connect(widget, &DisplayWidget::windowMouseWheelEvent, this, &CoreThread::onRenderWindowMouseWheelEvent);
}

void Host::OnSystemStarting()
{
  emit g_core_thread->systemStarting();
}

void Host::OnSystemStarted()
{
  g_core_thread->stopBackgroundControllerPollTimer();
  g_core_thread->wakeThread();

  emit g_core_thread->systemStarted();
}

void Host::OnSystemPaused()
{
  emit g_core_thread->systemPaused();
  g_core_thread->startBackgroundControllerPollTimer();
}

void Host::OnSystemResumed()
{
  emit g_core_thread->systemResumed();
  g_core_thread->wakeThread();

  g_core_thread->stopBackgroundControllerPollTimer();
}

void Host::OnSystemStopping()
{
  emit g_core_thread->systemStopping();
}

void Host::OnSystemDestroyed()
{
  g_core_thread->resetPerformanceCounters();
  g_core_thread->startBackgroundControllerPollTimer();
  emit g_core_thread->systemDestroyed();
}

void Host::OnSystemAbnormalShutdown(const std::string_view reason)
{
  Host::ReportErrorAsync(
    TRANSLATE_SV("QtHost", "Error"),
    fmt::format(
      TRANSLATE_FS("QtHost",
                   "Unfortunately, the virtual machine has abnormally shut down and cannot be recovered. Please use "
                   "the available support options for further assistance, and provide information about what you were "
                   "doing when the error occurred, as well as the details below:\n\n{}"),
      reason));
}

void Host::OnVideoThreadRunIdleChanged(bool is_active)
{
  g_core_thread->setVideoThreadRunIdle(is_active);
}

void CoreThread::reloadInputBindings()
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::reloadInputBindings, Qt::QueuedConnection);
    return;
  }

  System::ReloadInputBindings();
}

void CoreThread::confirmActionWithSafetyCheck(const QString& action, bool check_achievements,
                                              bool cancel_resume_on_accept, std::function<void(bool)> callback) const
{
  DebugAssert(isCurrentThread());
  if (!System::IsValid())
  {
    callback(true);
    return;
  }

  const bool saving = System::IsSavingMemoryCards();
  const u32 pending_unlock_count = Achievements::GetPendingUnlockCount();
  if (!saving && pending_unlock_count == 0)
  {
    callback(true);
    return;
  }

  Host::RunOnUIThread(
    [callback = std::move(callback), action, saving, pending_unlock_count, cancel_resume_on_accept]() mutable {
      auto lock = g_main_window->pauseAndLockSystem();

      bool result;
      if (saving)
      {
        result = (QtUtils::MessageBoxIcon(
                    lock.getDialogParent(), QMessageBox::Warning, tr("Memory Card Busy"),
                    tr("WARNING: Your game is still saving to the memory card. Continuing to %1 may "
                       "IRREVERSIBLY DESTROY YOUR MEMORY CARD. We recommend resuming your game and waiting 5 "
                       "seconds for it to finish saving.\n\nDo you want to %1 anyway?")
                      .arg(action),
                    QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::No);
      }
      else
      {
        result = (QtUtils::MessageBoxIcon(
                    lock.getDialogParent(), QMessageBox::Warning, tr("Achievement Unlocks Unconfirmed"),
                    tr("%1 achievement unlocks have not been confirmed by the server. Continuing to %2 will result in "
                       "loss of these unlocks. Once network connectivity has been re-established, these unlocks will "
                       "be confirmed automatically.\n\nDo you want to %2 anyway?")
                      .arg(pending_unlock_count)
                      .arg(action),
                    QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::No);
      }

      if (cancel_resume_on_accept && !QtHost::IsFullscreenUIStarted())
        lock.cancelResume();

      Host::RunOnCoreThread([result, callback = std::move(callback)]() { callback(result); });
    });
}

void CoreThread::shutdownSystem(bool save_state, bool check_safety)
{
  if (!isCurrentThread())
  {
    System::CancelPendingStartup();
    QMetaObject::invokeMethod(this, &CoreThread::shutdownSystem, Qt::QueuedConnection, save_state, check_safety);
    return;
  }

  if (check_safety && (System::IsSavingMemoryCards() || Achievements::GetPendingUnlockCount() > 0))
  {
    confirmActionWithSafetyCheck(tr("shut down"), true, true, [save_state](bool result) {
      if (result)
        g_core_thread->shutdownSystem(save_state, false);
      else
        g_core_thread->setSystemPaused(false);
    });
    return;
  }

  System::ShutdownSystem(save_state);
}

void CoreThread::resetSystem(bool check_memcard_busy)
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::resetSystem, Qt::QueuedConnection, check_memcard_busy);
    return;
  }

  if (check_memcard_busy && System::IsSavingMemoryCards())
  {
    confirmActionWithSafetyCheck(tr("reset"), false, false, [](bool result) {
      if (result)
        g_core_thread->resetSystem(false);
    });
    return;
  }

  System::ResetSystem();
}

void CoreThread::setSystemPaused(bool paused)
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::setSystemPaused, Qt::QueuedConnection, paused);
    return;
  }

  System::PauseSystem(paused);
}

void CoreThread::changeDisc(const QString& new_disc_filename, bool reset_system, bool check_memcard_busy)
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::changeDisc, Qt::QueuedConnection, new_disc_filename, reset_system,
                              check_memcard_busy);
    return;
  }

  if (check_memcard_busy && System::IsSavingMemoryCards())
  {
    confirmActionWithSafetyCheck(tr("change disc"), false, false, [new_disc_filename, reset_system](bool result) {
      if (result)
        g_core_thread->changeDisc(new_disc_filename, reset_system, false);
    });
    return;
  }

  if (System::IsShutdown())
    return;

  if (!new_disc_filename.isEmpty())
    System::InsertMedia(new_disc_filename.toStdString().c_str());
  else
    System::RemoveMedia();

  if (reset_system)
    System::ResetSystem();
}

void CoreThread::changeDiscFromPlaylist(quint32 index)
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::changeDiscFromPlaylist, Qt::QueuedConnection, index);
    return;
  }

  if (System::IsShutdown())
    return;

  if (!System::SwitchMediaSubImage(index))
    errorReported(tr("Error"), tr("Failed to switch to subimage %1").arg(index));
}

void CoreThread::setLidState(bool manual_control, bool manual_state)
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::setLidState, Qt::QueuedConnection, manual_control, manual_state);
    return;
  }

  if (!System::IsValid())
    return;

  CDROM::SetLidState(manual_control, manual_state);
}

void CoreThread::reloadCheats(bool reload_files, bool reload_enabled_list, bool verbose, bool verbose_if_changed)
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::reloadCheats, Qt::QueuedConnection, reload_files, reload_enabled_list,
                              verbose, verbose_if_changed);
    return;
  }

  if (System::IsValid())
  {
    // If the reloaded list is being enabled, we also need to reload the gameini file.
    if (reload_enabled_list)
      System::ReloadGameSettings(verbose);
    Cheats::ReloadCheats(reload_files, reload_enabled_list, verbose, verbose_if_changed, verbose);
  }
}

void CoreThread::applyCheat(const QString& name)
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::applyCheat, Qt::QueuedConnection, name);
    return;
  }

  if (System::IsValid())
    Cheats::ApplyManualCode(name.toStdString());
}

void CoreThread::reloadPostProcessingShaders()
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::reloadPostProcessingShaders, Qt::QueuedConnection);
    return;
  }

  if (System::IsValid())
    VideoPresenter::ReloadPostProcessingSettings(true, true, true);
}

void CoreThread::updatePostProcessingSettings(bool display, bool internal, bool force_reload)
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::updatePostProcessingSettings, Qt::QueuedConnection, display, internal,
                              force_reload);
    return;
  }

  if (System::IsValid())
    VideoPresenter::ReloadPostProcessingSettings(display, internal, force_reload);
}

void CoreThread::reloadTextureReplacements()
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::reloadTextureReplacements, Qt::QueuedConnection);
    return;
  }

  if (System::IsValid())
    VideoThread::RunOnThread([]() { GPUTextureCache::ReloadTextureReplacements(true, true); });
}

void CoreThread::captureGPUFrameDump()
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::captureGPUFrameDump, Qt::QueuedConnection);
    return;
  }

  if (System::IsValid())
    System::StartRecordingGPUDump();
}

void CoreThread::startControllerTest()
{
  static constexpr const char* PADTEST_URL =
    "https://github.com/stenzek/duckstation/raw/refs/heads/master/extras/padtest/padtest.psexe";

  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::startControllerTest, Qt::QueuedConnection);
    return;
  }

  if (System::IsValid())
    return;

  std::string path = Path::Combine(EmuFolders::UserResources, "padtest.psexe");
  if (FileSystem::FileExists(path.c_str()))
  {
    bootSystem(std::make_shared<SystemBootParameters>(std::move(path)));
    return;
  }

  Host::RunOnUIThread([path = std::move(path)]() mutable {
    if (QtHost::IsSystemValid())
      return;

    QMessageBox* const msgbox =
      QtUtils::NewMessageBox(g_main_window, QMessageBox::Question, tr("Confirm Download"),
                             tr("Your DuckStation installation does not have the padtest application "
                                "available.\n\nThis file is approximately 206KB, do you want to download it now?"),
                             QMessageBox::Yes | QMessageBox::No);
    msgbox->connect(msgbox, &QMessageBox::accepted, g_main_window, [path = std::move(path)]() mutable {
      QtHost::DownloadFile(
        g_main_window, PADTEST_URL, std::move(path), [](bool result, std::string path, const Error& error) {
          if (!result)
          {
            QtUtils::MessageBoxCritical(
              g_main_window, tr("Error"),
              tr("Failed to download padtest application: %1").arg(QString::fromStdString(error.GetDescription())));
            return;
          }

          g_core_thread->bootSystem(std::make_shared<SystemBootParameters>(std::move(path)));
        });
    });
    msgbox->open();
  });
}

void CoreThread::openGamePropertiesForCurrentGame(const QString& category)
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::openGamePropertiesForCurrentGame, Qt::QueuedConnection, category);
    return;
  }

  Error error;
  GameList::Entry dynamic_entry;
  if (System::PopulateGameListEntryFromCurrentGame(&dynamic_entry, &error))
  {
    Host::RunOnUIThread([dynamic_entry = std::move(dynamic_entry), category]() {
      SettingsWindow::openGamePropertiesDialog(&dynamic_entry,
                                               category.isEmpty() ? nullptr : category.toUtf8().constData());
    });
  }
  else
  {
    emit errorReported(tr("Error"), QString::fromStdString(error.GetDescription()));
  }
}

void CoreThread::runOnCoreThread(const std::function<void()>& callback)
{
  callback();
}

void Host::RunOnCoreThread(std::function<void()> function, bool block /* = false */)
{
  const bool self = g_core_thread->isCurrentThread();

  QMetaObject::invokeMethod(g_core_thread, &CoreThread::runOnCoreThread,
                            (block && !self) ? Qt::BlockingQueuedConnection : Qt::QueuedConnection,
                            std::move(function));
}

void Host::RunOnUIThread(std::function<void()> function, bool block /* = false*/)
{
  // main window always exists, so it's fine to attach it to that.
  QMetaObject::invokeMethod(g_main_window, &MainWindow::runOnUIThread,
                            block ? Qt::BlockingQueuedConnection : Qt::QueuedConnection, std::move(function));
}

QtAsyncTask::QtAsyncTask(WorkCallback callback)
{
  m_callback = std::move(callback);
}

QtAsyncTask::~QtAsyncTask() = default;

void QtAsyncTask::create(QObject* owner, WorkCallback callback)
{
  // NOTE: Must get connected before queuing, because otherwise you risk a race.
  QtAsyncTask* task = new QtAsyncTask(std::move(callback));
  connect(task, &QtAsyncTask::completed, owner, [task]() { std::get<CompletionCallback>(task->m_callback)(); });
  Host::QueueAsyncTask([task]() {
    task->m_callback = std::get<WorkCallback>(task->m_callback)();
    Host::RunOnUIThread([task]() {
      emit task->completed(task);
      delete task;
    });
  });
}

void Host::RefreshGameListAsync(bool invalidate_cache)
{
  QMetaObject::invokeMethod(g_main_window, &MainWindow::refreshGameList, Qt::QueuedConnection, invalidate_cache);
}

void Host::CancelGameListRefresh()
{
  QMetaObject::invokeMethod(g_main_window, &MainWindow::cancelGameListRefresh, Qt::BlockingQueuedConnection);
}

void Host::OnGameListEntriesChanged(std::span<const u32> changed_indices)
{
  QList<int> changed_rows;
  changed_rows.reserve(changed_indices.size());
  for (const u32 row : changed_indices)
    changed_rows.push_back(static_cast<int>(row));
  emit g_core_thread->gameListRowsChanged(changed_rows);
}

void CoreThread::loadState(const QString& path)
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, static_cast<void (CoreThread::*)(const QString&)>(&CoreThread::loadState),
                              Qt::QueuedConnection, path);
    return;
  }

  bootOrLoadState(path.toStdString());
}

void CoreThread::loadState(bool global, qint32 slot)
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, static_cast<void (CoreThread::*)(bool, qint32)>(&CoreThread::loadState),
                              Qt::QueuedConnection, global, slot);
    return;
  }

  if (System::IsValid())
  {
    System::LoadStateFromSlot(global, slot);
    return;
  }

  bootOrLoadState(global ? System::GetGlobalSaveStatePath(slot) :
                           System::GetGameSaveStatePath(System::GetGameSerial(), slot));
}

void CoreThread::saveState(const QString& path)
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, static_cast<void (CoreThread::*)(const QString&)>(&CoreThread::saveState),
                              Qt::QueuedConnection, path);
    return;
  }

  if (!System::IsValid())
    return;

  Error error;
  if (System::SaveState(path.toStdString(), &error, g_settings.create_save_state_backups, false))
    emit statusMessage(tr("State saved to %1.").arg(QFileInfo(path).fileName()));
  else
    emit errorReported(tr("Error"), tr("Failed to save state: %1").arg(QString::fromStdString(error.GetDescription())));
}

void CoreThread::saveState(bool global, qint32 slot)
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, static_cast<void (CoreThread::*)(bool, qint32)>(&CoreThread::saveState),
                              Qt::QueuedConnection, global, slot);
    return;
  }

  System::SaveStateToSlot(global, slot);
}

void CoreThread::undoLoadState()
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::undoLoadState, Qt::QueuedConnection);
    return;
  }

  System::UndoLoadState();
}

void CoreThread::setAudioOutputVolume(int volume, int fast_forward_volume)
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::setAudioOutputVolume, Qt::QueuedConnection, volume,
                              fast_forward_volume);
    return;
  }

  g_settings.audio_output_volume = static_cast<u8>(volume);
  g_settings.audio_fast_forward_volume = static_cast<u8>(fast_forward_volume);
  System::UpdateVolume();
}

void CoreThread::setAudioOutputMuted(bool muted)
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::setAudioOutputMuted, Qt::QueuedConnection, muted);
    return;
  }

  g_settings.audio_output_muted = muted;
  System::UpdateVolume();
}

void CoreThread::singleStepCPU()
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::singleStepCPU, Qt::BlockingQueuedConnection);
    return;
  }

  if (!System::IsValid())
    return;

  System::SingleStepCPU();
}

void CoreThread::dumpRAM(const QString& path)
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::dumpRAM, Qt::QueuedConnection, path);
    return;
  }

  Error error;
  const std::string path_str = path.toStdString();
  if (System::DumpRAM(path_str.c_str(), &error))
  {
    emit statusMessage(QStringLiteral("RAM dumped to %1.").arg(path));
  }
  else
  {
    emit errorReported(
      QStringLiteral("Error"),
      QStringLiteral("Failed to dump RAM to %1: %2").arg(path).arg(QString::fromStdString(error.GetDescription())));
  }
}

void CoreThread::dumpVRAM(const QString& path)
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::dumpVRAM, Qt::QueuedConnection, path);
    return;
  }

  Error error;
  const std::string path_str = path.toStdString();
  if (System::DumpVRAM(path_str.c_str(), &error))
  {
    emit statusMessage(QStringLiteral("VRAM dumped to %1.").arg(path));
  }
  else
  {
    emit errorReported(
      QStringLiteral("Error"),
      QStringLiteral("Failed to dump VRAM to %1: %2").arg(path).arg(QString::fromStdString(error.GetDescription())));
  }
}

void CoreThread::dumpSPURAM(const QString& path)
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::dumpSPURAM, Qt::QueuedConnection, path);
    return;
  }

  Error error;
  const std::string path_str = path.toStdString();
  if (System::DumpSPURAM(path_str.c_str(), &error))
  {
    emit statusMessage(QStringLiteral("SPU RAM dumped to %1.").arg(path));
  }
  else
  {
    emit errorReported(
      QStringLiteral("Error"),
      QStringLiteral("Failed to dump SPU RAM to %1: %2").arg(path).arg(QString::fromStdString(error.GetDescription())));
  }
}

void CoreThread::saveScreenshot()
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::saveScreenshot, Qt::QueuedConnection);
    return;
  }

  System::SaveScreenshot();
}

void CoreThread::applicationStateChanged(Qt::ApplicationState state)
{
  const bool background = (state != Qt::ApplicationActive);
  InputManager::OnApplicationBackgroundStateChanged(background);

  if (!System::IsValid())
    return;

  if (background)
  {
    if (g_settings.pause_on_focus_loss && !m_was_paused_by_focus_loss && !System::IsPaused())
    {
      setSystemPaused(true);
      m_was_paused_by_focus_loss = true;
    }

    // Clear the state of all keyboard binds.
    // That way, if we had a key held down, and lost focus, the bind won't be stuck enabled because we never
    // got the key release message, because it happened in another window which "stole" the event.
    InputManager::ClearBindStateFromSource(InputManager::MakeHostKeyboardKey(0));
  }
  else
  {
    if (m_was_paused_by_focus_loss)
    {
      if (System::IsPaused())
        setSystemPaused(false);
      m_was_paused_by_focus_loss = false;
    }
  }
}

void Host::OnAchievementsLoginRequested(Achievements::LoginRequestReason reason)
{
  emit g_core_thread->achievementsLoginRequested(reason);
}

void Host::OnAchievementsLoginSuccess(const char* username, u32 points, u32 sc_points, u32 unread_messages)
{
  emit g_core_thread->achievementsLoginSuccess(QString::fromUtf8(username), points, sc_points, unread_messages);
}

void Host::OnAchievementsActiveChanged(bool active)
{
  emit g_core_thread->achievementsActiveChanged(active);
}

void Host::OnAchievementsHardcoreModeChanged(bool enabled)
{
  emit g_core_thread->achievementsHardcoreModeChanged(enabled);
}

void Host::BeginTextInput()
{
  DEV_LOG("Host::BeginTextInput()");

  // NOTE: Called on GPU thread.
  QInputMethod* method = qApp->inputMethod();
  if (method)
    QMetaObject::invokeMethod(method, &QInputMethod::show, Qt::QueuedConnection);
}

void Host::EndTextInput()
{
  DEV_LOG("Host::EndTextInput()");

  QInputMethod* method = qApp->inputMethod();
  if (method)
    QMetaObject::invokeMethod(method, &QInputMethod::hide, Qt::QueuedConnection);
}

bool Host::CreateAuxiliaryRenderWindow(s32 x, s32 y, u32 width, u32 height, std::string_view title,
                                       std::string_view icon_name, AuxiliaryRenderWindowUserData userdata,
                                       AuxiliaryRenderWindowHandle* handle, WindowInfo* wi, Error* error)
{
  return emit g_core_thread->onCreateAuxiliaryRenderWindow(
    g_gpu_device->GetRenderAPI(), x, y, width, height, QtUtils::StringViewToQString(title),
    QtUtils::StringViewToQString(icon_name), userdata, handle, wi, error);
}

void Host::DestroyAuxiliaryRenderWindow(AuxiliaryRenderWindowHandle handle, s32* pos_x, s32* pos_y, u32* width,
                                        u32* height)
{
  QPoint pos;
  QSize size;
  emit g_core_thread->onDestroyAuxiliaryRenderWindow(handle, &pos, &size);

  if (pos_x)
    *pos_x = pos.x();
  if (pos_y)
    *pos_y = pos.y();
  if (width)
    *width = size.width();
  if (height)
    *height = size.height();

  // eat all pending events, to make sure we're not going to write input events back to a dead pointer
  if (g_core_thread->isCurrentThread())
    g_core_thread->getEventLoop()->processEvents(QEventLoop::AllEvents);
}

void CoreThread::queueAuxiliaryRenderWindowInputEvent(Host::AuxiliaryRenderWindowUserData userdata,
                                                      Host::AuxiliaryRenderWindowEvent event,
                                                      Host::AuxiliaryRenderWindowEventParam param1,
                                                      Host::AuxiliaryRenderWindowEventParam param2,
                                                      Host::AuxiliaryRenderWindowEventParam param3)
{
  DebugAssert(QThread::isMainThread());
  QMetaObject::invokeMethod(this, &CoreThread::processAuxiliaryRenderWindowInputEvent, Qt::QueuedConnection, userdata,
                            static_cast<quint32>(event), static_cast<quint32>(param1.uint_param),
                            static_cast<quint32>(param2.uint_param), static_cast<quint32>(param3.uint_param));
}

void CoreThread::processAuxiliaryRenderWindowInputEvent(void* userdata, quint32 event, quint32 param1, quint32 param2,
                                                        quint32 param3)
{
  DebugAssert(isCurrentThread());
  VideoThread::RunOnThread([userdata, event, param1, param2, param3]() {
    ImGuiManager::ProcessAuxiliaryRenderWindowInputEvent(userdata, static_cast<Host::AuxiliaryRenderWindowEvent>(event),
                                                         Host::AuxiliaryRenderWindowEventParam{.uint_param = param1},
                                                         Host::AuxiliaryRenderWindowEventParam{.uint_param = param2},
                                                         Host::AuxiliaryRenderWindowEventParam{.uint_param = param3});
  });
}

void CoreThread::createBackgroundControllerPollTimer()
{
  DebugAssert(!m_background_controller_polling_timer);
  m_background_controller_polling_timer = new QTimer(this);
  m_background_controller_polling_timer->setSingleShot(false);
  m_background_controller_polling_timer->setTimerType(Qt::CoarseTimer);
  connect(m_background_controller_polling_timer, &QTimer::timeout, &Core::IdleUpdate);
}

void CoreThread::destroyBackgroundControllerPollTimer()
{
  delete m_background_controller_polling_timer;
  m_background_controller_polling_timer = nullptr;
}

void CoreThread::startBackgroundControllerPollTimer()
{
  if (m_background_controller_polling_timer->isActive())
  {
    updateBackgroundControllerPollInterval();
    return;
  }

  const int interval = getBackgroundControllerPollInterval();
  DEV_LOG("Starting background controller polling timer with interval {} ms", interval);
  m_background_controller_polling_timer->start(interval);
}

void CoreThread::stopBackgroundControllerPollTimer()
{
  if (!m_background_controller_polling_timer->isActive())
    return;

  DEV_LOG("Stopping background controller polling timer");
  m_background_controller_polling_timer->stop();
}

void CoreThread::updateBackgroundControllerPollInterval()
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::updateBackgroundControllerPollInterval, Qt::QueuedConnection);
    return;
  }

  if (!m_background_controller_polling_timer || !m_background_controller_polling_timer->isActive())
    return;

  const int current_interval = m_background_controller_polling_timer->interval();
  const int new_interval = getBackgroundControllerPollInterval();
  if (current_interval != new_interval)
  {
    WARNING_LOG("Changed background polling interval from {} ms to {} ms", current_interval, new_interval);
    m_background_controller_polling_timer->setInterval(new_interval);
  }
}

int CoreThread::getBackgroundControllerPollInterval() const
{
#ifdef ENABLE_GDB_SERVER
  if (GDBServer::HasAnyClients())
    return GDB_SERVER_POLLING_INTERVAL;
#endif

  if (m_video_thread_run_idle)
    return FULLSCREEN_UI_CONTROLLER_POLLING_INTERVAL;
  else if (m_http_downloader_active)
    return DOWNLOAD_CONTROLLER_POLLING_INTERVAL;
  else if (InputManager::GetPollableDeviceCount() > 0)
    return BACKGROUND_CONTROLLER_POLLING_INTERVAL_WITH_DEVICES;
  else
    return BACKGROUND_CONTROLLER_POLLING_INTERVAL_WITHOUT_DEVICES;
}

void CoreThread::setHTTPDownloaderActive(bool active)
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::setHTTPDownloaderActive, Qt::QueuedConnection, active);
    return;
  }

  DEV_LOG("HTTP Downloader now {}", active ? "active" : "inactive");
  m_http_downloader_active = active;
  updateBackgroundControllerPollInterval();
}

void CoreThread::setVideoThreadRunIdle(bool active)
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::setVideoThreadRunIdle, Qt::QueuedConnection, active);
    return;
  }

  m_video_thread_run_idle = active;

  // break out of the event loop if we're not executing a system
  if (active && !g_settings.gpu_use_thread && !System::IsRunning())
    m_event_loop->quit();

  // adjust the timer speed to pick up controller input faster
  if (!m_background_controller_polling_timer->isActive())
    return;

  g_core_thread->updateBackgroundControllerPollInterval();
}

void CoreThread::updateFullscreenUITheme()
{
  if (!isCurrentThread())
  {
    QMetaObject::invokeMethod(this, &CoreThread::updateFullscreenUITheme, Qt::QueuedConnection);
    return;
  }

  // don't bother if nothing is running
  if (VideoThread::IsFullscreenUIRequested() || VideoThread::IsGPUBackendRequested())
    VideoThread::RunOnThread(&FullscreenUI::UpdateTheme);
}

void Host::OnHTTPDownloaderActiveChanged(bool active)
{
  g_core_thread->setHTTPDownloaderActive(active);
}

void CoreThread::stop()
{
  Assert(isRunning() && !isCurrentThread() && g_core_thread == this);

  QMetaObject::invokeMethod(this, &CoreThread::stopInThread, Qt::QueuedConnection);
  QtUtils::ProcessEventsWithSleep(QEventLoop::ExcludeUserInputEvents, [this]() { return isRunning(); });

  // Ensure settings are saved.
  if (QtHost::s_state.settings_save_timer)
  {
    QtHost::s_state.settings_save_timer.reset();
    QtHost::SaveSettings();
  }
}

void CoreThread::stopInThread()
{
  stopFullscreenUI();

  m_shutdown_flag = true;
  m_event_loop->quit();
}

void CoreThread::run()
{
  m_event_loop = new QEventLoop();

  // input source setup must happen on emu thread
  {
    Error startup_error;
    if (!Core::CoreThreadInitialize(false, &startup_error))
    {
      moveToThread(m_ui_thread);
      Host::ReportFatalError("Fatal Startup Error", startup_error.GetDescription());
      return;
    }
  }

  // connections
  connect(qApp, &QGuiApplication::applicationStateChanged, this, &CoreThread::applicationStateChanged);

  // enumerate all devices, even those which were added early
  m_input_device_list_model->enumerateDevices();

  // start background input polling
  createBackgroundControllerPollTimer();
  startBackgroundControllerPollTimer();

  // main loop
  while (!m_shutdown_flag)
  {
    if (System::IsRunning())
    {
      System::Execute();
    }
    else if (!VideoThread::IsUsingThread() && VideoThread::IsRunningIdle())
    {
      m_event_loop->processEvents(QEventLoop::AllEvents);

      // have to double-check the condition after processing events, because the events could shut us down
      if (!VideoThread::IsUsingThread() && VideoThread::IsRunningIdle())
        VideoThread::DoRunIdle();
    }
    else
    {
      m_event_loop->exec();
    }
  }

  if (System::IsValid())
    System::ShutdownSystem(false);

  destroyBackgroundControllerPollTimer();

  // and tidy up everything left
  Core::CoreThreadShutdown();

  // move back to UI thread
  moveToThread(m_ui_thread);
  delete m_event_loop;
  m_event_loop = nullptr;
}

void Host::FrameDoneOnVideoThread(GPUBackend* gpu_backend, u32 frame_number)
{
}

void CoreThread::wakeThread()
{
  if (isCurrentThread())
    m_event_loop->quit();
  else
    QMetaObject::invokeMethod(m_event_loop, &QEventLoop::quit, Qt::QueuedConnection);
}

void Host::ReportFatalError(std::string_view title, std::string_view message)
{
  auto cb = [title = QtUtils::StringViewToQString(title), message = QtUtils::StringViewToQString(message)]() {
    QMessageBox::critical(g_main_window && g_main_window->isVisible() ? g_main_window : nullptr, title, message);
#ifndef __APPLE__
    std::quick_exit(EXIT_FAILURE);
#else
    _exit(EXIT_FAILURE);
#endif
  };

  // https://stackoverflow.com/questions/34135624/how-to-properly-execute-gui-operations-in-qt-main-thread
  QTimer* timer = new QTimer();
  QThread* const ui_thread = qApp->thread();
  if (QThread::currentThread() == ui_thread)
  {
    // On UI thread, we can do it straight away.
    cb();
  }
  else
  {
    timer->moveToThread(ui_thread);
    timer->setSingleShot(true);
    QObject::connect(timer, &QTimer::timeout, std::move(cb));
    QMetaObject::invokeMethod(timer, static_cast<void (QTimer::*)(int)>(&QTimer::start), Qt::QueuedConnection,
                              static_cast<int>(0));

    // should never return
    while (ui_thread->isRunning())
      Timer::NanoSleep(1000000000ULL);
  }

  std::abort();
}

void Host::ReportErrorAsync(std::string_view title, std::string_view message)
{
  if (!title.empty() && !message.empty())
    ERROR_LOG("ReportErrorAsync: {}: {}", title, message);
  else if (!message.empty())
    ERROR_LOG("ReportErrorAsync: {}", message);

  QMetaObject::invokeMethod(g_main_window, &MainWindow::reportError, Qt::QueuedConnection,
                            title.empty() ? QString() : QString::fromUtf8(title.data(), title.size()),
                            message.empty() ? QString() : QString::fromUtf8(message.data(), message.size()));
}

void Host::ReportStatusMessage(std::string_view message)
{
  if (message.empty())
    return;

  emit g_core_thread->statusMessage(QtUtils::StringViewToQString(message));
}

void Host::ConfirmMessageAsync(std::string_view icon, std::string_view title, std::string_view message,
                               ConfirmMessageAsyncCallback callback, std::string_view yes_text,
                               std::string_view no_text)
{
  INFO_LOG("ConfirmMessageAsync({}, {})", title, message);

  // Default button titles.
  if (yes_text.empty())
    yes_text = TRANSLATE_SV("QtHost", "Yes");
  if (no_text.empty())
    no_text = TRANSLATE_SV("QtHost", "No");

  // Ensure it always comes from the CPU thread.
  if (!g_core_thread->isCurrentThread())
  {
    Host::RunOnCoreThread([title = std::string(title), icon = std::string(icon), message = std::string(message),
                           callback = std::move(callback), yes_text = std::string(yes_text),
                           no_text = std::string(no_text)]() mutable {
      ConfirmMessageAsync(icon, title, message, std::move(callback), yes_text, no_text);
    });
    return;
  }

  // Pause system while dialog is up.
  const bool needs_pause = System::IsValid() && !System::IsPaused();
  if (needs_pause)
    System::PauseSystem(true);

  // Use FSUI if we're ingame.
  if (System::IsValid() || g_core_thread->isFullscreenUIStarted())
  {
    VideoThread::RunOnThread([title = std::string(title), icon = std::string(icon), message = std::string(message),
                              callback = std::move(callback), yes_text = std::string(yes_text),
                              no_text = std::string(no_text), needs_pause]() mutable {
      // Need to reset run idle state _again_ after displaying.
      auto final_callback = [callback = std::move(callback), needs_pause](bool result) {
        FullscreenUI::UpdateRunIdleState();
        if (needs_pause)
        {
          Host::RunOnCoreThread([]() {
            if (System::IsValid())
              System::PauseSystem(false);
          });
        }
        callback(result);
      };

      if (icon.empty())
        icon = ICON_EMOJI_QUESTION_MARK;
      else if (StringUtil::GetUTF8CharacterCount(icon) > 1 && !Path::IsAbsolute(icon))
        icon = QtHost::GetResourcePath(icon, true);

      FullscreenUI::Initialize();
      FullscreenUI::OpenConfirmMessageDialog(std::move(icon), std::move(title), std::move(message),
                                             std::move(final_callback), fmt::format(ICON_FA_CHECK " {}", yes_text),
                                             fmt::format(ICON_FA_XMARK " {}", no_text));
      FullscreenUI::UpdateRunIdleState();
    });
  }
  else
  {
    QString qicon;
    if (!icon.empty())
      qicon = Path::IsAbsolute(icon) ? QtUtils::StringViewToQString(icon) : QtHost::GetResourceQPath(icon, true);

    // Otherwise, use the desktop UI.
    Host::RunOnUIThread([qicon = std::move(qicon), title = QtUtils::StringViewToQString(title),
                         message = QtUtils::StringViewToQString(message), callback = std::move(callback),
                         yes_text = QtUtils::StringViewToQString(yes_text),
                         no_text = QtUtils::StringViewToQString(no_text), needs_pause]() mutable {
      auto lock = g_main_window->pauseAndLockSystem();

      QWidget* const dialog_parent = lock.getDialogParent();
      QMessageBox* const msgbox =
        QtUtils::NewMessageBox(dialog_parent, QMessageBox::Question, title, message, QMessageBox::NoButton);
      if (!qicon.isEmpty())
        msgbox->setIconPixmap(SVGWidget::renderSVGToPixmap(qicon, QSize(64, 64), msgbox->devicePixelRatio(), QColor()));

      QPushButton* const yes_button = msgbox->addButton(yes_text, QMessageBox::AcceptRole);
      msgbox->addButton(no_text, QMessageBox::RejectRole);

      QObject::connect(msgbox, &QMessageBox::finished, dialog_parent,
                       [msgbox, yes_button, callback = std::move(callback), lock = std::move(lock), needs_pause]() {
                         const bool result = (msgbox->clickedButton() == yes_button);
                         callback(result);

                         if (needs_pause)
                         {
                           Host::RunOnCoreThread([]() {
                             if (System::IsValid())
                               System::PauseSystem(false);
                           });
                         }
                       });
      msgbox->open();
    });
  }
}

void Host::OpenURL(std::string_view url)
{
  Host::RunOnUIThread([url = QtUtils::StringViewToQString(url)]() { QtUtils::OpenURL(g_main_window, QUrl(url)); });
}

std::string Host::GetClipboardText()
{
  // Hope this doesn't deadlock...
  std::string ret;
  Host::RunOnUIThread(
    [&ret]() {
      QClipboard* clipboard = QGuiApplication::clipboard();
      if (clipboard)
        ret = clipboard->text().toStdString();
    },
    true);
  return ret;
}

bool Host::CopyTextToClipboard(std::string_view text)
{
  Host::RunOnUIThread([text = QtUtils::StringViewToQString(text)]() {
    QClipboard* clipboard = QGuiApplication::clipboard();
    if (clipboard)
      clipboard->setText(text);
  });
  return true;
}

QString QtHost::FormatNumber(Host::NumberFormatType type, s64 value)
{
  QString ret;

  if (type >= Host::NumberFormatType::ShortDate && type <= Host::NumberFormatType::LongDateTime)
  {
    QString format;
    switch (type)
    {
      case Host::NumberFormatType::ShortDate:
      case Host::NumberFormatType::LongDate:
      {
        format = s_state.app_locale.dateFormat((type == Host::NumberFormatType::LongDate) ? QLocale::LongFormat :
                                                                                            QLocale::ShortFormat);
      }
      break;

      case Host::NumberFormatType::ShortTime:
      case Host::NumberFormatType::LongTime:
      {
        format = s_state.app_locale.timeFormat((type == Host::NumberFormatType::LongTime) ? QLocale::LongFormat :
                                                                                            QLocale::ShortFormat);
      }
      break;

      case Host::NumberFormatType::ShortDateTime:
      case Host::NumberFormatType::LongDateTime:
      {
        format = s_state.app_locale.dateTimeFormat(
          (type == Host::NumberFormatType::LongDateTime) ? QLocale::LongFormat : QLocale::ShortFormat);

        // Remove time zone specifiers 't', 'tt', 'ttt', 'tttt'.
        format.remove(QRegularExpression("\\s*t+\\s*"));
      }
      break;

        DefaultCaseIsUnreachable();
    }

    ret = QDateTime::fromSecsSinceEpoch(value, QTimeZone::utc()).toLocalTime().toString(format);
  }
  else
  {
    ret = s_state.app_locale.toString(value);
  }

  return ret;
}

std::string Host::FormatNumber(NumberFormatType type, s64 value)
{
  return QtHost::FormatNumber(type, value).toStdString();
}

QString QtHost::FormatNumber(Host::NumberFormatType type, double value)
{
  QString ret;

  switch (type)
  {
    case Host::NumberFormatType::Number:
    default:
      ret = s_state.app_locale.toString(value);
      break;
  }

  return ret;
}

std::string Host::FormatNumber(NumberFormatType type, double value)
{
  return QtHost::FormatNumber(type, value).toStdString();
}

void QtHost::UpdateApplicationLanguage(QWidget* dialog_parent)
{
  for (QTranslator* translator : s_state.translators)
  {
    qApp->removeTranslator(translator);
    translator->deleteLater();
  }
  s_state.translators.clear();

  // Fixup automatic language.
  std::string language = Core::GetBaseStringSettingValue("Main", "Language", "");
  if (language.empty())
    language = GetSystemLanguage();
  QString qlanguage = QString::fromStdString(language);

  // install the base qt translation first
#ifndef __APPLE__
  const QString base_dir = QStringLiteral("%1/translations").arg(qApp->applicationDirPath());
#else
  const QString base_dir = QStringLiteral("%1/../Resources/translations").arg(qApp->applicationDirPath());
#endif

  // Qt base uses underscores instead of hyphens.
  const QString qtbase_language = QString(qlanguage).replace(QChar('-'), QChar('_'));
  QString base_path(QStringLiteral("%1/qt_%2.qm").arg(base_dir).arg(qtbase_language));
  bool has_base_ts = QFile::exists(base_path);
  if (!has_base_ts)
  {
    // Try without the country suffix.
    const qsizetype index = qlanguage.lastIndexOf('-');
    if (index > 0)
    {
      base_path = QStringLiteral("%1/qt_%2.qm").arg(base_dir).arg(qlanguage.left(index));
      has_base_ts = QFile::exists(base_path);
    }
  }
  if (has_base_ts)
  {
    QTranslator* base_translator = new QTranslator(qApp);
    if (!base_translator->load(base_path))
    {
      QtUtils::MessageBoxWarning(
        dialog_parent, QStringLiteral("Translation Error"),
        QStringLiteral("Failed to load base translation file for '%1':\n%2").arg(qlanguage).arg(base_path));
      delete base_translator;
    }
    else
    {
      s_state.translators.push_back(base_translator);
      qApp->installTranslator(base_translator);
    }
  }

  const QString path = QStringLiteral("%1/duckstation-qt_%3.qm").arg(base_dir).arg(qlanguage);
  if (!QFile::exists(path))
  {
    QtUtils::MessageBoxWarning(
      dialog_parent, QStringLiteral("Translation Error"),
      QStringLiteral("Failed to find translation file for language '%1':\n%2").arg(qlanguage).arg(path));
    return;
  }

  QTranslator* translator = new QTranslator(qApp);
  if (!translator->load(path))
  {
    QtUtils::MessageBoxWarning(
      dialog_parent, QStringLiteral("Translation Error"),
      QStringLiteral("Failed to load translation file for language '%1':\n%2").arg(qlanguage).arg(path));
    delete translator;
    return;
  }

  INFO_LOG("Loaded translation file for language {}", qlanguage.toUtf8().constData());
  qApp->installTranslator(translator);
  s_state.translators.push_back(translator);

  // We end up here both on language change, and on startup.
  UpdateFontOrder(language);
  UpdateApplicationLocale(language);
}

s32 Host::Internal::GetTranslatedStringImpl(std::string_view context, std::string_view msg,
                                            std::string_view disambiguation, char* tbuf, size_t tbuf_space)
{
  // This is really awful. Thankfully we're caching the results...
  const std::string temp_context(context);
  const std::string temp_msg(msg);
  const std::string temp_disambiguation(disambiguation);
  const QString translated_msg = QCoreApplication::translate(
    temp_context.c_str(), temp_msg.c_str(), disambiguation.empty() ? nullptr : temp_disambiguation.c_str());
  const QByteArray translated_utf8 = translated_msg.toUtf8();
  const size_t translated_size = translated_utf8.size();
  if (translated_size > tbuf_space)
    return -1;
  else if (translated_size > 0)
    std::memcpy(tbuf, translated_utf8.constData(), translated_size);

  return static_cast<s32>(translated_size);
}

std::string Host::TranslatePluralToString(const char* context, const char* msg, const char* disambiguation, int count)
{
  return QCoreApplication::translate(context, msg, disambiguation, count).toStdString();
}

SmallString Host::TranslatePluralToSmallString(const char* context, const char* msg, const char* disambiguation,
                                               int count)
{
  const QString qstr = QCoreApplication::translate(context, msg, disambiguation, count);
  SmallString ret;

#ifdef _WIN32
  // Cheeky way to avoid heap allocations.
  static_assert(sizeof(*qstr.utf16()) == sizeof(wchar_t));
  ret.assign(std::wstring_view(reinterpret_cast<const wchar_t*>(qstr.utf16()), qstr.length()));
#else
  const QByteArray utf8 = qstr.toUtf8();
  ret.assign(utf8.constData(), utf8.length());
#endif

  return ret;
}

std::span<const std::pair<const char*, const char*>> Host::GetAvailableLanguageList()
{
  static constexpr const std::pair<const char*, const char*> languages[] = {
    {QT_TRANSLATE_NOOP("QtHost", "System Language"), ""},

#define TRANSLATION_LIST_ENTRY(name, code, locale_code, qlanguage, qcountry) {name " [" code "]", code},
#include "qttranslations.inl"
#undef TRANSLATION_LIST_ENTRY
  };

  return languages;
}

const char* Host::GetLanguageName(std::string_view language_code)
{
  for (const auto& [name, code] : GetAvailableLanguageList())
  {
    if (language_code == code)
      return Host::TranslateToCString("QtHost", name);
  }

  return TRANSLATE("QtHost", "Unknown");
}

std::string_view QtHost::GetSystemLanguage()
{
  std::string locname = QLocale::system().name(QLocale::TagSeparator::Dash).toStdString();

  // Does this match any of our translations?
  for (const auto& [lname, lcode] : Host::GetAvailableLanguageList())
  {
    if (locname == lcode)
      return lcode;
  }

  // Check for a partial match, e.g. "zh" for "zh-CN".
  if (const std::string::size_type pos = locname.find('-'); pos != std::string::npos)
  {
    const std::string_view plocname = std::string_view(locname).substr(0, pos);
    for (const auto& [lname, lcode] : Host::GetAvailableLanguageList())
    {
      // Only some languages have a country code, so we need to check both.
      const std::string_view lcodev(lcode);
      if (lcodev == plocname)
      {
        return lcode;
      }
      else if (const std::string_view::size_type lpos = lcodev.find('-'); lpos != std::string::npos)
      {
        if (lcodev.substr(0, lpos) == plocname)
          return lcode;
      }
    }
  }

  // Fallback to English.
  return "en";
}

bool Host::ChangeLanguage(const char* new_language)
{
  Host::RunOnUIThread([new_language = std::string(new_language)]() {
    Core::SetBaseStringSettingValue("Main", "Language", new_language.c_str());
    Host::CommitBaseSettingChanges();
    QtHost::UpdateApplicationLanguage(g_main_window);
    g_main_window->recreate();
  });
  return true;
}

void QtHost::UpdateFontOrder(std::string_view language)
{
  // Why is this a thing? Because we want all glyphs to be available, but don't want to conflict
  // between codepoints shared between Chinese and Japanese. Therefore we prioritize the language
  // that the user has selected.
  ImGuiManager::LanguageFontOrder font_order;
#define TF(name) ImGuiManager::LanguageFont::name
  if (language == "ja")
    font_order = {TF(Default), TF(Japanese), TF(Chinese), TF(Korean)};
  else if (language == "ko")
    font_order = {TF(Default), TF(Korean), TF(Japanese), TF(Chinese)};
  else if (language == "zh-CN")
    font_order = {TF(Default), TF(Chinese), TF(Japanese), TF(Korean)};
  else
    font_order = ImGuiManager::GetLanguageTextFontOrder();
#undef TF

  if (g_core_thread)
  {
    Host::RunOnCoreThread([font_order]() mutable {
      VideoThread::RunOnThread([font_order]() mutable { ImGuiManager::SetLanguageFontOrder(font_order); });
      Host::ClearTranslationCache();
    });
  }
  else
  {
    // Startup, safe to set directly.
    ImGuiManager::SetLanguageFontOrder(font_order);
    Host::ClearTranslationCache();
  }
}

const QLocale& QtHost::GetApplicationLocale()
{
  return s_state.app_locale;
}

void QtHost::UpdateApplicationLocale(std::string_view language)
{
  static constexpr const std::tuple<const char*, QLocale::Language, QLocale::Country> lookup[] = {
#define TRANSLATION_LIST_ENTRY(name, code, locale_code, qlanguage, qcountry) {code, qlanguage, qcountry},
#include "qttranslations.inl"
#undef TRANSLATION_LIST_ENTRY
  };

  // If the system locale is using the same language, then use the system locale.
  // Otherwise we'll be using that ugly US date format in Straya mate.
  s_state.app_locale = QLocale::system();
  const QString system_locale_name = s_state.app_locale.name(QLocale::TagSeparator::Dash);
  if (system_locale_name.startsWith(QLatin1StringView(language), Qt::CaseInsensitive))
  {
    INFO_LOG("Using system locale for {}.", language);
    return;
  }

  for (const auto& [code, qlanguage, qcountry] : lookup)
  {
    if (language == code)
    {
      s_state.app_locale = QLocale(qlanguage, qcountry);
      return;
    }
  }
}

InputDeviceListModel::InputDeviceListModel(QObject* parent) : QAbstractListModel(parent)
{
}

InputDeviceListModel::~InputDeviceListModel() = default;

QIcon InputDeviceListModel::getIconForKey(const InputBindingKey& key)
{
  if (key.source_type == InputSourceType::Keyboard)
    return QIcon(u":/icons/monochrome/svg/keyboard-line.svg"_s);
  else if (key.source_type == InputSourceType::Pointer)
    return QIcon(u":/icons/monochrome/svg/mouse-line.svg"_s);
  else
    return QIcon(u":/icons/monochrome/svg/controller-line.svg"_s);
}

QString InputDeviceListModel::getDeviceName(const InputBindingKey& key)
{
  QString ret;
  for (const InputDeviceListModel::Device& device : m_devices)
  {
    if (device.key.source_type == key.source_type && device.key.source_index == key.source_index)
    {
      ret = device.display_name;
      break;
    }
  }

  return ret;
}

bool InputDeviceListModel::hasEffectsOfType(InputBindingInfo::Type type)
{
  return std::ranges::any_of(m_effects, [type](const auto& eff) { return (eff.type == type); });
}

int InputDeviceListModel::rowCount(const QModelIndex& parent /*= QModelIndex()*/) const
{
  return static_cast<int>(m_devices.size());
}

QVariant InputDeviceListModel::data(const QModelIndex& index, int role /*= Qt::DisplayRole*/) const
{
  const int row = index.row();
  if (index.column() != 0 || row < 0 || static_cast<qsizetype>(row) >= m_devices.size())
    return QVariant();

  if (role == Qt::DisplayRole)
  {
    const auto& dev = m_devices[static_cast<qsizetype>(row)];
    const InputBindingKey key = dev.key;

    // don't display device names for implicit keyboard/mouse
    if (key.source_type == InputSourceType::Keyboard ||
        (key.source_type == InputSourceType::Pointer && !InputManager::IsUsingRawInput()))
    {
      return dev.display_name;
    }
    else
    {
      return QStringLiteral("%1\n%2").arg(dev.identifier).arg(dev.display_name);
    }
  }
  else if (role == Qt::DecorationRole)
  {
    const auto& dev = m_devices[static_cast<qsizetype>(row)];
    return getIconForKey(dev.key);
  }
  else
  {
    return QVariant();
  }
}

void InputDeviceListModel::enumerateDevices()
{
  DebugAssert(g_core_thread->isCurrentThread());

  const InputManager::DeviceList devices = InputManager::EnumerateDevices();
  const InputManager::DeviceEffectList effects = InputManager::EnumerateDeviceEffects();

  DeviceList new_devices;
  new_devices.reserve(devices.size());
  for (const auto& [key, identifier, device_name] : devices)
    new_devices.emplace_back(key, QString::fromStdString(identifier), QString::fromStdString(device_name));

  EffectList new_effects;
  new_effects.reserve(effects.size());
  for (const auto& [type, key] : effects)
  {
    TinyString name = InputManager::ConvertInputBindingKeyToString(type, key);
    SmallString pretty_name(name);
    InputManager::PrettifyInputBinding(pretty_name, false);
    new_effects.emplace_back(type, key, std::string(name), std::string(pretty_name));
  }

  QMetaObject::invokeMethod(this, &InputDeviceListModel::resetLists, Qt::QueuedConnection, new_devices, new_effects);
}

void InputDeviceListModel::resetLists(const DeviceList& devices, const EffectList& effects)
{
  beginResetModel();

  m_devices = devices;
  m_effects = effects;

  endResetModel();
}

void InputDeviceListModel::onDeviceConnected(const InputBindingKey& key, const QString& identifier,
                                             const QString& device_name, const EffectList& effects)
{
  for (const auto& it : m_devices)
  {
    if (it.identifier == identifier)
      return;
  }

  const int index = static_cast<int>(m_devices.size());
  beginInsertRows(QModelIndex(), index, index);
  m_devices.emplace_back(key, identifier, device_name);
  endInsertRows();

  m_effects.append(effects);
}

void InputDeviceListModel::onDeviceDisconnected(const InputBindingKey& key, const QString& identifier)
{
  for (qsizetype i = 0; i < m_devices.size(); i++)
  {
    if (m_devices[i].identifier == identifier)
    {
      const int index = static_cast<int>(i);
      beginRemoveRows(QModelIndex(), index, index);
      m_devices.remove(i);
      endRemoveRows();

      // remove effects too
      const QString effect_prefix = QStringLiteral("%1/").arg(identifier);
      for (qsizetype j = 0; j < m_effects.size();)
      {
        if (m_effects[j].key.source_type == key.source_type && m_effects[j].key.source_index == key.source_index)
          m_effects.remove(j);
        else
          j++;
      }

      return;
    }
  }
}

void Host::OnInputDeviceConnected(InputBindingKey key, std::string_view identifier, std::string_view device_name)
{
  // get the effects for this device to append to the list
  InputDeviceListModel::EffectList qeffect_list;
  const InputManager::DeviceEffectList effect_list = InputManager::EnumerateDeviceEffects(std::nullopt, key);
  if (!effect_list.empty())
  {
    qeffect_list.reserve(effect_list.size());
    for (const auto& [eff_type, eff_key] : effect_list)
    {
      TinyString name = InputManager::ConvertInputBindingKeyToString(eff_type, eff_key);
      SmallString pretty_name(name);
      InputManager::PrettifyInputBinding(pretty_name, false);
      qeffect_list.emplace_back(eff_type, eff_key, std::string(name), std::string(pretty_name));
    }
  }

  QMetaObject::invokeMethod(g_core_thread->getInputDeviceListModel(), &InputDeviceListModel::onDeviceConnected,
                            Qt::QueuedConnection, key, QtUtils::StringViewToQString(identifier),
                            QtUtils::StringViewToQString(device_name), qeffect_list);
  g_core_thread->updateBackgroundControllerPollInterval();
}

void Host::OnInputDeviceDisconnected(InputBindingKey key, std::string_view identifier)
{
  QMetaObject::invokeMethod(g_core_thread->getInputDeviceListModel(), &InputDeviceListModel::onDeviceDisconnected,
                            Qt::QueuedConnection, key, QtUtils::StringViewToQString(identifier));
  g_core_thread->updateBackgroundControllerPollInterval();
}

void Host::AddFixedInputBindings(const SettingsInterface& si)
{
}

std::string QtHost::GetResourcePath(std::string_view filename, bool allow_override)
{
  return allow_override ? EmuFolders::GetOverridableResourcePath(filename) :
                          Path::Combine(EmuFolders::Resources, filename);
}

QString QtHost::GetResourceQPath(std::string_view name, bool allow_override)
{
  return QString::fromStdString(GetResourcePath(name, allow_override));
}

std::optional<std::string> QtHost::ReadResourceFileToString(std::string_view name, bool allow_override, Error* error)
{
  if (allow_override)
  {
    const std::string respath = Path::Combine(EmuFolders::UserResources, name.starts_with(":") ? name.substr(1) : name);
    if (std::optional<std::string> ret = FileSystem::ReadFileToString(respath.c_str(), error))
      return ret;
  }

  if (!name.starts_with(":"))
  {
    Error::SetStringFmt(error, "Failed to find resource file '{}'", name);
    return std::nullopt;
  }

  QFile file(QtUtils::StringViewToQString(name));
  s64 size;
  if (!file.open(QIODevice::ReadOnly) || (size = file.size()) <= 0)
  {
    Error::SetStringFmt(error, "Failed to open resource file '{}': {}", name, file.errorString().toStdString());
    return std::nullopt;
  }

  std::string ret;
  ret.resize(static_cast<size_t>(size));

  const s64 read_size = static_cast<s64>(ret.size());
  if (read_size > 0 && file.read(ret.data(), read_size) != read_size)
  {
    Error::SetStringFmt(error, "Failed to read resource file '{}': {}", name, file.errorString().toStdString());
    return std::nullopt;
  }

  return ret;
}

const QStringList& QtHost::GetRobotoFontFamilies()
{
  std::call_once(s_state.roboto_font_once_flag, []() {
    const int font_id = QFontDatabase::addApplicationFont(
      QString::fromStdString(Path::Combine(EmuFolders::Resources, "fonts/Roboto-VariableFont_wdth,wght.ttf")));
    if (font_id < 0)
    {
      ERROR_LOG("Failed to load Roboto font.");
      return;
    }

    s_state.roboto_font_families = QFontDatabase::applicationFontFamilies(font_id);
    if (s_state.roboto_font_families.isEmpty())
    {
      ERROR_LOG("Failed to get Roboto font family.");
      return;
    }
  });

  return s_state.roboto_font_families;
}

const QFont& QtHost::GetFixedFont()
{
  std::call_once(s_state.fixed_font_once_flag, []() {
    const int font_id = QFontDatabase::addApplicationFont(
      QString::fromStdString(Path::Combine(EmuFolders::Resources, "fonts/GoogleSansCode-VariableFont_wght.ttf")));
    if (font_id < 0)
    {
      ERROR_LOG("Failed to load fixed-width font.");
      return;
    }

    const QStringList families = QFontDatabase::applicationFontFamilies(font_id);
    if (families.isEmpty())
    {
      ERROR_LOG("Failed to get fixed-width font family.");
      return;
    }

    s_state.fixed_font.setFamilies(families);
    s_state.fixed_font.setWeight(QFont::Medium);
    s_state.fixed_font.setPixelSize(12);
    s_state.fixed_font.setHintingPreference(QFont::PreferNoHinting);
  });

  return s_state.fixed_font;
}

bool Host::ResourceFileExists(std::string_view filename, bool allow_override)
{
  const std::string path = QtHost::GetResourcePath(filename, allow_override);
  return FileSystem::FileExists(path.c_str());
}

std::optional<DynamicHeapArray<u8>> Host::ReadResourceFile(std::string_view filename, bool allow_override, Error* error)
{
  const std::string path = QtHost::GetResourcePath(filename, allow_override);
  const std::optional<DynamicHeapArray<u8>> ret = FileSystem::ReadBinaryFile(path.c_str(), error);
  if (!ret.has_value())
    Error::AddPrefixFmt(error, "Failed to read resource file '{}': ", filename);
  return ret;
}

std::optional<std::string> Host::ReadResourceFileToString(std::string_view filename, bool allow_override, Error* error)
{
  const std::string path = QtHost::GetResourcePath(filename, allow_override);
  const std::optional<std::string> ret = FileSystem::ReadFileToString(path.c_str(), error);
  if (!ret.has_value())
    Error::AddPrefixFmt(error, "Failed to read resource file '{}': ", filename);
  return ret;
}

std::optional<std::time_t> Host::GetResourceFileTimestamp(std::string_view filename, bool allow_override)
{
  const std::string path = QtHost::GetResourcePath(filename, allow_override);

  FILESYSTEM_STAT_DATA sd;
  if (!FileSystem::StatFile(path.c_str(), &sd))
  {
    ERROR_LOG("Failed to stat resource file '{}'", filename);
    return std::nullopt;
  }

  return sd.ModificationTime;
}

void Host::CommitBaseSettingChanges()
{
  QtHost::QueueSettingsSave();
}

std::optional<WindowInfo> Host::AcquireRenderWindow(RenderAPI render_api, bool fullscreen, bool exclusive_fullscreen,
                                                    Error* error)
{
  return g_core_thread->acquireRenderWindow(render_api, fullscreen, exclusive_fullscreen, error);
}

WindowInfoType Host::GetRenderWindowInfoType()
{
  return QtUtils::GetWindowInfoType();
}

void Host::ReleaseRenderWindow()
{
  g_core_thread->releaseRenderWindow();
}

bool Host::CanChangeFullscreenMode(bool new_fullscreen_state)
{
  // Don't mess with fullscreen while locked.
  return (!new_fullscreen_state || !QtHost::IsSystemLocked());
}

void CoreThread::updatePerformanceCounters(const GPUBackend* gpu_backend)
{
  const RenderAPI render_api = g_gpu_device->GetRenderAPI();
  const bool hardware_renderer = GPUBackend::IsUsingHardwareBackend();
  u32 render_width = 0;
  u32 render_height = 0;

  if (gpu_backend)
  {
    const u32 render_scale = gpu_backend->GetResolutionScale();
    std::tie(render_width, render_height) = GPU::GetFullDisplayResolution();
    render_width *= render_scale;
    render_height *= render_scale;
  }

  if (render_api != m_last_render_api || hardware_renderer != m_last_hardware_renderer)
  {
    const QString renderer_str =
      QString::fromUtf8(hardware_renderer ? GPUDevice::RenderAPIToString(render_api) :
                                            Settings::GetRendererDisplayName(GPURenderer::Software));
    QMetaObject::invokeMethod(g_main_window->getStatusRendererWidget(), &QLabel::setText, Qt::QueuedConnection,
                              renderer_str);
    m_last_render_api = render_api;
    m_last_hardware_renderer = hardware_renderer;
  }
  if (render_width != m_last_render_width || render_height != m_last_render_height)
  {
    const QString text =
      (render_width != 0 && render_height != 0) ? tr("%1x%2").arg(render_width).arg(render_height) : tr("No Image");
    QMetaObject::invokeMethod(g_main_window->getStatusResolutionWidget(), &QLabel::setText, Qt::QueuedConnection, text);
    m_last_render_width = render_width;
    m_last_render_height = render_height;
  }

  const float gfps = PerformanceCounters::GetFPS();
  if (gfps != m_last_game_fps)
  {
    QMetaObject::invokeMethod(g_main_window->getStatusFPSWidget(), &QLabel::setText, Qt::QueuedConnection,
                              tr("Game: %1 FPS").arg(gfps, 0, 'f', 0));
    m_last_game_fps = gfps;
  }

  const float speed = PerformanceCounters::GetEmulationSpeed();
  const float vfps = PerformanceCounters::GetVPS();
  if (speed != m_last_speed || vfps != m_last_video_fps)
  {
    QMetaObject::invokeMethod(g_main_window->getStatusVPSWidget(), &QLabel::setText, Qt::QueuedConnection,
                              tr("Video: %1 FPS (%2%)").arg(vfps, 0, 'f', 0).arg(speed, 0, 'f', 0));
    m_last_speed = speed;
    m_last_video_fps = vfps;
  }
}

void CoreThread::resetPerformanceCounters()
{
  m_last_speed = std::numeric_limits<float>::infinity();
  m_last_game_fps = std::numeric_limits<float>::infinity();
  m_last_video_fps = std::numeric_limits<float>::infinity();
  m_last_render_width = std::numeric_limits<u32>::max();
  m_last_render_height = std::numeric_limits<u32>::max();
  m_last_render_api = RenderAPI::None;
  m_last_hardware_renderer = false;

  const QString blank;
  QMetaObject::invokeMethod(g_main_window->getStatusRendererWidget(), &QLabel::setText, Qt::QueuedConnection, blank);
  QMetaObject::invokeMethod(g_main_window->getStatusResolutionWidget(), &QLabel::setText, Qt::QueuedConnection, blank);
  QMetaObject::invokeMethod(g_main_window->getStatusFPSWidget(), &QLabel::setText, Qt::QueuedConnection, blank);
  QMetaObject::invokeMethod(g_main_window->getStatusVPSWidget(), &QLabel::setText, Qt::QueuedConnection, blank);
}

void Host::OnPerformanceCountersUpdated(const GPUBackend* gpu_backend)
{
  g_core_thread->updatePerformanceCounters(gpu_backend);
}

void Host::OnSystemGameChanged(const std::string& disc_path, const std::string& game_serial,
                               const std::string& game_name, GameHash hash)
{
  emit g_core_thread->systemGameChanged(QString::fromStdString(disc_path), QString::fromStdString(game_serial),
                                        QString::fromStdString(game_name));
}

void Host::OnSystemUndoStateAvailabilityChanged(bool available, u64 timestamp)
{
  emit g_core_thread->systemUndoStateAvailabilityChanged(available, timestamp);
}

void Host::OnMediaCaptureStarted()
{
  emit g_core_thread->mediaCaptureStarted();
}

void Host::OnMediaCaptureStopped()
{
  emit g_core_thread->mediaCaptureStopped();
}

void Host::SetMouseMode(bool relative, bool hide_cursor)
{
  // Disable double-click handling when mouse is bound.
  const bool ignore_double_click = InputManager::HasAnyBindingsForKey(InputManager::MakePointerButtonKey(0, 0));
  emit g_core_thread->mouseModeRequested(relative, hide_cursor, ignore_double_click);
}

void Host::PumpMessagesOnCoreThread()
{
  g_core_thread->getEventLoop()->processEvents(QEventLoop::AllEvents);
}

void QtHost::SaveSettings()
{
  AssertMsg(!g_core_thread->isCurrentThread(), "Saving should happen on the UI thread.");

  {
    Error error;
    const auto lock = Core::GetSettingsLock();
    if (!Core::SaveBaseSettingsLayer(&error))
    {
      ERROR_LOG("Failed to save settings: {}", error.GetDescription());
      QtUtils::AsyncMessageBox(
        g_main_window, QMessageBox::Critical, QStringLiteral("DuckStation"),
        QStringLiteral("Failed to save settings: %1").arg(QString::fromStdString(error.GetDescription())));
    }
  }

  if (s_state.settings_save_timer)
  {
    s_state.settings_save_timer->deleteLater();
    s_state.settings_save_timer.release();
  }
}

void QtHost::QueueSettingsSave()
{
  if (!QThread::isMainThread())
  {
    Host::RunOnUIThread(QueueSettingsSave);
    return;
  }

  if (s_state.settings_save_timer)
    return;

  s_state.settings_save_timer = std::make_unique<QTimer>();
  s_state.settings_save_timer->connect(s_state.settings_save_timer.get(), &QTimer::timeout, SaveSettings);
  s_state.settings_save_timer->setSingleShot(true);
  s_state.settings_save_timer->start(SETTINGS_SAVE_DELAY);
}

bool QtHost::ShouldShowDebugOptions()
{
  return Core::GetBaseBoolSettingValue("Main", "ShowDebugMenu", false);
}

void Host::RequestSystemShutdown(bool allow_confirm, bool save_state, bool check_memcard_busy)
{
  if (!System::IsValid())
    return;

  QMetaObject::invokeMethod(g_main_window, &MainWindow::requestShutdown, Qt::QueuedConnection, allow_confirm, true,
                            save_state, check_memcard_busy, true, false, false);
}

void Host::RequestExitApplication(bool allow_confirm)
{
  QMetaObject::invokeMethod(g_main_window, &MainWindow::requestExit, Qt::QueuedConnection, allow_confirm);
}

void Host::RequestExitBigPicture()
{
  g_core_thread->exitFullscreenUI();
}

std::optional<WindowInfo> Host::GetTopLevelWindowInfo()
{
  std::optional<WindowInfo> ret;
  if (QThread::isMainThread())
    ret = g_main_window->getWindowInfo();
  else
    QMetaObject::invokeMethod(g_main_window, &MainWindow::getWindowInfo, Qt::BlockingQueuedConnection, &ret);
  return ret;
}

static void SignalHandler(int signal)
{
  // First try the normal (graceful) shutdown/exit.
  static bool graceful_shutdown_attempted = false;
  if (!graceful_shutdown_attempted && g_main_window)
  {
    std::fprintf(stderr, "Received CTRL+C, attempting graceful shutdown. Press CTRL+C again to force.\n");
    graceful_shutdown_attempted = true;

    // This could be a bit risky invoking from a signal handler... hopefully it's okay.
    QMetaObject::invokeMethod(g_main_window, &MainWindow::requestExit, Qt::QueuedConnection, true);
    return;
  }

  std::signal(signal, SIG_DFL);

  // MacOS is missing std::quick_exit() despite it being C++11...
#ifndef __APPLE__
  std::quick_exit(1);
#else
  _Exit(1);
#endif
}

void QtHost::HookSignals()
{
  std::signal(SIGINT, SignalHandler);
  std::signal(SIGTERM, SignalHandler);

#ifndef _WIN32
  // Ignore SIGCHLD by default on Linux, since we kick off aplay asynchronously.
  struct sigaction sa_chld = {};
  sigemptyset(&sa_chld.sa_mask);
  sa_chld.sa_handler = SIG_IGN;
  sa_chld.sa_flags = SA_RESTART | SA_NOCLDSTOP | SA_NOCLDWAIT;
  sigaction(SIGCHLD, &sa_chld, nullptr);
#endif
}

void QtHost::InitializeEarlyConsole()
{
  const bool was_console_enabled = Log::IsConsoleOutputEnabled();
  if (!was_console_enabled)
    Log::SetConsoleOutputParams(true);
  Log::SetLogLevel(Log::Level::Dev);
}

void QtHost::PrintCommandLineVersion()
{
  InitializeEarlyConsole();

  std::fprintf(stderr, "DuckStation Version %s (%s)\n", g_scm_tag_str, g_scm_branch_str);
  std::fprintf(stderr, "https://github.com/stenzek/duckstation\n");
  std::fprintf(stderr, "\n");
}

void QtHost::PrintCommandLineHelp(const char* progname)
{
  InitializeEarlyConsole();

  PrintCommandLineVersion();
  std::fprintf(stderr, "Usage: %s [parameters] [--] [boot filename]\n", progname);
  std::fprintf(stderr, "\n");
  std::fprintf(stderr, "  -help: Displays this information and exits.\n");
  std::fprintf(stderr, "  -version: Displays version information and exits.\n");
  std::fprintf(stderr, "  -batch: Enables batch mode (exits after powering off).\n");
  std::fprintf(stderr, "  -fastboot: Force fast boot for provided filename.\n");
  std::fprintf(stderr, "  -slowboot: Force slow boot for provided filename.\n");
  std::fprintf(stderr, "  -bios: Boot into the BIOS shell.\n");
  std::fprintf(stderr, "  -resume: Load resume save state. If a boot filename is provided,\n"
                       "    that game's resume state will be loaded, otherwise the most\n"
                       "    recent resume save state will be loaded.\n");
  std::fprintf(stderr, "  -state <index>: Loads specified save state by index. If a boot\n"
                       "    filename is provided, a per-game state will be loaded, otherwise\n"
                       "    a global state will be loaded.\n");
  std::fprintf(stderr, "  -statefile <filename>: Loads state from the specified filename.\n"
                       "    No boot filename is required with this option.\n");
  std::fprintf(stderr, "  -exe <filename>: Boot the specified exe instead of loading from disc.\n");
  std::fprintf(stderr, "  -fullscreen: Enters fullscreen mode immediately after starting.\n");
  std::fprintf(stderr, "  -nofullscreen: Prevents fullscreen mode from triggering if enabled.\n");
  std::fprintf(stderr, "  -nogui: Disables main window from being shown, exits on shutdown.\n");
  std::fprintf(stderr, "  -bigpicture: Automatically starts big picture UI.\n");
  std::fprintf(stderr, "  -earlyconsole: Creates console as early as possible, for logging.\n");
  std::fprintf(stderr, "  --: Signals that no more arguments will follow and the remaining\n"
                       "    parameters make up the filename. Use when the filename contains\n"
                       "    spaces or starts with a dash.\n");
  std::fprintf(stderr, "\n");
}

std::shared_ptr<SystemBootParameters>& AutoBoot(std::shared_ptr<SystemBootParameters>& autoboot)
{
  if (!autoboot)
    autoboot = std::make_shared<SystemBootParameters>();

  return autoboot;
}

bool QtHost::ParseCommandLineParametersAndInitializeConfig(QApplication& app,
                                                           std::shared_ptr<SystemBootParameters>& autoboot)
{
  const QStringList args(app.arguments());
  std::optional<s32> state_index;
  bool starting_bios = false;

  bool no_more_args = false;

  for (qsizetype i = 1; i < args.size(); i++)
  {
    if (!no_more_args)
    {
#define CHECK_ARG(str) (args[i] == str##_L1)
#define CHECK_ARG_PARAM(str) (args[i] == str##_L1 && ((i + 1) < args.size()))

      if (CHECK_ARG("-help"))
      {
        PrintCommandLineHelp(args[0].toUtf8().constData());
        return false;
      }
      else if (CHECK_ARG("-version"))
      {
        PrintCommandLineVersion();
        return false;
      }
      else if (CHECK_ARG("-batch"))
      {
        INFO_LOG("Command Line: Using batch mode.");
        s_state.batch_mode = true;
        continue;
      }
      else if (CHECK_ARG("-nogui"))
      {
        INFO_LOG("Command Line: Using NoGUI mode.");
        s_state.nogui_mode = true;
        continue;
      }
      else if (CHECK_ARG("-bios"))
      {
        INFO_LOG("Command Line: Starting BIOS.");
        AutoBoot(autoboot);
        starting_bios = true;
        continue;
      }
      else if (CHECK_ARG("-fastboot"))
      {
        INFO_LOG("Command Line: Forcing fast boot.");
        AutoBoot(autoboot)->override_fast_boot = true;
        continue;
      }
      else if (CHECK_ARG("-slowboot"))
      {
        INFO_LOG("Command Line: Forcing slow boot.");
        AutoBoot(autoboot)->override_fast_boot = false;
        continue;
      }
      else if (CHECK_ARG("-resume"))
      {
        state_index = -1;
        INFO_LOG("Command Line: Loading resume state.");
        continue;
      }
      else if (CHECK_ARG_PARAM("-state"))
      {
        state_index = args[++i].toInt();
        INFO_LOG("Command Line: Loading state index: {}", state_index.value());
        continue;
      }
      else if (CHECK_ARG_PARAM("-statefile"))
      {
        AutoBoot(autoboot)->save_state = args[++i].toStdString();
        INFO_LOG("Command Line: Loading state file: '{}'", autoboot->save_state);
        continue;
      }
      else if (CHECK_ARG_PARAM("-exe"))
      {
        AutoBoot(autoboot)->override_exe = args[++i].toStdString();
        INFO_LOG("Command Line: Overriding EXE file: '{}'", autoboot->override_exe);
        continue;
      }
      else if (CHECK_ARG("-fullscreen"))
      {
        INFO_LOG("Command Line: Using fullscreen.");
        AutoBoot(autoboot)->override_fullscreen = true;
        s_state.start_fullscreen_ui_fullscreen = true;
        continue;
      }
      else if (CHECK_ARG("-nofullscreen"))
      {
        INFO_LOG("Command Line: Not using fullscreen.");
        AutoBoot(autoboot)->override_fullscreen = false;
        continue;
      }
      else if (CHECK_ARG("-bigpicture"))
      {
        INFO_LOG("Command Line: Starting big picture mode.");
        s_state.start_fullscreen_ui = true;
        continue;
      }
      else if (CHECK_ARG("-setupwizard"))
      {
        s_state.run_setup_wizard = true;
        continue;
      }
      else if (CHECK_ARG("-earlyconsole"))
      {
        InitializeEarlyConsole();
        continue;
      }
      else if (CHECK_ARG("-updatecleanup"))
      {
        s_state.cleanup_after_update = true;
        continue;
      }
      else if (CHECK_ARG("--"))
      {
        no_more_args = true;
        continue;
      }
      else if (args[i][0] == QChar('-'))
      {
        QMessageBox::critical(nullptr, u"DuckStation"_s, u"Unknown parameter: %1"_s.arg(args[i]));
        return false;
      }

#undef CHECK_ARG
#undef CHECK_ARG_PARAM
    }

    if (autoboot && !autoboot->path.empty())
      autoboot->path += ' ';
    AutoBoot(autoboot)->path += args[i].toStdString();
  }

  // To do anything useful, we need the config initialized.
  Error error;
  if (!InitializeFoldersAndConfig(&error) || !LoadResources(&error))
  {
    // NOTE: No point translating this, because no config means the language won't be loaded anyway.
    QMessageBox::critical(nullptr, u"DuckStation"_s, QString::fromStdString(error.GetDescription()));
    return false;
  }

  // Check the file we're starting actually exists.
  if (autoboot && !autoboot->path.empty() && !CDImage::IsDeviceName(autoboot->path.c_str()))
  {
    autoboot->path = Path::RealPath(autoboot->path);

    if (!FileSystem::FileExists(autoboot->path.c_str()))
    {
      QMessageBox::critical(
        nullptr, QCoreApplication::translate("QtHost", "Error"),
        QCoreApplication::translate("QtHost", "File '%1' does not exist.").arg(QString::fromStdString(autoboot->path)));
      return false;
    }
  }

  if (state_index.has_value())
  {
    AutoBoot(autoboot);

    if (autoboot->path.empty())
    {
      // loading global state, -1 means resume the last game
      if (state_index.value() < 0)
        autoboot->save_state = System::GetMostRecentResumeSaveStatePath();
      else
        autoboot->save_state = System::GetGlobalSaveStatePath(state_index.value());
    }
    else
    {
      // loading game state
      const std::string game_serial = GameDatabase::GetSerialForPath(autoboot->path.c_str());
      autoboot->save_state = System::GetGameSaveStatePath(game_serial, state_index.value());
    }

    if (autoboot->save_state.empty() || !FileSystem::FileExists(autoboot->save_state.c_str()))
    {
      QMessageBox::critical(nullptr, QCoreApplication::translate("QtHost", "Error"),
                            QCoreApplication::translate("QtHost", "The specified save state does not exist."));
      return false;
    }
  }

  // check autoboot parameters, if we set something like fullscreen without a bios
  // or disc, we don't want to actually start.
  if (autoboot && autoboot->path.empty() && autoboot->save_state.empty() && !starting_bios)
    autoboot.reset();

  // nogui implies batch mode if not running big picture mode
  s_state.batch_mode = (s_state.batch_mode || (s_state.nogui_mode && !s_state.start_fullscreen_ui));

  // if we don't have autoboot, we definitely don't want batch mode (because that'll skip
  // scanning the game list).
  if (s_state.batch_mode && !autoboot && !s_state.start_fullscreen_ui)
  {
    QMessageBox::critical(
      nullptr, QCoreApplication::translate("QtHost", "Error"),
      s_state.nogui_mode ?
        QCoreApplication::translate("QtHost", "Cannot use no-gui mode, because no boot filename was specified.") :
        QCoreApplication::translate("QtHost", "Cannot use batch mode, because no boot filename was specified."));
    return false;
  }

  return true;
}

bool QtHost::RunSetupWizard()
{
  SetupWizardDialog dialog;
  if (dialog.exec() == QDialog::Rejected)
    return false;

  // Remove the flag.
  Core::SetBaseBoolSettingValue("Main", "SetupWizardIncomplete", false);
  Host::CommitBaseSettingChanges();
  return true;
}

int main(int argc, char* argv[])
{
  using namespace QtHost;

  if (!VeryEarlyProcessStartup())
    return EXIT_FAILURE;

  QApplication app(argc, argv);
  if (!PerformEarlyHardwareChecks())
    return EXIT_FAILURE;

#ifdef __linux__
  // Normally we'd have this shitfuckery in VeryEarlyProcessStartup(), but QApplication needs to be
  // created before the platform plugin is loaded. This is only here because GNOME plus Wankland
  // and their implementation of it is fucking terrible.
  ApplyWaylandWorkarounds();
#endif

  // Type registration has to happen after hardware checks, clang emits ptest instructions otherwise.
  RegisterTypes();

  std::shared_ptr<SystemBootParameters> autoboot;
  if (!ParseCommandLineParametersAndInitializeConfig(app, autoboot))
    return EXIT_FAILURE;

  if (!EarlyProcessStartup())
    return EXIT_FAILURE;

  // Remove any previous-version remnants.
  if (s_state.cleanup_after_update)
    AutoUpdaterDialog::cleanupAfterUpdate();

  // Set theme before creating any windows.
  UpdateApplicationTheme();

  // Build warning.
  AutoUpdaterDialog::warnAboutUnofficialBuild();

  // Setting/folder migrations.
  ApplyMigrations();

  // Create core thread object, but don't start it yet. That way the main window can connect to it,
  // and ensures that no signals are lost. Then we create and connect the main window.
  g_core_thread = new CoreThread();

  // Make sure the main window is the first window created.
  new MainWindow();

  // Start logging early, but don't show the window yet.
  // We want to catch the early messages, but if the window is shown the Windows taskbar will use
  // the log window icon as the application icon because it's the first window shown.
  LogWindow::updateSettings(true);

  // Now we can actually start the CPU thread.
  HookSignals();
  g_core_thread->start();

  // Optionally run setup wizard.
  int result;
  if (s_state.run_setup_wizard && !RunSetupWizard())
  {
    result = EXIT_FAILURE;
    goto shutdown_and_exit;
  }

#ifdef __linux__
  // Create desktop file if it does not exist.
  CheckDesktopFile();

  // I hate this so much. Turns out not only is window raising non-functional on GNOME for what I'm guessing
  // is purely political reasons, it's also very unreliable on KDE too. Sometimes raising works, other times
  // it doesn't. Deferring the request doesn't seem to help either. Can't be arsed to debug, just force all
  // Wayland down the reverse path of showing the log window first.
  if (IsRunningOnWayland())
  {
    LogWindow::deferredShow();
    QApplication::sync();
    QApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
  }
#endif

  // When running in batch mode, ensure game list is loaded, but don't scan for any new files.
  if (!s_state.batch_mode)
    g_main_window->refreshGameList(false);

  // Don't bother showing the window in no-gui mode.
  if (!s_state.nogui_mode)
    QtUtils::ShowOrRaiseWindow(g_main_window, nullptr, true);

  // Initialize big picture mode if requested.
  if (s_state.start_fullscreen_ui || Core::GetBaseBoolSettingValue("Main", "StartFullscreenUI", false))
    g_core_thread->startFullscreenUI();
  else
    s_state.start_fullscreen_ui_fullscreen = false;

  // Always kick off update check. It'll take over if the user is booting a game fullscreen.
  g_main_window->startupUpdateCheck();

  if (autoboot)
    g_core_thread->bootSystem(std::move(autoboot));

  // Bring the log window up last, so that its icon does not take precedence.
  if (LogWindow::deferredShow())
  {
    // But ensure the main window still has focus.
    QtUtils::RaiseWindow(g_main_window);
  }

  // This doesn't return until we exit.
  result = app.exec();

shutdown_and_exit:
  if (g_main_window)
    g_main_window->close();

  // Shutting down.
  g_core_thread->stop();
  delete g_core_thread;
  g_core_thread = nullptr;

  // Close main window.
  delete g_main_window;
  Assert(!g_main_window);

  ProcessShutdown();

  return result;
}
