#define IMGUI_DEFINE_MATH_OPERATORS

#include "fullscreen_ui.h"
#include "IconsFontAwesome5.h"
#include "cheevos.h"
#include "common/byte_stream.h"
#include "common/file_system.h"
#include "common/log.h"
#include "common/lru_cache.h"
#include "common/make_array.h"
#include "common/string.h"
#include "common/string_util.h"
#include "common_host_interface.h"
#include "controller_interface.h"
#include "core/cheats.h"
#include "core/cpu_core.h"
#include "core/gpu.h"
#include "core/host_display.h"
#include "core/host_interface_progress_callback.h"
#include "core/resources.h"
#include "core/settings.h"
#include "core/system.h"
#include "fullscreen_ui_progress_callback.h"
#include "game_list.h"
#include "icon.h"
#include "imgui.h"
#include "imgui_fullscreen.h"
#include "imgui_internal.h"
#include "imgui_stdlib.h"
#include "imgui_styles.h"
#include "scmversion/scmversion.h"
#include <bitset>
#include <thread>
Log_SetChannel(FullscreenUI);

static constexpr float LAYOUT_MAIN_MENU_BAR_SIZE = 20.0f; // Should be DPI scaled, not layout scaled!

using ImGuiFullscreen::g_large_font;
using ImGuiFullscreen::g_layout_padding_left;
using ImGuiFullscreen::g_layout_padding_top;
using ImGuiFullscreen::g_medium_font;
using ImGuiFullscreen::LAYOUT_LARGE_FONT_SIZE;
using ImGuiFullscreen::LAYOUT_MEDIUM_FONT_SIZE;
using ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT;
using ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY;
using ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING;
using ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING;
using ImGuiFullscreen::LAYOUT_SCREEN_HEIGHT;
using ImGuiFullscreen::LAYOUT_SCREEN_WIDTH;

using ImGuiFullscreen::ActiveButton;
using ImGuiFullscreen::BeginFullscreenColumns;
using ImGuiFullscreen::BeginFullscreenColumnWindow;
using ImGuiFullscreen::BeginFullscreenWindow;
using ImGuiFullscreen::BeginMenuButtons;
using ImGuiFullscreen::CloseChoiceDialog;
using ImGuiFullscreen::CloseFileSelector;
using ImGuiFullscreen::DPIScale;
using ImGuiFullscreen::EndFullscreenColumns;
using ImGuiFullscreen::EndFullscreenColumnWindow;
using ImGuiFullscreen::EndFullscreenWindow;
using ImGuiFullscreen::EndMenuButtons;
using ImGuiFullscreen::EnumChoiceButton;
using ImGuiFullscreen::FloatingButton;
using ImGuiFullscreen::LayoutScale;
using ImGuiFullscreen::MenuButton;
using ImGuiFullscreen::MenuButtonFrame;
using ImGuiFullscreen::MenuButtonWithValue;
using ImGuiFullscreen::MenuHeading;
using ImGuiFullscreen::MenuHeadingButton;
using ImGuiFullscreen::MenuImageButton;
using ImGuiFullscreen::OpenChoiceDialog;
using ImGuiFullscreen::OpenFileSelector;
using ImGuiFullscreen::RangeButton;
using ImGuiFullscreen::ToggleButton;

namespace FullscreenUI {

//////////////////////////////////////////////////////////////////////////
// Main
//////////////////////////////////////////////////////////////////////////
static void LoadSettings();
static void ClearImGuiFocus();
static void ReturnToMainWindow();
static void DrawLandingWindow();
static void DrawQuickMenu(MainWindowType type);
static void DrawAchievementWindow();
static void DrawLeaderboardsWindow();
static void DrawDebugMenu();
static void DrawStatsOverlay();
static void DrawOSDMessages();
static void DrawAboutWindow();
static void OpenAboutWindow();
static void SetDebugMenuEnabled(bool enabled);
static void UpdateDebugMenuVisibility();

static ALWAYS_INLINE bool IsCheevosHardcoreModeActive()
{
#ifdef WITH_CHEEVOS
  return Cheevos::IsChallengeModeActive();
#else
  return false;
#endif
}

static CommonHostInterface* s_host_interface;
static MainWindowType s_current_main_window = MainWindowType::Landing;
static std::bitset<static_cast<u32>(FrontendCommon::ControllerNavigationButton::Count)> s_nav_input_values{};
static bool s_debug_menu_enabled = false;
static bool s_debug_menu_allowed = false;
static bool s_show_status_indicators = false;
static bool s_quick_menu_was_open = false;
static bool s_was_paused_on_quick_menu_open = false;
static bool s_about_window_open = false;
static u32 s_close_button_state = 0;
static std::optional<u32> s_open_leaderboard_id;

//////////////////////////////////////////////////////////////////////////
// Resources
//////////////////////////////////////////////////////////////////////////
static std::unique_ptr<HostDisplayTexture> LoadTextureResource(const char* name, bool allow_fallback = true);
static bool LoadResources();
static void DestroyResources();

static HostDisplayTexture* GetCachedTexture(const std::string& name);
static ImTextureID ResolveTextureHandle(const std::string& name);

static std::unique_ptr<HostDisplayTexture> s_app_icon_texture;
static std::unique_ptr<HostDisplayTexture> s_placeholder_texture;
static std::array<std::unique_ptr<HostDisplayTexture>, static_cast<u32>(DiscRegion::Count)> s_disc_region_textures;
static std::array<std::unique_ptr<HostDisplayTexture>, static_cast<u32>(GameListCompatibilityRating::Count)>
  s_game_compatibility_textures;
static std::unique_ptr<HostDisplayTexture> s_fallback_disc_texture;
static std::unique_ptr<HostDisplayTexture> s_fallback_exe_texture;
static std::unique_ptr<HostDisplayTexture> s_fallback_psf_texture;
static std::unique_ptr<HostDisplayTexture> s_fallback_playlist_texture;
static LRUCache<std::string, std::unique_ptr<HostDisplayTexture>> s_texture_cache;

//////////////////////////////////////////////////////////////////////////
// Settings
//////////////////////////////////////////////////////////////////////////

enum class InputBindingType
{
  None,
  Button,
  Axis,
  Rumble
};

static constexpr double INPUT_BINDING_TIMEOUT_SECONDS = 5.0;

static void DrawSettingsWindow();
static void BeginInputBinding(InputBindingType type, const std::string_view& section, const std::string_view& key,
                              const std::string_view& display_name);
static void EndInputBinding();
static void ClearInputBinding(const char* section, const char* key);
static void DrawInputBindingWindow();

static SettingsPage s_settings_page = SettingsPage::InterfaceSettings;
static Settings s_settings_copy;
static InputBindingType s_input_binding_type = InputBindingType::None;
static TinyString s_input_binding_section;
static TinyString s_input_binding_key;
static TinyString s_input_binding_display_name;
static bool s_input_binding_keyboard_pressed;
static Common::Timer s_input_binding_timer;

//////////////////////////////////////////////////////////////////////////
// Save State List
//////////////////////////////////////////////////////////////////////////
struct SaveStateListEntry
{
  std::string title;
  std::string summary;
  std::string path;
  std::string media_path;
  std::unique_ptr<HostDisplayTexture> preview_texture;
  s32 slot;
  bool global;
};

static void InitializePlaceholderSaveStateListEntry(SaveStateListEntry* li, s32 slot, bool global);
static void InitializeSaveStateListEntry(SaveStateListEntry* li, CommonHostInterface::ExtendedSaveStateInfo* ssi);
static void PopulateSaveStateListEntries();
static void OpenSaveStateSelector(bool is_loading);
static void CloseSaveStateSelector();
static void DrawSaveStateSelector(bool is_loading, bool fullscreen);

static std::vector<SaveStateListEntry> s_save_state_selector_slots;
static bool s_save_state_selector_open = false;
static bool s_save_state_selector_loading = true;

//////////////////////////////////////////////////////////////////////////
// Game List
//////////////////////////////////////////////////////////////////////////
static void DrawGameListWindow();
static void SwitchToGameList();
static void SortGameList();
static HostDisplayTexture* GetTextureForGameListEntryType(GameListEntryType type);
static HostDisplayTexture* GetGameListCover(const GameListEntry* entry);
static HostDisplayTexture* GetCoverForCurrentGame();

// Lazily populated cover images.
static std::unordered_map<std::string, std::string> s_cover_image_map;
static std::vector<const GameListEntry*> s_game_list_sorted_entries;
static std::thread s_game_list_load_thread;

//////////////////////////////////////////////////////////////////////////
// Main
//////////////////////////////////////////////////////////////////////////

bool Initialize(CommonHostInterface* host_interface)
{
  s_host_interface = host_interface;
  s_texture_cache.SetMaxCapacity(128);
  if (!LoadResources())
    return false;

  s_settings_copy.Load(*s_host_interface->GetSettingsInterface());
  LoadSettings();
  UpdateDebugMenuVisibility();

  ImGuiFullscreen::UpdateLayoutScale();
  ImGuiFullscreen::UpdateFonts();
  ImGuiFullscreen::SetResolveTextureFunction(ResolveTextureHandle);

  if (System::IsValid())
    SystemCreated();
  if (System::IsPaused())
    SystemPaused(true);

  return true;
}

bool IsInitialized()
{
  return (s_host_interface != nullptr);
}

bool HasActiveWindow()
{
  return s_current_main_window != MainWindowType::None || s_save_state_selector_open ||
         ImGuiFullscreen::IsChoiceDialogOpen() || ImGuiFullscreen::IsFileSelectorOpen();
}

void LoadSettings()
{
  s_show_status_indicators = s_host_interface->GetBoolSettingValue("Display", "ShowStatusIndicators", true);
}

void UpdateSettings()
{
  LoadSettings();
}

void SystemCreated()
{
  s_current_main_window = MainWindowType::None;
  ClearImGuiFocus();
}

void SystemDestroyed()
{
  s_current_main_window = MainWindowType::Landing;
  s_quick_menu_was_open = false;
  ClearImGuiFocus();
}

void SystemPaused(bool paused)
{
  //
}

void OpenQuickMenu()
{
  if (!System::IsValid() || s_current_main_window != MainWindowType::None)
    return;

  s_was_paused_on_quick_menu_open = System::IsPaused();
  if (s_settings_copy.pause_on_menu && !s_was_paused_on_quick_menu_open)
    s_host_interface->RunLater([]() { s_host_interface->PauseSystem(true); });

  s_current_main_window = MainWindowType::QuickMenu;
  s_quick_menu_was_open = true;
  ClearImGuiFocus();
}

void CloseQuickMenu()
{
  if (!System::IsValid())
    return;

  if (System::IsPaused() && !s_was_paused_on_quick_menu_open)
    s_host_interface->RunLater([]() { s_host_interface->PauseSystem(false); });

  s_current_main_window = MainWindowType::None;
  s_quick_menu_was_open = false;
  ClearImGuiFocus();
}

bool OpenAchievementsWindow()
{
  const bool achievements_enabled = Cheevos::HasActiveGame() && (Cheevos::GetAchievementCount() > 0);
  if (!achievements_enabled)
    return false;

  s_current_main_window = MainWindowType::Achievements;
  s_quick_menu_was_open = false;
  return true;
}

bool OpenLeaderboardsWindow()
{
  const bool leaderboards_enabled = Cheevos::HasActiveGame() && (Cheevos::GetLeaderboardCount() > 0);
  if (!leaderboards_enabled)
    return false;

  s_current_main_window = MainWindowType::Leaderboards;
  s_open_leaderboard_id.reset();
  s_quick_menu_was_open = false;
  return true;
}

void Shutdown()
{
  if (s_game_list_load_thread.joinable())
    s_game_list_load_thread.join();

  CloseSaveStateSelector();
  s_cover_image_map.clear();
  s_nav_input_values = {};
  DestroyResources();

  s_host_interface = nullptr;
}

void Render()
{
  if (s_debug_menu_enabled)
    DrawDebugMenu();

  if (System::IsValid())
  {
    if (!s_debug_menu_enabled)
      DrawStatsOverlay();

    if (!IsCheevosHardcoreModeActive())
      s_host_interface->DrawDebugWindows();
  }

  ImGuiFullscreen::BeginLayout();

  switch (s_current_main_window)
  {
    case MainWindowType::Landing:
      DrawLandingWindow();
      break;
    case MainWindowType::GameList:
      DrawGameListWindow();
      break;
    case MainWindowType::Settings:
      DrawSettingsWindow();
      break;
    case MainWindowType::QuickMenu:
      DrawQuickMenu(s_current_main_window);
      break;
    case MainWindowType::Achievements:
      DrawAchievementWindow();
      break;
    case MainWindowType::Leaderboards:
      DrawLeaderboardsWindow();
      break;
    default:
      break;
  }

  if (s_save_state_selector_open)
    DrawSaveStateSelector(s_save_state_selector_loading, false);

  if (s_about_window_open)
    DrawAboutWindow();

  if (s_input_binding_type != InputBindingType::None)
    DrawInputBindingWindow();

  DrawOSDMessages();

  ImGuiFullscreen::EndLayout();
}

Settings& GetSettingsCopy()
{
  return s_settings_copy;
}

void SaveAndApplySettings()
{
  s_settings_copy.Save(*s_host_interface->GetSettingsInterface());
  s_host_interface->GetSettingsInterface()->Save();
  s_host_interface->ApplySettings(false);
  UpdateSettings();
}

void ClearImGuiFocus()
{
  ImGui::SetWindowFocus(nullptr);
  s_close_button_state = 0;
}

void ReturnToMainWindow()
{
  if (s_quick_menu_was_open)
    CloseQuickMenu();

  s_current_main_window = System::IsValid() ? MainWindowType::None : MainWindowType::Landing;
}

bool LoadResources()
{
  if (!(s_app_icon_texture = LoadTextureResource("logo.png", false)) &&
      !(s_app_icon_texture = LoadTextureResource("duck.png")))
  {
    return false;
  }

  if (!(s_placeholder_texture = s_host_interface->GetDisplay()->CreateTexture(
          PLACEHOLDER_ICON_WIDTH, PLACEHOLDER_ICON_HEIGHT, 1, 1, 1, HostDisplayPixelFormat::RGBA8,
          PLACEHOLDER_ICON_DATA, sizeof(u32) * PLACEHOLDER_ICON_WIDTH, false)))
  {
    return false;
  }

  if (!(s_disc_region_textures[static_cast<u32>(DiscRegion::NTSC_U)] = LoadTextureResource("flag-uc.png")) ||
      !(s_disc_region_textures[static_cast<u32>(DiscRegion::NTSC_J)] = LoadTextureResource("flag-jp.png")) ||
      !(s_disc_region_textures[static_cast<u32>(DiscRegion::PAL)] = LoadTextureResource("flag-eu.png")) ||
      !(s_disc_region_textures[static_cast<u32>(DiscRegion::Other)] = LoadTextureResource("flag-eu.png")) ||
      !(s_fallback_disc_texture = LoadTextureResource("media-cdrom.png")) ||
      !(s_fallback_exe_texture = LoadTextureResource("applications-system.png")) ||
      !(s_fallback_psf_texture = LoadTextureResource("multimedia-player.png")) ||
      !(s_fallback_playlist_texture = LoadTextureResource("address-book-new.png")))
  {
    return false;
  }

  for (u32 i = 0; i < static_cast<u32>(GameListCompatibilityRating::Count); i++)
  {
    if (!(s_game_compatibility_textures[i] = LoadTextureResource(TinyString::FromFormat("star-%u.png", i))))
      return false;
  }

  {
    std::unique_ptr<ByteStream> stream = s_host_interface->OpenPackageFile(
      "resources" FS_OSPATH_SEPARATOR_STR "fa-solid-900.ttf", BYTESTREAM_OPEN_READ | BYTESTREAM_OPEN_STREAMED);
    if (!stream)
      return false;

    std::vector<u8> font_data = FileSystem::ReadBinaryStream(stream.get());
    if (font_data.empty())
      return false;

    ImGuiFullscreen::SetIconFontData(std::move(font_data));
  }

  return true;
}

void DestroyResources()
{
  s_texture_cache.Clear();
  s_app_icon_texture.reset();
  s_placeholder_texture.reset();
  s_fallback_playlist_texture.reset();
  s_fallback_psf_texture.reset();
  s_fallback_exe_texture.reset();
  s_fallback_disc_texture.reset();
  for (auto& tex : s_game_compatibility_textures)
    tex.reset();
  for (auto& tex : s_disc_region_textures)
    tex.reset();
}

static std::unique_ptr<HostDisplayTexture> LoadTexture(const char* path, bool from_package)
{
  std::unique_ptr<ByteStream> stream;
  if (from_package)
    stream = s_host_interface->OpenPackageFile(path, BYTESTREAM_OPEN_READ);
  else
    stream = FileSystem::OpenFile(path, BYTESTREAM_OPEN_READ);
  if (!stream)
  {
    Log_ErrorPrintf("Failed to open texture resource '%s'", path);
    return {};
  }

  Common::RGBA8Image image;
  if (!Common::LoadImageFromStream(&image, stream.get()) && image.IsValid())
  {
    Log_ErrorPrintf("Failed to read texture resource '%s'", path);
    return {};
  }

  std::unique_ptr<HostDisplayTexture> texture = s_host_interface->GetDisplay()->CreateTexture(
    image.GetWidth(), image.GetHeight(), 1, 1, 1, HostDisplayPixelFormat::RGBA8, image.GetPixels(),
    image.GetByteStride());
  if (!texture)
  {
    Log_ErrorPrintf("failed to create %ux%u texture for resource", image.GetWidth(), image.GetHeight());
    return {};
  }

  Log_DevPrintf("Uploaded texture resource '%s' (%ux%u)", path, image.GetWidth(), image.GetHeight());
  return texture;
}

std::unique_ptr<HostDisplayTexture> LoadTextureResource(const char* name, bool allow_fallback /*= true*/)
{
  const std::string path(StringUtil::StdStringFromFormat("resources" FS_OSPATH_SEPARATOR_STR "%s", name));
  std::unique_ptr<HostDisplayTexture> texture = LoadTexture(path.c_str(), true);
  if (texture)
    return texture;

  if (!allow_fallback)
    return nullptr;

  Log_ErrorPrintf("Missing resource '%s', using fallback", name);

  texture = s_host_interface->GetDisplay()->CreateTexture(PLACEHOLDER_ICON_WIDTH, PLACEHOLDER_ICON_HEIGHT, 1, 1, 1,
                                                          HostDisplayPixelFormat::RGBA8, PLACEHOLDER_ICON_DATA,
                                                          sizeof(u32) * PLACEHOLDER_ICON_WIDTH, false);
  if (!texture)
    Panic("Failed to create placeholder texture");

  return texture;
}

HostDisplayTexture* GetCachedTexture(const std::string& name)
{
  std::unique_ptr<HostDisplayTexture>* tex_ptr = s_texture_cache.Lookup(name);
  if (!tex_ptr)
  {
    std::unique_ptr<HostDisplayTexture> tex = LoadTexture(name.c_str(), false);
    tex_ptr = s_texture_cache.Insert(name, std::move(tex));
  }

  return tex_ptr->get();
}

ImTextureID ResolveTextureHandle(const std::string& name)
{
  HostDisplayTexture* tex = GetCachedTexture(name);
  return tex ? tex->GetHandle() : nullptr;
}

bool InvalidateCachedTexture(const std::string& path)
{
  return s_texture_cache.Remove(path);
}

//////////////////////////////////////////////////////////////////////////
// Utility
//////////////////////////////////////////////////////////////////////////

static ImGuiFullscreen::FileSelectorFilters GetDiscImageFilters()
{
  return {"*.bin",   "*.cue", "*.iso", "*.img",     "*.chd", "*.ecm", "*.mds",
          "*.psexe", "*.exe", "*.psf", "*.minipsf", "*.m3u", "*.pbp"};
}

static void DoStartPath(const std::string& path, bool allow_resume)
{
  // we can never resume from exe/psf, or when challenge mode is active
  if (System::IsExeFileName(path.c_str()) || System::IsPsfFileName(path.c_str()) ||
      s_host_interface->IsCheevosChallengeModeActive())
    allow_resume = false;

  if (allow_resume && g_settings.save_state_on_exit)
  {
    s_host_interface->ResumeSystemFromState(path.c_str(), true);
    return;
  }

  s_host_interface->BootSystem(std::make_shared<SystemBootParameters>(path));
}

static void DoStartFile()
{
  auto callback = [](const std::string& path) {
    if (!path.empty())
      DoStartPath(path, false);

    ClearImGuiFocus();
    CloseFileSelector();
  };

  OpenFileSelector(ICON_FA_COMPACT_DISC "  Select Disc Image", false, std::move(callback), GetDiscImageFilters());
}

static void DoStartBIOS()
{
  s_host_interface->RunLater([]() { s_host_interface->BootSystem(std::make_shared<SystemBootParameters>()); });
  ClearImGuiFocus();
}

static void DoPowerOff()
{
  s_host_interface->RunLater([]() {
    if (!System::IsValid())
      return;

    s_host_interface->PowerOffSystem(s_host_interface->ShouldSaveResumeState());

    ReturnToMainWindow();
  });
  ClearImGuiFocus();
}

static void DoReset()
{
  s_host_interface->RunLater([]() {
    if (!System::IsValid())
      return;

    s_host_interface->ResetSystem();
  });
}

static void DoPause()
{
  s_host_interface->RunLater([]() {
    if (!System::IsValid())
      return;

    s_host_interface->PauseSystem(!System::IsPaused());
  });
}

static void DoCheatsMenu()
{
  CheatList* cl = System::GetCheatList();
  if (!cl)
  {
    if (!s_host_interface->LoadCheatListFromDatabase() || !(cl = System::GetCheatList()))
    {
      s_host_interface->AddFormattedOSDMessage(10.0f, "No cheats found for %s.", System::GetRunningTitle().c_str());
      ReturnToMainWindow();
      return;
    }
  }

  ImGuiFullscreen::ChoiceDialogOptions options;
  options.reserve(cl->GetCodeCount());
  for (u32 i = 0; i < cl->GetCodeCount(); i++)
  {
    const CheatCode& cc = cl->GetCode(i);
    options.emplace_back(cc.description.c_str(), cc.enabled);
  }

  auto callback = [](s32 index, const std::string& title, bool checked) {
    if (index < 0)
    {
      ReturnToMainWindow();
      return;
    }

    CheatList* cl = System::GetCheatList();
    if (!cl)
      return;

    const CheatCode& cc = cl->GetCode(static_cast<u32>(index));
    if (cc.activation == CheatCode::Activation::Manual)
      cl->ApplyCode(static_cast<u32>(index));
    else
      s_host_interface->SetCheatCodeState(static_cast<u32>(index), checked, true);
  };
  OpenChoiceDialog(ICON_FA_FROWN "  Cheat List", true, std::move(options), std::move(callback));
}

static void DoToggleAnalogMode()
{
  // hacky way to toggle analog mode
  for (u32 i = 0; i < NUM_CONTROLLER_AND_CARD_PORTS; i++)
  {
    Controller* ctrl = System::GetController(i);
    if (!ctrl)
      continue;

    std::optional<s32> code = Controller::GetButtonCodeByName(ctrl->GetType(), "Analog");
    if (!code.has_value())
      continue;

    ctrl->SetButtonState(code.value(), true);
    ctrl->SetButtonState(code.value(), false);
  }
}

static void DoChangeDiscFromFile()
{
  auto callback = [](const std::string& path) {
    if (!path.empty())
      System::InsertMedia(path.c_str());

    ClearImGuiFocus();
    CloseFileSelector();
    ReturnToMainWindow();
  };

  OpenFileSelector(ICON_FA_COMPACT_DISC "  Select Disc Image", false, std::move(callback), GetDiscImageFilters(),
                   std::string(FileSystem::GetPathDirectory(System::GetMediaFileName())));
}

static void DoChangeDisc()
{
  if (!System::HasMediaSubImages())
  {
    DoChangeDiscFromFile();
    return;
  }

  const u32 current_index = System::GetMediaSubImageIndex();
  const u32 count = System::GetMediaSubImageCount();
  ImGuiFullscreen::ChoiceDialogOptions options;
  options.reserve(count + 1);
  options.emplace_back("From File...", false);

  for (u32 i = 0; i < count; i++)
    options.emplace_back(System::GetMediaSubImageTitle(i), i == current_index);

  auto callback = [](s32 index, const std::string& title, bool checked) {
    if (index == 0)
    {
      CloseChoiceDialog();
      DoChangeDiscFromFile();
      return;
    }
    else if (index > 0)
    {
      System::SwitchMediaSubImage(static_cast<u32>(index - 1));
    }

    ClearImGuiFocus();
    CloseChoiceDialog();
    ReturnToMainWindow();
  };

  OpenChoiceDialog(ICON_FA_COMPACT_DISC "  Select Disc Image", true, std::move(options), std::move(callback));
}

//////////////////////////////////////////////////////////////////////////
// Landing Window
//////////////////////////////////////////////////////////////////////////

void DrawLandingWindow()
{
  BeginFullscreenColumns();

  if (BeginFullscreenColumnWindow(0.0f, 570.0f, "logo", ImVec4(0.11f, 0.15f, 0.17f, 1.00f)))
  {
    const float image_size = LayoutScale(380.f);
    ImGui::SetCursorPos(ImVec2((ImGui::GetWindowWidth() * 0.5f) - (image_size * 0.5f),
                               (ImGui::GetWindowHeight() * 0.5f) - (image_size * 0.5f)));
    ImGui::Image(s_app_icon_texture->GetHandle(), ImVec2(image_size, image_size));
  }
  EndFullscreenColumnWindow();

  if (BeginFullscreenColumnWindow(570.0f, LAYOUT_SCREEN_WIDTH, "menu"))
  {
    BeginMenuButtons(7, 0.5f);

    if (MenuButton(" " ICON_FA_PLAY_CIRCLE "  Resume",
                   "Starts the console from where it was before it was last closed.", !IsCheevosHardcoreModeActive()))
    {
      s_host_interface->RunLater([]() { s_host_interface->ResumeSystemFromMostRecentState(); });
      ClearImGuiFocus();
    }

    if (MenuButton(" " ICON_FA_LIST "  Open Game List",
                   "Launch a game from images scanned from your game directories."))
    {
      s_host_interface->RunLater(SwitchToGameList);
    }

    if (MenuButton(" " ICON_FA_FOLDER_OPEN "  Start File", "Launch a game by selecting a file/disc image."))
      s_host_interface->RunLater(DoStartFile);

    if (MenuButton(" " ICON_FA_TOOLBOX "  Start BIOS", "Start the console without any disc inserted."))
      s_host_interface->RunLater(DoStartBIOS);

    if (MenuButton(" " ICON_FA_UNDO "  Load State", "Loads a global save state.", !IsCheevosHardcoreModeActive()))
    {
      OpenSaveStateSelector(true);
    }

    if (MenuButton(" " ICON_FA_SLIDERS_H "  Settings", "Change settings for the emulator."))
      s_current_main_window = MainWindowType::Settings;

    if (MenuButton(" " ICON_FA_SIGN_OUT_ALT "  Exit", "Exits the program."))
      s_host_interface->RequestExit();

    {
      ImVec2 fullscreen_pos;
      if (FloatingButton(ICON_FA_WINDOW_CLOSE, 0.0f, 0.0f, -1.0f, -1.0f, 1.0f, 0.0f, true, g_large_font,
                         &fullscreen_pos))
      {
        s_host_interface->RequestExit();
      }

      if (FloatingButton(ICON_FA_EXPAND, fullscreen_pos.x, 0.0f, -1.0f, -1.0f, -1.0f, 0.0f, true, g_large_font,
                         &fullscreen_pos))
      {
        s_host_interface->RunLater([]() { s_host_interface->SetFullscreen(!s_host_interface->IsFullscreen()); });
      }

      if (FloatingButton(ICON_FA_QUESTION_CIRCLE, fullscreen_pos.x, 0.0f, -1.0f, -1.0f, -1.0f, 0.0f))
        OpenAboutWindow();
    }

    EndMenuButtons();
  }

  EndFullscreenColumnWindow();

  EndFullscreenColumns();
}

static ImGuiFullscreen::ChoiceDialogOptions GetGameListDirectoryOptions(bool recursive_as_checked)
{
  ImGuiFullscreen::ChoiceDialogOptions options;

  for (std::string& dir : s_host_interface->GetSettingsInterface()->GetStringList("GameList", "Paths"))
    options.emplace_back(std::move(dir), false);

  for (std::string& dir : s_host_interface->GetSettingsInterface()->GetStringList("GameList", "RecursivePaths"))
    options.emplace_back(std::move(dir), recursive_as_checked);

  std::sort(options.begin(), options.end(), [](const auto& lhs, const auto& rhs) {
    return (StringUtil::Strcasecmp(lhs.first.c_str(), rhs.first.c_str()) < 0);
  });

  return options;
}

static void DrawInputBindingButton(InputBindingType type, const char* section, const char* name,
                                   const char* display_name, bool show_type = true)
{
  TinyString title;
  title.Format("%s/%s", section, name);

  ImRect bb;
  bool visible, hovered, clicked;
  clicked =
    MenuButtonFrame(title, true, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, &visible, &hovered, &bb.Min, &bb.Max);
  if (!visible)
    return;

  const float midpoint = bb.Min.y + g_large_font->FontSize + LayoutScale(4.0f);
  const ImRect title_bb(bb.Min, ImVec2(bb.Max.x, midpoint));
  const ImRect summary_bb(ImVec2(bb.Min.x, midpoint), bb.Max);

  if (show_type)
  {
    switch (type)
    {
      case InputBindingType::Button:
        title.Format(ICON_FA_CIRCLE "  %s Button", display_name);
        break;
      case InputBindingType::Axis:
        title.Format(ICON_FA_BULLSEYE "  %s Axis", display_name);
        break;
      case InputBindingType::Rumble:
        title.Format(ICON_FA_BELL "  %s", display_name);
        break;
      default:
        title = display_name;
        break;
    }
  }

  ImGui::PushFont(g_large_font);
  ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, show_type ? title.GetCharArray() : display_name, nullptr,
                           nullptr, ImVec2(0.0f, 0.0f), &title_bb);
  ImGui::PopFont();

  // eek, potential heap allocation :/
  const std::string value = s_host_interface->GetSettingsInterface()->GetStringValue(section, name);
  ImGui::PushFont(g_medium_font);
  ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, value.empty() ? "(No Binding)" : value.c_str(), nullptr,
                           nullptr, ImVec2(0.0f, 0.0f), &summary_bb);
  ImGui::PopFont();

  if (clicked)
    BeginInputBinding(type, section, name, display_name);
  else if (ImGui::IsItemClicked(ImGuiMouseButton_Right))
    ClearInputBinding(section, name);
}

static void ClearInputBindingVariables()
{
  s_input_binding_type = InputBindingType::None;
  s_input_binding_section.Clear();
  s_input_binding_key.Clear();
  s_input_binding_display_name.Clear();
}

bool IsBindingInput()
{
  return s_input_binding_type != InputBindingType::None;
}

bool HandleKeyboardBinding(const char* keyName, bool pressed)
{
  if (s_input_binding_type == InputBindingType::None)
    return false;

  if (pressed)
  {
    s_input_binding_keyboard_pressed = true;
    return true;
  }

  if (!s_input_binding_keyboard_pressed)
    return false;

  TinyString value;
  value.Format("Keyboard/%s", keyName);

  {
    auto lock = s_host_interface->GetSettingsLock();
    s_host_interface->GetSettingsInterface()->SetStringValue(s_input_binding_section, s_input_binding_key, value);
  }

  EndInputBinding();
  s_host_interface->RunLater(SaveAndApplySettings);
  return true;
}

void BeginInputBinding(InputBindingType type, const std::string_view& section, const std::string_view& key,
                       const std::string_view& display_name)
{
  s_input_binding_type = type;
  s_input_binding_section = section;
  s_input_binding_key = key;
  s_input_binding_display_name = display_name;
  s_input_binding_timer.Reset();

  ControllerInterface* ci = s_host_interface->GetControllerInterface();
  if (ci)
  {
    auto callback = [](const ControllerInterface::Hook& hook) -> ControllerInterface::Hook::CallbackResult {
      // ignore if axis isn't at least halfway
      if (hook.type == ControllerInterface::Hook::Type::Axis && std::abs(std::get<float>(hook.value)) < 0.5f)
        return ControllerInterface::Hook::CallbackResult::ContinueMonitoring;

      TinyString value;
      switch (s_input_binding_type)
      {
        case InputBindingType::Axis:
        {
          if (hook.type == ControllerInterface::Hook::Type::Axis)
            value.Format("Controller%d/Axis%d", hook.controller_index, hook.button_or_axis_number);
        }
        break;

        case InputBindingType::Button:
        {
          if (hook.type == ControllerInterface::Hook::Type::Axis)
            value.Format("Controller%d/+Axis%d", hook.controller_index, hook.button_or_axis_number);
          else if (hook.type == ControllerInterface::Hook::Type::Button && std::get<float>(hook.value) > 0.0f)
            value.Format("Controller%d/Button%d", hook.controller_index, hook.button_or_axis_number);
        }
        break;

        case InputBindingType::Rumble:
        {
          if (hook.type == ControllerInterface::Hook::Type::Button && std::get<float>(hook.value) > 0.0f)
            value.Format("Controller%d", hook.controller_index);
        }
        break;

        default:
          break;
      }

      if (value.IsEmpty())
        return ControllerInterface::Hook::CallbackResult::ContinueMonitoring;

      {
        auto lock = s_host_interface->GetSettingsLock();
        s_host_interface->GetSettingsInterface()->SetStringValue(s_input_binding_section, s_input_binding_key, value);
      }

      ClearInputBindingVariables();
      s_host_interface->RunLater(SaveAndApplySettings);

      return ControllerInterface::Hook::CallbackResult::StopMonitoring;
    };
    ci->SetHook(std::move(callback));
  }
}

void EndInputBinding()
{
  ClearInputBindingVariables();

  ControllerInterface* ci = s_host_interface->GetControllerInterface();
  if (ci)
    ci->ClearHook();
}

void ClearInputBinding(const char* section, const char* key)
{
  {
    auto lock = s_host_interface->GetSettingsLock();
    s_host_interface->GetSettingsInterface()->DeleteValue(section, key);
  }

  s_host_interface->RunLater(SaveAndApplySettings);
}

void DrawInputBindingWindow()
{
  DebugAssert(s_input_binding_type != InputBindingType::None);

  const double time_remaining = INPUT_BINDING_TIMEOUT_SECONDS - s_input_binding_timer.GetTimeSeconds();
  if (time_remaining <= 0.0)
  {
    EndInputBinding();
    return;
  }

  const char* title = ICON_FA_GAMEPAD "  Set Input Binding";
  ImGui::SetNextWindowSize(LayoutScale(500.0f, 0.0f));
  ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
  ImGui::OpenPopup(title);

  ImGui::PushFont(g_large_font);
  ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
  ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING,
                                                              ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING));
  ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(10.0f, 10.0f));

  if (ImGui::BeginPopupModal(title, nullptr,
                             ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoInputs))
  {
    ImGui::TextWrapped("Setting %s binding %s.", s_input_binding_section.GetCharArray(),
                       s_input_binding_display_name.GetCharArray());
    ImGui::TextUnformatted("Push a controller button or axis now.");
    ImGui::NewLine();
    ImGui::Text("Timing out in %.0f seconds...", time_remaining);
    ImGui::EndPopup();
  }

  ImGui::PopStyleVar(3);
  ImGui::PopFont();
}

static bool SettingInfoButton(const SettingInfo& si, const char* section)
{
  // this.. isn't pretty :(
  TinyString title;
  title.Format("%s##%s/%s", si.visible_name, section, si.key);
  switch (si.type)
  {
    case SettingInfo::Type::Boolean:
    {
      bool value = s_host_interface->GetSettingsInterface()->GetBoolValue(
        section, si.key, StringUtil::FromChars<bool>(si.default_value).value_or(false));
      if (ToggleButton(title, si.description, &value))
      {
        s_host_interface->GetSettingsInterface()->SetBoolValue(section, si.key, value);
        return true;
      }

      return false;
    }

    case SettingInfo::Type::Integer:
    {
      int value = s_host_interface->GetSettingsInterface()->GetIntValue(
        section, si.key, StringUtil::FromChars<int>(si.default_value).value_or(0));
      const int min = StringUtil::FromChars<int>(si.min_value).value_or(0);
      const int max = StringUtil::FromChars<int>(si.max_value).value_or(0);
      const int step = StringUtil::FromChars<int>(si.step_value).value_or(0);
      if (RangeButton(title, si.description, &value, min, max, step))
      {
        s_host_interface->GetSettingsInterface()->SetIntValue(section, si.key, value);
        return true;
      }

      return false;
    }

    case SettingInfo::Type::Float:
    {
      float value = s_host_interface->GetSettingsInterface()->GetFloatValue(
        section, si.key, StringUtil::FromChars<float>(si.default_value).value_or(0));
      const float min = StringUtil::FromChars<float>(si.min_value).value_or(0);
      const float max = StringUtil::FromChars<float>(si.max_value).value_or(0);
      const float step = StringUtil::FromChars<float>(si.step_value).value_or(0);
      if (RangeButton(title, si.description, &value, min, max, step))
      {
        s_host_interface->GetSettingsInterface()->SetFloatValue(section, si.key, value);
        return true;
      }

      return false;
    }

    case SettingInfo::Type::Path:
    {
      std::string value = s_host_interface->GetSettingsInterface()->GetStringValue(section, si.key);
      if (MenuButtonWithValue(title, si.description, value.c_str()))
      {
        std::string section_copy(section);
        std::string key_copy(si.key);
        auto callback = [section_copy, key_copy](const std::string& path) {
          if (!path.empty())
          {
            s_host_interface->GetSettingsInterface()->SetStringValue(section_copy.c_str(), key_copy.c_str(),
                                                                     path.c_str());
            s_host_interface->RunLater(SaveAndApplySettings);
          }

          ClearImGuiFocus();
          CloseFileSelector();
        };
        OpenFileSelector(si.visible_name, false, std::move(callback), ImGuiFullscreen::FileSelectorFilters(),
                         std::string(FileSystem::GetPathDirectory(std::move(value))));
      }

      return false;
    }

    default:
      return false;
  }
}

static bool ToggleButtonForNonSetting(const char* title, const char* summary, const char* section, const char* key,
                                      bool default_value, bool enabled = true,
                                      float height = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT,
                                      ImFont* font = g_large_font, ImFont* summary_font = g_medium_font)
{
  bool value = s_host_interface->GetSettingsInterface()->GetBoolValue(section, key, default_value);
  if (!ToggleButton(title, summary, &value, enabled, height, font, summary_font))
    return false;

  s_host_interface->GetSettingsInterface()->SetBoolValue(section, key, value);
  return true;
}

#ifdef WITH_CHEEVOS

static void DrawAchievementsLoginWindow()
{
  ImGui::SetNextWindowSize(LayoutScale(700.0f, 0.0f));
  ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f));

  ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
  ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(10.0f, 10.0f));
  ImGui::PushFont(g_large_font);

  bool is_open = true;
  if (ImGui::BeginPopupModal("Achievements Login", &is_open, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize))
  {

    ImGui::TextWrapped("Please enter user name and password for retroachievements.org.");
    ImGui::NewLine();
    ImGui::TextWrapped(
      "Your password will not be saved in DuckStation, an access token will be generated and used instead.");

    ImGui::NewLine();

    static char username[256] = {};
    static char password[256] = {};

    ImGui::Text("User Name: ");
    ImGui::SameLine(LayoutScale(200.0f));
    ImGui::InputText("##username", username, sizeof(username));

    ImGui::Text("Password: ");
    ImGui::SameLine(LayoutScale(200.0f));
    ImGui::InputText("##password", password, sizeof(password), ImGuiInputTextFlags_Password);

    ImGui::NewLine();

    BeginMenuButtons();

    const bool login_enabled = (std::strlen(username) > 0 && std::strlen(password) > 0);

    if (ActiveButton(ICON_FA_KEY "  Login", false, login_enabled))
    {
      Cheevos::LoginAsync(username, password);
      std::memset(username, 0, sizeof(username));
      std::memset(password, 0, sizeof(password));
      ImGui::CloseCurrentPopup();
    }

    if (ActiveButton(ICON_FA_TIMES "  Cancel", false))
      ImGui::CloseCurrentPopup();

    EndMenuButtons();

    ImGui::EndPopup();
  }

  ImGui::PopFont();
  ImGui::PopStyleVar(2);
}

static bool ConfirmChallengeModeEnable()
{
  if (!System::IsValid())
    return true;

  const bool cheevos_enabled = s_host_interface->GetBoolSettingValue("Cheevos", "Enabled", false);
  const bool cheevos_hardcore = s_host_interface->GetBoolSettingValue("Cheevos", "ChallengeMode", false);
  if (!cheevos_enabled || !cheevos_hardcore)
    return true;

  SmallString message;
  message.AppendString("Enabling hardcore mode will shut down your current game.\n\n");

  if (s_host_interface->ShouldSaveResumeState())
  {
    message.AppendString(
      "The current state will be saved, but you will be unable to load it until you disable hardcore mode.\n\n");
  }

  message.AppendString("Do you want to continue?");

  if (!s_host_interface->ConfirmMessage(message))
    return false;

  SaveAndApplySettings();
  s_host_interface->PowerOffSystem(s_host_interface->ShouldSaveResumeState());
  return true;
}

#endif

static bool WantsToCloseMenu()
{
  // Wait for the Close button to be released, THEN pressed
  if (s_close_button_state == 0)
  {
    if (!ImGuiFullscreen::IsCancelButtonPressed())
      s_close_button_state = 1;
  }
  else if (s_close_button_state == 1)
  {
    if (ImGuiFullscreen::IsCancelButtonPressed())
    {
      s_close_button_state = 0;
      return true;
    }
  }
  return false;
}

void DrawSettingsWindow()
{
  BeginFullscreenColumns();

  if (BeginFullscreenColumnWindow(0.0f, 300.0f, "settings_category", ImVec4(0.18f, 0.18f, 0.18f, 1.00f)))
  {
    static constexpr std::array<const char*, static_cast<u32>(SettingsPage::Count)> titles = {
      {ICON_FA_WINDOW_MAXIMIZE "  Interface Settings", ICON_FA_LIST "  Game List Settings",
       ICON_FA_HDD "  Console Settings", ICON_FA_SLIDERS_H "  Emulation Settings", ICON_FA_MICROCHIP "  BIOS Settings",
       ICON_FA_GAMEPAD "  Controller Settings", ICON_FA_KEYBOARD "  Hotkey Settings",
       ICON_FA_SD_CARD "  Memory Card Settings", ICON_FA_TV "  Display Settings",
       ICON_FA_MAGIC "  Enhancement Settings", ICON_FA_HEADPHONES "  Audio Settings",
       ICON_FA_TROPHY "  Achievements Settings", ICON_FA_EXCLAMATION_TRIANGLE "  Advanced Settings"}};

    BeginMenuButtons();
    for (u32 i = 0; i < static_cast<u32>(titles.size()); i++)
    {
      if (ActiveButton(titles[i], s_settings_page == static_cast<SettingsPage>(i)))
        s_settings_page = static_cast<SettingsPage>(i);
    }

    ImGui::SetCursorPosY(ImGui::GetWindowHeight() - LayoutScale(50.0f));
    if (ActiveButton(ICON_FA_BACKWARD "  Back", false) || WantsToCloseMenu())
      ReturnToMainWindow();

    EndMenuButtons();
  }

  EndFullscreenColumnWindow();

  if (BeginFullscreenColumnWindow(300.0f, LAYOUT_SCREEN_WIDTH, "settings_parent"))
  {
    bool settings_changed = false;

    switch (s_settings_page)
    {
      case SettingsPage::InterfaceSettings:
      {
        BeginMenuButtons();

        MenuHeading("Behavior");

        settings_changed |=
          ToggleButton("Pause On Start", "Pauses the emulator when a game is started.", &s_settings_copy.start_paused);
        settings_changed |= ToggleButton("Pause On Focus Loss",
                                         "Pauses the emulator when you minimize the window or switch to another "
                                         "application, and unpauses when you switch back.",
                                         &s_settings_copy.pause_on_focus_loss);
        settings_changed |= ToggleButton(
          "Pause On Menu", "Pauses the emulator when you open the quick menu, and unpauses when you close it.",
          &s_settings_copy.pause_on_menu);
        settings_changed |=
          ToggleButton("Confirm Power Off",
                       "Determines whether a prompt will be displayed to confirm shutting down the emulator/game "
                       "when the hotkey is pressed.",
                       &s_settings_copy.confim_power_off);
        settings_changed |=
          ToggleButton("Save State On Exit",
                       "Automatically saves the emulator state when powering down or exiting. You can then "
                       "resume directly from where you left off next time.",
                       &s_settings_copy.save_state_on_exit);
        settings_changed |=
          ToggleButton("Start Fullscreen", "Automatically switches to fullscreen mode when the program is started.",
                       &s_settings_copy.start_fullscreen);
        settings_changed |= ToggleButtonForNonSetting(
          "Hide Cursor In Fullscreen", "Hides the mouse pointer/cursor when the emulator is in fullscreen mode.",
          "Main", "HideCursorInFullscreen", true);
        settings_changed |=
          ToggleButton("Load Devices From Save States",
                       "When enabled, memory cards and controllers will be overwritten when save states are loaded.",
                       &s_settings_copy.load_devices_from_save_states);
        settings_changed |= ToggleButton(
          "Apply Per-Game Settings",
          "When enabled, per-game settings will be applied, and incompatible enhancements will be disabled.",
          &s_settings_copy.apply_game_settings);
        settings_changed |=
          ToggleButton("Automatically Load Cheats", "Automatically loads and applies cheats on game start.",
                       &s_settings_copy.auto_load_cheats);

#ifdef WITH_DISCORD_PRESENCE
        MenuHeading("Integration");
        settings_changed |= ToggleButtonForNonSetting(
          "Enable Discord Presence", "Shows the game you are currently playing as part of your profile on Discord.",
          "Main", "EnableDiscordPresence", false);
#endif

        MenuHeading("Miscellaneous");

        static ControllerInterface::Backend cbtype = ControllerInterface::Backend::None;
        static bool cbtype_set = false;
        if (!cbtype_set)
        {
          cbtype = ControllerInterface::ParseBackendName(
                     s_host_interface->GetSettingsInterface()->GetStringValue("Main", "ControllerBackend").c_str())
                     .value_or(ControllerInterface::GetDefaultBackend());
          cbtype_set = true;
        }

        if (EnumChoiceButton("Controller Backend", "Sets the API which is used to receive controller input.", &cbtype,
                             ControllerInterface::GetBackendName, ControllerInterface::Backend::Count))
        {
          s_host_interface->GetSettingsInterface()->SetStringValue("Main", "ControllerBackend",
                                                                   ControllerInterface::GetBackendName(cbtype));
          settings_changed = true;
        }

        EndMenuButtons();
      }
      break;

      case SettingsPage::GameListSettings:
      {
        BeginMenuButtons();

        MenuHeading("Game List");

        if (MenuButton(ICON_FA_FOLDER_PLUS "  Add Search Directory", "Adds a new directory to the game search list."))
        {
          OpenFileSelector(ICON_FA_FOLDER_PLUS "  Add Search Directory", true, [](const std::string& dir) {
            if (!dir.empty())
            {
              s_host_interface->GetSettingsInterface()->AddToStringList("GameList", "RecursivePaths", dir.c_str());
              s_host_interface->GetSettingsInterface()->RemoveFromStringList("GameList", "Paths", dir.c_str());
              s_host_interface->GetSettingsInterface()->Save();
              QueueGameListRefresh();
            }

            CloseFileSelector();
          });
        }

        if (MenuButton(ICON_FA_FOLDER_OPEN "  Change Recursive Directories",
                       "Sets whether subdirectories are searched for each game directory"))
        {
          OpenChoiceDialog(
            ICON_FA_FOLDER_OPEN "  Change Recursive Directories", true, GetGameListDirectoryOptions(true),
            [](s32 index, const std::string& title, bool checked) {
              if (index < 0)
                return;

              if (checked)
              {
                s_host_interface->GetSettingsInterface()->RemoveFromStringList("GameList", "Paths", title.c_str());
                s_host_interface->GetSettingsInterface()->AddToStringList("GameList", "RecursivePaths", title.c_str());
              }
              else
              {
                s_host_interface->GetSettingsInterface()->RemoveFromStringList("GameList", "RecursivePaths",
                                                                               title.c_str());
                s_host_interface->GetSettingsInterface()->AddToStringList("GameList", "Paths", title.c_str());
              }

              s_host_interface->GetSettingsInterface()->Save();
              QueueGameListRefresh();
            });
        }

        if (MenuButton(ICON_FA_FOLDER_MINUS "  Remove Search Directory",
                       "Removes a directory from the game search list."))
        {
          OpenChoiceDialog(ICON_FA_FOLDER_MINUS "  Remove Search Directory", false, GetGameListDirectoryOptions(false),
                           [](s32 index, const std::string& title, bool checked) {
                             if (index < 0)
                               return;

                             s_host_interface->GetSettingsInterface()->RemoveFromStringList("GameList", "Paths",
                                                                                            title.c_str());
                             s_host_interface->GetSettingsInterface()->RemoveFromStringList(
                               "GameList", "RecursivePaths", title.c_str());
                             s_host_interface->GetSettingsInterface()->Save();
                             QueueGameListRefresh();
                             CloseChoiceDialog();
                           });
        }

        MenuHeading("Search Directories");
        for (const GameList::DirectoryEntry& entry : s_host_interface->GetGameList()->GetSearchDirectories())
        {
          MenuButton(entry.path.c_str(), entry.recursive ? "Scanning Subdirectories" : "Not Scanning Subdirectories",
                     false);
        }

        EndMenuButtons();
      }
      break;

      case SettingsPage::ConsoleSettings:
      {
        static constexpr auto cdrom_read_speeds =
          make_array("None (Double Speed)", "2x (Quad Speed)", "3x (6x Speed)", "4x (8x Speed)", "5x (10x Speed)",
                     "6x (12x Speed)", "7x (14x Speed)", "8x (16x Speed)", "9x (18x Speed)", "10x (20x Speed)");

        static constexpr auto cdrom_seek_speeds = make_array("Infinite/Instantaneous", "None (Normal Speed)", "2x",
                                                             "3x", "4x", "5x", "6x", "7x", "8x", "9x", "10x");

        BeginMenuButtons();

        MenuHeading("Console Settings");

        settings_changed |=
          EnumChoiceButton("Region", "Determines the emulated hardware type.", &s_settings_copy.region,
                           &Settings::GetConsoleRegionDisplayName, ConsoleRegion::Count);

        MenuHeading("CPU Emulation (MIPS R3000A Derivative)");

        settings_changed |= EnumChoiceButton(
          "Execution Mode", "Determines how the emulated CPU executes instructions. Recompiler is recommended.",
          &s_settings_copy.cpu_execution_mode, &Settings::GetCPUExecutionModeDisplayName, CPUExecutionMode::Count);

        settings_changed |=
          ToggleButton("Enable Overclocking", "When this option is chosen, the clock speed set below will be used.",
                       &s_settings_copy.cpu_overclock_enable);

        s32 overclock_percent =
          s_settings_copy.cpu_overclock_enable ? static_cast<s32>(s_settings_copy.GetCPUOverclockPercent()) : 100;
        if (RangeButton("Overclocking Percentage",
                        "Selects the percentage of the normal clock speed the emulated hardware will run at.",
                        &overclock_percent, 10, 1000, 10, "%d%%", s_settings_copy.cpu_overclock_enable))
        {
          s_settings_copy.SetCPUOverclockPercent(static_cast<u32>(overclock_percent));
          settings_changed = true;
        }

        MenuHeading("CD-ROM Emulation");

        const u32 read_speed_index =
          std::clamp<u32>(s_settings_copy.cdrom_read_speedup, 1u, static_cast<u32>(cdrom_read_speeds.size())) - 1u;
        if (MenuButtonWithValue("Read Speedup",
                                "Speeds up CD-ROM reads by the specified factor. May improve loading speeds in some "
                                "games, and break others.",
                                cdrom_read_speeds[read_speed_index]))
        {
          ImGuiFullscreen::ChoiceDialogOptions options;
          options.reserve(cdrom_read_speeds.size());
          for (u32 i = 0; i < static_cast<u32>(cdrom_read_speeds.size()); i++)
            options.emplace_back(cdrom_read_speeds[i], i == read_speed_index);
          OpenChoiceDialog("CD-ROM Read Speedup", false, std::move(options),
                           [](s32 index, const std::string& title, bool checked) {
                             if (index >= 0)
                               s_settings_copy.cdrom_read_speedup = static_cast<u32>(index) + 1;
                             CloseChoiceDialog();
                           });
        }

        const u32 seek_speed_index =
          std::min(s_settings_copy.cdrom_seek_speedup, static_cast<u32>(cdrom_seek_speeds.size()));
        if (MenuButtonWithValue("Seek Speedup",
                                "Speeds up CD-ROM seeks by the specified factor. May improve loading speeds in some "
                                "games, and break others.",
                                cdrom_seek_speeds[seek_speed_index]))
        {
          ImGuiFullscreen::ChoiceDialogOptions options;
          options.reserve(cdrom_seek_speeds.size());
          for (u32 i = 0; i < static_cast<u32>(cdrom_seek_speeds.size()); i++)
            options.emplace_back(cdrom_seek_speeds[i], i == seek_speed_index);
          OpenChoiceDialog("CD-ROM Seek Speedup", false, std::move(options),
                           [](s32 index, const std::string& title, bool checked) {
                             if (index >= 0)
                               s_settings_copy.cdrom_seek_speedup = static_cast<u32>(index);
                             CloseChoiceDialog();
                           });
        }

        settings_changed |= ToggleButton(
          "Enable Read Thread",
          "Reduces hitches in emulation by reading/decompressing CD data asynchronously on a worker thread.",
          &s_settings_copy.cdrom_read_thread);
        settings_changed |=
          ToggleButton("Enable Region Check", "Simulates the region check present in original, unmodified consoles.",
                       &s_settings_copy.cdrom_region_check);
        settings_changed |= ToggleButton(
          "Preload Images to RAM",
          "Loads the game image into RAM. Useful for network paths that may become unreliable during gameplay.",
          &s_settings_copy.cdrom_load_image_to_ram);
        settings_changed |= ToggleButtonForNonSetting(
          "Apply Image Patches",
          "Automatically applies patches to disc images when they are present, currently only PPF is supported.",
          "CDROM", "LoadImagePatches", false);

        MenuHeading("Controller Ports");

        settings_changed |= EnumChoiceButton("Multitap", nullptr, &s_settings_copy.multitap_mode,
                                             &Settings::GetMultitapModeDisplayName, MultitapMode::Count);

        EndMenuButtons();
      }
      break;

      case SettingsPage::EmulationSettings:
      {
        static constexpr auto emulation_speeds =
          make_array(0.0f, 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f, 2.5f,
                     3.0f, 3.5f, 4.0f, 4.5f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, 10.0f);
        static constexpr auto get_emulation_speed_options = [](float current_speed) {
          ImGuiFullscreen::ChoiceDialogOptions options;
          options.reserve(emulation_speeds.size());
          for (const float speed : emulation_speeds)
          {
            options.emplace_back(
              (speed != 0.0f) ?
                StringUtil::StdStringFromFormat("%d%% [%d FPS (NTSC) / %d FPS (PAL)]", static_cast<int>(speed * 100.0f),
                                                static_cast<int>(60.0f * speed), static_cast<int>(50.0f * speed)) :
                "Unlimited",
              speed == current_speed);
          }
          return options;
        };

        BeginMenuButtons();

        MenuHeading("Speed Control");

#define MAKE_EMULATION_SPEED(setting_title, setting_var)                                                               \
  if (MenuButtonWithValue(                                                                                             \
        setting_title,                                                                                                 \
        "Sets the target emulation speed. It is not guaranteed that this speed will be reached on all systems.",       \
        (setting_var != 0.0f) ? TinyString::FromFormat("%.0f%%", setting_var * 100.0f) : TinyString("Unlimited")))     \
  {                                                                                                                    \
    OpenChoiceDialog(setting_title, false, get_emulation_speed_options(setting_var),                                   \
                     [](s32 index, const std::string& title, bool checked) {                                           \
                       if (index >= 0)                                                                                 \
                       {                                                                                               \
                         setting_var = emulation_speeds[index];                                                        \
                         s_host_interface->RunLater(SaveAndApplySettings);                                             \
                       }                                                                                               \
                       CloseChoiceDialog();                                                                            \
                     });                                                                                               \
  }

        MAKE_EMULATION_SPEED("Emulation Speed", s_settings_copy.emulation_speed);
        MAKE_EMULATION_SPEED("Fast Forward Speed", s_settings_copy.fast_forward_speed);
        MAKE_EMULATION_SPEED("Turbo Speed", s_settings_copy.turbo_speed);

#undef MAKE_EMULATION_SPEED

        settings_changed |= ToggleButton("Sync To Host Refresh Rate",
                                         "Adjusts the emulation speed so the console's refresh rate matches the host "
                                         "when VSync and Audio Resampling are enabled.",
                                         &s_settings_copy.sync_to_host_refresh_rate,
                                         s_settings_copy.video_sync_enabled && s_settings_copy.audio_resampling);

        MenuHeading("Runahead/Rewind");

        settings_changed |=
          ToggleButton("Enable Rewinding", "Saves state periodically so you can rewind any mistakes while playing.",
                       &s_settings_copy.rewind_enable);
        settings_changed |= RangeButton(
          "Rewind Save Frequency",
          "How often a rewind state will be created. Higher frequencies have greater system requirements.",
          &s_settings_copy.rewind_save_frequency, 0.0f, 3600.0f, 0.1f, "%.2f Seconds", s_settings_copy.rewind_enable);
        settings_changed |=
          RangeButton("Rewind Save Frequency",
                      "How many saves will be kept for rewinding. Higher values have greater memory requirements.",
                      reinterpret_cast<s32*>(&s_settings_copy.rewind_save_slots), 1, 10000, 1, "%d Frames",
                      s_settings_copy.rewind_enable);

        TinyString summary;
        if (!s_settings_copy.IsRunaheadEnabled())
          summary = "Disabled";
        else
          summary.Format("%u Frames", s_settings_copy.runahead_frames);

        if (MenuButtonWithValue("Runahead",
                                "Simulates the system ahead of time and rolls back/replays to reduce input lag. Very "
                                "high system requirements.",
                                summary))
        {
          ImGuiFullscreen::ChoiceDialogOptions options;
          for (u32 i = 0; i <= 10; i++)
          {
            if (i == 0)
              options.emplace_back("Disabled", s_settings_copy.runahead_frames == i);
            else
              options.emplace_back(StringUtil::StdStringFromFormat("%u Frames", i),
                                   s_settings_copy.runahead_frames == i);
          }
          OpenChoiceDialog("Runahead", false, std::move(options),
                           [](s32 index, const std::string& title, bool checked) {
                             s_settings_copy.runahead_frames = index;
                             s_host_interface->RunLater(SaveAndApplySettings);
                             CloseChoiceDialog();
                           });
          settings_changed = true;
        }

        TinyString rewind_summary;
        if (s_settings_copy.IsRunaheadEnabled())
        {
          rewind_summary = "Rewind is disabled because runahead is enabled. Runahead will significantly increase "
                           "system requirements.";
        }
        else if (s_settings_copy.rewind_enable)
        {
          const float duration = ((s_settings_copy.rewind_save_frequency <= std::numeric_limits<float>::epsilon()) ?
                                    (1.0f / 60.0f) :
                                    s_settings_copy.rewind_save_frequency) *
                                 static_cast<float>(s_settings_copy.rewind_save_slots);

          u64 ram_usage, vram_usage;
          System::CalculateRewindMemoryUsage(s_settings_copy.rewind_save_slots, &ram_usage, &vram_usage);
          rewind_summary.Format("Rewind for %u frames, lasting %.2f seconds will require up to %" PRIu64
                                "MB of RAM and %" PRIu64 "MB of VRAM.",
                                s_settings_copy.rewind_save_slots, duration, ram_usage / 1048576, vram_usage / 1048576);
        }
        else
        {
          rewind_summary =
            "Rewind is not enabled. Please note that enabling rewind may significantly increase system requirements.";
        }

        ActiveButton(rewind_summary, false, false, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY,
                     g_medium_font);

        EndMenuButtons();
      }
      break;

      case SettingsPage::BIOSSettings:
      {
        static constexpr auto config_keys = make_array("", "PathNTSCJ", "PathNTSCU", "PathPAL");
        static std::string bios_region_filenames[static_cast<u32>(ConsoleRegion::Count)];
        static std::string bios_directory;
        static bool bios_filenames_loaded = false;

        if (!bios_filenames_loaded)
        {
          for (u32 i = 0; i < static_cast<u32>(ConsoleRegion::Count); i++)
          {
            if (i == static_cast<u32>(ConsoleRegion::Auto))
              continue;
            bios_region_filenames[i] = s_host_interface->GetSettingsInterface()->GetStringValue("BIOS", config_keys[i]);
          }
          bios_directory = s_host_interface->GetBIOSDirectory();
          bios_filenames_loaded = true;
        }

        BeginMenuButtons();

        MenuHeading("BIOS Selection");

        for (u32 i = 0; i < static_cast<u32>(ConsoleRegion::Count); i++)
        {
          const ConsoleRegion region = static_cast<ConsoleRegion>(i);
          if (region == ConsoleRegion::Auto)
            continue;

          TinyString title;
          title.Format("BIOS for %s", Settings::GetConsoleRegionName(region));

          if (MenuButtonWithValue(title,
                                  SmallString::FromFormat("BIOS to use when emulating %s consoles.",
                                                          Settings::GetConsoleRegionDisplayName(region)),
                                  bios_region_filenames[i].c_str()))
          {
            ImGuiFullscreen::ChoiceDialogOptions options;
            auto images = s_host_interface->FindBIOSImagesInDirectory(s_host_interface->GetBIOSDirectory().c_str());
            options.reserve(images.size() + 1);
            options.emplace_back("Auto-Detect", bios_region_filenames[i].empty());
            for (auto& [path, info] : images)
            {
              const bool selected = bios_region_filenames[i] == path;
              options.emplace_back(std::move(path), selected);
            }

            OpenChoiceDialog(title, false, std::move(options), [i](s32 index, const std::string& path, bool checked) {
              if (index >= 0)
              {
                bios_region_filenames[i] = path;
                s_host_interface->GetSettingsInterface()->SetStringValue("BIOS", config_keys[i], path.c_str());
                s_host_interface->GetSettingsInterface()->Save();
              }
              CloseChoiceDialog();
            });
          }
        }

        if (MenuButton("BIOS Directory", bios_directory.c_str()))
        {
          OpenFileSelector("BIOS Directory", true, [](const std::string& path) {
            if (!path.empty())
            {
              bios_directory = path;
              s_host_interface->GetSettingsInterface()->SetStringValue("BIOS", "SearchDirectory", path.c_str());
              s_host_interface->GetSettingsInterface()->Save();
            }
            CloseFileSelector();
          });
        }

        MenuHeading("Patches");

        settings_changed |=
          ToggleButton("Enable Fast Boot", "Patches the BIOS to skip the boot animation. Safe to enable.",
                       &s_settings_copy.bios_patch_fast_boot);
        settings_changed |= ToggleButton(
          "Enable TTY Output", "Patches the BIOS to log calls to printf(). Only use when debugging, can break games.",
          &s_settings_copy.bios_patch_tty_enable);

        EndMenuButtons();
      }
      break;

      case SettingsPage::ControllerSettings:
      {
        BeginMenuButtons();

        MenuHeading("Input Profiles");
        if (MenuButton(ICON_FA_FOLDER_OPEN "  Load Input Profile",
                       "Applies a saved configuration of controller types and bindings."))
        {
          CommonHostInterface::InputProfileList profiles(s_host_interface->GetInputProfileList());
          ImGuiFullscreen::ChoiceDialogOptions options;
          options.reserve(profiles.size());
          for (CommonHostInterface::InputProfileEntry& entry : profiles)
            options.emplace_back(std::move(entry.name), false);

          auto callback = [profiles](s32 index, const std::string& title, bool checked) {
            if (index < 0)
              return;

            // needs a reload...
            s_host_interface->ApplyInputProfile(profiles[index].path.c_str());
            s_settings_copy.Load(*s_host_interface->GetSettingsInterface());
            s_host_interface->RunLater(SaveAndApplySettings);
            CloseChoiceDialog();
          };
          OpenChoiceDialog(ICON_FA_FOLDER_OPEN "  Load Input Profile", false, std::move(options), std::move(callback));
        }

        static std::array<ControllerType, NUM_CONTROLLER_AND_CARD_PORTS> type_cache = {};
        static std::array<Controller::ButtonList, NUM_CONTROLLER_AND_CARD_PORTS> button_cache;
        static std::array<Controller::AxisList, NUM_CONTROLLER_AND_CARD_PORTS> axis_cache;
        static std::array<Controller::SettingList, NUM_CONTROLLER_AND_CARD_PORTS> setting_cache;
        static std::array<std::string,
                          NUM_CONTROLLER_AND_CARD_PORTS * CommonHostInterface::NUM_CONTROLLER_AUTOFIRE_BUTTONS>
          autofire_buttons_cache;
        TinyString section;
        TinyString key;

        std::array<TinyString, NUM_CONTROLLER_AND_CARD_PORTS> port_labels = s_settings_copy.GeneratePortLabels();

        for (u32 port = 0; port < NUM_CONTROLLER_AND_CARD_PORTS; port++)
        {
          if (port_labels[port].IsEmpty())
            continue;
          else
            MenuHeading(port_labels[port]);

          settings_changed |= EnumChoiceButton(
            TinyString::FromFormat(ICON_FA_GAMEPAD "  Controller Type##type%u", port),
            "Determines the simulated controller plugged into this port.", &s_settings_copy.controller_types[port],
            &Settings::GetControllerTypeDisplayName, ControllerType::Count);

          section.Format("Controller%u", port + 1);

          const ControllerType ctype = s_settings_copy.controller_types[port];
          if (ctype != type_cache[port])
          {
            type_cache[port] = ctype;
            button_cache[port] = Controller::GetButtonNames(ctype);
            axis_cache[port] = Controller::GetAxisNames(ctype);
            setting_cache[port] = Controller::GetSettings(ctype);

            for (u32 i = 0; i < CommonHostInterface::NUM_CONTROLLER_AUTOFIRE_BUTTONS; i++)
            {
              autofire_buttons_cache[port * CommonHostInterface::NUM_CONTROLLER_AUTOFIRE_BUTTONS + i] =
                s_host_interface->GetStringSettingValue(section, TinyString::FromFormat("AutoFire%uButton", i + 1));
            }
          }

          for (const auto& it : button_cache[port])
          {
            key.Format("Button%s", it.first.c_str());
            DrawInputBindingButton(InputBindingType::Button, section, key, it.first.c_str());
          }

          for (const auto& it : axis_cache[port])
          {
            key.Format("Axis%s", std::get<0>(it).c_str());
            DrawInputBindingButton(InputBindingType::Axis, section, key, std::get<0>(it).c_str());
          }

          if (Controller::GetVibrationMotorCount(ctype) > 0)
            DrawInputBindingButton(InputBindingType::Rumble, section, "Rumble", "Rumble/Vibration");

          for (const SettingInfo& it : setting_cache[port])
            settings_changed |= SettingInfoButton(it, section);

          for (u32 autofire_index = 0; autofire_index < CommonHostInterface::NUM_CONTROLLER_AUTOFIRE_BUTTONS;
               autofire_index++)
          {
            const u32 cache_index = port * CommonHostInterface::NUM_CONTROLLER_AUTOFIRE_BUTTONS + autofire_index;

            if (MenuButtonWithValue(TinyString::FromFormat("Auto Fire %u", autofire_index + 1),
                                    "Selects the button to toggle with this auto fire binding.",
                                    autofire_buttons_cache[cache_index].c_str()))

            {
              auto callback = [port, autofire_index, cache_index](s32 index, const std::string& title, bool checked) {
                if (index < 0)
                  return;

                auto lock = s_host_interface->GetSettingsLock();
                if (index == 0)
                {
                  s_host_interface->GetSettingsInterface()->DeleteValue(
                    TinyString::FromFormat("Controller%u", port + 1),
                    TinyString::FromFormat("AutoFire%uButton", autofire_index + 1));
                  std::string().swap(autofire_buttons_cache[cache_index]);
                }
                else
                {
                  s_host_interface->GetSettingsInterface()->SetStringValue(
                    TinyString::FromFormat("Controller%u", port + 1),
                    TinyString::FromFormat("AutoFire%uButton", autofire_index + 1),
                    button_cache[port][index - 1].first.c_str());
                  autofire_buttons_cache[cache_index] = button_cache[port][index - 1].first;
                }

                // needs a reload...
                s_host_interface->RunLater(SaveAndApplySettings);
                CloseChoiceDialog();
              };

              ImGuiFullscreen::ChoiceDialogOptions options;
              options.reserve(button_cache[port].size() + 1);
              options.emplace_back("(None)", autofire_buttons_cache[cache_index].empty());
              for (const auto& it : button_cache[port])
                options.emplace_back(it.first, autofire_buttons_cache[cache_index] == it.first);

              OpenChoiceDialog(ICON_FA_GAMEPAD "  Select Auto Fire Button", false, std::move(options),
                               std::move(callback));
            }

            if (autofire_buttons_cache[cache_index].empty())
              continue;

            key.Format("AutoFire%u", autofire_index + 1);
            DrawInputBindingButton(InputBindingType::Button, section, key,
                                   TinyString::FromFormat("Auto Fire %u Binding", autofire_index + 1), false);

            key.Format("AutoFire%uFrequency", autofire_index + 1);
            int frequency = s_host_interface->GetSettingsInterface()->GetIntValue(
              section, key, CommonHostInterface::DEFAULT_AUTOFIRE_FREQUENCY);
            settings_changed |= RangeButton(TinyString::FromFormat("Auto Fire %u Frequency", autofire_index + 1),
                                            "Sets the rate at which the auto fire will trigger on and off.", &frequency,
                                            1, 255, 1, "%d Frames");
          }
        }

        EndMenuButtons();
      }
      break;

      case SettingsPage::HotkeySettings:
      {
        BeginMenuButtons();

        TinyString last_category;
        for (const CommonHostInterface::HotkeyInfo& hotkey : s_host_interface->GetHotkeyInfoList())
        {
          if (hotkey.category != last_category)
          {
            MenuHeading(hotkey.category);
            last_category = hotkey.category;
          }

          DrawInputBindingButton(InputBindingType::Button, "Hotkeys", hotkey.name, hotkey.display_name);
        }

        EndMenuButtons();
      }
      break;

      case SettingsPage::MemoryCardSettings:
      {
        BeginMenuButtons();

        for (u32 i = 0; i < 2; i++)
        {
          MenuHeading(TinyString::FromFormat("Memory Card Port %u", i + 1));

          settings_changed |= EnumChoiceButton(
            TinyString::FromFormat("Memory Card %u Type", i + 1),
            SmallString::FromFormat("Sets which sort of memory card image will be used for slot %u.", i + 1),
            &s_settings_copy.memory_card_types[i], &Settings::GetMemoryCardTypeDisplayName, MemoryCardType::Count);

          settings_changed |= MenuButton(TinyString::FromFormat("Shared Memory Card %u Path", i + 1),
                                         s_settings_copy.memory_card_paths[i].c_str(),
                                         s_settings_copy.memory_card_types[i] == MemoryCardType::Shared);
        }

        MenuHeading("Shared Settings");

        settings_changed |= ToggleButton("Use Single Card For Sub-Images",
                                         "When using a multi-disc image (m3u/pbp) and per-game (title) memory cards, "
                                         "use a single memory card for all discs.",
                                         &s_settings_copy.memory_card_use_playlist_title);

        static std::string memory_card_directory;
        static bool memory_card_directory_set = false;
        if (!memory_card_directory_set)
        {
          memory_card_directory = s_host_interface->GetMemoryCardDirectory();
          memory_card_directory_set = true;
        }

        if (MenuButton("Memory Card Directory", memory_card_directory.c_str()))
        {
          OpenFileSelector("Memory Card Directory", true, [](const std::string& path) {
            if (!path.empty())
            {
              memory_card_directory = path;
              s_settings_copy.memory_card_directory = path;
              s_host_interface->RunLater(SaveAndApplySettings);
            }
            CloseFileSelector();
          });
        }

        if (MenuButton("Reset Memory Card Directory", "Resets memory card directory to default (user directory)."))
        {
          s_settings_copy.memory_card_directory.clear();
          s_host_interface->RunLater(SaveAndApplySettings);
          memory_card_directory_set = false;
        }

        EndMenuButtons();
      }
      break;

      case SettingsPage::DisplaySettings:
      {
        BeginMenuButtons();

        MenuHeading("Device Settings");

        settings_changed |=
          EnumChoiceButton("GPU Renderer", "Chooses the backend to use for rendering the console/game visuals.",
                           &s_settings_copy.gpu_renderer, &Settings::GetRendererDisplayName, GPURenderer::Count);

        static std::string fullscreen_mode;
        static bool fullscreen_mode_set;
        if (!fullscreen_mode_set)
        {
          fullscreen_mode = s_host_interface->GetSettingsInterface()->GetStringValue("GPU", "FullscreenMode", "");
          fullscreen_mode_set = true;
        }

        if (MenuButtonWithValue("Fullscreen Resolution", "Selects the resolution to use in fullscreen modes.",
                                fullscreen_mode.empty() ? "Borderless Fullscreen" : fullscreen_mode.c_str()))
        {
          HostDisplay::AdapterAndModeList aml(s_host_interface->GetDisplay()->GetAdapterAndModeList());

          ImGuiFullscreen::ChoiceDialogOptions options;
          options.reserve(aml.fullscreen_modes.size() + 1);
          options.emplace_back("Borderless Fullscreen", fullscreen_mode.empty());
          for (std::string& mode : aml.fullscreen_modes)
            options.emplace_back(std::move(mode), mode == fullscreen_mode);

          auto callback = [](s32 index, const std::string& title, bool checked) {
            if (index < 0)
              return;
            else if (index == 0)
              std::string().swap(fullscreen_mode);
            else
              fullscreen_mode = title;

            s_host_interface->GetSettingsInterface()->SetStringValue("GPU", "FullscreenMode", fullscreen_mode.c_str());
            s_host_interface->GetSettingsInterface()->Save();
            s_host_interface->AddOSDMessage("Resolution change will be applied after restarting.", 10.0f);
            CloseChoiceDialog();
          };
          OpenChoiceDialog(ICON_FA_TV "  Fullscreen Resolution", false, std::move(options), std::move(callback));
        }

        switch (s_settings_copy.gpu_renderer)
        {
#ifdef WIN32
          case GPURenderer::HardwareD3D11:
          {
            // TODO: FIXME
            bool use_blit_swap_chain = false;
            settings_changed |= ToggleButtonForNonSetting(
              "Use Blit Swap Chain",
              "Uses a blit presentation model instead of flipping. This may be needed on some systems.", "Display",
              "UseBlitSwapChain", false);
          }
          break;
#endif

          case GPURenderer::HardwareVulkan:
          {
            settings_changed |=
              ToggleButton("Threaded Presentation",
                           "Presents frames on a background thread when fast forwarding or vsync is disabled.",
                           &s_settings_copy.gpu_threaded_presentation);
          }
          break;

          case GPURenderer::Software:
          {
            settings_changed |= ToggleButton("Threaded Rendering",
                                             "Uses a second thread for drawing graphics. Speed boost, and safe to use.",
                                             &s_settings_copy.gpu_use_thread);
          }
          break;

          default:
            break;
        }

        if (!s_settings_copy.IsUsingSoftwareRenderer())
        {
          settings_changed |=
            ToggleButton("Use Software Renderer For Readbacks",
                         "Runs the software renderer in parallel for VRAM readbacks. On some systems, this may result "
                         "in greater performance.",
                         &s_settings_copy.gpu_use_software_renderer_for_readbacks);
        }

        settings_changed |=
          ToggleButton("Enable VSync",
                       "Synchronizes presentation of the console's frames to the host. Enable for smoother animations.",
                       &s_settings_copy.video_sync_enabled);

        settings_changed |= ToggleButton("Optimal Frame Pacing",
                                         "Ensures every frame generated is displayed for optimal pacing. Disable if "
                                         "you are having speed or sound issues.",
                                         &s_settings_copy.display_all_frames);

        MenuHeading("Screen Display");

        settings_changed |= EnumChoiceButton(
          "Aspect Ratio", "Changes the aspect ratio used to display the console's output to the screen.",
          &s_settings_copy.display_aspect_ratio, &Settings::GetDisplayAspectRatioName, DisplayAspectRatio::Count);

        settings_changed |= EnumChoiceButton(
          "Crop Mode", "Determines how much of the area typically not visible on a consumer TV set to crop/hide.",
          &s_settings_copy.display_crop_mode, &Settings::GetDisplayCropModeDisplayName, DisplayCropMode::Count);

        settings_changed |=
          EnumChoiceButton("Downsampling",
                           "Downsamples the rendered image prior to displaying it. Can improve "
                           "overall image quality in mixed 2D/3D games.",
                           &s_settings_copy.gpu_downsample_mode, &Settings::GetDownsampleModeDisplayName,
                           GPUDownsampleMode::Count, !s_settings_copy.IsUsingSoftwareRenderer());

        settings_changed |=
          ToggleButton("Linear Upscaling", "Uses a bilinear filter when upscaling to display, smoothing out the image.",
                       &s_settings_copy.display_linear_filtering, !s_settings_copy.display_integer_scaling);

        settings_changed |=
          ToggleButton("Integer Upscaling", "Adds padding to ensure pixels are a whole number in size.",
                       &s_settings_copy.display_integer_scaling);

        settings_changed |= ToggleButton(
          "Stretch To Fit", "Fills the window with the active display area, regardless of the aspect ratio.",
          &s_settings_copy.display_stretch);

        settings_changed |=
          ToggleButtonForNonSetting("Internal Resolution Screenshots",
                                    "Saves screenshots at internal render resolution and without postprocessing.",
                                    "Display", "InternalResolutionScreenshots", false);

        MenuHeading("On-Screen Display");

        settings_changed |= ToggleButton("Show OSD Messages", "Shows on-screen-display messages when events occur.",
                                         &s_settings_copy.display_show_osd_messages);
        settings_changed |= ToggleButton(
          "Show Game Frame Rate", "Shows the internal frame rate of the game in the top-right corner of the display.",
          &s_settings_copy.display_show_fps);
        settings_changed |= ToggleButton("Show Display FPS",
                                         "Shows the number of frames (or v-syncs) displayed per second by the system "
                                         "in the top-right corner of the display.",
                                         &s_settings_copy.display_show_vps);
        settings_changed |= ToggleButton(
          "Show Speed",
          "Shows the current emulation speed of the system in the top-right corner of the display as a percentage.",
          &s_settings_copy.display_show_speed);
        settings_changed |=
          ToggleButton("Show Resolution",
                       "Shows the current rendering resolution of the system in the top-right corner of the display.",
                       &s_settings_copy.display_show_resolution);
        settings_changed |= ToggleButtonForNonSetting(
          "Show Controller Input",
          "Shows the current controller state of the system in the bottom-left corner of the display.", "Display",
          "ShowInputs", false);

        EndMenuButtons();
      }
      break;

      case SettingsPage::EnhancementSettings:
      {
        static const auto resolution_scale_text_callback = [](u32 value) -> const char* {
          static constexpr std::array<const char*, 17> texts = {
            {"Automatic based on window size", "1x", "2x", "3x (for 720p)", "4x", "5x (for 1080p)", "6x (for 1440p)",
             "7x", "8x", "9x (for 4K)", "10x", "11x", "12x", "13x", "14x", "15x", "16x"

            }};
          return (value >= texts.size()) ? "" : texts[value];
        };

        BeginMenuButtons();

        MenuHeading("Rendering Enhancements");

        settings_changed |= EnumChoiceButton<u32, u32>(
          "Internal Resolution Scale",
          "Scales internal VRAM resolution by the specified multiplier. Some games require 1x VRAM resolution.",
          &s_settings_copy.gpu_resolution_scale, resolution_scale_text_callback, 17);
        settings_changed |= EnumChoiceButton(
          "Texture Filtering",
          "Smooths out the blockyness of magnified textures on 3D objects. Will have a greater effect "
          "on higher resolution scales.",
          &s_settings_copy.gpu_texture_filter, &Settings::GetTextureFilterDisplayName, GPUTextureFilter::Count);
        settings_changed |=
          ToggleButton("True Color Rendering",
                       "Disables dithering and uses the full 8 bits per channel of color information. May break "
                       "rendering in some games.",
                       &s_settings_copy.gpu_true_color);
        settings_changed |= ToggleButton(
          "Scaled Dithering",
          "Scales the dithering pattern with the internal rendering resolution, making it less noticeable. "
          "Usually safe to enable.",
          &s_settings_copy.gpu_scaled_dithering, s_settings_copy.gpu_resolution_scale > 1);
        settings_changed |= ToggleButton(
          "Widescreen Hack", "Increases the field of view from 4:3 to the chosen display aspect ratio in 3D games.",
          &s_settings_copy.gpu_widescreen_hack);

        MenuHeading("Display Enhancements");

        settings_changed |=
          ToggleButton("Disable Interlacing",
                       "Disables interlaced rendering and display in the GPU. Some games can render in 480p this way, "
                       "but others will break.",
                       &s_settings_copy.gpu_disable_interlacing);
        settings_changed |= ToggleButton(
          "Force NTSC Timings",
          "Forces PAL games to run at NTSC timings, i.e. 60hz. Some PAL games will run at their \"normal\" "
          "speeds, while others will break.",
          &s_settings_copy.gpu_force_ntsc_timings);
        settings_changed |=
          ToggleButton("Force 4:3 For 24-Bit Display",
                       "Switches back to 4:3 display aspect ratio when displaying 24-bit content, usually FMVs.",
                       &s_settings_copy.display_force_4_3_for_24bit);
        settings_changed |= ToggleButton(
          "Chroma Smoothing For 24-Bit Display",
          "Smooths out blockyness between colour transitions in 24-bit content, usually FMVs. Only applies "
          "to the hardware renderers.",
          &s_settings_copy.gpu_24bit_chroma_smoothing);

        MenuHeading("PGXP (Precision Geometry Transform Pipeline");

        settings_changed |=
          ToggleButton("PGXP Geometry Correction",
                       "Reduces \"wobbly\" polygons by attempting to preserve the fractional component through memory "
                       "transfers.",
                       &s_settings_copy.gpu_pgxp_enable);
        settings_changed |=
          ToggleButton("PGXP Texture Correction",
                       "Uses perspective-correct interpolation for texture coordinates and colors, straightening out "
                       "warped textures.",
                       &s_settings_copy.gpu_pgxp_texture_correction, s_settings_copy.gpu_pgxp_enable);
        settings_changed |=
          ToggleButton("PGXP Culling Correction",
                       "Increases the precision of polygon culling, reducing the number of holes in geometry.",
                       &s_settings_copy.gpu_pgxp_culling, s_settings_copy.gpu_pgxp_enable);
        settings_changed |=
          ToggleButton("PGXP Preserve Projection Precision",
                       "Adds additional precision to PGXP data post-projection. May improve visuals in some games.",
                       &s_settings_copy.gpu_pgxp_preserve_proj_fp, s_settings_copy.gpu_pgxp_enable);
        settings_changed |= ToggleButton(
          "PGXP Depth Buffer", "Reduces polygon Z-fighting through depth testing. Low compatibility with games.",
          &s_settings_copy.gpu_pgxp_depth_buffer,
          s_settings_copy.gpu_pgxp_enable && s_settings_copy.gpu_pgxp_texture_correction);
        settings_changed |= ToggleButton("PGXP CPU Mode", "Uses PGXP for all instructions, not just memory operations.",
                                         &s_settings_copy.gpu_pgxp_cpu, s_settings_copy.gpu_pgxp_enable);

        EndMenuButtons();
      }
      break;

      case SettingsPage::AudioSettings:
      {
        BeginMenuButtons();

        MenuHeading("Audio Control");

        settings_changed |= RangeButton("Output Volume", "Controls the volume of the audio played on the host.",
                                        &s_settings_copy.audio_output_volume, 0, 100, 1, "%d%%");
        settings_changed |= RangeButton("Fast Forward Volume",
                                        "Controls the volume of the audio played on the host when fast forwarding.",
                                        &s_settings_copy.audio_fast_forward_volume, 0, 100, 1, "%d%%");
        settings_changed |= ToggleButton("Mute All Sound", "Prevents the emulator from producing any audible sound.",
                                         &s_settings_copy.audio_output_muted);
        settings_changed |= ToggleButton("Mute CD Audio",
                                         "Forcibly mutes both CD-DA and XA audio from the CD-ROM. Can be used to "
                                         "disable background music in some games.",
                                         &s_settings_copy.cdrom_mute_cd_audio);

        MenuHeading("Backend Settings");

        settings_changed |= EnumChoiceButton(
          "Audio Backend",
          "The audio backend determines how frames produced by the emulator are submitted to the host.",
          &s_settings_copy.audio_backend, &Settings::GetAudioBackendDisplayName, AudioBackend::Count);
        settings_changed |= RangeButton(
          "Buffer Size", "The buffer size determines the size of the chunks of audio which will be pulled by the host.",
          reinterpret_cast<s32*>(&s_settings_copy.audio_buffer_size), 1024, 8192, 128, "%d Frames");

        settings_changed |= ToggleButton("Sync To Output",
                                         "Throttles the emulation speed based on the audio backend pulling audio "
                                         "frames. Enable to reduce the chances of crackling.",
                                         &s_settings_copy.audio_sync_enabled);
        settings_changed |= ToggleButton(
          "Resampling",
          "When running outside of 100% speed, resamples audio from the target speed instead of dropping frames.",
          &s_settings_copy.audio_resampling);

        EndMenuButtons();
      }
      break;

      case SettingsPage::AchievementsSetings:
      {
#ifdef WITH_CHEEVOS
        BeginMenuButtons();

        MenuHeading("Settings");
        if (ToggleButtonForNonSetting(ICON_FA_TROPHY "  Enable RetroAchievements",
                                      "When enabled and logged in, DuckStation will scan for achievements on startup.",
                                      "Cheevos", "Enabled", false))
        {
          s_host_interface->RunLater([]() {
            if (!ConfirmChallengeModeEnable())
              s_host_interface->GetSettingsInterface()->SetBoolValue("Cheevos", "Enabled", false);
          });
        }

        settings_changed |= ToggleButtonForNonSetting(
          ICON_FA_USER_FRIENDS "  Rich Presence",
          "When enabled, rich presence information will be collected and sent to the server where supported.",
          "Cheevos", "RichPresence", true);
        settings_changed |=
          ToggleButtonForNonSetting(ICON_FA_STETHOSCOPE "  Test Mode",
                                    "When enabled, DuckStation will assume all achievements are locked and not "
                                    "send any unlock notifications to the server.",
                                    "Cheevos", "TestMode", false);
        settings_changed |=
          ToggleButtonForNonSetting(ICON_FA_MEDAL "  Test Unofficial Achievements",
                                    "When enabled, DuckStation will list achievements from unofficial sets. These "
                                    "achievements are not tracked by RetroAchievements.",
                                    "Cheevos", "UnofficialTestMode", false);
        settings_changed |= ToggleButtonForNonSetting(ICON_FA_COMPACT_DISC "  Use First Disc From Playlist",
                                                      "When enabled, the first disc in a playlist will be used for "
                                                      "achievements, regardless of which disc is active.",
                                                      "Cheevos", "UseFirstDiscFromPlaylist", true);

        if (ToggleButtonForNonSetting(ICON_FA_HARD_HAT "  Hardcore Mode",
                                      "\"Challenge\" mode for achievements. Disables save state, cheats, and slowdown "
                                      "functions, but you receive double the achievement points.",
                                      "Cheevos", "ChallengeMode", false))
        {
          s_host_interface->RunLater([]() {
            if (!ConfirmChallengeModeEnable())
              s_host_interface->GetSettingsInterface()->SetBoolValue("Cheevos", "ChallengeMode", false);
          });
        }

        MenuHeading("Account");
        if (Cheevos::IsLoggedIn())
        {
          ImGui::PushStyleColor(ImGuiCol_TextDisabled, ImGui::GetStyle().Colors[ImGuiCol_Text]);
          ActiveButton(SmallString::FromFormat(ICON_FA_USER "  Username: %s", Cheevos::GetUsername().c_str()), false,
                       false, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);

          Timestamp ts;
          TinyString ts_string;
          ts.SetUnixTimestamp(StringUtil::FromChars<u64>(s_host_interface->GetSettingsInterface()->GetStringValue(
                                                           "Cheevos", "LoginTimestamp", "0"))
                                .value_or(0));
          ts.ToString(ts_string, "%Y-%m-%d %H:%M:%S");
          ActiveButton(SmallString::FromFormat(ICON_FA_CLOCK "  Login token generated on %s", ts_string.GetCharArray()),
                       false, false, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);
          ImGui::PopStyleColor();

          if (MenuButton(ICON_FA_KEY "  Logout", "Logs out of RetroAchievements."))
            Cheevos::Logout();
        }
        else if (Cheevos::IsActive())
        {
          ActiveButton(ICON_FA_USER "  Not Logged In", false, false,
                       ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);

          if (MenuButton(ICON_FA_KEY "  Login", "Logs in to RetroAchievements."))
            ImGui::OpenPopup("Achievements Login");

          DrawAchievementsLoginWindow();
        }
        else
        {
          ActiveButton(ICON_FA_USER "  Achievements are disabled.", false, false,
                       ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);
        }

        MenuHeading("Current Game");
        if (Cheevos::HasActiveGame())
        {
          ImGui::PushStyleColor(ImGuiCol_TextDisabled, ImGui::GetStyle().Colors[ImGuiCol_Text]);
          ActiveButton(TinyString::FromFormat(ICON_FA_BOOKMARK "  Game ID: %u", Cheevos::GetGameID()), false, false,
                       ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);
          ActiveButton(TinyString::FromFormat(ICON_FA_BOOK "  Game Title: %s", Cheevos::GetGameTitle().c_str()), false,
                       false, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);
          ActiveButton(
            TinyString::FromFormat(ICON_FA_DESKTOP "  Game Developer: %s", Cheevos::GetGameDeveloper().c_str()), false,
            false, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);
          ActiveButton(
            TinyString::FromFormat(ICON_FA_DESKTOP "  Game Publisher: %s", Cheevos::GetGamePublisher().c_str()), false,
            false, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);
          ActiveButton(TinyString::FromFormat(ICON_FA_TROPHY "  Achievements: %u (%u points)",
                                              Cheevos::GetAchievementCount(), Cheevos::GetMaximumPointsForGame()),
                       false, false, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);

          const std::string& rich_presence_string = Cheevos::GetRichPresenceString();
          if (!rich_presence_string.empty())
          {
            ActiveButton(SmallString::FromFormat(ICON_FA_MAP "  %s", rich_presence_string.c_str()), false, false,
                         ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);
          }
          else
          {
            ActiveButton(ICON_FA_MAP "  Rich presence inactive or unsupported.", false, false,
                         ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);
          }

          ImGui::PopStyleColor();
        }
        else
        {
          ActiveButton(ICON_FA_BAN "  Game not loaded or no RetroAchievements available.", false, false,
                       ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);
        }

        EndMenuButtons();
#else
        BeginMenuButtons();
        ActiveButton(ICON_FA_BAN "  This build was not compiled with RetroAchivements support.", false, false,
                     ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);
        EndMenuButtons();
#endif
        // ImGuiFullscreen::moda
        // if (ImGui::BeginPopup("))
      }
      break;

      case SettingsPage::AdvancedSettings:
      {
        BeginMenuButtons();

        MenuHeading("Logging Settings");
        settings_changed |=
          EnumChoiceButton("Log Level", "Sets the verbosity of messages logged. Higher levels will log more messages.",
                           &s_settings_copy.log_level, &Settings::GetLogLevelDisplayName, LOGLEVEL_COUNT);
        settings_changed |= ToggleButton("Log To System Console", "Logs messages to the console window.",
                                         &s_settings_copy.log_to_console);
        settings_changed |= ToggleButton("Log To Debug Console", "Logs messages to the debug console where supported.",
                                         &s_settings_copy.log_to_debug);
        settings_changed |= ToggleButton("Log To File", "Logs messages to duckstation.log in the user directory.",
                                         &s_settings_copy.log_to_file);

        MenuHeading("Debugging Settings");

        bool debug_menu = s_debug_menu_enabled;
        if (ToggleButton("Enable Debug Menu", "Shows a debug menu bar with additional statistics and quick settings.",
                         &debug_menu))
        {
          s_host_interface->RunLater([debug_menu]() { SetDebugMenuEnabled(debug_menu); });
        }

        settings_changed |=
          ToggleButton("Disable All Enhancements", "Temporarily disables all enhancements, useful when testing.",
                       &s_settings_copy.disable_all_enhancements);

        settings_changed |= ToggleButton(
          "Use Debug GPU Device", "Enable debugging when supported by the host's renderer API. Only for developer use.",
          &s_settings_copy.gpu_use_debug_device);

#ifdef WIN32
        settings_changed |=
          ToggleButton("Increase Timer Resolution", "Enables more precise frame pacing at the cost of battery life.",
                       &s_settings_copy.increase_timer_resolution);
#endif

        MenuHeading("Display Settings");
        settings_changed |= ToggleButtonForNonSetting("Show Status Indicators",
                                                      "Shows persistent icons when turbo is active or when paused.",
                                                      "Display", "ShowStatusIndicators", true);
        settings_changed |= RangeButton(
          "Display FPS Limit", "Limits how many frames are displayed to the screen. These frames are still rendered.",
          &s_settings_copy.display_max_fps, 0.0f, 500.0f, 1.0f, "%.2f FPS");

        MenuHeading("PGXP Settings");

        settings_changed |= ToggleButton(
          "Enable PGXP Vertex Cache", "Uses screen positions to resolve PGXP data. May improve visuals in some games.",
          &s_settings_copy.gpu_pgxp_vertex_cache, s_settings_copy.gpu_pgxp_enable);
        settings_changed |= RangeButton(
          "PGXP Geometry Tolerance",
          "Sets a threshold for discarding precise values when exceeded. May help with glitches in some games.",
          &s_settings_copy.gpu_pgxp_tolerance, -1.0f, 10.0f, 0.1f, "%.1f Pixels", s_settings_copy.gpu_pgxp_enable);
        settings_changed |= RangeButton(
          "PGXP Depth Clear Threshold",
          "Sets a threshold for discarding the emulated depth buffer. May help in some games.",
          &s_settings_copy.gpu_pgxp_tolerance, 0.0f, 4096.0f, 1.0f, "%.1f", s_settings_copy.gpu_pgxp_enable);

        MenuHeading("Texture Dumping/Replacements");

        settings_changed |= ToggleButton("Enable VRAM Write Texture Replacement",
                                         "Enables the replacement of background textures in supported games.",
                                         &s_settings_copy.texture_replacements.enable_vram_write_replacements);
        settings_changed |= ToggleButton("Preload Replacement Textures",
                                         "Loads all replacement texture to RAM, reducing stuttering at runtime.",
                                         &s_settings_copy.texture_replacements.preload_textures,
                                         s_settings_copy.texture_replacements.AnyReplacementsEnabled());
        settings_changed |=
          ToggleButton("Dump Replacable VRAM Writes", "Writes textures which can be replaced to the dump directory.",
                       &s_settings_copy.texture_replacements.dump_vram_writes);
        settings_changed |=
          ToggleButton("Set VRAM Write Dump Alpha Channel", "Clears the mask/transparency bit in VRAM write dumps.",
                       &s_settings_copy.texture_replacements.dump_vram_write_force_alpha_channel,
                       s_settings_copy.texture_replacements.dump_vram_writes);

        MenuHeading("CPU Emulation");

        settings_changed |=
          ToggleButton("Enable Recompiler ICache",
                       "Simulates the CPU's instruction cache in the recompiler. Can help with games running too fast.",
                       &s_settings_copy.cpu_recompiler_icache);
        settings_changed |= ToggleButton("Enable Recompiler Memory Exceptions",
                                         "Enables alignment and bus exceptions. Not needed for any known games.",
                                         &s_settings_copy.cpu_recompiler_memory_exceptions);
        settings_changed |= EnumChoiceButton("Recompiler Fast Memory Access",
                                             "Avoids calls to C++ code, significantly speeding up the recompiler.",
                                             &s_settings_copy.cpu_fastmem_mode, &Settings::GetCPUFastmemModeDisplayName,
                                             CPUFastmemMode::Count, !s_settings_copy.cpu_recompiler_memory_exceptions);

        EndMenuButtons();
      }
      break;
    }

    if (settings_changed)
      s_host_interface->RunLater(SaveAndApplySettings);
  }

  EndFullscreenColumnWindow();

  EndFullscreenColumns();
}

void DrawQuickMenu(MainWindowType type)
{
  ImDrawList* dl = ImGui::GetBackgroundDrawList();
  const ImVec2 display_size(ImGui::GetIO().DisplaySize);
  dl->AddRectFilled(ImVec2(0.0f, 0.0f), display_size, IM_COL32(0x21, 0x21, 0x21, 200));

  // title info
  {
    const std::string& title = System::GetRunningTitle();
    const std::string& code = System::GetRunningCode();

    SmallString subtitle;
    if (!code.empty())
      subtitle.Format("%s - ", code.c_str());
    subtitle.AppendString(FileSystem::GetFileNameFromPath(System::GetRunningPath()));

    const ImVec2 title_size(
      g_large_font->CalcTextSizeA(g_large_font->FontSize, std::numeric_limits<float>::max(), -1.0f, title.c_str()));
    const ImVec2 subtitle_size(
      g_medium_font->CalcTextSizeA(g_medium_font->FontSize, std::numeric_limits<float>::max(), -1.0f, subtitle));

    const ImVec2 title_pos(display_size.x - LayoutScale(20.0f + 50.0f + 20.0f) - title_size.x,
                           display_size.y - LayoutScale(20.0f + 50.0f));
    const ImVec2 subtitle_pos(display_size.x - LayoutScale(20.0f + 50.0f + 20.0f) - subtitle_size.x,
                              title_pos.y + g_large_font->FontSize + LayoutScale(4.0f));

    dl->AddText(g_large_font, g_large_font->FontSize, title_pos, IM_COL32(255, 255, 255, 255), title.c_str());
    dl->AddText(g_medium_font, g_medium_font->FontSize, subtitle_pos, IM_COL32(255, 255, 255, 255), subtitle);

    const ImVec2 image_min(display_size - LayoutScale(20.0f + 50.0f, 20.0f + 50.0f));
    const ImVec2 image_max(image_min + LayoutScale(50.0f, 50.0f));
    dl->AddImage(GetCoverForCurrentGame()->GetHandle(), image_min, image_max);
  }

  const ImVec2 window_size(LayoutScale(500.0f, LAYOUT_SCREEN_HEIGHT));
  const ImVec2 window_pos(0.0f, display_size.y - window_size.y);
  if (BeginFullscreenWindow(window_pos, window_size, "pause_menu", ImVec4(0.0f, 0.0f, 0.0f, 0.0f), 0.0f, 10.0f,
                            ImGuiWindowFlags_NoBackground))
  {
    BeginMenuButtons(13, 1.0f, ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING,
                     ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING,
                     ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);

    if (ActiveButton(ICON_FA_PLAY "  Resume Game", false) || WantsToCloseMenu())
      CloseQuickMenu();

    if (ActiveButton(ICON_FA_FAST_FORWARD "  Fast Forward", false))
    {
      s_host_interface->RunLater(
        []() { s_host_interface->SetFastForwardEnabled(!s_host_interface->IsFastForwardEnabled()); });
      CloseQuickMenu();
    }

#ifdef WITH_CHEEVOS
    const bool achievements_enabled = Cheevos::HasActiveGame() && (Cheevos::GetAchievementCount() > 0);
    if (ActiveButton(ICON_FA_TROPHY "  Achievements", false, achievements_enabled))
      OpenAchievementsWindow();

    const bool leaderboards_enabled = Cheevos::HasActiveGame() && (Cheevos::GetLeaderboardCount() > 0);
    if (ActiveButton(ICON_FA_STOPWATCH "  Leaderboards", false, leaderboards_enabled))
      OpenLeaderboardsWindow();

#else
    ActiveButton(ICON_FA_TROPHY "  Achievements", false, false);
    ActiveButton(ICON_FA_STOPWATCH "  Leaderboards", false, false);
#endif

    if (ActiveButton(ICON_FA_CAMERA "  Save Screenshot", false))
    {
      CloseQuickMenu();
      s_host_interface->RunLater([]() { s_host_interface->SaveScreenshot(); });
    }

    if (ActiveButton(ICON_FA_UNDO "  Load State", false, !IsCheevosHardcoreModeActive()))
    {
      s_current_main_window = MainWindowType::None;
      OpenSaveStateSelector(true);
    }

    if (ActiveButton(ICON_FA_SAVE "  Save State", false))
    {
      s_current_main_window = MainWindowType::None;
      OpenSaveStateSelector(false);
    }

    if (ActiveButton(ICON_FA_FROWN_OPEN "  Cheat List", false, !IsCheevosHardcoreModeActive()))
    {
      s_current_main_window = MainWindowType::None;
      DoCheatsMenu();
    }

    if (ActiveButton(ICON_FA_GAMEPAD "  Toggle Analog", false))
    {
      CloseQuickMenu();
      DoToggleAnalogMode();
    }

    if (ActiveButton(ICON_FA_COMPACT_DISC "  Change Disc", false))
    {
      s_current_main_window = MainWindowType::None;
      DoChangeDisc();
    }

    if (ActiveButton(ICON_FA_SLIDERS_H "  Settings", false))
      s_current_main_window = MainWindowType::Settings;

    if (ActiveButton(ICON_FA_SYNC "  Reset System", false))
    {
      CloseQuickMenu();
      s_host_interface->RunLater(DoReset);
    }

    if (ActiveButton(ICON_FA_POWER_OFF "  Exit Game", false))
    {
      CloseQuickMenu();
      s_host_interface->RunLater(DoPowerOff);
    }

    EndMenuButtons();

    EndFullscreenWindow();
  }
}

void InitializePlaceholderSaveStateListEntry(SaveStateListEntry* li, s32 slot, bool global)
{
  if (global)
    li->title = StringUtil::StdStringFromFormat("Global Slot %d##global_slot_%d", slot, slot);
  else
    li->title =
      StringUtil::StdStringFromFormat("%s Slot %d##game_slot_%d", System::GetRunningTitle().c_str(), slot, slot);

  li->summary = "No Save State";

  std::string().swap(li->path);
  std::string().swap(li->media_path);
  li->slot = slot;
  li->global = global;
}

void InitializeSaveStateListEntry(SaveStateListEntry* li, CommonHostInterface::ExtendedSaveStateInfo* ssi)
{
  if (ssi->global)
  {
    li->title =
      StringUtil::StdStringFromFormat("Global Save %d - %s##global_slot_%d", ssi->slot, ssi->title.c_str(), ssi->slot);
  }
  else
  {
    li->title = StringUtil::StdStringFromFormat("%s Slot %d##game_slot_%d", ssi->title.c_str(), ssi->slot, ssi->slot);
  }

  li->summary =
    StringUtil::StdStringFromFormat("%s - Saved %s", ssi->game_code.c_str(),
                                    Timestamp::FromUnixTimestamp(ssi->timestamp).ToString("%c").GetCharArray());
  li->slot = ssi->slot;
  li->global = ssi->global;
  li->path = std::move(ssi->path);
  li->media_path = std::move(ssi->media_path);

  li->preview_texture.reset();
  if (ssi && !ssi->screenshot_data.empty())
  {
    li->preview_texture = s_host_interface->GetDisplay()->CreateTexture(
      ssi->screenshot_width, ssi->screenshot_height, 1, 1, 1, HostDisplayPixelFormat::RGBA8,
      ssi->screenshot_data.data(), sizeof(u32) * ssi->screenshot_width, false);
  }
  else
  {
    li->preview_texture = s_host_interface->GetDisplay()->CreateTexture(
      PLACEHOLDER_ICON_WIDTH, PLACEHOLDER_ICON_HEIGHT, 1, 1, 1, HostDisplayPixelFormat::RGBA8, PLACEHOLDER_ICON_DATA,
      sizeof(u32) * PLACEHOLDER_ICON_WIDTH, false);
  }

  if (!li->preview_texture)
    Log_ErrorPrintf("Failed to upload save state image to GPU");
}

void PopulateSaveStateListEntries()
{
  s_save_state_selector_slots.clear();

  if (!System::GetRunningCode().empty())
  {
    for (s32 i = 1; i <= CommonHostInterface::PER_GAME_SAVE_STATE_SLOTS; i++)
    {
      std::optional<CommonHostInterface::ExtendedSaveStateInfo> ssi =
        s_host_interface->GetExtendedSaveStateInfo(System::GetRunningCode().c_str(), i);

      SaveStateListEntry li;
      if (ssi)
        InitializeSaveStateListEntry(&li, &ssi.value());
      else
        InitializePlaceholderSaveStateListEntry(&li, i, false);

      s_save_state_selector_slots.push_back(std::move(li));
    }
  }

  for (s32 i = 1; i <= CommonHostInterface::GLOBAL_SAVE_STATE_SLOTS; i++)
  {
    std::optional<CommonHostInterface::ExtendedSaveStateInfo> ssi =
      s_host_interface->GetExtendedSaveStateInfo(nullptr, i);

    SaveStateListEntry li;
    if (ssi)
      InitializeSaveStateListEntry(&li, &ssi.value());
    else
      InitializePlaceholderSaveStateListEntry(&li, i, true);

    s_save_state_selector_slots.push_back(std::move(li));
  }
}

#if 0
void DrawSaveStateSelector(bool is_loading, bool fullscreen)
{
  const HostDisplayTexture* selected_texture = s_placeholder_texture.get();
  if (!BeginFullscreenColumns())
  {
    EndFullscreenColumns();
    return;
  }

  // drawn back the front so the hover changes the image
  if (BeginFullscreenColumnWindow(570.0f, LAYOUT_SCREEN_WIDTH, "save_state_selector_slots"))
  {
    BeginMenuButtons(static_cast<u32>(s_save_state_selector_slots.size()), true);

    for (const SaveStateListEntry& entry : s_save_state_selector_slots)
    {
      if (MenuButton(entry.title.c_str(), entry.summary.c_str()))
      {
        const std::string& path = entry.path;
        s_host_interface->RunLater([path]() { s_host_interface->LoadState(path.c_str()); });
      }

      if (ImGui::IsItemHovered())
        selected_texture = entry.preview_texture.get();
    }

    EndMenuButtons();
  }
  EndFullscreenColumnWindow();

  if (BeginFullscreenColumnWindow(0.0f, 570.0f, "save_state_selector_preview", ImVec4(0.11f, 0.15f, 0.17f, 1.00f)))
  {
    ImGui::SetCursorPos(LayoutScale(20.0f, 20.0f));
    ImGui::PushFont(g_large_font);
    ImGui::TextUnformatted(is_loading ? ICON_FA_FOLDER_OPEN "  Load State" : ICON_FA_SAVE "  Save State");
    ImGui::PopFont();

    ImGui::SetCursorPos(LayoutScale(ImVec2(85.0f, 160.0f)));
    ImGui::Image(selected_texture ? selected_texture->GetHandle() : s_placeholder_texture->GetHandle(),
      LayoutScale(ImVec2(400.0f, 400.0f)));

    ImGui::SetCursorPosY(LayoutScale(670.0f));
    BeginMenuButtons(1, false);
    if (ActiveButton(ICON_FA_BACKWARD "  Back", false))
      ReturnToMainWindow();
    EndMenuButtons();
  }
  EndFullscreenColumnWindow();

  EndFullscreenColumns();
}
#endif

void OpenSaveStateSelector(bool is_loading)
{
  s_save_state_selector_loading = is_loading;
  s_save_state_selector_open = true;
  s_save_state_selector_slots.clear();
  PopulateSaveStateListEntries();
}

void CloseSaveStateSelector()
{
  s_save_state_selector_slots.clear();
  s_save_state_selector_open = false;
  ReturnToMainWindow();
}

void DrawSaveStateSelector(bool is_loading, bool fullscreen)
{
  if (fullscreen)
  {
    if (!BeginFullscreenColumns())
    {
      EndFullscreenColumns();
      return;
    }

    if (!BeginFullscreenColumnWindow(0.0f, LAYOUT_SCREEN_WIDTH, "save_state_selector_slots"))
    {
      EndFullscreenColumnWindow();
      EndFullscreenColumns();
      return;
    }
  }
  else
  {
    const char* window_title = is_loading ? "Load State" : "Save State";

    ImGui::PushFont(g_large_font);
    ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
    ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING,
                                                                ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING));

    ImGui::SetNextWindowSize(LayoutScale(1000.0f, 680.0f));
    ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
    ImGui::OpenPopup(window_title);
    bool is_open = !WantsToCloseMenu();
    if (!ImGui::BeginPopupModal(window_title, &is_open,
                                ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove) ||
        !is_open)
    {
      ImGui::PopStyleVar(2);
      ImGui::PopFont();
      CloseSaveStateSelector();
      return;
    }
  }

  BeginMenuButtons();

  constexpr float padding = 10.0f;
  constexpr float button_height = 96.0f;
  constexpr float max_image_width = 96.0f;
  constexpr float max_image_height = 96.0f;

  ImDrawList* dl = ImGui::GetWindowDrawList();
  for (const SaveStateListEntry& entry : s_save_state_selector_slots)
  {
    ImRect bb;
    bool visible, hovered;
    bool pressed = MenuButtonFrame(entry.title.c_str(), true, button_height, &visible, &hovered, &bb.Min, &bb.Max);
    if (!visible)
      continue;

    ImVec2 pos(bb.Min);

    // use aspect ratio of screenshot to determine height
    const HostDisplayTexture* image = entry.preview_texture ? entry.preview_texture.get() : s_placeholder_texture.get();
    const float image_height =
      max_image_width / (static_cast<float>(image->GetWidth()) / static_cast<float>(image->GetHeight()));
    const float image_margin = (max_image_height - image_height) / 2.0f;
    const ImRect image_bb(ImVec2(pos.x, pos.y + LayoutScale(image_margin)),
                          pos + LayoutScale(max_image_width, image_margin + image_height));
    pos.x += LayoutScale(max_image_width + padding);

    dl->AddImage(static_cast<ImTextureID>(entry.preview_texture ? entry.preview_texture->GetHandle() :
                                                                  s_placeholder_texture->GetHandle()),
                 image_bb.Min, image_bb.Max);

    ImRect text_bb(pos, ImVec2(bb.Max.x, pos.y + g_large_font->FontSize));
    ImGui::PushFont(g_large_font);
    ImGui::RenderTextClipped(text_bb.Min, text_bb.Max, entry.title.c_str(), nullptr, nullptr, ImVec2(0.0f, 0.0f),
                             &text_bb);
    ImGui::PopFont();

    ImGui::PushFont(g_medium_font);

    if (!entry.summary.empty())
    {
      text_bb.Min.y = text_bb.Max.y + LayoutScale(4.0f);
      text_bb.Max.y = text_bb.Min.y + g_medium_font->FontSize;
      ImGui::RenderTextClipped(text_bb.Min, text_bb.Max, entry.summary.c_str(), nullptr, nullptr, ImVec2(0.0f, 0.0f),
                               &text_bb);
    }

    if (!entry.path.empty())
    {
      text_bb.Min.y = text_bb.Max.y + LayoutScale(4.0f);
      text_bb.Max.y = text_bb.Min.y + g_medium_font->FontSize;
      ImGui::RenderTextClipped(text_bb.Min, text_bb.Max, entry.path.c_str(), nullptr, nullptr, ImVec2(0.0f, 0.0f),
                               &text_bb);
    }

    if (!entry.media_path.empty())
    {
      text_bb.Min.y = text_bb.Max.y + LayoutScale(4.0f);
      text_bb.Max.y = text_bb.Min.y + g_medium_font->FontSize;
      ImGui::RenderTextClipped(text_bb.Min, text_bb.Max, entry.media_path.c_str(), nullptr, nullptr, ImVec2(0.0f, 0.0f),
                               &text_bb);
    }

    ImGui::PopFont();

    if (pressed)
    {
      if (is_loading)
      {
        const std::string& path = entry.path;
        s_host_interface->RunLater([path]() {
          s_host_interface->LoadState(path.c_str());
          CloseSaveStateSelector();
        });
      }
      else
      {
        const s32 slot = entry.slot;
        const bool global = entry.global;
        s_host_interface->RunLater([slot, global]() {
          s_host_interface->SaveState(global, slot);
          CloseSaveStateSelector();
        });
      }
    }
  }

  EndMenuButtons();

  if (fullscreen)
  {
    EndFullscreenColumnWindow();
    EndFullscreenColumns();
  }
  else
  {
    ImGui::EndPopup();
    ImGui::PopStyleVar(2);
    ImGui::PopFont();
  }
}

void DrawGameListWindow()
{
  const GameListEntry* selected_entry = nullptr;

  if (!BeginFullscreenColumns())
  {
    EndFullscreenColumns();
    return;
  }

  if (BeginFullscreenColumnWindow(450.0f, LAYOUT_SCREEN_WIDTH, "game_list_entries"))
  {
    const ImVec2 image_size(LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT, LAYOUT_MENU_BUTTON_HEIGHT));

    BeginMenuButtons();

    SmallString summary;

    for (const GameListEntry* entry : s_game_list_sorted_entries)
    {
      ImRect bb;
      bool visible, hovered;
      bool pressed =
        MenuButtonFrame(entry->path.c_str(), true, LAYOUT_MENU_BUTTON_HEIGHT, &visible, &hovered, &bb.Min, &bb.Max);
      if (!visible)
        continue;

      HostDisplayTexture* cover_texture = GetGameListCover(entry);
      if (entry->code.empty())
        summary.Format("%s - ", Settings::GetDiscRegionName(entry->region));
      else
        summary.Format("%s - %s - ", entry->code.c_str(), Settings::GetDiscRegionName(entry->region));

      summary.AppendString(FileSystem::GetFileNameFromPath(entry->path));

      ImGui::GetWindowDrawList()->AddImage(cover_texture->GetHandle(), bb.Min, bb.Min + image_size, ImVec2(0.0f, 0.0f),
                                           ImVec2(1.0f, 1.0f), IM_COL32(255, 255, 255, 255));

      const float midpoint = bb.Min.y + g_large_font->FontSize + LayoutScale(4.0f);
      const float text_start_x = bb.Min.x + image_size.x + LayoutScale(15.0f);
      const ImRect title_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint));
      const ImRect summary_bb(ImVec2(text_start_x, midpoint), bb.Max);

      ImGui::PushFont(g_large_font);
      ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, entry->title.c_str(),
                               entry->title.c_str() + entry->title.size(), nullptr, ImVec2(0.0f, 0.0f), &title_bb);
      ImGui::PopFont();

      if (summary)
      {
        ImGui::PushFont(g_medium_font);
        ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, summary, summary.GetCharArray() + summary.GetLength(),
                                 nullptr, ImVec2(0.0f, 0.0f), &summary_bb);
        ImGui::PopFont();
      }

      if (pressed)
      {
        // launch game
        const std::string& path_to_launch(entry->path);
        s_host_interface->RunLater([path_to_launch]() { DoStartPath(path_to_launch, true); });
      }

      if (hovered)
        selected_entry = entry;
    }

    EndMenuButtons();
  }
  EndFullscreenColumnWindow();

  if (BeginFullscreenColumnWindow(0.0f, 450.0f, "game_list_info", ImVec4(0.11f, 0.15f, 0.17f, 1.00f)))
  {
    const ImGuiWindow* window = ImGui::GetCurrentWindow();

    ImGui::SetCursorPos(LayoutScale(ImVec2(50.0f, 50.0f)));
    ImGui::Image(selected_entry ? GetGameListCover(selected_entry)->GetHandle() :
                                  GetTextureForGameListEntryType(GameListEntryType::Count)->GetHandle(),
                 LayoutScale(ImVec2(350.0f, 350.0f)));

    const float work_width = ImGui::GetCurrentWindow()->WorkRect.GetWidth();
    constexpr float field_margin_y = 10.0f;
    constexpr float start_x = 50.0f;
    float text_y = 425.0f;
    float text_width;
    SmallString text;

    ImGui::SetCursorPos(LayoutScale(start_x, text_y));
    ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, field_margin_y));
    ImGui::BeginGroup();

    if (selected_entry)
    {
      // title
      ImGui::PushFont(g_large_font);
      text_width = ImGui::CalcTextSize(selected_entry->title.c_str(), nullptr, false, work_width).x;
      ImGui::SetCursorPosX((work_width - text_width) / 2.0f);
      ImGui::TextWrapped("%s", selected_entry->title.c_str());
      ImGui::PopFont();

      ImGui::PushFont(g_medium_font);

      // developer
      if (!selected_entry->developer.empty())
      {
        text_width = ImGui::CalcTextSize(selected_entry->developer.c_str(), nullptr, false, work_width).x;
        ImGui::SetCursorPosX((work_width - text_width) / 2.0f);
        ImGui::TextWrapped("%s", selected_entry->developer.c_str());
      }

      // code
      text_width = ImGui::CalcTextSize(selected_entry->code.c_str(), nullptr, false, work_width).x;
      ImGui::SetCursorPosX((work_width - text_width) / 2.0f);
      ImGui::TextWrapped("%s", selected_entry->code.c_str());
      ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 15.0f);

      // region
      ImGui::TextUnformatted("Region: ");
      ImGui::SameLine();
      ImGui::Image(s_disc_region_textures[static_cast<u32>(selected_entry->region)]->GetHandle(),
                   LayoutScale(23.0f, 16.0f));
      ImGui::SameLine();
      ImGui::Text(" (%s)", Settings::GetDiscRegionDisplayName(selected_entry->region));

      // genre
      ImGui::Text("Genre: %s", selected_entry->genre.c_str());

      // release date
      char release_date_str[64];
      selected_entry->GetReleaseDateString(release_date_str, sizeof(release_date_str));
      ImGui::Text("Release Date: %s", release_date_str);

      // compatibility
      ImGui::TextUnformatted("Compatibility: ");
      ImGui::SameLine();
      ImGui::Image(s_game_compatibility_textures[static_cast<u32>(selected_entry->compatibility_rating)]->GetHandle(),
                   LayoutScale(64.0f, 16.0f));
      ImGui::SameLine();
      ImGui::Text(" (%s)", GameList::GetGameListCompatibilityRatingString(selected_entry->compatibility_rating));

      // size
      ImGui::Text("Size: %.2f MB", static_cast<float>(selected_entry->total_size) / 1048576.0f);

      // game settings
      const u32 user_setting_count = selected_entry->settings.GetUserSettingsCount();
      if (user_setting_count > 0)
        ImGui::Text("%u Per-Game Settings Set", user_setting_count);
      else
        ImGui::TextUnformatted("No Per-Game Settings Set");

      ImGui::PopFont();
    }
    else
    {
      // title
      const char* title = "No Game Selected";
      ImGui::PushFont(g_large_font);
      text_width = ImGui::CalcTextSize(title, nullptr, false, work_width).x;
      ImGui::SetCursorPosX((work_width - text_width) / 2.0f);
      ImGui::TextWrapped("%s", title);
      ImGui::PopFont();
    }

    ImGui::EndGroup();
    ImGui::PopStyleVar();

    ImGui::SetCursorPosY(ImGui::GetWindowHeight() - LayoutScale(50.0f));
    BeginMenuButtons();
    if (ActiveButton(ICON_FA_BACKWARD "  Back", false))
      ReturnToMainWindow();
    EndMenuButtons();
  }
  EndFullscreenColumnWindow();

  EndFullscreenColumns();
}

void EnsureGameListLoaded()
{
  // not worth using a condvar here
  if (s_game_list_load_thread.joinable())
    s_game_list_load_thread.join();

  if (s_game_list_sorted_entries.empty())
    SortGameList();
}

static void GameListRefreshThread()
{
  ProgressCallback cb("game_list_refresh");
  s_host_interface->GetGameList()->Refresh(false, false, &cb);
}

void QueueGameListRefresh()
{
  if (s_game_list_load_thread.joinable())
    s_game_list_load_thread.join();

  s_game_list_sorted_entries.clear();
  s_host_interface->GetGameList()->SetSearchDirectoriesFromSettings(*s_host_interface->GetSettingsInterface());
  s_game_list_load_thread = std::thread(GameListRefreshThread);
}

void SwitchToGameList()
{
  EnsureGameListLoaded();
  s_current_main_window = MainWindowType::GameList;
}

void SortGameList()
{
  s_game_list_sorted_entries.clear();

  for (const GameListEntry& entry : s_host_interface->GetGameList()->GetEntries())
    s_game_list_sorted_entries.push_back(&entry);

  // TODO: Custom sort types
  std::sort(s_game_list_sorted_entries.begin(), s_game_list_sorted_entries.end(),
            [](const GameListEntry* lhs, const GameListEntry* rhs) { return lhs->title < rhs->title; });
}

HostDisplayTexture* GetGameListCover(const GameListEntry* entry)
{
  // lookup and grab cover image
  auto cover_it = s_cover_image_map.find(entry->path);
  if (cover_it == s_cover_image_map.end())
  {
    std::string cover_path(s_host_interface->GetGameList()->GetCoverImagePathForEntry(entry));
    cover_it = s_cover_image_map.emplace(entry->path, std::move(cover_path)).first;
  }

  if (!cover_it->second.empty())
    return GetCachedTexture(cover_it->second);

  return GetTextureForGameListEntryType(entry->type);
}

HostDisplayTexture* GetTextureForGameListEntryType(GameListEntryType type)
{
  switch (type)
  {
    case GameListEntryType::PSExe:
      return s_fallback_exe_texture.get();

    case GameListEntryType::Playlist:
      return s_fallback_playlist_texture.get();

    case GameListEntryType::PSF:
      return s_fallback_psf_texture.get();
      break;

    case GameListEntryType::Disc:
    default:
      return s_fallback_disc_texture.get();
  }
}

HostDisplayTexture* GetCoverForCurrentGame()
{
  EnsureGameListLoaded();

  const GameListEntry* entry = s_host_interface->GetGameList()->GetEntryForPath(System::GetRunningPath().c_str());
  if (!entry)
    return s_fallback_disc_texture.get();

  return GetGameListCover(entry);
}

//////////////////////////////////////////////////////////////////////////
// Overlays
//////////////////////////////////////////////////////////////////////////
void DrawStatsOverlay()
{
  if (!(g_settings.display_show_fps || g_settings.display_show_vps || g_settings.display_show_speed ||
        g_settings.display_show_resolution || System::IsPaused() || s_host_interface->IsFastForwardEnabled() ||
        s_host_interface->IsTurboEnabled()))
  {
    return;
  }

  const float margin = LayoutScale(10.0f);
  const float shadow_offset = DPIScale(1.0f);
  float position_y = ImGuiFullscreen::g_menu_bar_size + margin;
  ImDrawList* dl = ImGui::GetBackgroundDrawList();
  TinyString text;
  ImVec2 text_size;
  bool first = true;

#define DRAW_LINE(font, font_size, right_pad, color)                                                                   \
  do                                                                                                                   \
  {                                                                                                                    \
    text_size = font->CalcTextSizeA(font_size, std::numeric_limits<float>::max(), -1.0f, text,                         \
                                    text.GetCharArray() + text.GetLength(), nullptr);                                  \
    dl->AddText(font, font_size,                                                                                       \
                ImVec2(ImGui::GetIO().DisplaySize.x - (right_pad)-margin - text_size.x + shadow_offset,                \
                       position_y + shadow_offset),                                                                    \
                IM_COL32(0, 0, 0, 100), text, text.GetCharArray() + text.GetLength());                                 \
    dl->AddText(font, font_size, ImVec2(ImGui::GetIO().DisplaySize.x - (right_pad)-margin - text_size.x, position_y),  \
                color, text, text.GetCharArray() + text.GetLength());                                                  \
    position_y += text_size.y + margin;                                                                                \
  } while (0)

  const System::State state = System::GetState();
  if (System::GetState() == System::State::Running)
  {
    const float speed = System::GetEmulationSpeed();
    if (g_settings.display_show_fps)
    {
      text.AppendFormattedString("%.2f", System::GetFPS());
      first = false;
    }
    if (g_settings.display_show_vps)
    {
      text.AppendFormattedString("%s%.2f", first ? "" : " / ", System::GetVPS());
      first = false;
    }
    if (g_settings.display_show_speed)
    {
      text.AppendFormattedString("%s%u%%", first ? "" : " / ", static_cast<u32>(std::round(speed)));
      first = false;
    }
    if (!text.IsEmpty())
    {
      ImU32 color;
      if (speed < 95.0f)
        color = IM_COL32(255, 100, 100, 255);
      else if (speed > 105.0f)
        color = IM_COL32(100, 255, 100, 255);
      else
        color = IM_COL32(255, 255, 255, 255);

      DRAW_LINE(g_large_font, g_large_font->FontSize, 0.0f, color);
    }

    if (g_settings.display_show_resolution)
    {
      const auto [effective_width, effective_height] = g_gpu->GetEffectiveDisplayResolution();
      const bool interlaced = g_gpu->IsInterlacedDisplayEnabled();
      text.Format("%ux%u (%s)", effective_width, effective_height, interlaced ? "interlaced" : "progressive");
      DRAW_LINE(g_large_font, g_large_font->FontSize, 0.0f, IM_COL32(255, 255, 255, 255));
    }

    if (s_show_status_indicators && (s_host_interface->IsFastForwardEnabled() || s_host_interface->IsTurboEnabled()))
    {
      text.Assign(ICON_FA_FAST_FORWARD);
      DRAW_LINE(g_large_font, g_large_font->FontSize * 2.0f, margin, IM_COL32(255, 255, 255, 255));
    }
  }
  else if (s_show_status_indicators && state == System::State::Paused)
  {
    text.Assign(ICON_FA_PAUSE);
    DRAW_LINE(g_large_font, g_large_font->FontSize * 2.0f, margin, IM_COL32(255, 255, 255, 255));
  }

#undef DRAW_LINE
}

void DrawOSDMessages()
{
  s_host_interface->AcquirePendingOSDMessages();

  ImGui::PushFont(g_large_font);

  const float max_width = LayoutScale(1080.0f);
  const float spacing = LayoutScale(4.0f);
  const float margin = LayoutScale(10.0f);
  const float padding = LayoutScale(10.0f);
  float position_x = margin;
  float position_y = margin + ImGuiFullscreen::g_menu_bar_size;

  s_host_interface->EnumerateOSDMessages(
    [max_width, spacing, padding, &position_x, &position_y](const std::string& message, float time_remaining) -> bool {
      const float opacity = std::min(time_remaining, 1.0f);
      const u32 alpha = static_cast<u32>(opacity * 255.0f);

      if (position_y >= ImGui::GetIO().DisplaySize.y)
        return false;

      const ImVec2 pos(position_x, position_y);
      const ImVec2 text_size(ImGui::CalcTextSize(message.c_str(), nullptr, false, max_width));
      const ImVec2 size(text_size + LayoutScale(20.0f, 20.0f));
      const ImVec4 text_rect(pos.x + padding, pos.y + padding, pos.x + size.x - padding, pos.y + size.y - padding);

      // If we're in the landing page, draw the OSD over the windows (since it covers it)
      ImDrawList* dl = (s_current_main_window != MainWindowType::None) ? ImGui::GetForegroundDrawList() :
                                                                         ImGui::GetBackgroundDrawList();
      dl->AddRectFilled(pos, pos + size, IM_COL32(0x21, 0x21, 0x21, alpha), LayoutScale(10.0f));
      dl->AddRect(pos, pos + size, IM_COL32(0x48, 0x48, 0x48, alpha), LayoutScale(10.0f));
      dl->AddText(g_large_font, g_large_font->FontSize, ImVec2(text_rect.x, text_rect.y),
                  IM_COL32(0xff, 0xff, 0xff, alpha), message.c_str(), nullptr, max_width, &text_rect);
      position_y += size.y + spacing;
      return true;
    });

  ImGui::PopFont();
}

void OpenAboutWindow()
{
  s_about_window_open = true;
}

void DrawAboutWindow()
{
  ImGui::SetNextWindowSize(LayoutScale(1000.0f, 500.0f));
  ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
  ImGui::OpenPopup("About DuckStation");

  ImGui::PushFont(g_large_font);
  ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
  ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(10.0f, 10.0f));

  if (ImGui::BeginPopupModal("About DuckStation", &s_about_window_open,
                             ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize))
  {
    ImGui::TextWrapped("DuckStation is a free and open-source simulator/emulator of the Sony PlayStation(TM) console, "
                       "focusing on playability, speed, and long-term maintainability.");
    ImGui::NewLine();
    ImGui::TextWrapped("Contributor List: https://github.com/stenzek/duckstation/blob/master/CONTRIBUTORS.md");
    ImGui::NewLine();
    ImGui::TextWrapped("Duck icon by icons8 (https://icons8.com/icon/74847/platforms.undefined.short-title)");
    ImGui::NewLine();
    ImGui::TextWrapped("\"PlayStation\" and \"PSX\" are registered trademarks of Sony Interactive Entertainment Europe "
                       "Limited. This software is not affiliated in any way with Sony Interactive Entertainment.");

    ImGui::NewLine();

    BeginMenuButtons();
    if (ActiveButton(ICON_FA_GLOBE "  GitHub Repository", false))
    {
      s_host_interface->RunLater(
        []() { s_host_interface->ReportError("Go to https://github.com/stenzek/duckstation/"); });
    }
    if (ActiveButton(ICON_FA_BUG "  Issue Tracker", false))
    {
      s_host_interface->RunLater(
        []() { s_host_interface->ReportError("Go to https://github.com/stenzek/duckstation/issues"); });
    }
    if (ActiveButton(ICON_FA_COMMENT "  Discord Server", false))
    {
      s_host_interface->RunLater([]() { s_host_interface->ReportError("Go to https://discord.gg/Buktv3t"); });
    }

    if (ActiveButton(ICON_FA_WINDOW_CLOSE "  Close", false))
    {
      ImGui::CloseCurrentPopup();
      s_about_window_open = false;
    }
    EndMenuButtons();

    ImGui::EndPopup();
  }

  ImGui::PopStyleVar(2);
  ImGui::PopFont();
}

bool DrawErrorWindow(const char* message)
{
  bool is_open = true;

  ImGuiFullscreen::BeginLayout();

  ImGui::SetNextWindowSize(LayoutScale(500.0f, 0.0f));
  ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
  ImGui::OpenPopup("ReportError");

  ImGui::PushFont(g_large_font);
  ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
  ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(10.0f, 10.0f));

  if (ImGui::BeginPopupModal("ReportError", &is_open, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize))
  {
    ImGui::SetCursorPos(LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING));
    ImGui::TextWrapped("%s", message);
    ImGui::GetCurrentWindow()->DC.CursorPos.y += LayoutScale(5.0f);

    BeginMenuButtons();

    if (ActiveButton(ICON_FA_WINDOW_CLOSE "  Close", false))
    {
      ImGui::CloseCurrentPopup();
      is_open = false;
    }
    EndMenuButtons();

    ImGui::EndPopup();
  }

  ImGui::PopStyleVar(2);
  ImGui::PopFont();

  ImGuiFullscreen::EndLayout();
  return !is_open;
}

bool DrawConfirmWindow(const char* message, bool* result)
{
  bool is_open = true;

  ImGuiFullscreen::BeginLayout();

  ImGui::SetNextWindowSize(LayoutScale(500.0f, 0.0f));
  ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
  ImGui::OpenPopup("ConfirmMessage");

  ImGui::PushFont(g_large_font);
  ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
  ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(10.0f, 10.0f));

  if (ImGui::BeginPopupModal("ConfirmMessage", &is_open, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize))
  {
    ImGui::SetCursorPos(LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING));
    ImGui::TextWrapped("%s", message);
    ImGui::GetCurrentWindow()->DC.CursorPos.y += LayoutScale(5.0f);

    BeginMenuButtons();

    bool done = false;

    if (ActiveButton(ICON_FA_CHECK "  Yes", false))
    {
      *result = true;
      done = true;
    }

    if (ActiveButton(ICON_FA_TIMES "  No", false))
    {
      *result = false;
      done = true;
    }
    if (done)
    {
      ImGui::CloseCurrentPopup();
      is_open = false;
    }

    EndMenuButtons();

    ImGui::EndPopup();
  }

  ImGui::PopStyleVar(2);
  ImGui::PopFont();

  ImGuiFullscreen::EndLayout();
  return !is_open;
}

//////////////////////////////////////////////////////////////////////////
// Debug Menu
//////////////////////////////////////////////////////////////////////////

void SetDebugMenuAllowed(bool allowed)
{
  s_debug_menu_allowed = allowed;
  UpdateDebugMenuVisibility();
}

void SetDebugMenuEnabled(bool enabled)
{
  s_host_interface->GetSettingsInterface()->SetBoolValue("Main", "ShowDebugMenu", enabled);
  s_host_interface->GetSettingsInterface()->Save();
  UpdateDebugMenuVisibility();
}

void UpdateDebugMenuVisibility()
{
  const bool enabled =
    s_debug_menu_allowed && s_host_interface->GetSettingsInterface()->GetBoolValue("Main", "ShowDebugMenu", false);
  if (s_debug_menu_enabled == enabled)
    return;

  const float size = enabled ? DPIScale(LAYOUT_MAIN_MENU_BAR_SIZE) : 0.0f;
  s_host_interface->GetDisplay()->SetDisplayTopMargin(static_cast<s32>(size));
  ImGuiFullscreen::SetMenuBarSize(size);
  ImGuiFullscreen::UpdateLayoutScale();
  if (ImGuiFullscreen::UpdateFonts())
    s_host_interface->GetDisplay()->UpdateImGuiFontTexture();
  s_debug_menu_enabled = enabled;
}

static void DrawDebugStats();
static void DrawDebugSystemMenu();
static void DrawDebugSettingsMenu();
static void DrawDebugDebugMenu();

void DrawDebugMenu()
{
  if (!ImGui::BeginMainMenuBar())
    return;

  if (ImGui::BeginMenu("System"))
  {
    DrawDebugSystemMenu();
    ImGui::EndMenu();
  }

  if (ImGui::BeginMenu("Settings"))
  {
    DrawDebugSettingsMenu();
    ImGui::EndMenu();
  }

  if (ImGui::BeginMenu("Debug"))
  {
    DrawDebugDebugMenu();
    ImGui::EndMenu();
  }

  DrawDebugStats();

  ImGui::EndMainMenuBar();
}

void DrawDebugStats()
{
  if (!System::IsShutdown())
  {
    const float framebuffer_scale = ImGui::GetIO().DisplayFramebufferScale.x;
    const float framebuffer_width = ImGui::GetIO().DisplaySize.x;

    if (System::IsPaused())
    {
      ImGui::SetCursorPosX(framebuffer_width - (50.0f * framebuffer_scale));
      ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "Paused");
    }
    else
    {
      const auto [display_width, display_height] = g_gpu->GetEffectiveDisplayResolution();
      ImGui::SetCursorPosX(framebuffer_width - (580.0f * framebuffer_scale));
      ImGui::Text("%ux%u (%s)", display_width, display_height,
                  g_gpu->IsInterlacedDisplayEnabled() ? "interlaced" : "progressive");

      ImGui::SetCursorPosX(framebuffer_width - (420.0f * framebuffer_scale));
      ImGui::Text("Average: %.2fms", System::GetAverageFrameTime());

      ImGui::SetCursorPosX(framebuffer_width - (310.0f * framebuffer_scale));
      ImGui::Text("Worst: %.2fms", System::GetWorstFrameTime());

      ImGui::SetCursorPosX(framebuffer_width - (210.0f * framebuffer_scale));

      const float speed = System::GetEmulationSpeed();
      const u32 rounded_speed = static_cast<u32>(std::round(speed));
      if (speed < 90.0f)
        ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%u%%", rounded_speed);
      else if (speed < 110.0f)
        ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 1.0f), "%u%%", rounded_speed);
      else
        ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "%u%%", rounded_speed);

      ImGui::SetCursorPosX(framebuffer_width - (165.0f * framebuffer_scale));
      ImGui::Text("FPS: %.2f", System::GetFPS());

      ImGui::SetCursorPosX(framebuffer_width - (80.0f * framebuffer_scale));
      ImGui::Text("VPS: %.2f", System::GetVPS());
    }
  }
}

void DrawDebugSystemMenu()
{
  const bool system_enabled = static_cast<bool>(!System::IsShutdown());

  if (ImGui::MenuItem("Start Disc", nullptr, false, !system_enabled))
  {
    DoStartFile();
    ClearImGuiFocus();
  }

  if (ImGui::MenuItem("Start BIOS", nullptr, false, !system_enabled))
  {
    DoStartBIOS();
    ClearImGuiFocus();
  }

  ImGui::Separator();

  if (ImGui::MenuItem("Power Off", nullptr, false, system_enabled))
  {
    DoPowerOff();
    ClearImGuiFocus();
  }

  if (ImGui::MenuItem("Reset", nullptr, false, system_enabled))
  {
    DoReset();
    ClearImGuiFocus();
  }

  if (ImGui::MenuItem("Pause", nullptr, System::IsPaused(), system_enabled))
  {
    DoPause();
    ClearImGuiFocus();
  }

  ImGui::Separator();

  if (ImGui::MenuItem("Change Disc", nullptr, false, system_enabled))
  {
    DoChangeDisc();
    ClearImGuiFocus();
  }

  if (ImGui::MenuItem("Remove Disc", nullptr, false, system_enabled))
  {
    s_host_interface->RunLater([]() { System::RemoveMedia(); });
    ClearImGuiFocus();
  }

  ImGui::Separator();

  if (ImGui::BeginMenu("Load State", !IsCheevosHardcoreModeActive()))
  {
    for (u32 i = 1; i <= CommonHostInterface::GLOBAL_SAVE_STATE_SLOTS; i++)
    {
      char buf[16];
      std::snprintf(buf, sizeof(buf), "State %u", i);
      if (ImGui::MenuItem(buf))
      {
        s_host_interface->RunLater([i]() { s_host_interface->LoadState(true, i); });
        ClearImGuiFocus();
      }
    }
    ImGui::EndMenu();
  }

  if (ImGui::BeginMenu("Save State", system_enabled))
  {
    for (u32 i = 1; i <= CommonHostInterface::GLOBAL_SAVE_STATE_SLOTS; i++)
    {
      TinyString buf;
      buf.Format("State %u", i);
      if (ImGui::MenuItem(buf))
      {
        s_host_interface->RunLater([i]() { s_host_interface->SaveState(true, i); });
        ClearImGuiFocus();
      }
    }
    ImGui::EndMenu();
  }

  ImGui::Separator();

  if (ImGui::BeginMenu("Cheats", system_enabled && !IsCheevosHardcoreModeActive()))
  {
    const bool has_cheat_file = System::HasCheatList();
    if (ImGui::BeginMenu("Enabled Cheats", has_cheat_file))
    {
      CheatList* cl = System::GetCheatList();
      for (u32 i = 0; i < cl->GetCodeCount(); i++)
      {
        const CheatCode& cc = cl->GetCode(i);
        if (ImGui::MenuItem(cc.description.c_str(), nullptr, cc.enabled, true))
          s_host_interface->SetCheatCodeState(i, !cc.enabled, g_settings.auto_load_cheats);
      }

      ImGui::EndMenu();
    }

    if (ImGui::BeginMenu("Apply Cheat", has_cheat_file))
    {
      CheatList* cl = System::GetCheatList();
      for (u32 i = 0; i < cl->GetCodeCount(); i++)
      {
        const CheatCode& cc = cl->GetCode(i);
        if (ImGui::MenuItem(cc.description.c_str()))
          s_host_interface->ApplyCheatCode(i);
      }

      ImGui::EndMenu();
    }

    ImGui::EndMenu();
  }

  ImGui::Separator();

  if (ImGui::MenuItem("Exit"))
    s_host_interface->RequestExit();
}

void DrawDebugSettingsMenu()
{
  bool settings_changed = false;

  if (ImGui::BeginMenu("CPU Execution Mode"))
  {
    const CPUExecutionMode current = s_settings_copy.cpu_execution_mode;
    for (u32 i = 0; i < static_cast<u32>(CPUExecutionMode::Count); i++)
    {
      if (ImGui::MenuItem(Settings::GetCPUExecutionModeDisplayName(static_cast<CPUExecutionMode>(i)), nullptr,
                          i == static_cast<u32>(current)))
      {
        s_settings_copy.cpu_execution_mode = static_cast<CPUExecutionMode>(i);
        settings_changed = true;
      }
    }

    ImGui::EndMenu();
  }

  if (ImGui::MenuItem("CPU Clock Control", nullptr, &s_settings_copy.cpu_overclock_enable))
  {
    settings_changed = true;
    s_settings_copy.UpdateOverclockActive();
  }

  if (ImGui::BeginMenu("CPU Clock Speed"))
  {
    static constexpr auto values = make_array(10u, 25u, 50u, 75u, 100u, 125u, 150u, 175u, 200u, 225u, 250u, 275u, 300u,
                                              350u, 400u, 450u, 500u, 600u, 700u, 800u);
    const u32 percent = s_settings_copy.GetCPUOverclockPercent();
    for (u32 value : values)
    {
      if (ImGui::MenuItem(TinyString::FromFormat("%u%%", value), nullptr, percent == value))
      {
        s_settings_copy.SetCPUOverclockPercent(value);
        s_settings_copy.UpdateOverclockActive();
        settings_changed = true;
      }
    }

    ImGui::EndMenu();
  }

  settings_changed |=
    ImGui::MenuItem("Recompiler Memory Exceptions", nullptr, &s_settings_copy.cpu_recompiler_memory_exceptions);
  if (ImGui::BeginMenu("Recompiler Fastmem"))
  {
    for (u32 i = 0; i < static_cast<u32>(CPUFastmemMode::Count); i++)
    {
      if (ImGui::MenuItem(Settings::GetCPUFastmemModeDisplayName(static_cast<CPUFastmemMode>(i)), nullptr,
                          s_settings_copy.cpu_fastmem_mode == static_cast<CPUFastmemMode>(i)))
      {
        s_settings_copy.cpu_fastmem_mode = static_cast<CPUFastmemMode>(i);
        settings_changed = true;
      }
    }

    ImGui::EndMenu();
  }

  settings_changed |= ImGui::MenuItem("Recompiler ICache", nullptr, &s_settings_copy.cpu_recompiler_icache);

  ImGui::Separator();

  if (ImGui::BeginMenu("Renderer"))
  {
    const GPURenderer current = s_settings_copy.gpu_renderer;
    for (u32 i = 0; i < static_cast<u32>(GPURenderer::Count); i++)
    {
      if (ImGui::MenuItem(Settings::GetRendererDisplayName(static_cast<GPURenderer>(i)), nullptr,
                          i == static_cast<u32>(current)))
      {
        s_settings_copy.gpu_renderer = static_cast<GPURenderer>(i);
        settings_changed = true;
      }
    }

    settings_changed |= ImGui::MenuItem("GPU on Thread", nullptr, &s_settings_copy.gpu_use_thread);

    ImGui::EndMenu();
  }

  if (ImGui::MenuItem("Toggle Fullscreen"))
    s_host_interface->RunLater([] { s_host_interface->SetFullscreen(!s_host_interface->IsFullscreen()); });

  if (ImGui::BeginMenu("Resize to Game", System::IsValid()))
  {
    static constexpr auto scales = make_array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    for (const u32 scale : scales)
    {
      if (ImGui::MenuItem(TinyString::FromFormat("%ux Scale", scale)))
        s_host_interface->RunLater(
          [scale]() { s_host_interface->RequestRenderWindowScale(static_cast<float>(scale)); });
    }

    ImGui::EndMenu();
  }

  settings_changed |= ImGui::MenuItem("VSync", nullptr, &s_settings_copy.video_sync_enabled);

  ImGui::Separator();

  if (ImGui::BeginMenu("Resolution Scale"))
  {
    const u32 current_internal_resolution = s_settings_copy.gpu_resolution_scale;
    for (u32 scale = 1; scale <= GPU::MAX_RESOLUTION_SCALE; scale++)
    {
      char buf[32];
      std::snprintf(buf, sizeof(buf), "%ux (%ux%u)", scale, scale * VRAM_WIDTH, scale * VRAM_HEIGHT);

      if (ImGui::MenuItem(buf, nullptr, current_internal_resolution == scale))
      {
        s_settings_copy.gpu_resolution_scale = scale;
        settings_changed = true;
      }
    }

    ImGui::EndMenu();
  }

  if (ImGui::BeginMenu("Multisampling"))
  {
    const u32 current_multisamples = s_settings_copy.gpu_multisamples;
    const bool current_ssaa = s_settings_copy.gpu_per_sample_shading;

    if (ImGui::MenuItem("None", nullptr, (current_multisamples == 1)))
    {
      s_settings_copy.gpu_multisamples = 1;
      s_settings_copy.gpu_per_sample_shading = false;
      settings_changed = true;
    }

    for (u32 i = 2; i <= 32; i *= 2)
    {
      char buf[32];
      std::snprintf(buf, sizeof(buf), "%ux MSAA", i);

      if (ImGui::MenuItem(buf, nullptr, (current_multisamples == i && !current_ssaa)))
      {
        s_settings_copy.gpu_multisamples = i;
        s_settings_copy.gpu_per_sample_shading = false;
        settings_changed = true;
      }
    }

    for (u32 i = 2; i <= 32; i *= 2)
    {
      char buf[32];
      std::snprintf(buf, sizeof(buf), "%ux SSAA", i);

      if (ImGui::MenuItem(buf, nullptr, (current_multisamples == i && current_ssaa)))
      {
        s_settings_copy.gpu_multisamples = i;
        s_settings_copy.gpu_per_sample_shading = true;
        settings_changed = true;
      }
    }

    ImGui::EndMenu();
  }

  if (ImGui::BeginMenu("PGXP"))
  {
    settings_changed |= ImGui::MenuItem("PGXP Enabled", nullptr, &s_settings_copy.gpu_pgxp_enable);
    settings_changed |=
      ImGui::MenuItem("PGXP Culling", nullptr, &s_settings_copy.gpu_pgxp_culling, s_settings_copy.gpu_pgxp_enable);
    settings_changed |= ImGui::MenuItem("PGXP Texture Correction", nullptr,
                                        &s_settings_copy.gpu_pgxp_texture_correction, s_settings_copy.gpu_pgxp_enable);
    settings_changed |= ImGui::MenuItem("PGXP Vertex Cache", nullptr, &s_settings_copy.gpu_pgxp_vertex_cache,
                                        s_settings_copy.gpu_pgxp_enable);
    settings_changed |=
      ImGui::MenuItem("PGXP CPU Instructions", nullptr, &s_settings_copy.gpu_pgxp_cpu, s_settings_copy.gpu_pgxp_enable);
    settings_changed |= ImGui::MenuItem("PGXP Preserve Projection Precision", nullptr,
                                        &s_settings_copy.gpu_pgxp_preserve_proj_fp, s_settings_copy.gpu_pgxp_enable);
    settings_changed |= ImGui::MenuItem("PGXP Depth Buffer", nullptr, &s_settings_copy.gpu_pgxp_depth_buffer,
                                        s_settings_copy.gpu_pgxp_enable);
    ImGui::EndMenu();
  }

  settings_changed |= ImGui::MenuItem("True (24-Bit) Color", nullptr, &s_settings_copy.gpu_true_color);
  settings_changed |= ImGui::MenuItem("Scaled Dithering", nullptr, &s_settings_copy.gpu_scaled_dithering);

  if (ImGui::BeginMenu("Texture Filtering"))
  {
    const GPUTextureFilter current = s_settings_copy.gpu_texture_filter;
    for (u32 i = 0; i < static_cast<u32>(GPUTextureFilter::Count); i++)
    {
      if (ImGui::MenuItem(Settings::GetTextureFilterDisplayName(static_cast<GPUTextureFilter>(i)), nullptr,
                          i == static_cast<u32>(current)))
      {
        s_settings_copy.gpu_texture_filter = static_cast<GPUTextureFilter>(i);
        settings_changed = true;
      }
    }

    ImGui::EndMenu();
  }

  ImGui::Separator();

  settings_changed |= ImGui::MenuItem("Disable Interlacing", nullptr, &s_settings_copy.gpu_disable_interlacing);
  settings_changed |= ImGui::MenuItem("Widescreen Hack", nullptr, &s_settings_copy.gpu_widescreen_hack);
  settings_changed |= ImGui::MenuItem("Force NTSC Timings", nullptr, &s_settings_copy.gpu_force_ntsc_timings);
  settings_changed |= ImGui::MenuItem("24-Bit Chroma Smoothing", nullptr, &s_settings_copy.gpu_24bit_chroma_smoothing);

  ImGui::Separator();

  settings_changed |= ImGui::MenuItem("Display Linear Filtering", nullptr, &s_settings_copy.display_linear_filtering);
  settings_changed |= ImGui::MenuItem("Display Integer Scaling", nullptr, &s_settings_copy.display_integer_scaling);

  if (ImGui::BeginMenu("Aspect Ratio"))
  {
    for (u32 i = 0; i < static_cast<u32>(DisplayAspectRatio::Count); i++)
    {
      if (ImGui::MenuItem(Settings::GetDisplayAspectRatioName(static_cast<DisplayAspectRatio>(i)), nullptr,
                          s_settings_copy.display_aspect_ratio == static_cast<DisplayAspectRatio>(i)))
      {
        s_settings_copy.display_aspect_ratio = static_cast<DisplayAspectRatio>(i);
        settings_changed = true;
      }
    }

    ImGui::EndMenu();
  }

  if (ImGui::BeginMenu("Crop Mode"))
  {
    for (u32 i = 0; i < static_cast<u32>(DisplayCropMode::Count); i++)
    {
      if (ImGui::MenuItem(Settings::GetDisplayCropModeDisplayName(static_cast<DisplayCropMode>(i)), nullptr,
                          s_settings_copy.display_crop_mode == static_cast<DisplayCropMode>(i)))
      {
        s_settings_copy.display_crop_mode = static_cast<DisplayCropMode>(i);
        settings_changed = true;
      }
    }

    ImGui::EndMenu();
  }

  if (ImGui::BeginMenu("Downsample Mode"))
  {
    for (u32 i = 0; i < static_cast<u32>(GPUDownsampleMode::Count); i++)
    {
      if (ImGui::MenuItem(Settings::GetDownsampleModeDisplayName(static_cast<GPUDownsampleMode>(i)), nullptr,
                          s_settings_copy.gpu_downsample_mode == static_cast<GPUDownsampleMode>(i)))
      {
        s_settings_copy.gpu_downsample_mode = static_cast<GPUDownsampleMode>(i);
        settings_changed = true;
      }
    }

    ImGui::EndMenu();
  }

  settings_changed |= ImGui::MenuItem("Force 4:3 For 24-bit", nullptr, &s_settings_copy.display_force_4_3_for_24bit);

  ImGui::Separator();

  if (ImGui::MenuItem("Dump Audio", nullptr, s_host_interface->IsDumpingAudio(), System::IsValid()))
  {
    if (!s_host_interface->IsDumpingAudio())
      s_host_interface->StartDumpingAudio();
    else
      s_host_interface->StopDumpingAudio();
  }

  if (ImGui::MenuItem("Save Screenshot"))
    s_host_interface->RunLater([]() { s_host_interface->SaveScreenshot(); });

  if (settings_changed)
    s_host_interface->RunLater(SaveAndApplySettings);
}

void DrawDebugDebugMenu()
{
  const bool system_valid = System::IsValid();
  Settings::DebugSettings& debug_settings = g_settings.debugging;
  bool settings_changed = false;

  if (ImGui::BeginMenu("Log Level"))
  {
    for (u32 i = LOGLEVEL_NONE; i < LOGLEVEL_COUNT; i++)
    {
      if (ImGui::MenuItem(Settings::GetLogLevelDisplayName(static_cast<LOGLEVEL>(i)), nullptr,
                          g_settings.log_level == static_cast<LOGLEVEL>(i)))
      {
        s_settings_copy.log_level = static_cast<LOGLEVEL>(i);
        settings_changed = true;
      }
    }

    ImGui::EndMenu();
  }

  settings_changed |= ImGui::MenuItem("Log To Console", nullptr, &s_settings_copy.log_to_console);
  settings_changed |= ImGui::MenuItem("Log To Debug", nullptr, &s_settings_copy.log_to_debug);
  settings_changed |= ImGui::MenuItem("Log To File", nullptr, &s_settings_copy.log_to_file);

  ImGui::Separator();

  settings_changed |= ImGui::MenuItem("Disable All Enhancements", nullptr, &s_settings_copy.disable_all_enhancements);
  settings_changed |= ImGui::MenuItem("Dump CPU to VRAM Copies", nullptr, &debug_settings.dump_cpu_to_vram_copies);
  settings_changed |= ImGui::MenuItem("Dump VRAM to CPU Copies", nullptr, &debug_settings.dump_vram_to_cpu_copies);

  if (ImGui::MenuItem("CPU Trace Logging", nullptr, CPU::IsTraceEnabled(), system_valid))
  {
    if (!CPU::IsTraceEnabled())
      CPU::StartTrace();
    else
      CPU::StopTrace();
  }

  ImGui::Separator();

  settings_changed |= ImGui::MenuItem("Show VRAM", nullptr, &debug_settings.show_vram);
  settings_changed |= ImGui::MenuItem("Show GPU State", nullptr, &debug_settings.show_gpu_state);
  settings_changed |= ImGui::MenuItem("Show CDROM State", nullptr, &debug_settings.show_cdrom_state);
  settings_changed |= ImGui::MenuItem("Show SPU State", nullptr, &debug_settings.show_spu_state);
  settings_changed |= ImGui::MenuItem("Show Timers State", nullptr, &debug_settings.show_timers_state);
  settings_changed |= ImGui::MenuItem("Show MDEC State", nullptr, &debug_settings.show_mdec_state);
  settings_changed |= ImGui::MenuItem("Show DMA State", nullptr, &debug_settings.show_dma_state);

  if (settings_changed)
  {
    // have to apply it to the copy too, otherwise it won't save
    Settings::DebugSettings& debug_settings_copy = s_settings_copy.debugging;
    debug_settings_copy.show_gpu_state = debug_settings.show_gpu_state;
    debug_settings_copy.show_vram = debug_settings.show_vram;
    debug_settings_copy.dump_cpu_to_vram_copies = debug_settings.dump_cpu_to_vram_copies;
    debug_settings_copy.dump_vram_to_cpu_copies = debug_settings.dump_vram_to_cpu_copies;
    debug_settings_copy.show_cdrom_state = debug_settings.show_cdrom_state;
    debug_settings_copy.show_spu_state = debug_settings.show_spu_state;
    debug_settings_copy.show_timers_state = debug_settings.show_timers_state;
    debug_settings_copy.show_mdec_state = debug_settings.show_mdec_state;
    debug_settings_copy.show_dma_state = debug_settings.show_dma_state;
    s_host_interface->RunLater(SaveAndApplySettings);
  }
}

#ifdef WITH_CHEEVOS

static void DrawAchievement(const Cheevos::Achievement& cheevo)
{
  static constexpr float alpha = 0.8f;

  TinyString id_str;
  id_str.Format("%u", cheevo.id);

  ImRect bb;
  bool visible, hovered;
  bool pressed =
    MenuButtonFrame(id_str, true, LAYOUT_MENU_BUTTON_HEIGHT, &visible, &hovered, &bb.Min, &bb.Max, 0, alpha);
  if (!visible)
    return;

  const ImVec2 image_size(LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT, LAYOUT_MENU_BUTTON_HEIGHT));
  const std::string& badge_path = cheevo.locked ? cheevo.locked_badge_path : cheevo.unlocked_badge_path;
  if (!badge_path.empty())
  {
    HostDisplayTexture* badge = GetCachedTexture(badge_path);
    if (badge)
    {
      ImGui::GetWindowDrawList()->AddImage(badge->GetHandle(), bb.Min, bb.Min + image_size, ImVec2(0.0f, 0.0f),
                                           ImVec2(1.0f, 1.0f), IM_COL32(255, 255, 255, 255));
    }
  }

  const float midpoint = bb.Min.y + g_large_font->FontSize + LayoutScale(4.0f);
  const float text_start_x = bb.Min.x + image_size.x + LayoutScale(15.0f);
  const ImRect title_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint));
  const ImRect summary_bb(ImVec2(text_start_x, midpoint), bb.Max);
  SmallString text;

  ImGui::PushFont(g_large_font);
  ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, cheevo.title.c_str(), cheevo.title.c_str() + cheevo.title.size(),
                           nullptr, ImVec2(0.0f, 0.0f), &title_bb);
  ImGui::PopFont();

  if (!cheevo.description.empty())
  {
    ImGui::PushFont(g_medium_font);
    ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, cheevo.description.c_str(),
                             cheevo.description.c_str() + cheevo.description.size(), nullptr, ImVec2(0.0f, 0.0f),
                             &summary_bb);
    ImGui::PopFont();
  }

#if 0
  // The API doesn't seem to send us this :(
  if (!cheevo.locked)
  {
    ImGui::PushFont(g_medium_font);

    const ImRect time_bb(ImVec2(text_start_x, bb.Min.y),
      ImVec2(bb.Max.x, bb.Min.y + g_medium_font->FontSize + LayoutScale(4.0f)));
    text.Format("Unlocked 21 Feb, 2019 @ 3:14am");
    ImGui::RenderTextClipped(time_bb.Min, time_bb.Max, text.GetCharArray(), text.GetCharArray() + text.GetLength(),
      nullptr, ImVec2(1.0f, 0.0f), &time_bb);
    ImGui::PopFont();
  }
#endif

  if (pressed)
  {
    // TODO: What should we do here?
    // Display information or something..
  }
}

void DrawAchievementWindow()
{
  static constexpr float alpha = 0.8f;
  static constexpr float heading_height_unscaled = 110.0f;

  ImGui::SetNextWindowBgAlpha(alpha);

  const ImVec4 background(0.13f, 0.13f, 0.13f, alpha);
  const ImVec2 display_size(ImGui::GetIO().DisplaySize);
  const float heading_height = LayoutScale(heading_height_unscaled);

  if (BeginFullscreenWindow(
        ImVec2(0.0f, 0.0f), ImVec2(display_size.x, heading_height), "achievements_heading", background, 0.0f, 0.0f,
        ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoScrollWithMouse))
  {
    ImRect bb;
    bool visible, hovered;
    bool pressed = MenuButtonFrame("achievements_heading", false, heading_height_unscaled, &visible, &hovered, &bb.Min,
                                   &bb.Max, 0, alpha);
    UNREFERENCED_VARIABLE(pressed);

    if (visible)
    {
      const float padding = LayoutScale(10.0f);
      const float spacing = LayoutScale(10.0f);
      const float image_height = LayoutScale(85.0f);

      const ImVec2 icon_min(bb.Min + ImVec2(padding, padding));
      const ImVec2 icon_max(icon_min + ImVec2(image_height, image_height));

      const std::string& icon_path = Cheevos::GetGameIcon();
      if (!icon_path.empty())
      {
        HostDisplayTexture* badge = GetCachedTexture(icon_path);
        if (badge)
        {
          ImGui::GetWindowDrawList()->AddImage(badge->GetHandle(), icon_min, icon_max, ImVec2(0.0f, 0.0f),
                                               ImVec2(1.0f, 1.0f), IM_COL32(255, 255, 255, 255));
        }
      }

      float left = bb.Min.x + padding + image_height + spacing;
      float right = bb.Max.x - padding;
      float top = bb.Min.y + padding;
      ImDrawList* dl = ImGui::GetWindowDrawList();
      SmallString text;
      ImVec2 text_size;

      const u32 unlocked_count = Cheevos::GetUnlockedAchiementCount();
      const u32 achievement_count = Cheevos::GetAchievementCount();
      const u32 current_points = Cheevos::GetCurrentPointsForGame();
      const u32 total_points = Cheevos::GetMaximumPointsForGame();

      if (FloatingButton(ICON_FA_WINDOW_CLOSE, 10.0f, 10.0f, -1.0f, -1.0f, 1.0f, 0.0f, true, g_large_font) ||
          WantsToCloseMenu())
      {
        ReturnToMainWindow();
      }

      const ImRect title_bb(ImVec2(left, top), ImVec2(right, top + g_large_font->FontSize));
      text.Assign(Cheevos::GetGameTitle());

      if (Cheevos::IsChallengeModeActive())
        text.AppendString(" (Hardcore Mode)");

      top += g_large_font->FontSize + spacing;

      ImGui::PushFont(g_large_font);
      ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, text.GetCharArray(), text.GetCharArray() + text.GetLength(),
                               nullptr, ImVec2(0.0f, 0.0f), &title_bb);
      ImGui::PopFont();

      const ImRect summary_bb(ImVec2(left, top), ImVec2(right, top + g_medium_font->FontSize));
      if (unlocked_count == achievement_count)
      {
        text.Format("You have unlocked all achievements and earned %u points!", total_points);
      }
      else
      {
        text.Format("You have unlocked %u of %u achievements, earning %u of %u possible points.", unlocked_count,
                    achievement_count, current_points, total_points);
      }

      top += g_medium_font->FontSize + spacing;

      ImGui::PushFont(g_medium_font);
      ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, text.GetCharArray(),
                               text.GetCharArray() + text.GetLength(), nullptr, ImVec2(0.0f, 0.0f), &summary_bb);
      ImGui::PopFont();

      const float progress_height = LayoutScale(20.0f);
      const ImRect progress_bb(ImVec2(left, top), ImVec2(right, top + progress_height));
      const float fraction = static_cast<float>(unlocked_count) / static_cast<float>(achievement_count);
      dl->AddRectFilled(progress_bb.Min, progress_bb.Max, ImGui::GetColorU32(ImGuiFullscreen::UIPrimaryDarkColor()));
      dl->AddRectFilled(progress_bb.Min,
                        ImVec2(progress_bb.Min.x + fraction * progress_bb.GetWidth(), progress_bb.Max.y),
                        ImGui::GetColorU32(ImGuiFullscreen::UISecondaryColor()));

      text.Format("%d%%", static_cast<int>(std::round(fraction * 100.0f)));
      text_size = ImGui::CalcTextSize(text);
      const ImVec2 text_pos(progress_bb.Min.x + ((progress_bb.Max.x - progress_bb.Min.x) / 2.0f) - (text_size.x / 2.0f),
                            progress_bb.Min.y + ((progress_bb.Max.y - progress_bb.Min.y) / 2.0f) -
                              (text_size.y / 2.0f));
      dl->AddText(g_medium_font, g_medium_font->FontSize, text_pos,
                  ImGui::GetColorU32(ImGuiFullscreen::UIPrimaryTextColor()), text.GetCharArray(),
                  text.GetCharArray() + text.GetLength());
      top += progress_height + spacing;
    }
  }
  EndFullscreenWindow();

  ImGui::SetNextWindowBgAlpha(alpha);

  if (BeginFullscreenWindow(ImVec2(0.0f, heading_height), ImVec2(display_size.x, display_size.y - heading_height),
                            "achievements", background, 0.0f, 0.0f, 0))
  {
    BeginMenuButtons();

    static bool unlocked_achievements_collapsed = false;

    unlocked_achievements_collapsed ^= MenuHeadingButton(
      "Unlocked Achievements", unlocked_achievements_collapsed ? ICON_FA_CHEVRON_DOWN : ICON_FA_CHEVRON_UP);
    if (!unlocked_achievements_collapsed)
    {
      Cheevos::EnumerateAchievements([](const Cheevos::Achievement& cheevo) -> bool {
        if (!cheevo.locked)
          DrawAchievement(cheevo);

        return true;
      });
    }

    if (Cheevos::GetUnlockedAchiementCount() != Cheevos::GetAchievementCount())
    {
      static bool locked_achievements_collapsed = false;
      locked_achievements_collapsed ^= MenuHeadingButton(
        "Locked Achievements", locked_achievements_collapsed ? ICON_FA_CHEVRON_DOWN : ICON_FA_CHEVRON_UP);
      if (!locked_achievements_collapsed)
      {
        Cheevos::EnumerateAchievements([](const Cheevos::Achievement& cheevo) -> bool {
          if (cheevo.locked)
            DrawAchievement(cheevo);

          return true;
        });
      }
    }

    EndMenuButtons();
  }
  EndFullscreenWindow();
}

static void DrawLeaderboardListEntry(const Cheevos::Leaderboard& lboard)
{
  static constexpr float alpha = 0.8f;

  TinyString id_str;
  id_str.Format("%u", lboard.id);

  ImRect bb;
  bool visible, hovered;
  bool pressed =
    MenuButtonFrame(id_str, true, LAYOUT_MENU_BUTTON_HEIGHT, &visible, &hovered, &bb.Min, &bb.Max, 0, alpha);
  if (!visible)
    return;

  const float midpoint = bb.Min.y + g_large_font->FontSize + LayoutScale(4.0f);
  const float text_start_x = bb.Min.x + LayoutScale(15.0f);
  const ImRect title_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint));
  const ImRect summary_bb(ImVec2(text_start_x, midpoint), bb.Max);

  ImGui::PushFont(g_large_font);
  ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, lboard.title.c_str(), lboard.title.c_str() + lboard.title.size(),
                           nullptr, ImVec2(0.0f, 0.0f), &title_bb);
  ImGui::PopFont();

  if (!lboard.description.empty())
  {
    ImGui::PushFont(g_medium_font);
    ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, lboard.description.c_str(),
                             lboard.description.c_str() + lboard.description.size(), nullptr, ImVec2(0.0f, 0.0f),
                             &summary_bb);
    ImGui::PopFont();
  }

  if (pressed)
  {
    s_open_leaderboard_id = lboard.id;
  }
}

static void DrawLeaderboardEntry(const Cheevos::LeaderboardEntry& lbEntry, float rank_column_width,
                                 float name_column_width, float column_spacing)
{
  static constexpr float alpha = 0.8f;

  ImRect bb;
  bool visible, hovered;
  bool pressed = MenuButtonFrame(lbEntry.user.c_str(), true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, &visible, &hovered,
                                 &bb.Min, &bb.Max, 0, alpha);
  if (!visible)
    return;

  const float spacing = LayoutScale(10.0f);
  const float midpoint = bb.Min.y + g_large_font->FontSize + LayoutScale(4.0f);
  float text_start_x = bb.Min.x + LayoutScale(15.0f);
  SmallString text;

  text.Format("%u", lbEntry.rank);

  ImGui::PushFont(g_large_font);
  if (lbEntry.is_self)
  {
    ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(255, 242, 0, 255));
  }

  const ImRect rank_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint));
  ImGui::RenderTextClipped(rank_bb.Min, rank_bb.Max, text.GetCharArray(), text.GetCharArray() + text.GetLength(),
                           nullptr, ImVec2(0.0f, 0.0f), &rank_bb);
  text_start_x += rank_column_width + column_spacing;

  const ImRect user_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint));
  ImGui::RenderTextClipped(user_bb.Min, user_bb.Max, lbEntry.user.c_str(), lbEntry.user.c_str() + lbEntry.user.size(),
                           nullptr, ImVec2(0.0f, 0.0f), &user_bb);
  text_start_x += name_column_width + column_spacing;

  const ImRect score_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint));
  ImGui::RenderTextClipped(score_bb.Min, score_bb.Max, lbEntry.formatted_score.c_str(),
                           lbEntry.formatted_score.c_str() + lbEntry.formatted_score.size(), nullptr,
                           ImVec2(0.0f, 0.0f), &score_bb);

  if (lbEntry.is_self)
  {
    ImGui::PopStyleColor();
  }

  ImGui::PopFont();

  // This API DOES list the submission date/time, but is it relevant?
#if 0
  if (!cheevo.locked)
  {
    ImGui::PushFont(g_medium_font);

    const ImRect time_bb(ImVec2(text_start_x, bb.Min.y),
      ImVec2(bb.Max.x, bb.Min.y + g_medium_font->FontSize + LayoutScale(4.0f)));
    text.Format("Unlocked 21 Feb, 2019 @ 3:14am");
    ImGui::RenderTextClipped(time_bb.Min, time_bb.Max, text.GetCharArray(), text.GetCharArray() + text.GetLength(),
      nullptr, ImVec2(1.0f, 0.0f), &time_bb);
    ImGui::PopFont();
  }
#endif

  if (pressed)
  {
    // Anything?
  }
}

void DrawLeaderboardsWindow()
{
  static constexpr float alpha = 0.8f;
  static constexpr float heading_height_unscaled = 110.0f;

  ImGui::SetNextWindowBgAlpha(alpha);

  const bool is_leaderboard_open = s_open_leaderboard_id.has_value();
  bool close_leaderboard_on_exit = false;

  const ImVec4 background(0.13f, 0.13f, 0.13f, alpha);
  const ImVec2 display_size(ImGui::GetIO().DisplaySize);
  const float padding = LayoutScale(10.0f);
  const float spacing = LayoutScale(10.0f);
  float heading_height = LayoutScale(heading_height_unscaled);
  if (is_leaderboard_open)
  {
    // Add space for a legend - spacing + 1 line of text + spacing + line
    heading_height += spacing + LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY) + spacing;
  }

  const float rank_column_width =
    g_large_font->CalcTextSizeA(g_large_font->FontSize, std::numeric_limits<float>::max(), -1.0f, "99999").x;
  const float name_column_width =
    g_large_font
      ->CalcTextSizeA(g_large_font->FontSize, std::numeric_limits<float>::max(), -1.0f, "WWWWWWWWWWWWWWWWWWWW")
      .x;
  const float column_spacing = spacing * 2.0f;

  if (BeginFullscreenWindow(
        ImVec2(0.0f, 0.0f), ImVec2(display_size.x, heading_height), "leaderboards_heading", background, 0.0f, 0.0f,
        ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoScrollWithMouse))
  {
    ImRect bb;
    bool visible, hovered;
    bool pressed = MenuButtonFrame("leaderboards_heading", false, heading_height_unscaled, &visible, &hovered, &bb.Min,
                                   &bb.Max, 0, alpha);
    UNREFERENCED_VARIABLE(pressed);

    if (visible)
    {
      const float image_height = LayoutScale(85.0f);

      const ImVec2 icon_min(bb.Min + ImVec2(padding, padding));
      const ImVec2 icon_max(icon_min + ImVec2(image_height, image_height));

      const std::string& icon_path = Cheevos::GetGameIcon();
      if (!icon_path.empty())
      {
        HostDisplayTexture* badge = GetCachedTexture(icon_path);
        if (badge)
        {
          ImGui::GetWindowDrawList()->AddImage(badge->GetHandle(), icon_min, icon_max, ImVec2(0.0f, 0.0f),
                                               ImVec2(1.0f, 1.0f), IM_COL32(255, 255, 255, 255));
        }
      }

      float left = bb.Min.x + padding + image_height + spacing;
      float right = bb.Max.x - padding;
      float top = bb.Min.y + padding;
      ImDrawList* dl = ImGui::GetWindowDrawList();
      SmallString text;
      ImVec2 text_size;

      const u32 leaderboard_count = Cheevos::GetLeaderboardCount();

      if (!is_leaderboard_open)
      {
        if (FloatingButton(ICON_FA_WINDOW_CLOSE, 10.0f, 10.0f, -1.0f, -1.0f, 1.0f, 0.0f, true, g_large_font) ||
            WantsToCloseMenu())
        {
          ReturnToMainWindow();
        }
      }
      else
      {
        if (FloatingButton(ICON_FA_CARET_SQUARE_LEFT, 10.0f, 10.0f, -1.0f, -1.0f, 1.0f, 0.0f, true, g_large_font) ||
            WantsToCloseMenu())
        {
          close_leaderboard_on_exit = true;
        }
      }

      const ImRect title_bb(ImVec2(left, top), ImVec2(right, top + g_large_font->FontSize));
      text.Assign(Cheevos::GetGameTitle());

      top += g_large_font->FontSize + spacing;

      ImGui::PushFont(g_large_font);
      ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, text.GetCharArray(), text.GetCharArray() + text.GetLength(),
                               nullptr, ImVec2(0.0f, 0.0f), &title_bb);
      ImGui::PopFont();

      if (s_open_leaderboard_id)
      {
        const Cheevos::Leaderboard* lboard = Cheevos::GetLeaderboardByID(*s_open_leaderboard_id);
        if (lboard != nullptr)
        {
          const ImRect subtitle_bb(ImVec2(left, top), ImVec2(right, top + g_large_font->FontSize));
          text.Assign(lboard->title);

          top += g_large_font->FontSize + spacing;

          ImGui::PushFont(g_large_font);
          ImGui::RenderTextClipped(subtitle_bb.Min, subtitle_bb.Max, text.GetCharArray(),
                                   text.GetCharArray() + text.GetLength(), nullptr, ImVec2(0.0f, 0.0f), &subtitle_bb);
          ImGui::PopFont();

          text.Assign(lboard->description);
        }
        else
        {
          text.Clear();
        }
      }
      else
      {
        text.Format("This game has %u leaderboards.", leaderboard_count);
      }

      const ImRect summary_bb(ImVec2(left, top), ImVec2(right, top + g_medium_font->FontSize));
      top += g_medium_font->FontSize + spacing;

      ImGui::PushFont(g_medium_font);
      ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, text.GetCharArray(),
                               text.GetCharArray() + text.GetLength(), nullptr, ImVec2(0.0f, 0.0f), &summary_bb);

      if (!IsCheevosHardcoreModeActive())
      {
        const ImRect hardcore_warning_bb(ImVec2(left, top), ImVec2(right, top + g_medium_font->FontSize));
        top += g_medium_font->FontSize + spacing;

        ImGui::RenderTextClipped(
          hardcore_warning_bb.Min, hardcore_warning_bb.Max,
          "Submitting scores is DISABLED because Hardcore Mode is off. Leaderboards are read-only.", nullptr, nullptr,
          ImVec2(0.0f, 0.0f), &hardcore_warning_bb);
      }

      ImGui::PopFont();
    }

    if (is_leaderboard_open)
    {
      pressed = MenuButtonFrame("legend", false, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, &visible, &hovered, &bb.Min,
                                &bb.Max, 0, alpha);

      UNREFERENCED_VARIABLE(pressed);

      if (visible)
      {
        const Cheevos::Leaderboard* lboard = Cheevos::GetLeaderboardByID(*s_open_leaderboard_id);

        const float midpoint = bb.Min.y + g_large_font->FontSize + LayoutScale(4.0f);
        float text_start_x = bb.Min.x + LayoutScale(15.0f) + padding;

        ImGui::PushFont(g_large_font);

        const ImRect rank_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint));
        ImGui::RenderTextClipped(rank_bb.Min, rank_bb.Max, "Rank", nullptr, nullptr, ImVec2(0.0f, 0.0f), &rank_bb);
        text_start_x += rank_column_width + column_spacing;

        const ImRect user_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint));
        ImGui::RenderTextClipped(user_bb.Min, user_bb.Max, "Name", nullptr, nullptr, ImVec2(0.0f, 0.0f), &user_bb);
        text_start_x += name_column_width + column_spacing;

        const ImRect score_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint));
        ImGui::RenderTextClipped(score_bb.Min, score_bb.Max,
                                 lboard != nullptr && Cheevos::IsLeaderboardTimeType(*lboard) ? "Time" : "Score",
                                 nullptr, nullptr, ImVec2(0.0f, 0.0f), &score_bb);

        ImGui::PopFont();

        const float line_thickness = LayoutScale(1.0f);
        const float line_padding = LayoutScale(5.0f);
        const ImVec2 line_start(bb.Min.x, bb.Min.y + g_large_font->FontSize + line_padding);
        const ImVec2 line_end(bb.Max.x, line_start.y);
        ImGui::GetWindowDrawList()->AddLine(line_start, line_end, ImGui::GetColorU32(ImGuiCol_TextDisabled),
                                            line_thickness);
      }
    }
  }
  EndFullscreenWindow();

  ImGui::SetNextWindowBgAlpha(alpha);

  if (!is_leaderboard_open)
  {
    if (BeginFullscreenWindow(ImVec2(0.0f, heading_height), ImVec2(display_size.x, display_size.y - heading_height),
                              "leaderboards", background, 0.0f, 0.0f, 0))
    {
      BeginMenuButtons();

      Cheevos::EnumerateLeaderboards([](const Cheevos::Leaderboard& lboard) -> bool {
        DrawLeaderboardListEntry(lboard);

        return true;
      });

      EndMenuButtons();
    }
    EndFullscreenWindow();
  }
  else
  {
    if (BeginFullscreenWindow(ImVec2(0.0f, heading_height), ImVec2(display_size.x, display_size.y - heading_height),
                              "leaderboard", background, 0.0f, 0.0f, 0))
    {
      BeginMenuButtons();

      const auto result = Cheevos::TryEnumerateLeaderboardEntries(
        *s_open_leaderboard_id,
        [rank_column_width, name_column_width, column_spacing](const Cheevos::LeaderboardEntry& lbEntry) -> bool {
          DrawLeaderboardEntry(lbEntry, rank_column_width, name_column_width, column_spacing);
          return true;
        });

      if (!result.has_value())
      {
        ImGui::PushFont(g_large_font);

        const ImVec2 pos_min(0.0f, heading_height);
        const ImVec2 pos_max(display_size.x, display_size.y);
        ImGui::RenderTextClipped(pos_min, pos_max, "Downloading leaderboard data, please wait...", nullptr, nullptr,
                                 ImVec2(0.5f, 0.5f));

        ImGui::PopFont();
      }

      EndMenuButtons();
    }
    EndFullscreenWindow();
  }

  if (close_leaderboard_on_exit)
    s_open_leaderboard_id.reset();
}

#else

void DrawAchievementWindow() {}
void DrawLeaderboardsWindow() {}

#endif

bool SetControllerNavInput(FrontendCommon::ControllerNavigationButton button, bool value)
{
  s_nav_input_values[static_cast<u32>(button)] = value;
  if (!HasActiveWindow())
    return false;

  // This is a bit hacky..
  ImGuiIO& io = ImGui::GetIO();

#define MAP_KEY(nbutton, imkey)                                                                                        \
  if (button == nbutton)                                                                                               \
  {                                                                                                                    \
    io.KeysDown[io.KeyMap[imkey]] = value;                                                                             \
  }

  MAP_KEY(FrontendCommon::ControllerNavigationButton::LeftShoulder, ImGuiKey_PageUp);
  MAP_KEY(FrontendCommon::ControllerNavigationButton::RightShoulder, ImGuiKey_PageDown);

#undef MAP_KEY

  return true;
}

void SetImGuiNavInputs()
{
  if (!HasActiveWindow())
    return;

  ImGuiIO& io = ImGui::GetIO();

#define MAP_BUTTON(button, imbutton) io.NavInputs[imbutton] = s_nav_input_values[static_cast<u32>(button)] ? 1.0f : 0.0f

  MAP_BUTTON(FrontendCommon::ControllerNavigationButton::Activate, ImGuiNavInput_Activate);
  MAP_BUTTON(FrontendCommon::ControllerNavigationButton::Cancel, ImGuiNavInput_Cancel);
  MAP_BUTTON(FrontendCommon::ControllerNavigationButton::DPadLeft, ImGuiNavInput_DpadLeft);
  MAP_BUTTON(FrontendCommon::ControllerNavigationButton::DPadRight, ImGuiNavInput_DpadRight);
  MAP_BUTTON(FrontendCommon::ControllerNavigationButton::DPadUp, ImGuiNavInput_DpadUp);
  MAP_BUTTON(FrontendCommon::ControllerNavigationButton::DPadDown, ImGuiNavInput_DpadDown);

#undef MAP_BUTTON
}

} // namespace FullscreenUI