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

#include "achievements_private.h"
#include "fullscreenui_private.h"
#include "host.h"
#include "system.h"
#include "video_thread.h"

#include "util/gpu_device.h"
#include "util/gpu_texture.h"
#include "util/http_cache.h"
#include "util/imgui_manager.h"
#include "util/translation.h"

#include "common/assert.h"
#include "common/easing.h"
#include "common/log.h"
#include "common/string_util.h"
#include "common/time_helpers.h"
#include "common/timer.h"

#include <IconsEmoji.h>
#include <IconsPromptFont.h>
#include <rc_client.h>

#include <bitset>

LOG_CHANNEL(FullscreenUI);

#ifndef __ANDROID__

namespace FullscreenUI {

static constexpr const char* ACHEIVEMENT_DETAILS_URL_TEMPLATE = "https://retroachievements.org/achievement/{}";

static constexpr float WINDOW_ALPHA = 0.9f;
static constexpr float WINDOW_HEADING_ALPHA = 0.95f;
static constexpr float PROGRESS_BAR_ALPHA = 0.5f;

static constexpr u32 LEADERBOARD_NEARBY_ENTRIES_TO_FETCH = 20;
static constexpr u32 LEADERBOARD_ALL_FETCH_SIZE = 50;

// How long the last progress update is shown in the pause menu.
static constexpr float PAUSE_MENU_PROGRESS_DISPLAY_TIME = 60.0f;

// Notification animation times.
static constexpr float NOTIFICATION_APPEAR_ANIMATION_TIME = 0.2f;
static constexpr float NOTIFICATION_DISAPPEAR_ANIMATION_TIME = 0.5f;

static constexpr float CHALLENGE_INDICATOR_FADE_IN_TIME = 0.1f;
static constexpr float CHALLENGE_INDICATOR_FADE_OUT_TIME = 0.3f;

namespace {

struct Notification
{
  std::string key;
  std::string title;
  std::string text;
  std::string note;
  std::string image_url;
  u64 start_time;
  u64 move_time;
  float duration;
  float target_y;
  float last_y;
  u16 min_width;
  AchievementNotificationNoteType note_type;
  bool small_font;
};

struct PauseMenuAchievementInfo
{
  std::string title;
  std::string description;
  std::string badge_url;
  u32 achievement_id;
  float measured_percent;
};

struct PauseMenuAchievementInfoWithPoints : PauseMenuAchievementInfo
{
  u32 points;
};

struct PauseMenuMeasuredAchievementInfo : PauseMenuAchievementInfo
{
  std::string measured_progress;
};

struct PauseMenuTimedMeasuredAchievementInfo : PauseMenuMeasuredAchievementInfo
{
  // can't use imgui deltatime here because this is only updated when paused
  Timer::Value show_time;
};

struct PauseMenuLeaderboardInfo
{
  std::string title;
  std::string description;
  std::string tracker_value;
  u32 leaderboard_id;
  u32 format;
};

} // namespace

static float EffectiveNotificationScale(s16 scale);
static void DrawNotifications(NotificationLayout& layout);
static void DrawIndicators(NotificationLayout& layout);
static void UpdateAchievementOverlaysRunIdle();

static void AddSubsetInfo(const rc_client_subset_t* subset);
static bool IsCoreSubsetOpen();
static void SetCurrentSubsetID(u32 subset_id);
static void DrawSubsetSelector();
template<typename T>
static void CollectSubsetsFromList(const T* list, bool include_achievements, bool include_leaderboards);
template<typename T>
static bool IsBucketVisibleInCurrentSubset(const T& bucket);
static void SortLockedAchievements();

template<typename T>
static void CachePauseMenuAchievementInfo(const rc_client_achievement_t* achievement, std::optional<T>& value);

static void DrawAchievement(const rc_client_achievement_t* cheevo, const ImVec2& prefetch_range);
static void OpenAchievementDetails(u32 achievement_id);
static void SetAchievementPinned(u32 achievement_id, bool pinned);

static void LeaderboardFetchNearbyCallback(int result, const char* error_message,
                                           rc_client_leaderboard_entry_list_t* list, rc_client_t* client,
                                           void* callback_userdata);
static void LeaderboardFetchAllCallback(int result, const char* error_message, rc_client_leaderboard_entry_list_t* list,
                                        rc_client_t* client, void* callback_userdata);

static bool OpenLeaderboardById(u32 leaderboard_id);
static void FetchNextLeaderboardEntries();
static void CloseLeaderboard();
static void DrawLeaderboardListEntry(const rc_client_leaderboard_t* lboard);
static bool DrawLeaderboardEntry(const rc_client_leaderboard_entry_t& entry, u32 index, bool is_self,
                                 float rank_column_width, float name_column_width, float time_column_width,
                                 float column_spacing, std::time_t current_time, const std::tm& current_tm);
static void DrawLeaderboardLoadingIndicator(float pos_y, float avail_height, bool short_text);
static SmallString FormatRelativeTimestamp(std::time_t timestamp, std::time_t current_time, const std::tm& current_tm);

namespace {

struct SubsetInfo
{
  std::string full_name;
  std::string short_name;
  std::string badge_path;
  u32 subset_id;
  u32 num_leaderboards;
  rc_client_user_game_summary_t summary;
};

struct AchievementsLocals
{
  std::vector<Notification> notifications;

  // Shared by both achievements and leaderboards
  std::vector<SubsetInfo> subset_info_list;
  const SubsetInfo* open_subset = nullptr;

  std::optional<PauseMenuAchievementInfoWithPoints> most_recent_unlock;
  std::optional<PauseMenuMeasuredAchievementInfo> achievement_nearest_completion;
  std::optional<PauseMenuTimedMeasuredAchievementInfo> most_recent_progress_update;
  std::vector<PauseMenuLeaderboardInfo> active_leaderboards;

  rc_client_achievement_list_t* achievement_list = nullptr;
  std::bitset<NUM_RC_CLIENT_ACHIEVEMENT_BUCKETS> achievement_buckets_collapsed = {};
  u32 scroll_to_achievement_id = 0;

  rc_client_leaderboard_list_t* leaderboard_list = nullptr;
  const rc_client_leaderboard_t* open_leaderboard = nullptr;
  rc_client_async_handle_t* leaderboard_fetch_handle = nullptr;
  std::vector<rc_client_leaderboard_entry_list_t*> leaderboard_entry_lists;
  rc_client_leaderboard_entry_list_t* leaderboard_nearby_entries;
  bool is_showing_all_leaderboard_entries = false;
  bool has_fetched_all_leaderboard_entries = false;

  u32 last_open_subset_id = 0;
};

} // namespace

ALIGN_TO_CACHE_LINE static AchievementsLocals s_achievements_locals;

} // namespace FullscreenUI

void FullscreenUI::ClearAchievementsState()
{
  // NOTE: can be called on the CPU thread. don't mess with any GPU thread state
  // will already be held if we're clearing as a result of achievements shutting down

  const auto lock = Achievements::GetLock();

  CloseLeaderboard();

  s_achievements_locals.notifications = {};

  s_achievements_locals.leaderboard_entry_lists = {};
  if (s_achievements_locals.leaderboard_list)
  {
    rc_client_destroy_leaderboard_list(s_achievements_locals.leaderboard_list);
    s_achievements_locals.leaderboard_list = nullptr;
  }

  if (s_achievements_locals.achievement_list)
  {
    rc_client_destroy_achievement_list(s_achievements_locals.achievement_list);
    s_achievements_locals.achievement_list = nullptr;
  }
  s_achievements_locals.scroll_to_achievement_id = 0;

  s_achievements_locals.open_subset = nullptr;
  s_achievements_locals.subset_info_list.clear();
  s_achievements_locals.last_open_subset_id = 0;

  s_achievements_locals.most_recent_unlock.reset();
  s_achievements_locals.achievement_nearest_completion.reset();
  s_achievements_locals.most_recent_progress_update.reset();

  UpdateAchievementOverlaysRunIdle();
}

void FullscreenUI::DrawAchievementsOverlays()
{
  if (!Achievements::IsActive())
    return;

  const auto lock = Achievements::GetLock();

  NotificationLayout layout(g_gpu_settings.achievements_notification_location);
  DrawNotifications(layout);

  if (Achievements::HasActiveGame() && GetCurrentMainWindow() == MainWindowType::None)
  {
    // need to group them together if they're in the same location
    if (g_gpu_settings.achievements_indicator_location != layout.GetLocation())
      layout = NotificationLayout(g_gpu_settings.achievements_indicator_location);

    DrawIndicators(layout);
  }
}

void FullscreenUI::AddAchievementNotification(std::string key, float duration, std::string image_url, std::string title,
                                              std::string text, std::string note,
                                              AchievementNotificationNoteType note_type, u16 min_width, bool small_font)
{
  const bool prev_had_notifications = !s_achievements_locals.notifications.empty();
  const Timer::Value current_time = Timer::GetCurrentValue();

  if (!key.empty())
  {
    for (auto it = s_achievements_locals.notifications.begin(); it != s_achievements_locals.notifications.end(); ++it)
    {
      if (it->key == key)
      {
        it->duration = duration;
        it->title = std::move(title);
        it->text = std::move(text);
        it->note = std::move(note);
        it->image_url = std::move(image_url);
        it->min_width = min_width;
        it->note_type = note_type;
        it->small_font = small_font;

        // Don't fade it in again
        const float time_passed = static_cast<float>(Timer::ConvertValueToSeconds(current_time - it->start_time));
        it->start_time =
          current_time - Timer::ConvertSecondsToValue(std::min(time_passed, NOTIFICATION_APPEAR_ANIMATION_TIME));
        return;
      }
    }
  }

  Notification notif;
  notif.key = std::move(key);
  notif.duration = duration;
  notif.title = std::move(title);
  notif.text = std::move(text);
  notif.note = std::move(note);
  notif.image_url = std::move(image_url);
  notif.start_time = current_time;
  notif.move_time = current_time;
  notif.target_y = -1.0f;
  notif.last_y = -1.0f;
  notif.min_width = min_width;
  notif.note_type = note_type;
  notif.small_font = small_font;
  s_achievements_locals.notifications.push_back(std::move(notif));

  if (!prev_had_notifications)
    UpdateAchievementOverlaysRunIdle();
}

float FullscreenUI::EffectiveNotificationScale(s16 scale)
{
  // <0 = osd scale, 0 = layout scale, >0 = custom scale
  if (scale < 0)
    return ImGuiManager::GetGlobalScale();

  if (scale == Settings::ACHIEVEMENT_NOTIFICATION_SCALE_AUTO)
    return UIStyle.LayoutScale;

  // apply window scale as well so it's independent of displays
  const float window_scale = g_gpu_device->HasMainSwapChain() ? g_gpu_device->GetMainSwapChain()->GetScale() : 1.0f;
  return static_cast<float>(scale) * window_scale * 0.01f;
}

void FullscreenUI::DrawNotifications(NotificationLayout& layout)
{
  if (s_achievements_locals.notifications.empty())
    return;

  static constexpr float MOVE_DURATION = 0.5f;
  const Timer::Value current_time = Timer::GetCurrentValue();

  const float scale = EffectiveNotificationScale(g_gpu_settings.achievements_notification_scale);
  const float normal_horizontal_padding = ImCeil(20.0f * scale);
  const float normal_vertical_padding = ImCeil(15.0f * scale);
  const float small_horizontal_padding = ImCeil(10.0f * scale);
  const float small_vertical_padding = small_horizontal_padding;
  const float horizontal_spacing = ImCeil(10.0f * scale);
  const float larger_horizontal_spacing = ImCeil(18.0f * scale);
  const float vertical_spacing = ImCeil(4.0f * scale);
  const float normal_badge_size = ImCeil(48.0f * scale);
  const float small_badge_size = ImCeil(32.0f * scale);
  const float min_width = ImCeil(200.0f * scale);
  const float max_width = ImCeil(600.0f * scale);
  const float max_text_width = max_width - normal_badge_size - (normal_horizontal_padding * 2.0f) - horizontal_spacing;
  const float normal_min_height = (normal_vertical_padding * 2.0f) + normal_badge_size;
  const float small_min_height = (small_vertical_padding * 2.0f) + small_badge_size;
  const float note_icon_padding = ImCeil(30.0f * scale);
  ImFont*& font = UIStyle.Font;
  const float normal_title_font_size = ImCeil(LAYOUT_LARGE_FONT_SIZE * scale);
  const float small_title_font_size = ImCeil(LAYOUT_MEDIUM_FONT_SIZE * scale);
  static constexpr const float& title_font_weight = UIStyle.BoldFontWeight;
  const float normal_text_font_size = ImCeil(LAYOUT_MEDIUM_FONT_SIZE * scale);
  const float small_text_font_size = ImCeil(LAYOUT_MEDIUM_SMALL_FONT_SIZE * scale);
  static constexpr const float& text_font_weight = UIStyle.NormalFontWeight;
  const float note_text_size = ImCeil(LAYOUT_MEDIUM_FONT_SIZE * scale);
  static constexpr const float& note_text_weight = UIStyle.BoldFontWeight;
  const float note_icon_size = ImCeil(LAYOUT_LARGE_FONT_SIZE * scale);
  const ImVec4 left_background_color = DarkerColor(UIStyle.ToastBackgroundColor, 1.3f);
  const ImVec4 right_background_color = DarkerColor(UIStyle.ToastBackgroundColor, 0.8f);
  const bool blur_background = g_gpu_settings.display_blur_message_backgrounds && !FullscreenUI::HasActiveWindow() &&
                               FullscreenUI::CanBlurBackground();
  ImDrawList* const dl = ImGui::GetForegroundDrawList();

  for (auto iter = s_achievements_locals.notifications.begin(); iter != s_achievements_locals.notifications.end();)
  {
    Notification& notif = *iter;
    const float time_passed = static_cast<float>(Timer::ConvertValueToSeconds(current_time - notif.start_time));
    if (time_passed >= notif.duration)
    {
      iter = s_achievements_locals.notifications.erase(iter);
      continue;
    }

    // place note to the right of the title
    GPUTexture* note_image = nullptr;
    float note_font_size, note_font_weight, note_offset_y, note_spacing;
    ImVec2 note_size;
    switch (notif.note_type)
    {
      case AchievementNotificationNoteType::Text:
        note_font_size = note_text_size;
        note_font_weight = note_text_weight;
        note_size = font->CalcTextSizeA(note_font_size, note_font_weight, FLT_MAX, 0.0f, IMSTR_START_END(notif.note));
        note_offset_y = 0.0f;
        note_spacing = larger_horizontal_spacing;
        break;

      case AchievementNotificationNoteType::IconText:
        note_font_size = note_icon_size;
        note_font_weight = UIStyle.NormalFontWeight;
        note_size = font->CalcTextSizeA(note_font_size, note_font_weight, FLT_MAX, 0.0f, IMSTR_START_END(notif.note));
        note_offset_y = 0.0f;
        note_spacing = note_icon_padding;
        break;

      case AchievementNotificationNoteType::Spinner:
        note_font_size = 0.0f;
        note_font_weight = 0.0f;
        note_size = ImVec2(note_text_size, note_text_size);
        note_offset_y = ImFloor((note_icon_size - note_text_size) * 0.5f);
        note_spacing = note_icon_padding;
        break;

      case AchievementNotificationNoteType::Image:
        note_font_size = 0.0f;
        note_font_weight = 0.0f;
        note_image =
          GetCachedTextureAsync(notif.note, static_cast<u32>(note_text_size), static_cast<u32>(note_text_size));
        note_size = (note_image && note_image->GetWidth() > note_image->GetHeight()) ?
                      ImVec2(note_text_size * (static_cast<float>(note_image->GetWidth()) /
                                               static_cast<float>(note_image->GetHeight())),
                             note_text_size) :
                      ImVec2(note_text_size, note_text_size * (static_cast<float>(note_image->GetHeight()) /
                                                               static_cast<float>(note_image->GetWidth())));
        note_offset_y = ImFloor((note_icon_size - note_text_size) * 0.5f);
        note_spacing = note_icon_padding;
        break;

      case AchievementNotificationNoteType::None:
      default:
        note_font_size = 0.0f;
        note_font_weight = 0.0f;
        note_size = ImVec2();
        note_offset_y = 0.0f;
        note_spacing = 0.0f;
        break;
    }

    const float title_font_size = notif.small_font ? small_title_font_size : normal_title_font_size;
    const float text_font_size = notif.small_font ? small_text_font_size : normal_text_font_size;
    const ImVec2 title_size = font->CalcTextSizeA(title_font_size, title_font_weight, max_text_width - note_size.x,
                                                  max_text_width - note_size.x, IMSTR_START_END(notif.title));
    const ImVec2 text_size = font->CalcTextSizeA(text_font_size, text_font_weight, max_text_width, max_text_width,
                                                 IMSTR_START_END(notif.text));

    const float badge_size = notif.small_font ? small_badge_size : normal_badge_size;
    const float horizontal_padding = notif.small_font ? small_horizontal_padding : normal_horizontal_padding;
    const float vertical_padding = notif.small_font ? small_vertical_padding : normal_vertical_padding;
    const float box_width = std::max((horizontal_padding * 2.0f) + badge_size + horizontal_spacing +
                                       ImCeil(std::max(title_size.x + note_spacing + note_size.x, text_size.x)),
                                     std::max(static_cast<float>(ImCeil(notif.min_width * scale)), min_width));
    const float box_height =
      std::max((vertical_padding * 2.0f) + ImCeil(title_size.y) + vertical_spacing + ImCeil(text_size.y),
               notif.small_font ? small_min_height : normal_min_height);

    const auto& [expected_pos, opacity] =
      layout.GetNextPosition(box_width, box_height, time_passed, notif.duration, NOTIFICATION_APPEAR_ANIMATION_TIME,
                             NOTIFICATION_DISAPPEAR_ANIMATION_TIME, 0.2f);

    float actual_y;
    if (!layout.IsVerticalAnimation() || opacity == 1.0f)
    {
      actual_y = notif.last_y;
      if (notif.target_y != expected_pos.y)
      {
        notif.move_time = current_time;
        notif.target_y = expected_pos.y;
        notif.last_y = (notif.last_y < 0.0f) ? expected_pos.y : notif.last_y;
        actual_y = notif.last_y;
      }
      else if (actual_y != expected_pos.y)
      {
        const float time_since_move = static_cast<float>(Timer::ConvertValueToSeconds(current_time - notif.move_time));
        if (time_since_move >= MOVE_DURATION)
        {
          notif.move_time = current_time;
          notif.last_y = notif.target_y;
          actual_y = notif.last_y;
        }
        else
        {
          const float frac = Easing::OutExpo(time_since_move / MOVE_DURATION);
          actual_y = notif.last_y - ((notif.last_y - notif.target_y) * frac);
        }
      }
    }
    else
    {
      actual_y = expected_pos.y;
    }

    const ImVec2 box_min(expected_pos.x, actual_y);
    const ImVec2 box_max(box_min.x + box_width, box_min.y + box_height);

    if (blur_background && BeginBlurBackground(dl, box_min, box_max))
    {
      dl->AddRectFilled(box_min, box_max, ImGui::GetColorU32(ModAlpha(left_background_color, opacity)),
                        horizontal_padding);
      EndBlurBackground(dl);
    }
    else
    {
      const float background_opacity = opacity * 0.95f;
      const float min_rounded_width = horizontal_padding * 2.0f;
      DrawRoundedGradientRect(
        dl, box_min, box_max, ImGui::GetColorU32(ModAlpha(left_background_color, background_opacity)),
        ImGui::GetColorU32(ModAlpha(ImLerp(left_background_color, right_background_color,
                                           (box_width - min_rounded_width) / (max_width - min_rounded_width)),
                                    background_opacity)),
        horizontal_padding);
    }

    const ImVec2 badge_min(box_min.x + horizontal_padding, box_min.y + vertical_padding);
    const ImVec2 badge_max(badge_min.x + badge_size, badge_min.y + badge_size);
    if (!notif.image_url.empty())
    {
      GPUTexture* tex =
        GetCachedTextureAsync(notif.image_url, static_cast<u32>(badge_size), static_cast<u32>(badge_size));
      if (tex)
      {
        dl->AddImage(tex, badge_min, badge_max, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f),
                     ImGui::GetColorU32(ImVec4(1.0f, 1.0f, 1.0f, opacity)));
      }
    }

    const u32 title_col = ImGui::GetColorU32(ModAlpha(UIStyle.ToastTextColor, opacity));
    const u32 text_col = ImGui::GetColorU32(ModAlpha(DarkerColor(UIStyle.ToastTextColor), opacity));

    const ImVec2 title_pos = ImVec2(badge_max.x + horizontal_spacing, box_min.y + vertical_padding);
    const ImRect title_bb = ImRect(title_pos, title_pos + title_size);
    RenderShadowedTextClipped(dl, font, title_font_size, title_font_weight, title_bb.Min, title_bb.Max, title_col,
                              notif.title, &title_size, ImVec2(0.0f, 0.0f), max_text_width - note_size.x, &title_bb);

    const ImVec2 text_pos = ImVec2(badge_max.x + horizontal_spacing, title_bb.Max.y + vertical_spacing);
    const ImRect text_bb = ImRect(text_pos, text_pos + text_size);
    RenderShadowedTextClipped(dl, font, text_font_size, text_font_weight, text_bb.Min, text_bb.Max, text_col,
                              notif.text, &text_size, ImVec2(0.0f, 0.0f), max_text_width, &text_bb);

    const ImVec2 note_pos =
      ImVec2((box_min.x + box_width) - horizontal_padding - note_size.x, box_min.y + vertical_padding + note_offset_y);
    switch (notif.note_type)
    {
      case AchievementNotificationNoteType::Text:
      case AchievementNotificationNoteType::IconText:
      {
        const ImRect note_bb = ImRect(note_pos, note_pos + note_size);
        RenderShadowedTextClipped(dl, font, note_font_size, note_font_weight, note_bb.Min, note_bb.Max, title_col,
                                  notif.note, &note_size, ImVec2(0.0f, 0.0f), max_text_width, &note_bb);
      }
      break;

      case AchievementNotificationNoteType::Spinner:
      {
        DrawSpinner(dl, note_pos, title_col, note_size.x, ImCeil(4.0f * scale));
      }
      break;

      case AchievementNotificationNoteType::Image:
      {
        if (note_image)
        {
          const ImRect image_rect = CenterImage(note_size, note_image);
          dl->AddImage(note_image, note_pos + image_rect.Min, note_pos + image_rect.Max, ImVec2(0.0f, 0.0f),
                       ImVec2(1.0f, 1.0f), ImGui::GetColorU32(ImVec4(1.0f, 1.0f, 1.0f, opacity)));
        }
      }
      break;

      case AchievementNotificationNoteType::None:
      default:
        break;
    }

    ++iter;
  }

  // cleared?
  if (s_achievements_locals.notifications.empty())
    VideoThread::SetRunIdleReason(VideoThread::RunIdleReason::AchievementOverlaysActive, false);
}

void FullscreenUI::DrawIndicators(NotificationLayout& layout)
{
  static constexpr float INDICATOR_WIDTH_COEFF = 0.3f;

  static constexpr const float& font_weight = UIStyle.BoldFontWeight;

  static constexpr float bg_opacity = 0.8f;

  const float scale = EffectiveNotificationScale(g_gpu_settings.achievements_indicator_scale);
  const float spacing = ImCeil(10.0f * scale);
  const ImVec2 padding = ImVec2(ImCeil(16.0f * scale), ImCeil(10.0f * scale));
  const float rounding = ImCeil(10.0f * scale);
  const float image_size = ImCeil(50.0f * scale);
  const float font_size = ImCeil(LAYOUT_MEDIUM_FONT_SIZE * scale);
  const ImGuiIO& io = ImGui::GetIO();
  const bool blur_background = g_gpu_settings.display_blur_message_backgrounds && !FullscreenUI::HasActiveWindow() &&
                               FullscreenUI::CanBlurBackground();
  ImDrawList* const dl = ImGui::GetBackgroundDrawList();

  if (std::vector<Achievements::ActiveChallengeIndicator>& indicators = Achievements::GetActiveChallengeIndicators();
      !indicators.empty() &&
      (g_gpu_settings.achievements_challenge_indicator_mode == AchievementChallengeIndicatorMode::PersistentIcon ||
       g_gpu_settings.achievements_challenge_indicator_mode == AchievementChallengeIndicatorMode::TemporaryIcon))
  {
    const bool use_time_remaining =
      (g_gpu_settings.achievements_challenge_indicator_mode == AchievementChallengeIndicatorMode::TemporaryIcon);
    const float x_advance = image_size + spacing;
    const float total_width = image_size + (static_cast<float>(indicators.size() - 1) * x_advance);
    bool space_reserved = false;
    ImVec2 current_position;

    for (auto it = indicators.begin(); it != indicators.end();)
    {
      Achievements::ActiveChallengeIndicator& indicator = *it;
      bool draw_icon = indicator.active;
      if (use_time_remaining)
      {
        indicator.time_remaining = std::max(indicator.time_remaining - io.DeltaTime, 0.0f);
        draw_icon = (draw_icon && indicator.time_remaining > 0.0f);

        // Skip drawing when not visible.
        if (!draw_icon && indicator.opacity < 0.01f)
        {
          if (!indicator.active)
          {
            DEV_LOG("Remove non-visible challenge indicator");
            it = indicators.erase(it);
          }
          else
          {
            ++it;
          }

          continue;
        }
      }

      const float target_opacity = draw_icon ? 1.0f : 0.0f;
      const float rate = draw_icon ? CHALLENGE_INDICATOR_FADE_IN_TIME : -CHALLENGE_INDICATOR_FADE_OUT_TIME;
      indicator.opacity =
        (indicator.opacity != target_opacity) ? ImSaturate(indicator.opacity + (io.DeltaTime / rate)) : target_opacity;

      if (!space_reserved)
      {
        space_reserved = true;
        current_position = layout.GetFixedPosition(total_width, image_size);
      }

      GPUTexture* badge = FullscreenUI::GetCachedTextureAsync(indicator.badge_url);
      if (badge)
      {
        dl->AddImage(badge, current_position, current_position + ImVec2(image_size, image_size), ImVec2(0.0f, 0.0f),
                     ImVec2(1.0f, 1.0f), ImGui::GetColorU32(ImVec4(1.0f, 1.0f, 1.0f, indicator.opacity)));
      }

      current_position.x += x_advance;

      if (!indicator.active && indicator.opacity < 0.01f)
      {
        DEV_LOG("Remove challenge indicator");
        it = indicators.erase(it);
      }
      else
      {
        ++it;
      }
    }
  }

  if (std::optional<Achievements::AchievementProgressIndicator>& indicator = Achievements::GetActiveProgressIndicator();
      indicator.has_value() &&
      g_gpu_settings.achievements_progress_indicator_mode != AchievementProgressIndicatorMode::Disabled)
  {
    indicator->time += indicator->active ? io.DeltaTime : -io.DeltaTime;

    const bool show_title =
      (g_gpu_settings.achievements_progress_indicator_mode == AchievementProgressIndicatorMode::IconAndTitle);
    const ImVec4 left_background_color = DarkerColor(UIStyle.ToastBackgroundColor, 1.3f);
    const ImVec4 right_background_color = DarkerColor(UIStyle.ToastBackgroundColor, 0.8f);
    const float progress_image_size = ImCeil(32.0f * scale);
    const std::string_view progress_text = indicator->achievement->measured_progress;
    const float& progress_text_weight = show_title ? UIStyle.NormalFontWeight : font_weight;
    const ImVec2 progress_text_size =
      UIStyle.Font->CalcTextSizeA(font_size, progress_text_weight, FLT_MAX, 0.0f, IMSTR_START_END(progress_text));
    const std::string_view title_text =
      show_title ? std::string_view(indicator->achievement->title) : std::string_view();
    const ImVec2 title_text_size =
      show_title ? UIStyle.Font->CalcTextSizeA(font_size, font_weight, FLT_MAX, 0.0f, indicator->achievement->title) :
                   ImVec2();

    const float box_width =
      progress_image_size + std::max(progress_text_size.x, title_text_size.x) + spacing + padding.x * 2.0f;
    const float box_height = progress_image_size + padding.y * 2.0f;

    const auto& [box_min, opacity] = layout.GetNextPosition(
      box_width, box_height, indicator->active, indicator->time, Achievements::INDICATOR_FADE_IN_TIME,
      Achievements::INDICATOR_FADE_OUT_TIME, INDICATOR_WIDTH_COEFF);
    const ImVec2 box_max = box_min + ImVec2(box_width, box_height);

    if (blur_background && BeginBlurBackground(dl, box_min, box_max))
    {
      dl->AddRectFilled(box_min, box_max, ImGui::GetColorU32(ModAlpha(left_background_color, opacity)), rounding);
      EndBlurBackground(dl);
    }
    else
    {
      DrawRoundedGradientRect(dl, box_min, box_max,
                              ImGui::GetColorU32(ModAlpha(left_background_color, opacity * bg_opacity)),
                              ImGui::GetColorU32(ModAlpha(right_background_color, opacity * bg_opacity)), rounding);
    }

    GPUTexture* const badge = FullscreenUI::GetCachedTextureAsync(indicator->badge_url);
    if (badge)
    {
      const ImVec2 badge_pos = box_min + padding;
      dl->AddImage(badge, badge_pos, badge_pos + ImVec2(progress_image_size, progress_image_size), ImVec2(0.0f, 0.0f),
                   ImVec2(1.0f, 1.0f), ImGui::GetColorU32(ImVec4(1.0f, 1.0f, 1.0f, opacity)));
    }

    // Show:
    // [ badge ] [ title ]
    // [ badge ] [ progress text ]
    // Or:
    // [ badge ] [ progress text centered vertically ]
    if (show_title)
    {
      const float space = box_width - (padding.x * 2.0f) - progress_image_size - spacing;
      const ImRect title_clip_rect(ImVec2(box_min.x + padding.x + progress_image_size + spacing +
                                            std::max(ImFloor((space - title_text_size.x) * 0.5f), 0.0f),
                                          box_min.y + padding.y),
                                   box_max);
      RenderShadowedTextClipped(dl, UIStyle.Font, font_size, font_weight, title_clip_rect.Min, title_clip_rect.Max,
                                ImGui::GetColorU32(ModAlpha(UIStyle.ToastTextColor, opacity)), title_text,
                                &title_text_size, ImVec2(0.0f, 0.0f), 0.0f, &title_clip_rect);

      const ImRect text_clip_rect(ImVec2(box_min.x + padding.x + progress_image_size + spacing +
                                           std::max(ImFloor((space - progress_text_size.x) * 0.5f), 0.0f),
                                         box_min.y + padding.y + title_text_size.y),
                                  box_max);
      RenderShadowedTextClipped(dl, UIStyle.Font, font_size, progress_text_weight, text_clip_rect.Min,
                                text_clip_rect.Max,
                                ImGui::GetColorU32(ModAlpha(DarkerColor(UIStyle.ToastTextColor), opacity)),
                                progress_text, &progress_text_size, ImVec2(0.0f, 0.0f), 0.0f, &text_clip_rect);
    }
    else
    {
      const ImRect text_clip_rect(box_min + ImVec2(padding.x + progress_image_size + spacing,
                                                   ImFloor((box_max.y - box_min.y - progress_text_size.y) * 0.5f)),
                                  box_max);
      RenderShadowedTextClipped(dl, UIStyle.Font, font_size, progress_text_weight, text_clip_rect.Min,
                                text_clip_rect.Max, ImGui::GetColorU32(ModAlpha(UIStyle.ToastTextColor, opacity)),
                                progress_text, &progress_text_size, ImVec2(0.0f, 0.0f), 0.0f, &text_clip_rect);
    }

    if (!indicator->active && opacity <= 0.01f)
    {
      DEV_LOG("Remove progress indicator");
      indicator.reset();
    }
  }

  if (std::vector<Achievements::LeaderboardTrackerIndicator>& trackers =
        Achievements::GetLeaderboardTrackerIndicators();
      !trackers.empty())
  {
    const ImVec4 left_background_color = DarkerColor(UIStyle.ToastBackgroundColor, 1.3f);
    const ImVec4 right_background_color = DarkerColor(UIStyle.ToastBackgroundColor, 0.8f);
    TinyString tstr;

    const auto measure_tracker = [&font_size, &tstr](const Achievements::LeaderboardTrackerIndicator& indicator) {
      tstr.assign(ICON_FA_STOPWATCH " ");
      for (u32 i = 0; i < indicator.text.length(); i++)
      {
        // 8 is typically the widest digit
        if (indicator.text[i] >= '0' && indicator.text[i] <= '9')
          tstr.append('8');
        else
          tstr.append(indicator.text[i]);
      }

      return UIStyle.Font->CalcTextSizeA(font_size, font_weight, FLT_MAX, 0.0f, IMSTR_START_END(tstr));
    };

    const auto draw_tracker =
      [&padding, &rounding, &font_size, &blur_background, &dl, &left_background_color, &right_background_color, &tstr,
       &measure_tracker](Achievements::LeaderboardTrackerIndicator& indicator, const ImVec2& pos, float opacity) {
        const ImVec2 size = measure_tracker(indicator);
        const float box_width = size.x + padding.x * 2.0f;
        const float box_height = size.y + padding.y * 2.0f;
        const ImRect box(pos, ImVec2(pos.x + box_width, pos.y + box_height));

        if (blur_background && BeginBlurBackground(dl, box.Min, box.Max))
        {
          dl->AddRectFilled(box.Min, box.Max, ImGui::GetColorU32(ModAlpha(left_background_color, opacity)), rounding);
          EndBlurBackground(dl);
        }
        else
        {
          DrawRoundedGradientRect(dl, box.Min, box.Max,
                                  ImGui::GetColorU32(ModAlpha(left_background_color, opacity * bg_opacity)),
                                  ImGui::GetColorU32(ModAlpha(right_background_color, opacity * bg_opacity)), rounding);
        }

        tstr.format(ICON_FA_STOPWATCH " {}", indicator.text);

        const u32 text_col = ImGui::GetColorU32(ModAlpha(UIStyle.ToastTextColor, opacity));
        RenderShadowedTextClipped(dl, UIStyle.Font, font_size, font_weight, box.Min + padding, box.Max, text_col, tstr,
                                  nullptr, ImVec2(0.0f, 0.0f), 0.0f, &box);

        return box_width;
      };

    // animations are not currently handled for more than one tracker... but this should be rare
    if (trackers.size() > 1)
    {
      float total_width = 0.0f;
      float max_height = 0.0f;
      for (const Achievements::LeaderboardTrackerIndicator& indicator : trackers)
      {
        const ImVec2 size = measure_tracker(indicator);
        total_width += ((total_width > 0.0f) ? spacing : 0.0f) + size.x + padding.x * 2.0f;
        max_height = std::max(max_height, size.y);
      }

      ImVec2 current_pos = layout.GetFixedPosition(total_width, max_height + padding.y * 2.0f);
      for (auto it = trackers.begin(); it != trackers.end();)
      {
        Achievements::LeaderboardTrackerIndicator& indicator = *it;
        indicator.time += indicator.active ? io.DeltaTime : -io.DeltaTime;

        current_pos.x += draw_tracker(indicator, current_pos, 1.0f) + spacing;

        if (!indicator.active)
        {
          DEV_LOG("Remove tracker indicator");
          it = trackers.erase(it);
        }
        else
        {
          ++it;
        }
      }
    }
    else
    {
      // don't need to precalc size here either :D
      Achievements::LeaderboardTrackerIndicator& indicator = trackers.front();
      indicator.time += indicator.active ? io.DeltaTime : -io.DeltaTime;

      const ImVec2 size = measure_tracker(indicator);
      const float box_width = size.x + padding.x * 2.0f;
      const float box_height = size.y + padding.y * 2.0f;
      const auto& [box_pos, opacity] = layout.GetNextPosition(
        box_width, box_height, indicator.active, indicator.time, Achievements::INDICATOR_FADE_IN_TIME,
        Achievements::INDICATOR_FADE_OUT_TIME, INDICATOR_WIDTH_COEFF);
      draw_tracker(indicator, box_pos, opacity);

      if (!indicator.active && opacity <= 0.01f)
      {
        DEV_LOG("Remove tracker indicator");
        trackers.clear();
      }
    }
  }

  if (const std::vector<Achievements::PinnedAchievementIndicator>& pinned =
        Achievements::GetPinnedAchievementIndicators();
      !pinned.empty())
  {
    const ImVec4 left_background_color = DarkerColor(UIStyle.ToastBackgroundColor, 1.3f);
    const ImVec4 right_background_color = DarkerColor(UIStyle.ToastBackgroundColor, 0.8f);
    const float pinned_image_size = ImCeil(20.0f * scale);
    const float pinned_vertical_spacing = ImCeil(4.0f * scale);
    static constexpr float pinned_bg_opacity = 0.7f;

    // Get max width, we want to draw all these in one box.
    // TODO: This looks up achievements multiple times, not fast..
    float box_width = 0.0f;
    float box_height = 0.0f;
    for (const Achievements::PinnedAchievementIndicator& indicator : pinned)
    {
      const rc_client_achievement_t* achievement =
        rc_client_get_achievement_info(Achievements::GetClient(), indicator.achievement_id);
      if (!achievement || achievement->measured_progress[0] == '\0')
        continue;

      const ImVec2 text_size =
        UIStyle.Font->CalcTextSizeA(font_size, font_weight, FLT_MAX, 0.0f, achievement->measured_progress);
      box_width = std::max(text_size.x, box_width);
      box_height += ((box_height > 0.0f) ? pinned_vertical_spacing : 0.0f) + pinned_image_size;
    }

    // If the measuring hasn't begun yet, we don't want to display nothing
    if (box_height > 0.0f)
    {
      // Add image width
      box_width += pinned_image_size + spacing;
      const float box_padded_width = box_width + padding.x * 2.0f;
      const float box_padded_height = box_height + padding.y * 2.0f;
      ImVec2 box_min = layout.GetFixedPosition(box_padded_width, box_padded_height);
      ImVec2 box_max = box_min + ImVec2(box_padded_width, box_padded_height);

      // NOTE: Not blurred since they're persistent.
      DrawRoundedGradientRect(dl, box_min, box_max,
                              ImGui::GetColorU32(ModAlpha(left_background_color, pinned_bg_opacity)),
                              ImGui::GetColorU32(ModAlpha(right_background_color, pinned_bg_opacity)), rounding);

      box_min += padding;
      box_max -= padding;

      ImVec2 pos = box_min;
      for (const Achievements::PinnedAchievementIndicator& indicator : pinned)
      {
        const rc_client_achievement_t* achievement =
          rc_client_get_achievement_info(Achievements::GetClient(), indicator.achievement_id);
        if (!achievement || achievement->measured_progress[0] == '\0')
          continue;

        const std::string_view text(achievement->measured_progress);
        const ImVec2 text_size =
          UIStyle.Font->CalcTextSizeA(font_size, font_weight, FLT_MAX, 0.0f, IMSTR_START_END(text));
        const float total_width = (pinned_image_size + spacing + text_size.x);
        const float start_x = pos.x + ImFloor((box_width - total_width) * 0.5f);

        GPUTexture* const badge = FullscreenUI::GetCachedTextureAsync(indicator.badge_url);
        if (badge)
        {
          const ImVec2 badge_pos = ImVec2(start_x, pos.y);
          dl->AddImage(badge, badge_pos, badge_pos + ImVec2(pinned_image_size, pinned_image_size), ImVec2(0.0f, 0.0f),
                       ImVec2(1.0f, 1.0f), IM_COL32(255, 255, 255, 255));
        }

        const ImRect text_clip_rect(ImVec2(start_x + pinned_image_size + spacing, pos.y),
                                    ImVec2(box_max.x, pos.y + pinned_image_size));
        RenderShadowedTextClipped(dl, UIStyle.Font, font_size, font_weight, text_clip_rect.Min, text_clip_rect.Max,
                                  ImGui::GetColorU32(UIStyle.ToastTextColor), text, &text_size, ImVec2(0.0f, 0.5f),
                                  0.0f, &text_clip_rect);

        pos.y += pinned_image_size + pinned_vertical_spacing;
      }
    }
  }
}

void FullscreenUI::UpdateAchievementOverlaysRunIdle()
{
  // early out if we're already on the GPU thread
  if (VideoThread::IsOnThread())
  {
    VideoThread::SetRunIdleReason(VideoThread::RunIdleReason::AchievementOverlaysActive,
                                  !s_achievements_locals.notifications.empty());
    return;
  }

  // need to check it again once we're executing on the gpu thread, it could've changed since
  VideoThread::RunOnThread([]() {
    bool is_active;
    {
      const auto lock = Achievements::GetLock();
      is_active = !s_achievements_locals.notifications.empty();
    }
    VideoThread::SetRunIdleReason(VideoThread::RunIdleReason::AchievementOverlaysActive, is_active);
  });
}

template<typename T>
void FullscreenUI::CachePauseMenuAchievementInfo(const rc_client_achievement_t* achievement, std::optional<T>& value)
{
  if (!achievement)
  {
    value.reset();
    return;
  }

  if (!value.has_value())
    value.emplace();

  // have to take a copy because with RAIntegration the achievement pointer does not persist
  value->title = achievement->title;
  value->description = achievement->description;
  value->badge_url = Achievements::GetAchievementBadgeURL(achievement, false);
  value->measured_percent = achievement->measured_percent;
  value->achievement_id = achievement->id;

  if constexpr (std::is_base_of_v<PauseMenuAchievementInfoWithPoints, T>)
    value->points = achievement->points;
  if constexpr (std::is_base_of_v<PauseMenuMeasuredAchievementInfo, T>)
    value->measured_progress = achievement->measured_progress;
  if constexpr (std::is_same_v<PauseMenuTimedMeasuredAchievementInfo, T>)
    value->show_time = Timer::GetCurrentValue();
}

void FullscreenUI::UpdateAchievementsPauseScreenInfo()
{
  const auto lock = Achievements::GetLock();
  if (!Achievements::HasActiveGame())
  {
    s_achievements_locals.most_recent_unlock.reset();
    s_achievements_locals.achievement_nearest_completion.reset();
    s_achievements_locals.most_recent_progress_update.reset();
    s_achievements_locals.active_leaderboards.clear();
    return;
  }

  rc_client_achievement_list_t* const achievements =
    Achievements::HasAchievements() ?
      rc_client_create_achievement_list(Achievements::GetClient(), RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL,
                                        RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS) :
      nullptr;
  if (achievements)
  {
    const rc_client_achievement_t* most_recent_unlock = nullptr;
    const rc_client_achievement_t* nearest_completion = nullptr;

    for (u32 i = 0; i < achievements->num_buckets; i++)
    {
      const rc_client_achievement_bucket_t& bucket = achievements->buckets[i];
      for (u32 j = 0; j < bucket.num_achievements; j++)
      {
        const rc_client_achievement_t* achievement = bucket.achievements[j];

        if (achievement->state == RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED)
        {
          if (!most_recent_unlock || achievement->unlock_time > most_recent_unlock->unlock_time)
            most_recent_unlock = achievement;
        }
        else
        {
          // find the achievement with the greatest normalized progress, but skip anything below 80%,
          // matching the rc_client definition of "almost there"
          const float percent_cutoff = 80.0f;
          if (achievement->measured_percent >= percent_cutoff &&
              (!nearest_completion || achievement->measured_percent > nearest_completion->measured_percent))
          {
            nearest_completion = achievement;
          }
        }
      }
    }

    CachePauseMenuAchievementInfo(most_recent_unlock, s_achievements_locals.most_recent_unlock);
    CachePauseMenuAchievementInfo(nearest_completion, s_achievements_locals.achievement_nearest_completion);

    rc_client_destroy_achievement_list(achievements);
  }
  else
  {
    s_achievements_locals.most_recent_unlock.reset();
    s_achievements_locals.achievement_nearest_completion.reset();
  }

  rc_client_leaderboard_list_t* const leaderboards =
    Achievements::HasLeaderboards() ?
      rc_client_create_leaderboard_list(Achievements::GetClient(), RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE) :
      nullptr;
  if (leaderboards)
  {
    std::vector<PauseMenuLeaderboardInfo>& active_lbs = s_achievements_locals.active_leaderboards;
    size_t num_active_lbs = 0;

    for (u32 i = 0; i < leaderboards->num_buckets; i++)
    {
      const rc_client_leaderboard_bucket_t& bucket = leaderboards->buckets[i];
      for (u32 j = 0; j < bucket.num_leaderboards; j++)
      {
        const rc_client_leaderboard_t* leaderboard = bucket.leaderboards[j];
        if (leaderboard->state != RC_CLIENT_LEADERBOARD_STATE_TRACKING)
          continue;

        // avoid alloc if unnecessary
        if (num_active_lbs >= active_lbs.size() || active_lbs[num_active_lbs].leaderboard_id != leaderboard->id)
        {
          if (num_active_lbs < active_lbs.size())
            active_lbs.erase(active_lbs.begin() + num_active_lbs, active_lbs.end());

          PauseMenuLeaderboardInfo& lbinfo = active_lbs.emplace_back();
          lbinfo.leaderboard_id = leaderboard->id;
          lbinfo.format = leaderboard->format;
          lbinfo.title = leaderboard->title;
          if (leaderboard->description)
            lbinfo.description = leaderboard->description;
        }

        if (leaderboard->tracker_value)
          active_lbs[num_active_lbs].tracker_value = leaderboard->tracker_value;

        num_active_lbs++;
      }
    }

    // remove extras
    if (num_active_lbs < active_lbs.size())
      active_lbs.erase(active_lbs.begin() + num_active_lbs, active_lbs.end());

    rc_client_destroy_leaderboard_list(leaderboards);
  }
  else
  {
    s_achievements_locals.active_leaderboards.clear();
  }
}

void FullscreenUI::UpdateAchievementsLastProgressUpdate(const rc_client_achievement_t* achievement)
{
  CachePauseMenuAchievementInfo(achievement, s_achievements_locals.most_recent_progress_update);
}

void FullscreenUI::DrawAchievementsPauseMenuOverlays(float start_pos_y)
{
  if (!Achievements::HasActiveGame() || !Achievements::HasAchievements())
    return;

  const auto lock = Achievements::GetLock();
  rc_client_t* const client = Achievements::GetClient();

  const ImVec2& display_size = ImGui::GetIO().DisplaySize;
  const ImVec2 display_margin = LayoutScale(16.0f, 16.0f);
  const float box_margin = LayoutScale(10.0f);
  const float box_width = LayoutScale(450.0f);
  const float box_padding = LayoutScale(12.0f);
  const float box_content_width = box_width - box_padding - box_padding;
  const float box_rounding = LayoutScale(20.0f);
  const u32 box_background_color =
    ImGui::GetColorU32(ModAlpha(UIStyle.PopupBackgroundColor, UIStyle.BlurMenuBackground ? 1.0f : 0.8f));
  const ImU32 box_title_text_color =
    ImGui::GetColorU32(DarkerColor(UIStyle.BackgroundTextColor, 0.9f)) | IM_COL32_A_MASK;
  const ImU32 title_text_color = ImGui::GetColorU32(UIStyle.BackgroundTextColor) | IM_COL32_A_MASK;
  const ImU32 text_color =
    ImGui::GetColorU32(DarkerColor(DarkerColor(UIStyle.BackgroundTextColor, 0.9f))) | IM_COL32_A_MASK;
  const float paragraph_spacing = LayoutScale(10.0f);
  const float text_spacing = LayoutScale(2.0f);

  const float progress_height = LayoutScale(20.0f);
  const float progress_rounding = LayoutScale(5.0f);
  const float badge_size = LayoutScale(32.0f);
  const float badge_text_width = box_content_width - badge_size - (text_spacing * 3.0f);
  const bool disconnected = rc_client_is_disconnected(client);
  const int pending_count = disconnected ? rc_client_get_award_achievement_pending_count(client) : 0;

  ImDrawList* dl = ImGui::GetBackgroundDrawList();

  float box_height = box_padding + box_padding + UIStyle.MediumFontSize + paragraph_spacing + progress_height +
                     ((pending_count > 0) ? (paragraph_spacing + UIStyle.MediumFontSize) : 0.0f);

  ImVec2 box_min = ImVec2(display_size.x - box_width - display_margin.x, start_pos_y + display_margin.y);
  ImVec2 box_max = ImVec2(box_min.x + box_width, box_min.y + box_height);
  ImVec2 text_pos = ImVec2(box_min.x + box_padding, box_min.y + box_padding);
  ImVec2 text_size;
  TinyString buffer;

  if (UIStyle.BlurMenuBackground && BeginBlurBackground(dl, box_min, box_max))
  {
    dl->AddRectFilled(box_min, box_max, box_background_color, box_rounding);
    EndBlurBackground(dl);
  }
  else
  {
    dl->AddRectFilled(box_min, box_max, box_background_color, box_rounding);
  }

  // title
  {
    const rc_client_user_game_summary_t& summary = Achievements::GetGameSummary();

    buffer.format(ICON_EMOJI_UNLOCKED " {}",
                  TRANSLATE_DISAMBIG_SV("Achievements", "Achievements Unlocked", "Pause Menu"));
    dl->AddText(UIStyle.Font, UIStyle.MediumFontSize, UIStyle.BoldFontWeight, text_pos, box_title_text_color,
                IMSTR_START_END(buffer));
    const float unlocked_fraction =
      (summary.num_core_achievements > 0) ?
        (static_cast<float>(summary.num_unlocked_achievements) / static_cast<float>(summary.num_core_achievements)) :
        0.0f;
    buffer.format("{}%", static_cast<u32>(std::round(unlocked_fraction * 100.0f)));
    text_size = UIStyle.Font->CalcTextSizeA(UIStyle.MediumFontSize, UIStyle.BoldFontWeight, FLT_MAX, 0.0f,
                                            IMSTR_START_END(buffer));
    dl->AddText(UIStyle.Font, UIStyle.MediumFontSize, UIStyle.BoldFontWeight,
                ImVec2(text_pos.x + (box_content_width - text_size.x), text_pos.y), text_color,
                IMSTR_START_END(buffer));
    text_pos.y += UIStyle.MediumFontSize + paragraph_spacing;

    const ImRect progress_bb(text_pos, text_pos + ImVec2(box_content_width, progress_height));
    dl->AddRectFilled(progress_bb.Min, progress_bb.Max,
                      ImGui::GetColorU32(ModAlpha(UIStyle.PrimaryDarkColor, PROGRESS_BAR_ALPHA)), progress_rounding);
    if (summary.num_unlocked_achievements > 0)
    {
      ImGui::RenderRectFilledInRangeH(
        dl, progress_bb, ImGui::GetColorU32(DarkerColor(UIStyle.SecondaryColor)), progress_bb.Min.x,
        progress_bb.Min.x + (unlocked_fraction * progress_bb.GetWidth()), progress_rounding);
    }

    buffer.format("{}/{}", summary.num_unlocked_achievements, summary.num_core_achievements);
    text_size = UIStyle.Font->CalcTextSizeA(UIStyle.MediumFontSize, UIStyle.BoldFontWeight, FLT_MAX, 0.0f,
                                            IMSTR_START_END(buffer));
    dl->AddText(UIStyle.Font, UIStyle.MediumFontSize, UIStyle.BoldFontWeight,
                ImVec2(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)),
                ImGui::GetColorU32(UIStyle.PrimaryTextColor), IMSTR_START_END(buffer));
    text_pos.y += progress_height;

    if (pending_count > 0)
    {
      text_pos.y += paragraph_spacing;
      buffer.format(ICON_EMOJI_WARNING " {}",
                    TRANSLATE_PLURAL_SSTR("Achievements", "%n unlocks have not been confirmed by the server.",
                                          "Pause Menu", pending_count));
      dl->AddText(UIStyle.Font, UIStyle.MediumFontSize, UIStyle.BoldFontWeight, text_pos, title_text_color,
                  IMSTR_START_END(buffer));
      text_pos.y += UIStyle.MediumFontSize;
    }
  }

  const auto draw_achievement_in_box = [&box_margin, &box_width, &box_padding, &box_rounding, &box_content_width,
                                        &box_background_color, &box_min, &box_max, &badge_text_width, &dl,
                                        &box_title_text_color, &title_text_color, &text_color, &paragraph_spacing,
                                        &text_spacing, &progress_rounding, &text_pos, &badge_size](
                                         std::string_view box_title, std::string_view title,
                                         std::string_view description, const std::string& badge_path,
                                         std::string_view measured_progress, float measured_percent, u32 points) {
    const ImVec2 description_size =
      description.empty() ? ImVec2(0.0f, 0.0f) :
                            UIStyle.Font->CalcTextSizeA(UIStyle.MediumSmallFontSize, UIStyle.NormalFontWeight, FLT_MAX,
                                                        badge_text_width, IMSTR_START_END(description));

    const float box_height = box_padding + box_padding + UIStyle.MediumFontSize + paragraph_spacing +
                             std::max((title.empty() ? 0.0f : UIStyle.MediumSmallFontSize) +
                                        (description.empty() ? 0.0f : (text_spacing + description_size.y)),
                                      badge_size);

    box_min = ImVec2(box_min.x, box_max.y + box_margin);
    box_max = ImVec2(box_min.x + box_width, box_min.y + box_height);
    text_pos = ImVec2(box_min.x + box_padding, box_min.y + box_padding);

    if (UIStyle.BlurMenuBackground && BeginBlurBackground(dl, box_min, box_max))
    {
      dl->AddRectFilled(box_min, box_max, box_background_color, box_rounding);
      EndBlurBackground(dl);
    }
    else
    {
      dl->AddRectFilled(box_min, box_max, box_background_color, box_rounding);
    }

    ImVec4 clip_rect = ImVec4(text_pos.x, text_pos.y, text_pos.x + box_content_width, box_max.y);
    dl->AddText(UIStyle.Font, UIStyle.MediumFontSize, UIStyle.BoldFontWeight, text_pos, box_title_text_color,
                IMSTR_START_END(box_title), 0.0f, &clip_rect);

    if (!measured_progress.empty())
    {
      const float progress_width = LayoutScale(100.0f);
      const float progress_height = UIStyle.MediumFontSize;
      const ImRect progress_bb(ImVec2(text_pos.x + box_content_width - progress_width, text_pos.y),
                               ImVec2(text_pos.x + box_content_width, text_pos.y + progress_height));
      dl->AddRectFilled(progress_bb.Min, progress_bb.Max,
                        ImGui::GetColorU32(ModAlpha(UIStyle.PrimaryDarkColor, PROGRESS_BAR_ALPHA)), progress_rounding);
      if (measured_percent > 0.0f)
      {
        ImGui::RenderRectFilledInRangeH(
          dl, progress_bb, ImGui::GetColorU32(DarkerColor(UIStyle.SecondaryColor)), progress_bb.Min.x,
          progress_bb.Min.x + ((measured_percent * 0.01f) * progress_bb.GetWidth()), progress_rounding);
      }

      const ImVec2 measured_progress_size =
        UIStyle.Font->CalcTextSizeA(UIStyle.MediumSmallFontSize, UIStyle.BoldFontWeight, FLT_MAX, badge_text_width,
                                    IMSTR_START_END(measured_progress));

      dl->AddText(
        UIStyle.Font, UIStyle.MediumSmallFontSize, UIStyle.BoldFontWeight,
        ImVec2(progress_bb.Min.x + ((progress_bb.Max.x - progress_bb.Min.x) / 2.0f) - (measured_progress_size.x / 2.0f),
               progress_bb.Min.y + ((progress_bb.Max.y - progress_bb.Min.y) / 2.0f) -
                 (measured_progress_size.y / 2.0f)),
        ImGui::GetColorU32(UIStyle.PrimaryTextColor), IMSTR_START_END(measured_progress));
    }

    text_pos.y += UIStyle.MediumFontSize + paragraph_spacing;

    const ImVec2 image_max = ImVec2(text_pos.x + badge_size, text_pos.y + badge_size);
    GPUTexture* const badge_tex = GetCachedTextureAsync(badge_path);
    dl->AddImage(badge_tex, text_pos, image_max);

    TinyString points_text;
    float points_width = 0.0f;
    if (points > 0)
    {
      points_text.format(ICON_EMOJI_TROPHY " {}", points);
      points_width = UIStyle.Font
                       ->CalcTextSizeA(UIStyle.MediumSmallFontSize, UIStyle.BoldFontWeight, FLT_MAX, 0.0f,
                                       IMSTR_START_END(points_text))
                       .x;
    }

    ImVec2 badge_text_pos = ImVec2(image_max.x + (text_spacing * 3.0f), text_pos.y);

    if (!title.empty())
    {
      clip_rect =
        ImVec4(badge_text_pos.x, badge_text_pos.y, badge_text_pos.x + badge_text_width - points_width, box_max.y);

      dl->AddText(UIStyle.Font, UIStyle.MediumSmallFontSize, UIStyle.BoldFontWeight, badge_text_pos, title_text_color,
                  IMSTR_START_END(title), 0.0f, &clip_rect);

      if (points > 0)
      {
        clip_rect = ImVec4(clip_rect.z, clip_rect.y, clip_rect.z + points_width, clip_rect.w);
        dl->AddText(UIStyle.Font, UIStyle.MediumSmallFontSize, UIStyle.BoldFontWeight, ImVec2(clip_rect.x, clip_rect.y),
                    title_text_color, IMSTR_START_END(points_text), 0.0f, &clip_rect);
      }

      badge_text_pos.y += UIStyle.MediumSmallFontSize;
    }

    if (!description.empty())
    {
      clip_rect = ImVec4(badge_text_pos.x, badge_text_pos.y, badge_text_pos.x + badge_text_width, box_max.y);

      badge_text_pos.y += text_spacing;
      dl->AddText(UIStyle.Font, UIStyle.MediumSmallFontSize, UIStyle.NormalFontWeight, badge_text_pos, text_color,
                  IMSTR_START_END(description), badge_text_width, &clip_rect);
      badge_text_pos.y += description_size.y;
    }
  };

  const auto get_achievement_height = [&badge_size, &badge_text_width, &text_spacing](std::string_view description) {
    const ImVec2 description_size =
      description.empty() ? ImVec2() :
                            UIStyle.Font->CalcTextSizeA(UIStyle.MediumSmallFontSize, UIStyle.NormalFontWeight, FLT_MAX,
                                                        badge_text_width, IMSTR_START_END(description));
    const float text_height =
      UIStyle.MediumSmallFontSize + (description.empty() ? 0.0f : (text_spacing + description_size.y));
    return std::max(text_height, badge_size);
  };

  const auto draw_achievement_with_summary = [&box_max, &badge_text_width, &dl, &title_text_color, &text_color,
                                              &text_spacing, &text_pos,
                                              &badge_size](std::string_view title, std::string_view description,
                                                           const std::string& badge_path) {
    const ImVec2 image_max = ImVec2(text_pos.x + badge_size, text_pos.y + badge_size);
    ImVec2 badge_text_pos = ImVec2(image_max.x + (text_spacing * 3.0f), text_pos.y);
    const ImVec4 clip_rect = ImVec4(badge_text_pos.x, badge_text_pos.y, badge_text_pos.x + badge_text_width, box_max.y);
    ImVec2 text_size = description.empty() ?
                         ImVec2() :
                         UIStyle.Font->CalcTextSizeA(UIStyle.MediumSmallFontSize, UIStyle.NormalFontWeight, FLT_MAX,
                                                     badge_text_width, IMSTR_START_END(description));

    GPUTexture* badge_tex = GetCachedTextureAsync(badge_path);
    dl->AddImage(badge_tex, text_pos, image_max);

    if (!title.empty())
    {
      dl->AddText(UIStyle.Font, UIStyle.MediumSmallFontSize, UIStyle.BoldFontWeight, badge_text_pos, title_text_color,
                  IMSTR_START_END(title), 0.0f, &clip_rect);
      badge_text_pos.y += UIStyle.MediumSmallFontSize;
    }

    if (!description.empty())
    {
      badge_text_pos.y += text_spacing;
      dl->AddText(UIStyle.Font, UIStyle.MediumSmallFontSize, UIStyle.NormalFontWeight, badge_text_pos, text_color,
                  IMSTR_START_END(description), badge_text_width, &clip_rect);
      badge_text_pos.y += text_size.y;
    }

    text_pos.y = badge_text_pos.y;
  };

  if (s_achievements_locals.most_recent_unlock.has_value())
  {
    buffer.format(ICON_FA_LOCK_OPEN " {}", TRANSLATE_DISAMBIG_SV("Achievements", "Most Recent", "Pause Menu"));
    draw_achievement_in_box(
      buffer, s_achievements_locals.most_recent_unlock->title, s_achievements_locals.most_recent_unlock->description,
      s_achievements_locals.most_recent_unlock->badge_url, {}, 0.0f, s_achievements_locals.most_recent_unlock->points);

    // extra spacing if we have two
    text_pos.y += s_achievements_locals.achievement_nearest_completion ? (paragraph_spacing + paragraph_spacing) : 0.0f;
  }

  // don't duplicate nearest completion if it was also the most recent progress update
  if (s_achievements_locals.achievement_nearest_completion.has_value() &&
      (!s_achievements_locals.most_recent_progress_update ||
       s_achievements_locals.most_recent_progress_update->achievement_id !=
         s_achievements_locals.achievement_nearest_completion->achievement_id))
  {
    buffer.format(ICON_FA_FLAG_CHECKERED " {}",
                  TRANSLATE_DISAMBIG_SV("Achievements", "Nearest Completion", "Pause Menu"));
    draw_achievement_in_box(buffer, s_achievements_locals.achievement_nearest_completion->title,
                            s_achievements_locals.achievement_nearest_completion->description,
                            s_achievements_locals.achievement_nearest_completion->badge_url,
                            s_achievements_locals.achievement_nearest_completion->measured_progress,
                            s_achievements_locals.achievement_nearest_completion->measured_percent, 0);
    text_pos.y += paragraph_spacing;
  }

  if (s_achievements_locals.most_recent_progress_update.has_value())
  {
    if (Timer::ConvertValueToSeconds(Timer::GetCurrentValue() -
                                     s_achievements_locals.most_recent_progress_update->show_time) <
        PAUSE_MENU_PROGRESS_DISPLAY_TIME)
    {
      buffer.format(ICON_FA_RULER_HORIZONTAL " {}",
                    TRANSLATE_DISAMBIG_SV("Achievements", "Last Progress Update", "Pause Menu"));
      draw_achievement_in_box(buffer, s_achievements_locals.most_recent_progress_update->title,
                              s_achievements_locals.most_recent_progress_update->description,
                              s_achievements_locals.most_recent_progress_update->badge_url,
                              s_achievements_locals.most_recent_progress_update->measured_progress,
                              s_achievements_locals.most_recent_progress_update->measured_percent, 0);
      text_pos.y += paragraph_spacing;
    }
    else
    {
      s_achievements_locals.most_recent_progress_update.reset();
    }
  }

  // Challenge indicators

  if (const std::span<const Achievements::ActiveChallengeIndicator> challenge_indicators =
        Achievements::GetActiveChallengeIndicators();
      !challenge_indicators.empty())
  {
    box_height = box_padding + box_padding + UIStyle.MediumFontSize;
    for (size_t i = 0; i < challenge_indicators.size(); i++)
    {
      const Achievements::ActiveChallengeIndicator& indicator = challenge_indicators[i];
      box_height += paragraph_spacing + get_achievement_height(indicator.achievement->description) +
                    ((i == (challenge_indicators.size() - 1)) ? 0.0f : paragraph_spacing);
    }

    box_min = ImVec2(box_min.x, box_max.y + box_margin);
    box_max = ImVec2(box_min.x + box_width, box_min.y + box_height);
    text_pos = ImVec2(box_min.x + box_padding, box_min.y + box_padding);

    if (UIStyle.BlurMenuBackground && BeginBlurBackground(dl, box_min, box_max))
    {
      dl->AddRectFilled(box_min, box_max, box_background_color, box_rounding);
      EndBlurBackground(dl);
    }
    else
    {
      dl->AddRectFilled(box_min, box_max, box_background_color, box_rounding);
    }

    buffer.format(ICON_FA_HAND_FIST " {}",
                  TRANSLATE_DISAMBIG_SV("Achievements", "Active Challenge Achievements", "Pause Menu"));
    dl->AddText(UIStyle.Font, UIStyle.MediumFontSize, UIStyle.BoldFontWeight, text_pos, box_title_text_color,
                IMSTR_START_END(buffer));
    text_pos.y += UIStyle.MediumFontSize;

    for (const Achievements::ActiveChallengeIndicator& indicator : challenge_indicators)
    {
      text_pos.y += paragraph_spacing;
      draw_achievement_with_summary(indicator.achievement->title, indicator.achievement->description,
                                    indicator.badge_url);
      text_pos.y += paragraph_spacing;
    }
  }

  // Leaderboards
  if (!s_achievements_locals.active_leaderboards.empty())
  {
    box_height = box_padding + box_padding + UIStyle.MediumFontSize;

    const std::string_view icon_template = Achievements::GetLeaderboardFormatIcon(RC_CLIENT_LEADERBOARD_FORMAT_TIME);
    const float leaderboard_icon_size =
      UIStyle.Font
        ->CalcTextSizeA(UIStyle.LargeFontSize, UIStyle.NormalFontWeight, FLT_MAX, 0.0f, IMSTR_START_END(icon_template))
        .x;
    const float leaderboard_icon_reserve = leaderboard_icon_size + (text_spacing * 3.0f);
    const float avail_text_width = box_content_width - leaderboard_icon_reserve;

    for (size_t i = 0; i < s_achievements_locals.active_leaderboards.size(); i++)
    {
      const PauseMenuLeaderboardInfo& lbinfo = s_achievements_locals.active_leaderboards[i];
      box_height += paragraph_spacing;

      const ImVec2 tracker_size = lbinfo.tracker_value.empty() ?
                                    ImVec2() :
                                    UIStyle.Font->CalcTextSizeA(UIStyle.MediumSmallFontSize, UIStyle.BoldFontWeight,
                                                                FLT_MAX, 0.0f, IMSTR_START_END(lbinfo.tracker_value));
      const float avail_title_width =
        avail_text_width - ((tracker_size.x > 0.0f) ? (tracker_size.x + text_spacing) : 0.0f);
      const ImVec2 title_size = UIStyle.Font->CalcTextSizeA(UIStyle.MediumSmallFontSize, UIStyle.BoldFontWeight,
                                                            FLT_MAX, avail_title_width, IMSTR_START_END(lbinfo.title));
      box_height += title_size.y;

      if (!lbinfo.description.empty())
      {
        const ImVec2 description_size =
          UIStyle.Font->CalcTextSizeA(UIStyle.MediumSmallFontSize, UIStyle.NormalFontWeight, FLT_MAX, avail_text_width,
                                      IMSTR_START_END(lbinfo.description));
        box_height += text_spacing + description_size.y;
      }

      box_height += ((i == (s_achievements_locals.active_leaderboards.size() - 1)) ? 0.0f : paragraph_spacing);
    }

    box_min = ImVec2(box_min.x, box_max.y + box_margin);
    box_max = ImVec2(box_min.x + box_width, box_min.y + box_height);
    text_pos = ImVec2(box_min.x + box_padding, box_min.y + box_padding);

    if (UIStyle.BlurMenuBackground && BeginBlurBackground(dl, box_min, box_max))
    {
      dl->AddRectFilled(box_min, box_max, box_background_color, box_rounding);
      EndBlurBackground(dl);
    }
    else
    {
      dl->AddRectFilled(box_min, box_max, box_background_color, box_rounding);
    }

    buffer.format(ICON_FA_STOPWATCH " {}",
                  TRANSLATE_DISAMBIG_SV("Achievements", "Active Leaderboard Attempts", "Pause Menu"));
    dl->AddText(UIStyle.Font, UIStyle.MediumFontSize, UIStyle.BoldFontWeight, text_pos, box_title_text_color,
                IMSTR_START_END(buffer));
    text_pos.y += UIStyle.MediumFontSize;

    const ImVec4 clip_rect = ImVec4(text_pos.x, text_pos.y, text_pos.x + box_content_width, text_pos.y + box_height);

    for (const PauseMenuLeaderboardInfo& lbinfo : s_achievements_locals.active_leaderboards)
    {
      text_pos.y += paragraph_spacing;

      const std::string_view icon = Achievements::GetLeaderboardFormatIcon(lbinfo.format);
      dl->AddText(UIStyle.Font, UIStyle.LargeFontSize, UIStyle.NormalFontWeight, text_pos, title_text_color,
                  IMSTR_START_END(icon), 0.0f, &clip_rect);

      const ImVec2 tracker_size = lbinfo.tracker_value.empty() ?
                                    ImVec2() :
                                    UIStyle.Font->CalcTextSizeA(UIStyle.MediumSmallFontSize, UIStyle.BoldFontWeight,
                                                                FLT_MAX, 0.0f, IMSTR_START_END(lbinfo.tracker_value));
      const float avail_title_width =
        avail_text_width - ((tracker_size.x > 0.0f) ? (tracker_size.x + text_spacing) : 0.0f);
      const ImVec2 title_size = UIStyle.Font->CalcTextSizeA(UIStyle.MediumSmallFontSize, UIStyle.BoldFontWeight,
                                                            FLT_MAX, avail_title_width, IMSTR_START_END(lbinfo.title));

      dl->AddText(UIStyle.Font, UIStyle.MediumSmallFontSize, UIStyle.BoldFontWeight,
                  ImVec2(text_pos.x + leaderboard_icon_reserve, text_pos.y), title_text_color,
                  IMSTR_START_END(lbinfo.title), avail_title_width, &clip_rect);

      if (!lbinfo.tracker_value.empty())
      {
        dl->AddText(UIStyle.Font, UIStyle.MediumSmallFontSize, UIStyle.BoldFontWeight,
                    ImVec2(box_max.x - box_padding - tracker_size.x, text_pos.y), title_text_color,
                    IMSTR_START_END(lbinfo.tracker_value), 0.0f, &clip_rect);
      }

      text_pos.y += title_size.y;

      if (!lbinfo.description.empty())
      {
        text_pos.y += text_spacing;

        const ImVec2 description_size =
          UIStyle.Font->CalcTextSizeA(UIStyle.MediumSmallFontSize, UIStyle.NormalFontWeight, FLT_MAX, avail_text_width,
                                      IMSTR_START_END(lbinfo.description));
        dl->AddText(UIStyle.Font, UIStyle.MediumSmallFontSize, UIStyle.NormalFontWeight,
                    ImVec2(text_pos.x + leaderboard_icon_reserve, text_pos.y), text_color,
                    IMSTR_START_END(lbinfo.description), avail_text_width, &clip_rect);

        text_pos.y += description_size.y;
      }

      text_pos.y += paragraph_spacing;
    }
  }
}

void FullscreenUI::OpenAchievementsWindow()
{
  // NOTE: Called from CPU thread.
  if (!System::IsValid())
    return;

  if (const auto lock = Achievements::GetLock(); !Achievements::IsActive() || !Achievements::HasAchievements())
  {
    Host::AddIconOSDMessage(OSDMessageType::Info, "AchievementsUnavailable", Achievements::RA_LOGO_SVG_ICON_NAME,
                            TRANSLATE_STR("Achievements", "Achievements are not available."),
                            Achievements::IsActive() ?
                              TRANSLATE_STR("Achievements", "This game has no achievements.") :
                              TRANSLATE_STR("Achievements", "Achievements are disabled in settings."));
    return;
  }

  PauseAndOpenMenuFromCoreThread([]() {
    BeginTransition(TransitionEffect::ZoomIn, DEFAULT_TRANSITION_TIME, []() {
      ForceKeyNavEnabled();
      EnqueueSoundEffect(SFX_NAV_ACTIVATE);
      SwitchToAchievements();
    });
  });
}

void FullscreenUI::AddSubsetInfo(const rc_client_subset_t* subset)
{
  const std::string_view game_title = Achievements::GetCurrentGameTitle();

  SubsetInfo info;
  info.subset_id = subset->id;

  // is this the core subset?
  std::string_view subset_title = subset->title;
  if (game_title == subset_title)
  {
    info.full_name = subset_title;
    info.short_name = TRANSLATE_STR("Achievements", "Core");
  }
  else if (subset_title.starts_with(game_title))
  {
    info.full_name = subset_title;
    info.short_name = StringUtil::StripWhitespace(subset_title.substr(game_title.size()));
    if (info.short_name.starts_with("-"))
      info.short_name = StringUtil::StripWhitespace(info.short_name.substr(1));
    if (info.short_name.empty())
      info.short_name = StringUtil::ToChars(subset->id);
  }
  else
  {
    info.full_name = fmt::format(TRANSLATE_FS("Achievements", "{0} - {1}"), game_title, subset_title);
    info.short_name = subset_title;
  }

  info.badge_path = Achievements::GetSubsetBadgeURL(subset);
  info.num_leaderboards = subset->num_leaderboards;

  info.summary = {};
  rc_client_get_user_subset_summary(Achievements::GetClient(), subset->id, &info.summary);

  s_achievements_locals.subset_info_list.push_back(std::move(info));
}

void FullscreenUI::DrawSubsetSelector()
{
  DebugAssert(s_achievements_locals.open_subset);

  const float& font_size = UIStyle.MediumFontSize;
  constexpr float font_weight = UIStyle.BoldFontWeight;
  constexpr float nav_x_padding = 12.0f;
  constexpr float nav_y_padding = 8.0f;

  float nav_width = 0.0f;
  for (const SubsetInfo& subset : s_achievements_locals.subset_info_list)
    nav_width += CalcFloatingNavBarButtonWidth(subset.short_name, font_size, font_size, font_weight, nav_x_padding);

  BeginFloatingNavBar(30.0f, 10.0f, nav_width, font_size, 1.0f, 0.0f, nav_x_padding, nav_y_padding);

  for (size_t i = 0; i < s_achievements_locals.subset_info_list.size(); i++)
  {
    const SubsetInfo& subset = s_achievements_locals.subset_info_list[i];
    GPUTexture* badge = GetCachedTextureAsync(subset.badge_path);

    if (FloatingNavBarIcon(subset.short_name, badge, (&subset == s_achievements_locals.open_subset), font_size,
                           font_size, font_weight))
    {
      BeginTransition(TransitionEffect::Fade, DEFAULT_TRANSITION_TIME,
                      [new_subset_id = subset.subset_id]() { SetCurrentSubsetID(new_subset_id); });
    }
  }

  const size_t i = s_achievements_locals.open_subset - &s_achievements_locals.subset_info_list[0];

  if (ImGui::IsKeyPressed(ImGuiKey_GamepadDpadLeft, true) || ImGui::IsKeyPressed(ImGuiKey_NavGamepadTweakSlow, true) ||
      ImGui::IsKeyPressed(ImGuiKey_LeftArrow, true))
  {
    EnqueueSoundEffect(SFX_NAV_MOVE);
    BeginTransition(TransitionEffect::SlideLeft, DEFAULT_TRANSITION_TIME,
                    [new_subset_id = (i == 0) ? s_achievements_locals.subset_info_list.back().subset_id :
                                                s_achievements_locals.subset_info_list[i - 1].subset_id]() {
                      SetCurrentSubsetID(new_subset_id);
                    });
    ;
  }
  else if (ImGui::IsKeyPressed(ImGuiKey_GamepadDpadRight, true) ||
           ImGui::IsKeyPressed(ImGuiKey_NavGamepadTweakFast, true) || ImGui::IsKeyPressed(ImGuiKey_RightArrow, true))
  {
    EnqueueSoundEffect(SFX_NAV_MOVE);
    BeginTransition(TransitionEffect::SlideRight, DEFAULT_TRANSITION_TIME,
                    [new_subset_id = ((i + 1) == s_achievements_locals.subset_info_list.size()) ?
                                       s_achievements_locals.subset_info_list.front().subset_id :
                                       s_achievements_locals.subset_info_list[i + 1].subset_id]() {
                      SetCurrentSubsetID(new_subset_id);
                    });
  }

  EndFloatingNavBar();
}

bool FullscreenUI::IsCoreSubsetOpen()
{
  return (!s_achievements_locals.open_subset ||
          s_achievements_locals.open_subset == &s_achievements_locals.subset_info_list[0]);
}

void FullscreenUI::SetCurrentSubsetID(u32 subset_id)
{
  for (const SubsetInfo& info : s_achievements_locals.subset_info_list)
  {
    if (info.subset_id == subset_id)
    {
      s_achievements_locals.open_subset = &info;
      s_achievements_locals.last_open_subset_id = subset_id;
      QueueResetFocus(FocusResetType::ViewChanged);
      return;
    }
  }
}

template<typename T>
void FullscreenUI::CollectSubsetsFromList(const T* list, bool include_achievements, bool include_leaderboards)
{
  s_achievements_locals.open_subset = nullptr;
  s_achievements_locals.subset_info_list.clear();

  // Prefer rc_client grabbing subsets if possible. Old external clients won't support this.
  rc_client_subset_list_t* subset_list = rc_client_create_subset_list(Achievements::GetClient());
  if (subset_list && subset_list->num_subsets > 0)
  {
    // If there is only a single subset, we don't want to show a selector.
    const auto subset_active = [&include_achievements, &include_leaderboards](const rc_client_subset_t* subset) {
      return ((include_achievements && subset->num_achievements > 0) ||
              (include_leaderboards && subset->num_leaderboards > 0));
    };
    if (std::count_if(subset_list->subsets, subset_list->subsets + subset_list->num_subsets, subset_active) > 1)
    {
      for (u32 i = 0; i < subset_list->num_subsets; i++)
      {
        const rc_client_subset_t* subset = subset_list->subsets[i];
        if (subset_active(subset))
          AddSubsetInfo(subset);
      }
    }
  }
  else if (std::any_of(list->buckets, list->buckets + list->num_buckets,
                       [&list](const auto& it) { return it.subset_id != list->buckets[0].subset_id; }))
  {
    for (u32 bucket_idx = 0; bucket_idx < list->num_buckets; bucket_idx++)
    {
      const auto& bucket = list->buckets[bucket_idx];
      if (std::ranges::none_of(s_achievements_locals.subset_info_list,
                               [&bucket](const SubsetInfo& info) { return info.subset_id == bucket.subset_id; }))
      {
        const rc_client_subset_t* subset = rc_client_get_subset_info(Achievements::GetClient(), bucket.subset_id);
        if (subset)
          AddSubsetInfo(subset);
      }
    }
  }

  if (subset_list)
    rc_client_destroy_subset_list(subset_list);

  // hopefully the first will be core...
  if (!s_achievements_locals.subset_info_list.empty())
  {
    const auto it = std::ranges::find_if(s_achievements_locals.subset_info_list, [](const SubsetInfo& info) {
      return info.subset_id == s_achievements_locals.last_open_subset_id;
    });
    if (it != s_achievements_locals.subset_info_list.end())
      s_achievements_locals.open_subset = &(*it);
    else
      s_achievements_locals.open_subset = &s_achievements_locals.subset_info_list[0];
  }
}

template<typename T>
bool FullscreenUI::IsBucketVisibleInCurrentSubset(const T& bucket)
{
  // Show e.g. active challenges/leaderboards in all subsets.
  if (bucket.subset_id == 0)
    return true;

  if (s_achievements_locals.open_subset && bucket.subset_id != s_achievements_locals.open_subset->subset_id)
    return false;

  return true;
}

void FullscreenUI::SortLockedAchievements()
{
  // Sort achievements to put pinned achievements at the top.
  // Don't do this with RAIntegration, since we're modifying its data and who knows what it's doing..
  if (!s_achievements_locals.achievement_list || Achievements::IsUsingRAIntegration() ||
      Achievements::GetPinnedAchievementIndicators().empty())
  {
    return;
  }

  for (u32 i = 0; i < s_achievements_locals.achievement_list->num_buckets; i++)
  {
    const rc_client_achievement_bucket_t* bucket = &s_achievements_locals.achievement_list->buckets[i];
    if (bucket->bucket_type != RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED)
      continue;

    std::sort(bucket->achievements, bucket->achievements + bucket->num_achievements,
              [](const rc_client_achievement_t* a, const rc_client_achievement_t* b) {
                return (Achievements::IsAchievementPinned(a->id) && !Achievements::IsAchievementPinned(b->id));
              });
  }
}

void FullscreenUI::SwitchToAchievements()
{
  const auto lock = Achievements::GetLock();
  if (!Achievements::HasAchievements())
  {
    ClosePauseMenuImmediately();
    return;
  }

  if (s_achievements_locals.achievement_list)
    rc_client_destroy_achievement_list(s_achievements_locals.achievement_list);
  s_achievements_locals.achievement_list =
    rc_client_create_achievement_list(Achievements::GetClient(), RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL,
                                      RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_SUBSET_BUCKETS);
  if (!s_achievements_locals.achievement_list)
  {
    ERROR_LOG("rc_client_create_achievement_list() returned null");
    ClosePauseMenuImmediately();
    return;
  }

  // reset collapsed buckets
  s_achievements_locals.achievement_buckets_collapsed.reset();
  s_achievements_locals.scroll_to_achievement_id = 0;

  // sort unlocked achievements by unlock time
  for (size_t i = 0; i < s_achievements_locals.achievement_list->num_buckets; i++)
  {
    const rc_client_achievement_bucket_t* bucket = &s_achievements_locals.achievement_list->buckets[i];
    if (bucket->bucket_type == RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED)
    {
      std::sort(bucket->achievements, bucket->achievements + bucket->num_achievements,
                [](const rc_client_achievement_t* a, const rc_client_achievement_t* b) {
                  return a->unlock_time > b->unlock_time;
                });
    }
  }

  CollectSubsetsFromList(s_achievements_locals.achievement_list, true, false);
  SortLockedAchievements();
  SwitchToMainWindow(MainWindowType::Achievements);
}

void FullscreenUI::DrawAchievementsWindow()
{
  const auto lock = Achievements::GetLock();

  // achievements can get turned off via the main UI
  if (!s_achievements_locals.achievement_list)
  {
    ReturnToPreviousWindow(TransitionEffect::ZoomOut);
    return;
  }

  const rc_client_user_game_summary_t& summary =
    s_achievements_locals.open_subset ? s_achievements_locals.open_subset->summary : Achievements::GetGameSummary();
  const bool is_core_subset = IsCoreSubsetOpen();
  const float heading_height_unscaled = ((summary.beaten_time > 0 || summary.completed_time) ? 122.0f : 102.0f) +
                                        ((summary.num_unsupported_achievements > 0) ? 20.0f : 0.0f);

  const ImVec4 background = ModAlpha(UIStyle.BackgroundColor, WINDOW_ALPHA);
  const ImVec4 heading_background = ModAlpha(DarkerColor(UIStyle.BackgroundColor, 1.5f), WINDOW_HEADING_ALPHA);
  const ImVec2 display_size = ImGui::GetIO().DisplaySize;
  const float heading_height = LayoutScale(heading_height_unscaled);
  bool close_window = false;

  if (BeginFullscreenWindow(ImVec2(), ImVec2(display_size.x, heading_height), "achievements_heading",
                            heading_background, 0.0f, ImVec2(10.0f, 10.0f),
                            ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoScrollWithMouse,
                            true))
  {
    const ImVec2 pos = ImGui::GetCursorScreenPos() + ImGui::GetStyle().FramePadding;
    const float spacing = LayoutScale(LAYOUT_MENU_ITEM_TITLE_SUMMARY_SPACING);
    const float image_size = LayoutScale(75.0f);

    if (const std::string& badge_url = Achievements::GetCurrentGameBadgeURL(); !badge_url.empty())
    {
      GPUTexture* badge = GetCachedTextureAsync(badge_url);
      if (badge)
      {
        ImGui::GetWindowDrawList()->AddImage(badge, pos, pos + ImVec2(image_size, image_size), ImVec2(0.0f, 0.0f),
                                             ImVec2(1.0f, 1.0f), IM_COL32(255, 255, 255, 255));
      }
    }

    float left = pos.x + image_size + LayoutScale(10.0f);
    float right = pos.x + GetMenuButtonAvailableWidth();
    float top = pos.y;
    ImDrawList* dl = ImGui::GetWindowDrawList();
    SmallString text;
    ImVec2 text_size;

    if (s_achievements_locals.open_subset)
      DrawSubsetSelector();

    close_window =
      (FloatingButton(ICON_FA_XMARK, 10.0f, 10.0f, 1.0f, 0.0f, true) || (!AreAnyDialogsOpen() && WantsToCloseMenu()));

    const ImRect title_bb(ImVec2(left, top), ImVec2(right, top + UIStyle.LargeFontSize));
    text.assign(Achievements::GetCurrentGameTitle());

    if (rc_client_get_hardcore_enabled(Achievements::GetClient()))
      text.append(TRANSLATE_SV("Achievements", " (Hardcore Mode)"));

    top += UIStyle.LargeFontSize + spacing;

    RenderShadowedTextClipped(UIStyle.Font, UIStyle.LargeFontSize, UIStyle.BoldFontWeight, title_bb.Min, title_bb.Max,
                              ImGui::GetColorU32(ImGuiCol_Text), text, nullptr, ImVec2(0.0f, 0.0f), 0.0f, &title_bb);

    const ImRect summary_bb(ImVec2(left, top), ImVec2(right, top + UIStyle.MediumFontSize));
    if (summary.num_core_achievements > 0)
    {
      text.assign(ICON_EMOJI_UNLOCKED " ");
      if (IsCoreSubsetOpen())
      {
        if (summary.num_unlocked_achievements == summary.num_core_achievements)
        {
          text.append(TRANSLATE_PLURAL_SSTR("Achievements", "You have unlocked all achievements and earned %n points!",
                                            "Point count", summary.points_unlocked));
        }
        else
        {
          text.append_format(
            TRANSLATE_FS("Achievements",
                         "You have unlocked {0} of {1} achievements, earning {2} of {3} possible points."),
            summary.num_unlocked_achievements, summary.num_core_achievements, summary.points_unlocked,
            summary.points_core);
        }
      }
      else
      {
        if (summary.num_unlocked_achievements == summary.num_core_achievements)
        {
          text.append(TRANSLATE_PLURAL_SSTR("Achievements",
                                            "You have unlocked all achievements in this subset and earned %n points!",
                                            "Point count", summary.points_unlocked));
        }
        else
        {
          text.append_format(
            TRANSLATE_FS(
              "Achievements",
              "You have unlocked {0} of {1} achievements in this subset, earning {2} of {3} possible points."),
            summary.num_unlocked_achievements, summary.num_core_achievements, summary.points_unlocked,
            summary.points_core);
        }
      }
    }
    else
    {
      text.format(ICON_FA_BAN " {}", is_core_subset ? TRANSLATE_SV("Achievements", "This game has no achievements.") :
                                                      TRANSLATE_SV("Achievements", "This subset has no achievements."));
    }

    top += UIStyle.MediumFontSize + spacing;

    RenderShadowedTextClipped(UIStyle.Font, UIStyle.MediumFontSize, UIStyle.NormalFontWeight, summary_bb.Min,
                              summary_bb.Max, ImGui::GetColorU32(DarkerColor(ImGui::GetStyle().Colors[ImGuiCol_Text])),
                              text, nullptr, ImVec2(0.0f, 0.0f), 0.0f, &summary_bb);

    if (summary.num_unsupported_achievements)
    {
      text.format("{} {}", ICON_EMOJI_WARNING,
                  TRANSLATE_PLURAL_SSTR("Achievements",
                                        "%n achievements are not supported by DuckStation and cannot be unlocked.",
                                        "Unsupported achievement count", summary.num_unsupported_achievements));

      const ImRect unsupported_bb(ImVec2(left, top), ImVec2(right, top + UIStyle.MediumFontSize));
      RenderShadowedTextClipped(UIStyle.Font, UIStyle.MediumFontSize, UIStyle.BoldFontWeight, unsupported_bb.Min,
                                unsupported_bb.Max,
                                ImGui::GetColorU32(DarkerColor(ImGui::GetStyle().Colors[ImGuiCol_Text])), text, nullptr,
                                ImVec2(0.0f, 0.0f), 0.0f, &unsupported_bb);

      top += UIStyle.MediumFontSize + spacing;
    }

    if (summary.beaten_time > 0 || summary.completed_time > 0)
    {
      text.assign(ICON_EMOJI_CHECKMARK_BUTTON " ");

      if (summary.beaten_time > 0)
      {
        const std::string beaten_time =
          Host::FormatNumber(Host::NumberFormatType::ShortDate, static_cast<s64>(summary.beaten_time));
        if (summary.completed_time > 0)
        {
          const std::string completion_time =
            Host::FormatNumber(Host::NumberFormatType::ShortDate, static_cast<s64>(summary.completed_time));
          if (is_core_subset)
          {
            text.append_format(TRANSLATE_FS("Achievements", "Game was beaten on {0}, and completed on {1}."),
                               beaten_time, completion_time);
          }
          else
          {
            text.append_format(TRANSLATE_FS("Achievements", "Subset was beaten on {0}, and completed on {1}."),
                               beaten_time, completion_time);
          }
        }
        else
        {
          if (is_core_subset)
            text.append_format(TRANSLATE_FS("Achievements", "Game was beaten on {0}."), beaten_time);
          else
            text.append_format(TRANSLATE_FS("Achievements", "Subset was beaten on {0}."), beaten_time);
        }
      }
      else
      {
        const std::string completion_time =
          Host::FormatNumber(Host::NumberFormatType::ShortDate, static_cast<s64>(summary.completed_time));
        if (is_core_subset)
          text.append_format(TRANSLATE_FS("Achievements", "Game was completed on {0}."), completion_time);
        else
          text.append_format(TRANSLATE_FS("Achievements", "Subset was completed on {0}."), completion_time);
      }

      const ImRect beaten_bb(ImVec2(left, top), ImVec2(right, top + UIStyle.MediumFontSize));
      RenderShadowedTextClipped(UIStyle.Font, UIStyle.MediumFontSize, UIStyle.NormalFontWeight, beaten_bb.Min,
                                beaten_bb.Max, ImGui::GetColorU32(DarkerColor(ImGui::GetStyle().Colors[ImGuiCol_Text])),
                                text, nullptr, ImVec2(0.0f, 0.0f), 0.0f, &beaten_bb);

      top += UIStyle.MediumFontSize + spacing;
    }

    if (summary.num_core_achievements > 0)
    {
      const float progress_height = LayoutScale(20.0f);
      const float progress_rounding = LayoutScale(5.0f);
      const ImRect progress_bb(ImVec2(left, top), ImVec2(right, top + progress_height));
      const float fraction =
        static_cast<float>(summary.num_unlocked_achievements) / static_cast<float>(summary.num_core_achievements);
      dl->AddRectFilled(progress_bb.Min, progress_bb.Max,
                        ImGui::GetColorU32(ModAlpha(UIStyle.PrimaryDarkColor, PROGRESS_BAR_ALPHA)), progress_rounding);
      if (summary.num_unlocked_achievements > 0)
      {
        ImGui::RenderRectFilledInRangeH(dl, progress_bb, ImGui::GetColorU32(UIStyle.SecondaryColor), progress_bb.Min.x,
                                        progress_bb.Min.x + (fraction * progress_bb.GetWidth()), progress_rounding);
      }

      text.format("{}%", static_cast<u32>(std::round(fraction * 100.0f)));
      text_size = UIStyle.Font->CalcTextSizeA(UIStyle.MediumFontSize, UIStyle.NormalFontWeight, FLT_MAX, 0.0f,
                                              IMSTR_START_END(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(UIStyle.Font, UIStyle.MediumFontSize, UIStyle.NormalFontWeight, text_pos,
                  ImGui::GetColorU32(UIStyle.PrimaryTextColor), IMSTR_START_END(text));
      // top += progress_height + spacing;
    }
  }
  EndFullscreenWindow();

  // See note in FullscreenUI::DrawSettingsWindow().
  if (IsFocusResetFromWindowChange())
    ImGui::SetNextWindowScroll(ImVec2(0.0f, 0.0f));

  if (BeginFullscreenWindow(ImVec2(0.0f, heading_height),
                            ImVec2(display_size.x, display_size.y - heading_height - LayoutScale(LAYOUT_FOOTER_HEIGHT)),
                            "achievements", background, 0.0f,
                            ImVec2(LAYOUT_MENU_WINDOW_X_PADDING, LAYOUT_MENU_WINDOW_Y_PADDING), 0, true))
  {
    static constexpr std::pair<const char*, const char*> bucket_names[] = {
      {ICON_FA_CIRCLE_QUESTION, TRANSLATE_NOOP("Achievements", "Unknown")},
      {ICON_FA_LOCK, TRANSLATE_NOOP("Achievements", "Locked")},
      {ICON_FA_UNLOCK, TRANSLATE_NOOP("Achievements", "Unlocked")},
      {ICON_FA_TRIANGLE_EXCLAMATION, TRANSLATE_NOOP("Achievements", "Unsupported")},
      {ICON_FA_FLASK_VIAL, TRANSLATE_NOOP("Achievements", "Unofficial")},
      {ICON_FA_LOCK_OPEN, TRANSLATE_NOOP("Achievements", "Recently Unlocked")},
      {ICON_FA_HAND_FIST, TRANSLATE_NOOP("Achievements", "Active Challenges")},
      {ICON_FA_FLAG_CHECKERED, TRANSLATE_NOOP("Achievements", "Almost There")},
      {ICON_EMOJI_WARNING, TRANSLATE_NOOP("Achievements", "Not Confirmed")},
      {ICON_FA_UNLOCK, TRANSLATE_NOOP("Achievements", "Unlocked in Softcore")},
    };
    static_assert(std::size(bucket_names) == NUM_RC_CLIENT_ACHIEVEMENT_BUCKETS);

    ResetFocusHere();
    BeginMenuButtons();

    // Prefetch badges for up to 3 screens worth of achievements based on current scroll position. This should help
    // reduce loading placeholders when scrolling through a large list of achievements.
    const ImGuiWindow* const win = ImGui::GetCurrentWindowRead();
    const ImVec2 prefetch_range = ImVec2(win->Scroll.y, win->Scroll.y + (win->ClipRect.GetHeight() * 3.0f));

    for (u32 bucket_type : {RC_CLIENT_ACHIEVEMENT_BUCKET_UNSYNCED, RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE,
                            RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED, RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE,
                            RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED_SOFTCORE,
                            RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED, RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL,
                            RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED})
    {
      for (u32 bucket_idx = 0; bucket_idx < s_achievements_locals.achievement_list->num_buckets; bucket_idx++)
      {
        const rc_client_achievement_bucket_t& bucket = s_achievements_locals.achievement_list->buckets[bucket_idx];
        if (bucket.bucket_type != bucket_type || !IsBucketVisibleInCurrentSubset(bucket))
          continue;

        DebugAssert(bucket.bucket_type < NUM_RC_CLIENT_ACHIEVEMENT_BUCKETS);

        if (MenuHeadingButton(TinyString::from_format(
                                "{} {}", bucket_names[bucket.bucket_type].first,
                                Host::TranslateToStringView("Achievements", bucket_names[bucket.bucket_type].second)),
                              s_achievements_locals.achievement_buckets_collapsed[bucket.bucket_type] ?
                                ICON_FA_CHEVRON_DOWN :
                                ICON_FA_CHEVRON_UP,
                              UIStyle.MediumLargeFontSize))
        {
          s_achievements_locals.achievement_buckets_collapsed.flip(bucket.bucket_type);
        }
        if (!s_achievements_locals.achievement_buckets_collapsed[bucket.bucket_type])
        {
          for (u32 i = 0; i < bucket.num_achievements; i++)
            DrawAchievement(bucket.achievements[i], prefetch_range);
        }
      }
    }

    EndMenuButtons();
  }
  EndFullscreenWindow();

  if (IsGamepadInputSource())
  {
    if (s_achievements_locals.open_subset)
    {
      SetFullscreenFooterText(
        std::array{std::make_pair(ICON_PF_XBOX_DPAD_LEFT_RIGHT, TRANSLATE_SV("Achievements", "Change Subset")),
                   std::make_pair(ICON_PF_XBOX_DPAD_UP_DOWN, TRANSLATE_SV("Achievements", "Change Selection")),
                   std::make_pair(ICON_PF_BUTTON_X, TRANSLATE_SV("Achievements", "Pin Achievement")),
                   std::make_pair(ICON_PF_BUTTON_A, TRANSLATE_SV("Achievements", "View Details")),
                   std::make_pair(ICON_PF_BUTTON_B, TRANSLATE_SV("Achievements", "Back"))});
    }
    else
    {
      SetFullscreenFooterText(
        std::array{std::make_pair(ICON_PF_XBOX_DPAD_UP_DOWN, TRANSLATE_SV("Achievements", "Change Selection")),
                   std::make_pair(ICON_PF_BUTTON_X, TRANSLATE_SV("Achievements", "Pin Achievement")),
                   std::make_pair(ICON_PF_BUTTON_A, TRANSLATE_SV("Achievements", "View Details")),
                   std::make_pair(ICON_PF_BUTTON_B, TRANSLATE_SV("Achievements", "Back"))});
    }
  }
  else
  {
    if (s_achievements_locals.open_subset)
    {
      SetFullscreenFooterText(std::array{
        std::make_pair(ICON_PF_ARROW_LEFT ICON_PF_ARROW_RIGHT, TRANSLATE_SV("Achievements", "Change Subset")),
        std::make_pair(ICON_PF_ARROW_UP ICON_PF_ARROW_DOWN, TRANSLATE_SV("Achievements", "Change Selection")),
        std::make_pair(ICON_PF_F4, TRANSLATE_SV("Achievements", "View Details")),
        std::make_pair(ICON_PF_ESC, TRANSLATE_SV("Achievements", "Back"))});
    }
    else
    {
      SetFullscreenFooterText(std::array{
        std::make_pair(ICON_PF_ARROW_UP ICON_PF_ARROW_DOWN, TRANSLATE_SV("Achievements", "Change Selection")),
        std::make_pair(ICON_PF_F4, TRANSLATE_SV("Achievements", "Pin Achievement")),
        std::make_pair(ICON_PF_ENTER, TRANSLATE_SV("Achievements", "View Details")),
        std::make_pair(ICON_PF_ESC, TRANSLATE_SV("Achievements", "Back"))});
    }
  }

  if (close_window)
    ReturnToPreviousWindow(TransitionEffect::ZoomOut);
}

void FullscreenUI::DrawAchievement(const rc_client_achievement_t* cheevo, const ImVec2& prefetch_range)
{
  static constexpr const float progress_height_unscaled = 20.0f;
  static constexpr const float progress_rounding_unscaled = 5.0f;

  static constexpr const float& title_font_size = UIStyle.LargeFontSize;
  static constexpr const float& title_font_weight = UIStyle.BoldFontWeight;
  static constexpr const float& subtitle_font_size = UIStyle.MediumFontSize;
  static constexpr const float& subtitle_font_weight = UIStyle.NormalFontWeight;
  static constexpr const float& type_badge_font_size = UIStyle.MediumSmallFontSize;
  static constexpr const float& type_badge_font_weight = UIStyle.BoldFontWeight;

  const std::string_view title(cheevo->title);
  const std::string_view description = cheevo->description ? std::string_view(cheevo->description) : std::string_view();
  const std::string_view measured_progress(cheevo->measured_progress);
  const bool is_unlocked = (cheevo->state == RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED);
  const bool is_pinned = (!is_unlocked && Achievements::IsAchievementPinned(cheevo->id));
  const bool is_measured = (!is_unlocked && !measured_progress.empty());

  ImVec2 type_badge_padding;
  ImVec2 type_badge_size;
  float type_badge_spacing = 0.0f;
  float type_badge_rounding = 0.0f;
  ImU32 type_badge_bg_color = 0;
  TinyString type_badge_text;
  switch (cheevo->type)
  {
    case RC_CLIENT_ACHIEVEMENT_TYPE_MISSABLE:
      type_badge_text.format(ICON_PF_ACHIEVEMENTS_MISSABLE " {}", TRANSLATE_SV("Achievements", "Missable"));
      type_badge_bg_color = IM_COL32(205, 45, 32, 255);
      break;

    case RC_CLIENT_ACHIEVEMENT_TYPE_PROGRESSION:
      type_badge_text.format(ICON_PF_ACHIEVEMENTS_PROGRESSION " {}", TRANSLATE_SV("Achievements", "Progression"));
      type_badge_bg_color = IM_COL32(13, 71, 161, 255);
      break;

    case RC_CLIENT_ACHIEVEMENT_TYPE_WIN:
      type_badge_text.format(ICON_PF_ACHIEVEMENTS_PROGRESSION " {}", TRANSLATE_SV("Achievements", "Win Condition"));
      type_badge_bg_color = IM_COL32(50, 110, 30, 255);
      break;
  }

  static constexpr const float& pin_font_size = UIStyle.MediumLargeFontSize;
  const std::string_view pin_text = is_pinned ? std::string_view(ICON_FA_THUMBTACK) : std::string_view();
  const ImVec2 pin_size = pin_text.empty() ?
                            ImVec2() :
                            UIStyle.Font->CalcTextSizeA(pin_font_size, 0.0f, FLT_MAX, 0.0f, IMSTR_START_END(pin_text));
  const float pin_spacing = pin_text.empty() ? 0.0f : LayoutScale(5.0f);

  if (!type_badge_text.empty())
  {
    type_badge_padding = LayoutScale(5.0f, 3.0f);
    type_badge_spacing = LayoutScale(10.0f);
    type_badge_rounding = LayoutScale(3.0f);
    type_badge_size = UIStyle.Font->CalcTextSizeA(type_badge_font_size, type_badge_font_weight, FLT_MAX, 0.0f,
                                                  IMSTR_START_END(type_badge_text));
    type_badge_size += type_badge_padding * 2.0f;
  }

  const ImVec2 image_size = LayoutScale(50.0f, 50.0f);
  const float image_right_padding = LayoutScale(15.0f);
  const float avail_width = GetMenuButtonAvailableWidth();
  const float spacing = LayoutScale(4.0f);
  const ImVec2 right_side_size = UIStyle.Font->CalcTextSizeA(UIStyle.MediumFontSize, UIStyle.NormalFontWeight, FLT_MAX,
                                                             0.0f, TRANSLATE("Achievements", "XXX points"));
  const float max_text_width =
    avail_width - (image_size.x + image_right_padding + (spacing * 2.0f) + right_side_size.x);
  const float max_title_width = max_text_width -
                                (type_badge_text.empty() ? 0.0f : type_badge_size.x + type_badge_spacing) -
                                (pin_text.empty() ? 0.0f : pin_size.x + pin_spacing);
  const ImVec2 title_size = UIStyle.Font->CalcTextSizeA(UIStyle.LargeFontSize, UIStyle.BoldFontWeight, FLT_MAX,
                                                        max_title_width, IMSTR_START_END(title));
  const ImVec2 description_size = description.empty() ?
                                    ImVec2() :
                                    UIStyle.Font->CalcTextSizeA(UIStyle.MediumFontSize, UIStyle.NormalFontWeight,
                                                                FLT_MAX, max_text_width, IMSTR_START_END(description));
  const float content_height = (title_size.y + spacing + description_size.y + spacing + UIStyle.MediumFontSize) +
                               (is_measured ? (spacing + LayoutScale(progress_height_unscaled)) : 0.0f) +
                               LayoutScale(LAYOUT_MENU_ITEM_EXTRA_HEIGHT);

  SmallString text;
  text.format("chv_{}", cheevo->id);

  ImRect bb;
  bool visible, hovered;
  const bool clicked = MenuButtonFrame(text, content_height, true, &bb, &visible, &hovered);
  if (!visible)
  {
    const float pos_y = ImGui::GetCursorPosY();
    if (pos_y >= prefetch_range.x && pos_y <= prefetch_range.y)
    {
      if (const std::string_view badge_url = Achievements::GetAchievementBadgeURL(cheevo, !is_unlocked);
          !badge_url.empty())
      {
        // prefill the cache
        GetCachedTextureAsync(badge_url);
      }
    }

    return;
  }

  if (s_achievements_locals.scroll_to_achievement_id == cheevo->id)
  {
    s_achievements_locals.scroll_to_achievement_id = 0;
    ImGui::ScrollToItem(ImGuiScrollFlags_KeepVisibleEdgeY);
  }

  ImDrawList* const dl = ImGui::GetWindowDrawList();

  if (const std::string_view badge_url = Achievements::GetAchievementBadgeURL(cheevo, !is_unlocked); !badge_url.empty())
  {
    GPUTexture* badge = GetCachedTextureAsync(badge_url);
    if (badge)
    {
      const ImRect image_bb = CenterImage(ImRect(bb.Min, bb.Min + image_size), badge);
      dl->AddImage(badge, image_bb.Min, image_bb.Max, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f),
                   IM_COL32(255, 255, 255, 255));
    }
  }

  // make it easier to compute bounding boxes...
  ImVec2 current_pos = ImVec2(bb.Min.x + image_size.x + image_right_padding, bb.Min.y);

  // -- Title + Pin Icon --
  const u32 text_color = ImGui::GetColorU32(UIStyle.SecondaryTextColor);
  const u32 description_color = ImGui::GetColorU32(DarkerColor(UIStyle.SecondaryTextColor));
  ImRect title_bb(current_pos, current_pos + title_size);
  if (!pin_text.empty())
  {
    const ImVec2 pin_pos = ImVec2(title_bb.Min.x, title_bb.Min.y + LayoutScale(1.0f));
    RenderShadowedTextClipped(dl, UIStyle.Font, pin_font_size, 0.0f, pin_pos, pin_pos + pin_size, description_color,
                              pin_text, &pin_size, ImVec2(0.0f, 0.0f), 0.0, &title_bb);
    title_bb.Min.x += pin_size.x + pin_spacing;
    title_bb.Max.x += pin_size.x + pin_spacing;
  }

  // -- Title --
  RenderShadowedTextClipped(dl, UIStyle.Font, title_font_size, title_font_weight, title_bb.Min, title_bb.Max,
                            text_color, title, &title_size, ImVec2(0.0f, 0.0f), max_title_width, &title_bb);
  current_pos.y += title_size.y + spacing;

  // -- Type Badge --
  if (!type_badge_text.empty())
  {
    const ImVec2 type_badge_pos(title_bb.Max.x + type_badge_spacing,
                                ImFloor(title_bb.Min.y + (title_font_size - type_badge_size.y) * 0.5f));
    dl->AddRectFilled(type_badge_pos, type_badge_pos + type_badge_size, type_badge_bg_color, type_badge_rounding);

    const ImVec2 type_badge_text_pos = type_badge_pos + type_badge_padding;
    const ImVec4 type_badge_text_clip = ImVec4(type_badge_pos.x, type_badge_pos.y, type_badge_pos.x + type_badge_size.x,
                                               type_badge_pos.y + type_badge_size.y);
    dl->AddText(UIStyle.Font, type_badge_font_size, type_badge_font_weight, type_badge_text_pos,
                IM_COL32(255, 255, 255, 255), IMSTR_START_END(type_badge_text), 0.0f, &type_badge_text_clip);
  }

  // -- Description --
  if (!description.empty())
  {
    const ImRect description_bb(current_pos, current_pos + description_size);
    RenderShadowedTextClipped(dl, UIStyle.Font, subtitle_font_size, subtitle_font_weight, description_bb.Min,
                              description_bb.Max, description_color, description, &description_size, ImVec2(0.0f, 0.0f),
                              max_text_width, &description_bb);
    current_pos.y += description_size.y + spacing;
  }

  // -- Rarity --
  // display hc if hc is active
  const float rarity_to_display =
    rc_client_get_hardcore_enabled(Achievements::GetClient()) ? cheevo->rarity_hardcore : cheevo->rarity;
  const ImRect rarity_bb(current_pos, ImVec2(current_pos.x + max_text_width, current_pos.y + UIStyle.MediumFontSize));
  const u32 rarity_color = ImGui::GetColorU32(DarkerColor(DarkerColor(UIStyle.SecondaryTextColor)));
  if (is_unlocked)
  {
    const std::string date =
      Host::FormatNumber(Host::NumberFormatType::LongDateTime, static_cast<s64>(cheevo->unlock_time));
    text.format(TRANSLATE_FS("Achievements", "Unlocked: {} | {:.1f}% of players have this achievement"), date,
                rarity_to_display);

    RenderShadowedTextClipped(dl, UIStyle.Font, subtitle_font_size, subtitle_font_weight, rarity_bb.Min, rarity_bb.Max,
                              rarity_color, text, nullptr, ImVec2(0.0f, 0.0f), 0.0f, &rarity_bb);
  }
  else
  {
    text.format(TRANSLATE_FS("Achievements", "{:.1f}% of players have this achievement"), rarity_to_display);
    RenderShadowedTextClipped(dl, UIStyle.Font, subtitle_font_size, subtitle_font_weight, rarity_bb.Min, rarity_bb.Max,
                              rarity_color, text, nullptr, ImVec2(0.0f, 0.0f), 0.0f, &rarity_bb);
  }
  current_pos.y += UIStyle.MediumFontSize + spacing;

  if (is_measured)
  {
    const float progress_rounding = LayoutScale(progress_rounding_unscaled);
    const ImRect progress_bb(
      current_pos, ImVec2(current_pos.x + max_text_width, current_pos.y + LayoutScale(progress_height_unscaled)));
    const float fraction = cheevo->measured_percent * 0.01f;
    dl->AddRectFilled(progress_bb.Min, progress_bb.Max,
                      ImGui::GetColorU32(ModAlpha(UIStyle.PrimaryDarkColor, PROGRESS_BAR_ALPHA)), progress_rounding);
    ImGui::RenderRectFilledInRangeH(dl, progress_bb, ImGui::GetColorU32(UIStyle.SecondaryColor), progress_bb.Min.x,
                                    progress_bb.Min.x + (fraction * progress_bb.GetWidth()), progress_rounding);

    const ImVec2 text_size = UIStyle.Font->CalcTextSizeA(subtitle_font_size, subtitle_font_weight, FLT_MAX, 0.0f,
                                                         IMSTR_START_END(measured_progress));
    const ImVec2 text_pos =
      ImFloor(ImVec2(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(UIStyle.Font, subtitle_font_size, subtitle_font_weight, text_pos,
                ImGui::GetColorU32(UIStyle.PrimaryTextColor), IMSTR_START_END(measured_progress));
  }

  // right side items
  current_pos = ImVec2(bb.Max.x - right_side_size.x, bb.Min.y);

  // -- Lock Icon and Points --
  const std::string_view lock_text = is_unlocked ? ICON_EMOJI_UNLOCKED : ICON_FA_LOCK;
  const ImVec2 lock_size =
    UIStyle.Font->CalcTextSizeA(title_font_size, 0.0f, FLT_MAX, 0.0f, IMSTR_START_END(lock_text));
  const ImRect lock_bb(current_pos, ImVec2(bb.Max.x, current_pos.y + lock_size.y));
  RenderShadowedTextClipped(dl, UIStyle.Font, title_font_size, 0.0f, lock_bb.Min, lock_bb.Max, text_color, lock_text,
                            &lock_size, ImVec2(0.5f, 0.0f), 0.0f, &lock_bb);
  current_pos.y += lock_size.y + spacing;

  text = TRANSLATE_PLURAL_SSTR("Achievements", "%n points", "Achievement points", cheevo->points);
  const ImVec2 points_size =
    UIStyle.Font->CalcTextSizeA(subtitle_font_size, subtitle_font_weight, FLT_MAX, 0.0f, IMSTR_START_END(text));
  const ImRect points_bb(current_pos, ImVec2(bb.Max.x, current_pos.y + points_size.y));
  RenderShadowedTextClipped(dl, UIStyle.Font, subtitle_font_size, subtitle_font_weight, points_bb.Min, points_bb.Max,
                            description_color, text, &points_size, ImVec2(0.5f, 0.0f), 0.0f, &points_bb);
  current_pos.y += points_size.y + spacing;

  if (clicked)
  {
    // Open non-measured achievements directly.
    if (is_measured || is_pinned)
    {
      ChoiceDialogOptions options;
      options.emplace_back(FSUI_ICONSTR(ICON_FA_LINK, "Open on RetroAchievements"), false);
      options.emplace_back(is_pinned ? FSUI_ICONSTR(ICON_FA_THUMBTACK_SLASH, "Unpin from OSD") :
                                       FSUI_ICONSTR(ICON_FA_THUMBTACK, "Pin to OSD"),
                           false);

      OpenChoiceDialog(cheevo->title, false, std::move(options),
                       [achievement_id = cheevo->id, is_pinned](s32 index, const std::string& title, bool checked) {
                         switch (index)
                         {
                           case 0: // Open on RetroAchievements
                             OpenAchievementDetails(achievement_id);
                             break;
                           case 1: // Pin/Unpin
                             SetAchievementPinned(achievement_id, !is_pinned);
                             break;
                           default:
                             break;
                         }
                       });
    }
    else
    {
      OpenAchievementDetails(cheevo->id);
    }
  }
  else if (hovered)
  {
    if (ImGui::IsKeyPressed(ImGuiKey_F4, false) || ImGui::IsKeyPressed(ImGuiKey_NavGamepadMenu, false))
    {
      if (is_measured)
        SetAchievementPinned(cheevo->id, !Achievements::IsAchievementPinned(cheevo->id));
      else
        ShowToast(OSDMessageType::Error, {}, FSUI_STR("Only measured achievements can be pinned."));
    }
  }
}

void FullscreenUI::OpenAchievementDetails(u32 achievement_id)
{
  const std::string url = fmt::format(fmt::runtime(ACHEIVEMENT_DETAILS_URL_TEMPLATE), achievement_id);
  INFO_LOG("Opening achievement details: {}", url);
  Host::OpenURL(url);
}

void FullscreenUI::SetAchievementPinned(u32 achievement_id, bool pinned)
{
  DebugAssert(VideoThread::IsOnThread());

  BeginTransition(DEFAULT_TRANSITION_TIME, [achievement_id, pinned]() {
    const rc_client_achievement_t* achievement =
      rc_client_get_achievement_info(Achievements::GetClient(), achievement_id);
    if (!achievement)
      return;

    ShowToast(
      OSDMessageType::Info, {},
      pinned ?
        FSUI_ICONSTR(ICON_FA_THUMBTACK, SmallString::from_format(FSUI_FSTR("{} pinned."), achievement->title)) :
        FSUI_ICONSTR(ICON_FA_THUMBTACK_SLASH, SmallString::from_format(FSUI_FSTR("{} unpinned."), achievement->title)));
    Achievements::SetAchievementPinned(achievement_id, pinned);
    SortLockedAchievements();
    s_achievements_locals.scroll_to_achievement_id = achievement_id;
  });
}

void FullscreenUI::OpenLeaderboardsWindow()
{
  if (!System::IsValid())
    return;

  if (const auto lock = Achievements::GetLock(); !Achievements::IsActive() || !Achievements::HasLeaderboards())
  {
    Host::AddIconOSDMessage(OSDMessageType::Info, "LeaderboardsUnavailable", Achievements::RA_LOGO_SVG_ICON_NAME,
                            TRANSLATE_STR("Achievements", "Leaderboards are not available."),
                            Achievements::IsActive() ?
                              TRANSLATE_STR("Achievements", "This game has no leaderboards.") :
                              TRANSLATE_STR("Achievements", "Achievements are disabled in settings."));
    return;
  }

  PauseAndOpenMenuFromCoreThread([]() {
    BeginTransition(TransitionEffect::ZoomIn, DEFAULT_TRANSITION_TIME, []() {
      ForceKeyNavEnabled();
      EnqueueSoundEffect(SFX_NAV_ACTIVATE);
      SwitchToLeaderboards();
    });
  });
}

void FullscreenUI::SwitchToLeaderboards()
{
  const auto lock = Achievements::GetLock();
  if (!Achievements::HasLeaderboards())
  {
    ClosePauseMenuImmediately();
    return;
  }

  CloseLeaderboard();
  if (s_achievements_locals.leaderboard_list)
    rc_client_destroy_leaderboard_list(s_achievements_locals.leaderboard_list);
  s_achievements_locals.leaderboard_list =
    rc_client_create_leaderboard_list(Achievements::GetClient(), RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE);
  if (!s_achievements_locals.leaderboard_list)
  {
    ERROR_LOG("rc_client_create_leaderboard_list() returned null");
    ClosePauseMenuImmediately();
    return;
  }

  CollectSubsetsFromList(s_achievements_locals.leaderboard_list, false, true);
  SwitchToMainWindow(MainWindowType::Leaderboards);
}

void FullscreenUI::DrawLeaderboardsWindow()
{
  static constexpr float heading_height_unscaled = 102.0f;

  const auto lock = Achievements::GetLock();
  if (!s_achievements_locals.leaderboard_list)
  {
    ReturnToPreviousWindow(TransitionEffect::ZoomOut);
    return;
  }

  const bool is_leaderboard_open = (s_achievements_locals.open_leaderboard != nullptr);
  bool close_leaderboard_on_exit = false;

  SmallString text;

  const ImVec4 background = ModAlpha(UIStyle.BackgroundColor, WINDOW_ALPHA);
  const ImVec4 heading_background = ModAlpha(DarkerColor(UIStyle.BackgroundColor, 1.5f), WINDOW_HEADING_ALPHA);
  const ImVec2 display_size = ImGui::GetIO().DisplaySize;
  const u32 text_color = ImGui::GetColorU32(ImGuiCol_Text);
  const float spacing = LayoutScale(10.0f);
  const float spacing_small = ImFloor(spacing * 0.5f);
  const float heading_height = LayoutScale(heading_height_unscaled);

  if (BeginFullscreenWindow(ImVec2(), ImVec2(display_size.x, heading_height), "leaderboards_heading",
                            heading_background, 0.0f, ImVec2(10.0f, 10.0f),
                            ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoScrollWithMouse,
                            true))
  {
    const ImVec2 heading_pos = ImGui::GetCursorScreenPos() + ImGui::GetStyle().FramePadding;
    const float image_size = LayoutScale(75.0f);

    if (const std::string& badge_url = Achievements::GetCurrentGameBadgeURL(); !badge_url.empty())
    {
      GPUTexture* badge = GetCachedTextureAsync(badge_url);
      if (badge)
      {
        ImGui::GetWindowDrawList()->AddImage(badge, heading_pos, heading_pos + ImVec2(image_size, image_size),
                                             ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), IM_COL32(255, 255, 255, 255));
      }
    }

    float left = heading_pos.x + image_size + spacing;
    float right = heading_pos.x + GetMenuButtonAvailableWidth();
    float top = heading_pos.y;

    const ImRect title_bb(ImVec2(left, top), ImVec2(right, top + UIStyle.LargeFontSize));
    if (s_achievements_locals.open_subset)
      text.assign(s_achievements_locals.open_subset->full_name);
    else
      text.assign(Achievements::GetCurrentGameTitle());

    top += UIStyle.LargeFontSize + spacing_small;

    RenderShadowedTextClipped(UIStyle.Font, UIStyle.LargeFontSize, UIStyle.BoldFontWeight, title_bb.Min, title_bb.Max,
                              text_color, text, nullptr, ImVec2(0.0f, 0.0f), 0.0f, &title_bb);

    u32 summary_color;
    if (is_leaderboard_open)
    {
      const ImRect subtitle_bb(ImVec2(left, top), ImVec2(right, top + UIStyle.LargeFontSize));
      text.assign(s_achievements_locals.open_leaderboard->title);

      top += UIStyle.MediumLargeFontSize + spacing_small;

      RenderShadowedTextClipped(UIStyle.Font, UIStyle.MediumLargeFontSize, UIStyle.BoldFontWeight, subtitle_bb.Min,
                                subtitle_bb.Max,
                                ImGui::GetColorU32(DarkerColor(ImGui::GetStyle().Colors[ImGuiCol_Text])), text, nullptr,
                                ImVec2(0.0f, 0.0f), 0.0f, &subtitle_bb);

      text.assign(s_achievements_locals.open_leaderboard->description);
      summary_color = ImGui::GetColorU32(DarkerColor(DarkerColor(ImGui::GetStyle().Colors[ImGuiCol_Text])));
    }
    else
    {
      u32 count = 0;
      for (u32 i = 0; i < s_achievements_locals.leaderboard_list->num_buckets; i++)
      {
        const rc_client_leaderboard_bucket_t& bucket = s_achievements_locals.leaderboard_list->buckets[i];
        if (IsBucketVisibleInCurrentSubset(bucket))
          count += bucket.num_leaderboards;
      }

      if (IsCoreSubsetOpen())
        text = TRANSLATE_PLURAL_SSTR("Achievements", "This game has %n leaderboards.", "Leaderboard count", count);
      else
        text = TRANSLATE_PLURAL_SSTR("Achievements", "This subset has %n leaderboards.", "Leaderboard count", count);

      summary_color = ImGui::GetColorU32(DarkerColor(ImGui::GetStyle().Colors[ImGuiCol_Text]));
    }

    const ImRect summary_bb(ImVec2(left, top), ImVec2(right, top + UIStyle.MediumFontSize));
    top += UIStyle.MediumFontSize + spacing_small;

    RenderShadowedTextClipped(UIStyle.Font, UIStyle.MediumFontSize, UIStyle.NormalFontWeight, summary_bb.Min,
                              summary_bb.Max, summary_color, text, nullptr, ImVec2(0.0f, 0.0f), 0.0f, &summary_bb);

    if (!is_leaderboard_open && !Achievements::IsHardcoreModeActive())
    {
      const ImRect hardcore_warning_bb(ImVec2(left, top), ImVec2(right, top + UIStyle.MediumFontSize));
      top += UIStyle.MediumFontSize + spacing_small;

      text.format(
        ICON_EMOJI_WARNING " {}",
        TRANSLATE_SV("Achievements",
                     "Submitting scores is disabled because hardcore mode is off. Leaderboards are read-only."));

      RenderShadowedTextClipped(UIStyle.Font, UIStyle.MediumFontSize, UIStyle.BoldFontWeight, hardcore_warning_bb.Min,
                                hardcore_warning_bb.Max,
                                ImGui::GetColorU32(DarkerColor(DarkerColor(ImGui::GetStyle().Colors[ImGuiCol_Text]))),
                                text, nullptr, ImVec2(0.0f, 0.0f), 0.0f, &hardcore_warning_bb);
    }

    if (!is_leaderboard_open)
    {
      if (s_achievements_locals.open_subset)
        DrawSubsetSelector();

      if (FloatingButton(ICON_FA_XMARK, 10.0f, 10.0f, 1.0f, 0.0f, true) || WantsToCloseMenu())
        ReturnToPreviousWindow(TransitionEffect::ZoomOut);
    }
    else
    {
      const auto show_nearby_title = FullscreenUI::IconStackString(ICON_EMOJI_CLIPBOARD, FSUI_NSTR("Show Nearby"));
      const auto show_all_title = FullscreenUI::IconStackString(ICON_EMOJI_NOTEBOOK, FSUI_NSTR("Show All"));

      const float& nav_font_size = UIStyle.MediumFontSize;
      constexpr float nav_font_weight = UIStyle.BoldFontWeight;
      constexpr float nav_x_padding = 12.0f;
      constexpr float nav_y_padding = 8.0f;
      const float nav_width =
        CalcFloatingNavBarButtonWidth(show_nearby_title, 0.0f, nav_font_size, nav_font_weight, nav_x_padding) +
        CalcFloatingNavBarButtonWidth(show_all_title, 0.0f, nav_font_size, nav_font_weight, nav_x_padding);

      const ImVec2 saved_cursor_pos = ImGui::GetCursorPos();
      BeginFloatingNavBar(30.0f, 10.0f, nav_width, nav_font_size, 1.0f, 0.0f, nav_x_padding, nav_y_padding);

      bool new_view = s_achievements_locals.is_showing_all_leaderboard_entries;
      TransitionEffect new_view_effect = TransitionEffect::Fade;
      if (ImGui::IsKeyPressed(ImGuiKey_GamepadDpadLeft, false) ||
          ImGui::IsKeyPressed(ImGuiKey_NavGamepadTweakSlow, false) || ImGui::IsKeyPressed(ImGuiKey_LeftArrow, false))
      {
        new_view = !new_view;
        new_view_effect = TransitionEffect::SlideLeft;
      }
      else if (ImGui::IsKeyPressed(ImGuiKey_GamepadDpadRight, false) ||
               ImGui::IsKeyPressed(ImGuiKey_NavGamepadTweakFast, false) ||
               ImGui::IsKeyPressed(ImGuiKey_RightArrow, false))
      {
        new_view = !new_view;
        new_view_effect = TransitionEffect::SlideRight;
      }
      for (const bool show_all : {false, true})
      {
        if (FloatingNavBarIcon(show_all ? show_all_title.view() : show_nearby_title.view(), nullptr,
                               s_achievements_locals.is_showing_all_leaderboard_entries == show_all, 0.0f,
                               nav_font_size, nav_font_weight))
        {
          new_view = show_all;
        }
      }
      if (s_achievements_locals.is_showing_all_leaderboard_entries != new_view)
      {
        BeginTransition(new_view_effect, DEFAULT_TRANSITION_TIME, [new_view]() {
          s_achievements_locals.is_showing_all_leaderboard_entries = new_view;
          QueueResetFocus(FocusResetType::ViewChanged);
        });
      }

      EndFloatingNavBar();
      ImGui::SetCursorPos(saved_cursor_pos);

      if (FloatingButton(ICON_FA_XMARK, 10.0f, 10.0f, 1.0f, 0.0f, true) || WantsToCloseMenu())
        close_leaderboard_on_exit = true;
    }
  }
  EndFullscreenWindow();

  // See note in FullscreenUI::DrawSettingsWindow().
  if (IsFocusResetFromWindowChange())
    ImGui::SetNextWindowScroll(ImVec2(0.0f, 0.0f));

  if (!is_leaderboard_open)
  {
    if (BeginFullscreenWindow(
          ImVec2(0.0f, heading_height),
          ImVec2(display_size.x, display_size.y - heading_height - LayoutScale(LAYOUT_FOOTER_HEIGHT)), "leaderboards",
          background, 0.0f, ImVec2(LAYOUT_MENU_WINDOW_X_PADDING, LAYOUT_MENU_WINDOW_Y_PADDING), 0, true))
    {
      ResetFocusHere();
      BeginMenuButtons();

      for (u32 bucket_index = 0; bucket_index < s_achievements_locals.leaderboard_list->num_buckets; bucket_index++)
      {
        const rc_client_leaderboard_bucket_t& bucket = s_achievements_locals.leaderboard_list->buckets[bucket_index];
        if (!IsBucketVisibleInCurrentSubset(bucket))
          continue;

        for (u32 i = 0; i < bucket.num_leaderboards; i++)
          DrawLeaderboardListEntry(bucket.leaderboards[i]);
      }

      EndMenuButtons();
    }
    EndFullscreenWindow();

    if (IsGamepadInputSource())
    {
      if (s_achievements_locals.open_subset)
      {
        SetFullscreenFooterText(
          std::array{std::make_pair(ICON_PF_XBOX_DPAD_LEFT_RIGHT, TRANSLATE_SV("Achievements", "Change Subset")),
                     std::make_pair(ICON_PF_XBOX_DPAD_UP_DOWN, TRANSLATE_SV("Achievements", "Change Selection")),
                     std::make_pair(ICON_PF_BUTTON_A, TRANSLATE_SV("Achievements", "Open Leaderboard")),
                     std::make_pair(ICON_PF_BUTTON_B, TRANSLATE_SV("Achievements", "Back"))});
      }
      else
      {
        SetFullscreenFooterText(
          std::array{std::make_pair(ICON_PF_XBOX_DPAD_UP_DOWN, TRANSLATE_SV("Achievements", "Change Selection")),
                     std::make_pair(ICON_PF_BUTTON_A, TRANSLATE_SV("Achievements", "Open Leaderboard")),
                     std::make_pair(ICON_PF_BUTTON_B, TRANSLATE_SV("Achievements", "Back"))});
      }
    }
    else
    {
      if (s_achievements_locals.open_subset)
      {
        SetFullscreenFooterText(std::array{
          std::make_pair(ICON_PF_ARROW_LEFT ICON_PF_ARROW_RIGHT, TRANSLATE_SV("Achievements", "Change Subset")),
          std::make_pair(ICON_PF_ARROW_UP ICON_PF_ARROW_DOWN, TRANSLATE_SV("Achievements", "Change Selection")),
          std::make_pair(ICON_PF_ENTER, TRANSLATE_SV("Achievements", "Open Leaderboard")),
          std::make_pair(ICON_PF_ESC, TRANSLATE_SV("Achievements", "Back"))});
      }
      else
      {
        SetFullscreenFooterText(std::array{
          std::make_pair(ICON_PF_ARROW_UP ICON_PF_ARROW_DOWN, TRANSLATE_SV("Achievements", "Change Selection")),
          std::make_pair(ICON_PF_ENTER, TRANSLATE_SV("Achievements", "Open Leaderboard")),
          std::make_pair(ICON_PF_ESC, TRANSLATE_SV("Achievements", "Back"))});
      }
    }
  }
  else
  {
    if (BeginFullscreenWindow(
          ImVec2(0.0f, heading_height),
          ImVec2(display_size.x, display_size.y - heading_height - LayoutScale(LAYOUT_FOOTER_HEIGHT)), "leaderboard",
          background, 0.0f, ImVec2(LAYOUT_MENU_WINDOW_X_PADDING, LAYOUT_MENU_WINDOW_Y_PADDING), 0, true))
    {
      const ImVec2 heading_start_pos = ImGui::GetCursorScreenPos();
      ImVec2 column_heading_pos = heading_start_pos;
      float end_x = column_heading_pos.x + ImGui::GetContentRegionAvail().x;

      // and the padding for the frame itself
      column_heading_pos.x += LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING);
      end_x -= LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING);

      const float rank_column_width = UIStyle.Font
                                        ->CalcTextSizeA(UIStyle.LargeFontSize, UIStyle.BoldFontWeight,
                                                        std::numeric_limits<float>::max(), -1.0f, "99999")
                                        .x;
      const float name_column_width =
        UIStyle.Font
          ->CalcTextSizeA(UIStyle.LargeFontSize, UIStyle.BoldFontWeight, std::numeric_limits<float>::max(), -1.0f,
                          "WWWWWWWWWWWWWWWWWWWWWW")
          .x;
      const float time_column_width = UIStyle.Font
                                        ->CalcTextSizeA(UIStyle.LargeFontSize, UIStyle.BoldFontWeight,
                                                        std::numeric_limits<float>::max(), -1.0f, "WWWWWWWWWWW")
                                        .x;
      const float column_spacing = spacing * 2.0f;
      const u32 heading_color = ImGui::GetColorU32(DarkerColor(ImGui::GetStyle().Colors[ImGuiCol_Text]));

      const float midpoint = column_heading_pos.y + UIStyle.MediumLargeFontSize + LayoutScale(4.0f);
      float text_start_x = column_heading_pos.x;

      const ImRect rank_bb(ImVec2(text_start_x, column_heading_pos.y), ImVec2(end_x, midpoint));
      RenderShadowedTextClipped(UIStyle.Font, UIStyle.MediumLargeFontSize, UIStyle.BoldFontWeight, rank_bb.Min,
                                rank_bb.Max, heading_color, TRANSLATE_SV("Achievements", "Rank"), nullptr,
                                ImVec2(0.0f, 0.0f), 0.0f, &rank_bb);
      text_start_x += rank_column_width + column_spacing;

      const ImRect user_bb(ImVec2(text_start_x, column_heading_pos.y), ImVec2(end_x, midpoint));
      RenderShadowedTextClipped(UIStyle.Font, UIStyle.MediumLargeFontSize, UIStyle.BoldFontWeight, user_bb.Min,
                                user_bb.Max, heading_color, TRANSLATE_SV("Achievements", "Name"), nullptr,
                                ImVec2(0.0f, 0.0f), 0.0f, &user_bb);
      text_start_x += name_column_width + column_spacing;

      static const char* value_headings[NUM_RC_CLIENT_LEADERBOARD_FORMATS] = {
        TRANSLATE_NOOP("Achievements", "Time"),
        TRANSLATE_NOOP("Achievements", "Score"),
        TRANSLATE_NOOP("Achievements", "Value"),
      };

      const ImRect score_bb(ImVec2(text_start_x, column_heading_pos.y), ImVec2(end_x, midpoint));
      RenderShadowedTextClipped(
        UIStyle.Font, UIStyle.MediumLargeFontSize, UIStyle.BoldFontWeight, score_bb.Min, score_bb.Max, heading_color,
        Host::TranslateToStringView("Achievements",
                                    value_headings[std::min<u8>(s_achievements_locals.open_leaderboard->format,
                                                                NUM_RC_CLIENT_LEADERBOARD_FORMATS - 1)]),
        nullptr, ImVec2(0.0f, 0.0f), 0.0f, &score_bb);
      text_start_x += time_column_width + column_spacing;

      const ImRect date_bb(ImVec2(text_start_x, column_heading_pos.y), ImVec2(end_x, midpoint));
      RenderShadowedTextClipped(UIStyle.Font, UIStyle.MediumLargeFontSize, UIStyle.BoldFontWeight, date_bb.Min,
                                date_bb.Max, heading_color, TRANSLATE_SV("Achievements", "Date Submitted"), nullptr,
                                ImVec2(0.0f, 0.0f), 0.0f, &date_bb);

      const float line_thickness = LayoutScale(1.0f);
      const float line_padding = LayoutScale(5.0f);
      const ImVec2 line_start(column_heading_pos.x, column_heading_pos.y + UIStyle.MediumLargeFontSize + line_padding);
      const ImVec2 line_end(end_x, line_start.y);
      ImGui::GetWindowDrawList()->AddLine(line_start, line_end, ImGui::GetColorU32(ImGuiCol_TextDisabled),
                                          line_thickness);

      // keep imgui happy
      ImGui::Dummy(ImVec2(end_x - heading_start_pos.x, spacing + UIStyle.MediumLargeFontSize));

      BeginMenuButtons(0, 0.0f, LAYOUT_MENU_BUTTON_X_PADDING, 8.0f, 0.0f, 4.0f);
      ResetFocusHere();

      // for drawing time popups
      const std::time_t current_time = std::time(nullptr);
      std::optional<std::tm> current_tm = Common::LocalTime(current_time);
      if (!current_tm.has_value())
        current_tm = std::tm{};
      ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
      ImGui::PushStyleVar(ImGuiStyleVar_PopupBorderSize, 0.0f);

      u32 total_count = 0;
      u32 last_visible_count = 0;
      if (!s_achievements_locals.is_showing_all_leaderboard_entries)
      {
        if (s_achievements_locals.leaderboard_nearby_entries)
        {
          for (u32 i = 0; i < s_achievements_locals.leaderboard_nearby_entries->num_entries; i++)
          {
            total_count++;

            if (DrawLeaderboardEntry(s_achievements_locals.leaderboard_nearby_entries->entries[i], i,
                                     static_cast<s32>(i) ==
                                       s_achievements_locals.leaderboard_nearby_entries->user_index,
                                     rank_column_width, name_column_width, time_column_width, column_spacing,
                                     current_time, current_tm.value()))
            {
              last_visible_count = total_count;
            }
          }
        }
        else
        {
          DrawLeaderboardLoadingIndicator(heading_height, display_size.y - heading_height, false);
        }
      }
      else
      {
        for (const rc_client_leaderboard_entry_list_t* list : s_achievements_locals.leaderboard_entry_lists)
        {
          for (u32 i = 0; i < list->num_entries; i++)
          {
            total_count++;

            if (DrawLeaderboardEntry(list->entries[i], i, static_cast<s32>(i) == list->user_index, rank_column_width,
                                     name_column_width, time_column_width, column_spacing, current_time,
                                     current_tm.value()))
            {
              last_visible_count = total_count;
            }
          }
        }

        if (!s_achievements_locals.has_fetched_all_leaderboard_entries)
        {
          // if showing the last few, fetch the next batch
          if ((total_count - last_visible_count) <= 3 && !s_achievements_locals.leaderboard_fetch_handle)
            FetchNextLeaderboardEntries();

          // show the loading indicator in the bottom-right
          if (s_achievements_locals.leaderboard_fetch_handle)
          {
            const ImRect& win_rc = ImGui::GetCurrentWindow()->InnerRect;
            DrawLeaderboardLoadingIndicator(win_rc.Min.y, win_rc.GetHeight(), true);
          }
        }
      }

      ImGui::PopStyleVar(2);

      EndMenuButtons();
    }
    EndFullscreenWindow();

    if (IsGamepadInputSource())
    {
      SetFullscreenFooterText(
        std::array{std::make_pair(ICON_PF_XBOX_DPAD_LEFT_RIGHT, TRANSLATE_SV("Achievements", "Change Page")),
                   std::make_pair(ICON_PF_XBOX_DPAD_UP_DOWN, TRANSLATE_SV("Achievements", "Change Selection")),
                   std::make_pair(ICON_PF_BUTTON_A, TRANSLATE_SV("Achievements", "View Profile")),
                   std::make_pair(ICON_PF_BUTTON_B, TRANSLATE_SV("Achievements", "Back"))});
    }
    else
    {
      SetFullscreenFooterText(std::array{
        std::make_pair(ICON_PF_ARROW_LEFT ICON_PF_ARROW_RIGHT, TRANSLATE_SV("Achievements", "Change Page")),
        std::make_pair(ICON_PF_ARROW_UP ICON_PF_ARROW_DOWN, TRANSLATE_SV("Achievements", "Change Selection")),
        std::make_pair(ICON_PF_ENTER, TRANSLATE_SV("Achievements", "View Profile")),
        std::make_pair(ICON_PF_ESC, TRANSLATE_SV("Achievements", "Back"))});
    }
  }

  if (close_leaderboard_on_exit)
  {
    BeginTransition(TransitionEffect::ZoomOut, DEFAULT_TRANSITION_TIME, []() {
      CloseLeaderboard();
      QueueResetFocus(FocusResetType::ViewChanged);
    });
  }
}

bool FullscreenUI::DrawLeaderboardEntry(const rc_client_leaderboard_entry_t& entry, u32 index, bool is_self,
                                        float rank_column_width, float name_column_width, float time_column_width,
                                        float column_spacing, std::time_t current_time, const std::tm& current_tm)
{
  ImRect bb;
  bool visible, hovered;
  bool pressed = MenuButtonFrame(entry.user, UIStyle.MediumLargeFontSize, true, &bb, &visible, &hovered);
  if (!visible)
    return false;

  const float midpoint = bb.Min.y + UIStyle.MediumLargeFontSize + LayoutScale(4.0f);
  float text_start_x = bb.Min.x;
  SmallString text;

  text.format("{}", entry.rank);

  const u32 text_color =
    is_self ? IM_COL32(255, 242, 0, 255) :
              ImGui::GetColorU32(((index % 2) == 0) ? DarkerColor(ImGui::GetStyle().Colors[ImGuiCol_Text]) :
                                                      ImGui::GetStyle().Colors[ImGuiCol_Text]);

  const ImRect rank_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint));
  const float font_weight = is_self ? UIStyle.BoldFontWeight : UIStyle.NormalFontWeight;
  RenderShadowedTextClipped(UIStyle.Font, UIStyle.MediumLargeFontSize, font_weight, rank_bb.Min, rank_bb.Max,
                            text_color, text, nullptr, ImVec2(0.0f, 0.0f), 0.0f, &rank_bb);
  text_start_x += rank_column_width + column_spacing;

  const float icon_size = bb.Max.y - bb.Min.y;
  const ImRect icon_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint));

  // Use an alias to avoid allocating a string for the URL every time.
  text.format("__lb_user_{}", entry.user);
  GPUTexture* icon_tex = FindCachedTexture(text);
  if (!icon_tex)
  {
    std::string url = Achievements::GetUserBadgeURL(entry.user);
    if (!url.empty())
      icon_tex = GetCachedTextureAsync(std::move(url), text);
  }
  if (icon_tex)
  {
    const ImRect fit_icon_bb = CenterImage(ImRect(icon_bb.Min, icon_bb.Min + ImVec2(icon_size, icon_size)), icon_tex);
    ImGui::GetWindowDrawList()->AddImage(reinterpret_cast<ImTextureID>(icon_tex), fit_icon_bb.Min, fit_icon_bb.Max);
  }

  const float icon_spacing = LayoutScale(10.0f);
  const ImRect user_bb(ImVec2(text_start_x + icon_spacing + icon_size, bb.Min.y), ImVec2(bb.Max.x, midpoint));
  RenderShadowedTextClipped(UIStyle.Font, UIStyle.MediumLargeFontSize, font_weight, user_bb.Min, user_bb.Max,
                            text_color, entry.user, nullptr, ImVec2(0.0f, 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));
  RenderShadowedTextClipped(UIStyle.Font, UIStyle.MediumLargeFontSize, font_weight, score_bb.Min, score_bb.Max,
                            text_color, entry.display, nullptr, ImVec2(0.0f, 0.0f), 0.0f, &score_bb);
  text_start_x += time_column_width + column_spacing;

  const ImRect time_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint));

  const SmallString relative_time =
    FormatRelativeTimestamp(static_cast<std::time_t>(entry.submitted), current_time, current_tm);
  RenderShadowedTextClipped(UIStyle.Font, UIStyle.MediumLargeFontSize, font_weight, time_bb.Min, time_bb.Max,
                            text_color, relative_time, nullptr, ImVec2(0.0f, 0.0f), 0.0f, &time_bb);

  if (time_bb.Contains(ImGui::GetIO().MousePos) && ImGui::BeginItemTooltip())
  {
    const std::string submit_time =
      Host::FormatNumber(Host::NumberFormatType::LongDateTime, static_cast<s64>(entry.submitted));
    ImGui::PushFont(UIStyle.Font, UIStyle.MediumLargeFontSize, UIStyle.NormalFontWeight);
    ImGui::Text(ICON_EMOJI_CLOCK_FIVE_OCLOCK " %s", submit_time.c_str());
    ImGui::PopFont();
    ImGui::EndTooltip();
  }

  if (pressed)
  {
    const std::string url = Achievements::GetProfileURL(entry.user);
    INFO_LOG("Opening profile details: {}", url);
    Host::OpenURL(url);
  }

  return true;
}

void FullscreenUI::DrawLeaderboardLoadingIndicator(float pos_y, float avail_height, bool short_text)
{
  static constexpr const float& font_weight = UIStyle.BoldFontWeight;
  const float font_size = short_text ? UIStyle.MediumFontSize : UIStyle.MediumLargeFontSize;
  const u32 color = ImGui::GetColorU32(DarkerColor(ImGui::GetStyle().Colors[ImGuiCol_Text], 0.9f));
  const std::string_view text = short_text ?
                                  TRANSLATE_SV("Achievements", "Loading...") :
                                  TRANSLATE_SV("Achievements", "Downloading leaderboard data, please wait...");
  const ImVec2 text_size = UIStyle.Font->CalcTextSizeA(font_size, font_weight, FLT_MAX, 0.0f, IMSTR_START_END(text));
  const float spinner_size = font_size;
  const float spinner_spacing = short_text ? LayoutScale(12.0f) : LayoutScale(16.0f);
  const float total_width = spinner_size + spinner_spacing + text_size.x;
  const float display_width = ImGui::GetIO().DisplaySize.x;

  // position in right side of screen if short text, center otherwise
  const ImVec2 pos = short_text ? ImVec2((display_width - total_width) - LayoutScale(40.0f),
                                         pos_y + avail_height - font_size - LayoutScale(25.0f)) :
                                  ImVec2(ImFloor((display_width - total_width) * 0.5f),
                                         ImFloor(pos_y + (avail_height - font_size) * 0.5f));

  // for short text, draw a background box
  if (short_text)
  {
    const ImVec2 padding = ImVec2(LayoutScale(16.0f), LayoutScale(12.0f));
    ImGui::GetWindowDrawList()->AddRectFilled(ImVec2(pos.x - padding.x, pos.y - padding.y),
                                              ImVec2(pos.x + total_width + padding.x, pos.y + font_size + padding.y),
                                              ImGui::GetColorU32(ModAlpha(UIStyle.PopupBackgroundColor, 0.5f)),
                                              LayoutScale(LAYOUT_MENU_ITEM_BORDER_ROUNDING));
  }

  DrawSpinner(ImGui::GetWindowDrawList(), pos, color, spinner_size, LayoutScale(3.0f));
  const ImVec2 text_pos = ImVec2(pos.x + spinner_size + spinner_spacing, pos.y);
  RenderShadowedTextClipped(UIStyle.Font, font_size, UIStyle.BoldFontWeight, text_pos, text_pos + text_size, color,
                            text, &text_size);
}

SmallString FullscreenUI::FormatRelativeTimestamp(time_t timestamp, time_t current_time, const std::tm& current_tm)
{
  const s64 diff = static_cast<s64>(current_time) - static_cast<s64>(timestamp);

  constexpr s64 MINUTE = 60;
  constexpr s64 HOUR = 60 * MINUTE;
  constexpr s64 DAY = 24 * HOUR;
  constexpr s64 WEEK = 7 * DAY;

  if (diff < MINUTE)
    return SmallString(TRANSLATE_SV("Achievements", "Just now"));

  if (diff < HOUR)
  {
    const int minutes = static_cast<int>(diff / MINUTE);
    return TRANSLATE_PLURAL_SSTR("Achievements", "%n minutes ago", "Relative time", minutes);
  }

  if (diff < DAY)
  {
    const int hours = static_cast<int>(diff / HOUR);
    return TRANSLATE_PLURAL_SSTR("Achievements", "%n hours ago", "Relative time", hours);
  }

  if (diff < DAY * 2)
  {
    // Check if it's actually today vs yesterday
    const std::optional<std::tm> timestamp_tm = Common::LocalTime(timestamp);
    if (timestamp_tm.has_value() && timestamp_tm->tm_yday == current_tm.tm_yday &&
        timestamp_tm->tm_year == current_tm.tm_year)
    {
      return SmallString(TRANSLATE_SV("Achievements", "Today"));
    }

    return SmallString(TRANSLATE_SV("Achievements", "Yesterday"));
  }

  if (diff < WEEK)
  {
    const int days = static_cast<int>(diff / DAY);
    return TRANSLATE_PLURAL_SSTR("Achievements", "%n days ago", "Relative time", days);
  }

  const std::optional<std::tm> timestamp_tm = Common::LocalTime(timestamp);
  if (!timestamp_tm.has_value())
    return SmallString();

  const int year_diff = current_tm.tm_year - timestamp_tm->tm_year;
  const int month_diff = current_tm.tm_mon - timestamp_tm->tm_mon;
  const int total_months = year_diff * 12 + month_diff;

  if (total_months == 0)
  {
    // Less than a month - use weeks
    const int weeks = static_cast<int>(diff / WEEK);
    return TRANSLATE_PLURAL_SSTR("Achievements", "%n weeks ago", "Relative time", weeks);
  }

  if (total_months < 12)
    return TRANSLATE_PLURAL_SSTR("Achievements", "%n months ago", "Relative time", total_months);

  // For years, adjust if we haven't reached the anniversary yet
  int years = year_diff;
  if (current_tm.tm_mon < timestamp_tm->tm_mon ||
      (current_tm.tm_mon == timestamp_tm->tm_mon && current_tm.tm_mday < timestamp_tm->tm_mday))
  {
    years--;
  }

  // Edge case: less than a full year but more than 11 months
  if (years < 1)
    return TRANSLATE_PLURAL_SSTR("Achievements", "%n months ago", "Relative time", total_months);

  return TRANSLATE_PLURAL_SSTR("Achievements", "%n years ago", "Relative time", years);
}

void FullscreenUI::DrawLeaderboardListEntry(const rc_client_leaderboard_t* lboard)
{
  SmallString title;
  title.format("{}##{}", lboard->title, lboard->id);

  std::string_view summary;
  if (lboard->description && lboard->description[0] != '\0')
    summary = lboard->description;

  if (MenuButton(title, summary))
    BeginTransition(TransitionEffect::ZoomIn, DEFAULT_TRANSITION_TIME,
                    [id = lboard->id]() { OpenLeaderboardById(id); });
}

bool FullscreenUI::OpenLeaderboardById(u32 leaderboard_id)
{
  const auto lock = Achievements::GetLock();
  if (!Achievements::IsActive())
    return false;

  rc_client_t* const client = Achievements::GetClient();
  const rc_client_leaderboard_t* lb = rc_client_get_leaderboard_info(client, leaderboard_id);
  if (!lb)
    return false;

  DEV_LOG("Opening leaderboard '{}' ({})", lb->title, lb->id);

  CloseLeaderboard();

  s_achievements_locals.open_leaderboard = lb;
  s_achievements_locals.is_showing_all_leaderboard_entries = false;
  s_achievements_locals.has_fetched_all_leaderboard_entries = false;
  s_achievements_locals.leaderboard_fetch_handle = rc_client_begin_fetch_leaderboard_entries_around_user(
    client, lb->id, LEADERBOARD_NEARBY_ENTRIES_TO_FETCH, LeaderboardFetchNearbyCallback, nullptr);
  QueueResetFocus(FocusResetType::Other);
  return true;
}

void FullscreenUI::LeaderboardFetchNearbyCallback(int result, const char* error_message,
                                                  rc_client_leaderboard_entry_list_t* list, rc_client_t* client,
                                                  void* callback_userdata)
{
  // should be already locked
  s_achievements_locals.leaderboard_fetch_handle = nullptr;

  if (result != RC_OK)
  {
    VideoThread::RunOnThread([error_message = std::string(error_message)]() mutable {
      ShowToast(OSDMessageType::Error, TRANSLATE_STR("Achievements", "Leaderboard download failed"),
                std::move(error_message));
    });
    CloseLeaderboard();
    return;
  }

  if (s_achievements_locals.leaderboard_nearby_entries)
    rc_client_destroy_leaderboard_entry_list(s_achievements_locals.leaderboard_nearby_entries);
  s_achievements_locals.leaderboard_nearby_entries = list;
  QueueResetFocus(FocusResetType::Other);
}

void FullscreenUI::LeaderboardFetchAllCallback(int result, const char* error_message,
                                               rc_client_leaderboard_entry_list_t* list, rc_client_t* client,
                                               void* callback_userdata)
{
  // should be already locked
  s_achievements_locals.leaderboard_fetch_handle = nullptr;

  if (result != RC_OK)
  {
    VideoThread::RunOnThread([error_message = std::string(error_message)]() mutable {
      ShowToast(OSDMessageType::Error, TRANSLATE_STR("Achievements", "Leaderboard download failed"),
                std::move(error_message));
    });
    CloseLeaderboard();
    return;
  }

  if (s_achievements_locals.leaderboard_entry_lists.empty())
    QueueResetFocus(FocusResetType::Other);

  s_achievements_locals.leaderboard_entry_lists.push_back(list);

  // at the end if we don't have the request size full of entries
  s_achievements_locals.has_fetched_all_leaderboard_entries |= (list->num_entries < LEADERBOARD_ALL_FETCH_SIZE);
}

void FullscreenUI::FetchNextLeaderboardEntries()
{
  u32 start = 1;
  for (rc_client_leaderboard_entry_list_t* list : s_achievements_locals.leaderboard_entry_lists)
    start += list->num_entries;

  DEV_LOG("Fetching entries {} to {}", start, start + LEADERBOARD_ALL_FETCH_SIZE);

  rc_client_t* const client = Achievements::GetClient();
  if (s_achievements_locals.leaderboard_fetch_handle)
    rc_client_abort_async(client, s_achievements_locals.leaderboard_fetch_handle);
  s_achievements_locals.leaderboard_fetch_handle =
    rc_client_begin_fetch_leaderboard_entries(client, s_achievements_locals.open_leaderboard->id, start,
                                              LEADERBOARD_ALL_FETCH_SIZE, LeaderboardFetchAllCallback, nullptr);
}

void FullscreenUI::CloseLeaderboard()
{
  for (auto iter = s_achievements_locals.leaderboard_entry_lists.rbegin();
       iter != s_achievements_locals.leaderboard_entry_lists.rend(); ++iter)
  {
    rc_client_destroy_leaderboard_entry_list(*iter);
  }
  s_achievements_locals.leaderboard_entry_lists.clear();

  if (s_achievements_locals.leaderboard_nearby_entries)
  {
    rc_client_destroy_leaderboard_entry_list(s_achievements_locals.leaderboard_nearby_entries);
    s_achievements_locals.leaderboard_nearby_entries = nullptr;
  }

  if (s_achievements_locals.leaderboard_fetch_handle)
  {
    rc_client_abort_async(Achievements::GetClient(), s_achievements_locals.leaderboard_fetch_handle);
    s_achievements_locals.leaderboard_fetch_handle = nullptr;
  }

  s_achievements_locals.open_leaderboard = nullptr;
  s_achievements_locals.has_fetched_all_leaderboard_entries = false;
  s_achievements_locals.is_showing_all_leaderboard_entries = false;
}

#endif // __ANDROID__
