UI/HomeScreen.razor
@using System.Threading.Tasks

@namespace sGBA

@inherits PanelComponent

<root class="@RootClass">
	<div class="home-screen">
		<HomeBackdrop Current=@_currentBackdrop Previous=@_previousBackdrop PreviousOpacity=@PreviousBackdropOpacity />

		@if (ToastVisible)
		{
			<HomeToast Title=@_toast.Title Message=@_toast.Message Icon=@_toast.Icon ColorClass=@_toast.ColorClass InlineStyle=@ToastStyle />
		}

		@if (_allSoftwareOpen)
		{
			<div class="all-software-screen">
				<div class="all-software-rule top" />
				<div class="all-software-rule bottom" />

				<div class="all-software-scroll" @ref=AllSoftwareScroll>
					<div class="all-software-grid" style="@AllSoftwareGridStyle">
					@foreach (var gridItem in AllSoftwareGridItems)
					{
						Texture gridCover = null;
						if (gridItem.Game != null)
							_boxArt.TryGetValue(gridItem.Game.Path, out gridCover);

						<AllSoftwareCard @key="gridItem.Key"
							[email protected]
							Cover=@gridCover
							Selected=@IsAllSoftwareSelected(gridItem.Index)
							PositionStyle=@AllSoftwareGridItemStyle(gridItem.Row, gridItem.Column)
							CardLeft=@(gridItem.Column * (AllSoftwareCardWidth + AllSoftwareColumnGap))
							CardWidth=@AllSoftwareCardWidth
							GridWidth=@AllSoftwareGridWidth
							ShowTitleOnTop=@ShouldShowAllSoftwareTitleOnTop(gridItem.Index)
							OnActivate=@(() => ActivateAllSoftwareItemWithMouse(gridItem.Index)) />
					}

					<div class="all-add-row" style="@AllSoftwareAddRowStyle">
						<div class="@AllSoftwareAddButtonClass" onmouseenter=@UseMouseInAllSoftware onclick=@ActivateAllSoftwareAdd>
							<SelectionRing Active=@IsAllSoftwareAddSelected StrokeWidth=@(8f) CornerRadius=@(10f) Gap=@(0f) class="all-add-button-ring" />
							<div class="all-add-button-fill" />
							<div class="all-add-button-label">#addgames.card.label</div>
						</div>
					</div>
					</div>
				</div>

				@if (AllSoftwareShowsScrollbar)
				{
					<div class="all-scrollbar" @ref=AllSoftwareScrollbar onmousedown=@OnAllSoftwareScrollbarMouseDown>
						<div class="all-scrollbar-thumb" @ref=AllSoftwareScrollbarThumb style="@AllSoftwareScrollbarThumbStyle" />
					</div>
				}
			</div>
		}
		else
		{
			<div class="profile-avatar" style="background-image: url( avatar:@LocalSteamId )" />

			@if (!IsHomeViewMoreSelected)
			{
				<HomeLogo Logo=@SelectedLogo Title=@SelectedTitle />
			}

			<HomeCarousel Items=@CarouselItems SelectedIndex=@_selectedIndex NavFocused=@_navFocused ShowSelection=@ShowHomeSelection RenderVersion=@_homeCarouselRenderVersion ArtworkVersion=@_artworkVersion MountedRange=@CarouselMountedRange Suppress=@_suppressHomeCarouselTransitions CoverFor=@((GameEntry g) => _boxArt.GetValueOrDefault(g.Path)) OnActivate=@((int index) => ActivateCarouselItem(index)) />

			<HomeNavPill Items=@NavItems SelectedIndex=@_navSelection Focused=@_navFocused ShowSelection=@(ShowHomeSelection && _input.UseGamepad && _navFocused) OnHover=@((int i) => SelectNavItem(i, useGamepad: false)) OnActivate=@((int i) => ActivateNavItem(i, useGamepad: false)) />
		}

	</div>
</root>

@code
{
	public static HomeScreen Current { get; private set; }

	private List<GameEntry> _games = new();
	private List<GameEntry> _allGames = new();
	private List<GameEntry> _homeItems = new();
	private HashSet<string> _knownPaths = new();
	private float _pollTimer;
	private const float PollInterval = 3f;
	private Dictionary<string, Texture> _boxArt = new();
	private Dictionary<string, Texture> _logos = new();
	private Dictionary<string, Texture> _snaps = new();
	private Dictionary<string, Texture> _titles = new();
	private int _artworkVersion;
	private int _artworkRequestId;
	private int _selectedIndex;
	private int _allSoftwareSelection;
	private bool _homeOrderDirty;
	private bool _suppressHomeCarouselTransitions;
	private int _homeCarouselTransitionSuppressionFrames;
	private int _homeCarouselRenderVersion;
	private float _homeCarouselSettleRemaining;
	private GameEntry _queuedHomeLaunchGame;
	private string _pendingFrozenHomeLaunchPath;
	private int _pendingFrozenHomeLaunchFrames;
	private string _pendingHomeSelectionPath;
	private int _navSelection = 2;
	private bool _navFocused;
	private bool _isVisible = true;
	private bool _allSoftwareOpen;
	private bool _addGamesModalOpen;
	private readonly HomeToastState _toast = new();
	private const int HomeGameLimit = 12;
	private const int CarouselMountedRange = 9;
	private const float BackdropRetainDuration = 0.32f;
	private Texture _currentBackdrop;
	private Texture _previousBackdrop;
	private float _previousBackdropAge;
	private bool _previousBackdropFading;
	private int _backdropVersion;
	private readonly List<PendingTextureDispose> _pendingTextureDisposals = new();
	private const float HomeCarouselSettleDuration = 0.2f;

	private sealed record ArtworkRequest(GameEntry Game, ThumbType Kind, Dictionary<string, Texture> Target);
	private sealed record ArtworkLoad(ArtworkRequest Request, Texture Texture);
	private readonly record struct PendingTextureDispose(string Key, Texture Texture, float Age);

	private readonly NavItem[] NavItems =
	[
		new("group", "orange", "#nav.netplay"),
		new("newspaper", "green", "#nav.news"),
		new("cloud", "blue", "#nav.cloud"),
		new("settings", "gray", "#nav.settings"),
		new("power_settings_new", "gray", "#nav.quit")
	];

	private readonly FocusInput _input = new();

	public bool IsVisible => _isVisible;
	private string RootClass => (IsVisible ? "visible" : "") + (_suppressHomeCarouselTransitions ? " suppress-home-carousel-transitions" : "");
	public bool AllSoftwareOpen => _allSoftwareOpen;
	public bool NavigationFocused => _navFocused;
	public bool HasSelectedGame => SelectedGame != null;
	public bool CanOpenDetails => !_navFocused && SelectedGame != null;
	public bool HasHomeAction => IsHomeViewMoreSelected;
	public string PrimaryActionLabel => _allSoftwareOpen || IsHomeViewMoreSelected ? "#prompt.ok" : "#prompt.start";
	private long LocalSteamId => Connection.Local is null ? 0L : Connection.Local.SteamId;
	private int HomeGameCount => Math.Min(_homeItems.Count, HomeGameLimit);
	private int HomeEntryCount => HomeGameCount + 1;
	private int HomeViewMoreIndex => HomeGameCount;
	private bool IsHomeViewMoreSelected => _selectedIndex == HomeViewMoreIndex;
	private bool AddGamesModalOpen => _addGamesModalOpen || AddGamesModal.Current?.IsVisible == true;
	private bool SettingsModalOpen => SettingsModal.Current?.IsVisible == true;
	private bool ShowHomeSelection => !AddGamesModalOpen && !SettingsModalOpen;
	private bool ToastBlocked => AddGamesModalOpen || SettingsModalOpen || DetailsModal.Current?.IsVisible == true;
	private bool ToastVisible => _toast.IsVisible(ToastBlocked);
	private string ToastStyle => _toast.GetStyle(ToastVisible);
	private GameEntry SelectedGame => _allSoftwareOpen
		? _allSoftwareSelection >= 0 && _allSoftwareSelection < _allGames.Count ? _allGames[_allSoftwareSelection] : null
		: !IsHomeViewMoreSelected && _selectedIndex >= 0 && _selectedIndex < _homeItems.Count ? _homeItems[_selectedIndex] : null;
	private string SelectedTitle => SelectedGame?.DisplayTitle ?? "sGBA";
	private Texture SelectedLogo => SelectedGame != null && _logos.TryGetValue(SelectedGame.Path, out var logo) ? logo : null;
	private Texture SelectedBackdrop => SelectedGame != null && _snaps.TryGetValue(SelectedGame.Path, out var snap) ? snap : SelectedGame != null && _boxArt.TryGetValue(SelectedGame.Path, out var cover) ? cover : null;
	private float PreviousBackdropOpacity => _previousBackdrop == null ? 0f : _previousBackdropFading ? 0f : 1f;

	private IEnumerable<HomeCarouselEntry> CarouselItems
	{
		get
		{
			if (HomeEntryCount == 0)
				yield break;

			int selected = _selectedIndex.Clamp(0, HomeEntryCount - 1);
			int first = Math.Max(0, selected - CarouselMountedRange);
			int last = Math.Min(HomeEntryCount - 1, selected + CarouselMountedRange);
			for (int i = first; i <= last; i++)
			{
				bool isViewMore = i == HomeViewMoreIndex;
				yield return new HomeCarouselEntry(isViewMore ? null : _homeItems[i], i, i - selected, isViewMore, isViewMore ? "view-more" : _homeItems[i].Path);
			}
		}
	}

	protected override void OnTreeFirstBuilt()
	{
		Current = this;
		EnsureInputHintsPanel();
		_games = GameEntry.Discover();
		_knownPaths = GameEntry.GetInstalledPaths();
		RebuildGameLists(preserveSelection: false);
		SyncBackdropTexture();
		QueueArtworkRefresh();
	}

	private void QueueArtworkRefresh()
	{
		var requestId = ++_artworkRequestId;
		_ = LoadArtworkAsync(requestId);
	}

	private async Task LoadArtworkAsync(int requestId)
	{
		var visibleTask = LoadVisibleCarouselThumbnailsAsync();
		var selectedTask = LoadSelectedFeatureArtAsync();

		await visibleTask;
		await selectedTask;

		if (requestId != _artworkRequestId)
			return;

		var artworkVersion = _artworkVersion;
		PruneArtworkTextures();
		if (_artworkVersion != artworkVersion)
			StateHasChanged();
	}

	private Task<bool> LoadSelectedFeatureArtAsync()
	{
		var game = SelectedGame;
		if (game == null)
			return Task.FromResult(false);

		return LoadArtworkBatchAsync([
			new ArtworkRequest(game, ThumbType.Snap, _snaps),
			new ArtworkRequest(game, ThumbType.Logo, _logos),
			new ArtworkRequest(game, ThumbType.Title, _titles)
		]);
	}

	private Task<bool> LoadVisibleCarouselThumbnailsAsync()
	{
		var homeRequests = CarouselItems
			.Where(item => item.Game != null)
			.Select(item => new ArtworkRequest(item.Game, ThumbType.BoxArt, _boxArt));

		var allSoftwareRequests = _allSoftwareOpen
			? AllSoftwareVisibleItems.Select(item => new ArtworkRequest(item.Game, ThumbType.BoxArt, _boxArt))
			: [];

		var requests = homeRequests
			.Concat(allSoftwareRequests)
			.ToList();

		return LoadArtworkBatchAsync(requests);
	}

	private async Task<bool> LoadArtworkBatchAsync(IReadOnlyList<ArtworkRequest> requests)
	{
		var pending = requests
			.Where(request => request.Game != null && !request.Target.ContainsKey(request.Game.Path))
			.ToList();

		if (pending.Count == 0)
			return false;

		foreach (var request in pending)
			CancelPendingTextureDispose(GetArtworkKey(request.Kind, request.Game.Path));

		var changed = false;
		var loadTasks = pending.Select(LoadArtworkAsync).ToList();
		while (loadTasks.Count > 0)
		{
			var completedTask = await Task.WhenAny(loadTasks);
			loadTasks.Remove(completedTask);

			if (!ApplyArtworkLoad(await completedTask))
				continue;

			changed = true;
			_artworkVersion++;
			StateHasChanged();
		}

		return changed;
	}

	private static async Task<ArtworkLoad> LoadArtworkAsync(ArtworkRequest request)
	{
		var texture = await Thumbnails.LoadAsync(request.Game, request.Kind);
		return new ArtworkLoad(request, texture);
	}

	private bool ApplyArtworkLoad(ArtworkLoad load)
	{
		var game = load.Request.Game;
		var kind = load.Request.Kind;
		var target = load.Request.Target;
		var texture = load.Texture;

		if (texture == null || game == null)
			return false;

		CancelPendingTextureDispose(GetArtworkKey(kind, game.Path));

		if (target.ContainsKey(game.Path))
		{
			return false;
		}

		if (!ShouldRetainArtwork(game, kind))
		{
			ScheduleTextureDispose(GetArtworkKey(kind, game.Path), texture);
			return false;
		}

		target[game.Path] = texture;
		if (game == SelectedGame)
			SyncBackdropTexture();

		return true;
	}

	private bool ShouldRetainArtwork(GameEntry game, ThumbType kind)
	{
		if (game == null)
			return false;

		if (game == SelectedGame)
			return true;

		return kind == ThumbType.BoxArt && RetainedBoxArtPaths().Contains(game.Path);
	}

	private HashSet<string> RetainedBoxArtPaths()
	{
		var retained = CarouselItems
			.Where(item => IsCarouselOffsetMounted(item.Offset))
			.Where(item => item.Game != null)
			.Select(item => item.Game.Path)
			.ToHashSet();

		if (_allSoftwareOpen)
		{
			foreach (var item in AllSoftwareVisibleItems)
			{
				if (item.Game != null)
					retained.Add(item.Game.Path);
			}
		}

		return retained;
	}

	private HashSet<string> RetainedFeatureArtworkPaths()
	{
		var retained = new HashSet<string>();
		if (SelectedGame != null)
			retained.Add(SelectedGame.Path);
		return retained;
	}

	private void PruneArtworkTextures()
	{
		PruneArtworkDictionary(_boxArt, ThumbType.BoxArt, RetainedBoxArtPaths());
		var retainedFeatureArtwork = RetainedFeatureArtworkPaths();
		PruneArtworkDictionary(_logos, ThumbType.Logo, retainedFeatureArtwork);
		PruneArtworkDictionary(_snaps, ThumbType.Snap, retainedFeatureArtwork);
		PruneArtworkDictionary(_titles, ThumbType.Title, retainedFeatureArtwork);
	}

	private void PruneArtworkDictionary(Dictionary<string, Texture> textures, ThumbType kind, HashSet<string> retainedPaths)
	{
		foreach (var (path, texture) in textures.ToList())
		{
			if (retainedPaths.Contains(path))
				continue;

			if (texture == _currentBackdrop || texture == _previousBackdrop)
				continue;

			textures.Remove(path);
			ScheduleTextureDispose(GetArtworkKey(kind, path), texture);
			_artworkVersion++;
		}
	}

	private static string GetArtworkKey(ThumbType kind, string path)
	{
		return $"{kind}:{path}";
	}

	private void ScheduleTextureDispose(string key, Texture texture)
	{
		if (texture == null)
			return;

		_pendingTextureDisposals.RemoveAll(item => item.Key == key || item.Texture == texture);
		_pendingTextureDisposals.Add(new PendingTextureDispose(key, texture, 0f));
	}

	private void CancelPendingTextureDispose(string key)
	{
		_pendingTextureDisposals.RemoveAll(item => item.Key == key);
	}

	private void UpdatePendingTextureDisposals()
	{
		if (_pendingTextureDisposals.Count == 0)
			return;

		for (int i = _pendingTextureDisposals.Count - 1; i >= 0; i--)
		{
			var pending = _pendingTextureDisposals[i];
			var age = pending.Age + Time.Delta;
			if (age < 0.5f)
			{
				_pendingTextureDisposals[i] = pending with { Age = age };
				continue;
			}

			if (!IsArtworkTextureStillReferenced(pending.Texture))
				Thumbnails.ReleaseTexture(pending.Texture);

			_pendingTextureDisposals.RemoveAt(i);
		}
	}

	private bool IsArtworkTextureStillReferenced(Texture texture)
	{
		if (texture == null)
			return false;

		if (texture == _currentBackdrop || texture == _previousBackdrop)
			return true;

		return _boxArt.ContainsValue(texture) || _logos.ContainsValue(texture) || _snaps.ContainsValue(texture);
	}

	public void Show()
	{
		if (_homeOrderDirty)
		{
			RebuildGameLists(preserveSelection: false);
			SelectPendingHomeGame();
			_homeOrderDirty = false;
			_pendingHomeSelectionPath = null;
			_suppressHomeCarouselTransitions = true;
			_homeCarouselTransitionSuppressionFrames = 2;
			Panel?.SkipTransitions();
			SyncBackdropTexture();
			QueueArtworkRefresh();
		}

		_isVisible = true;
		_navFocused = false;
		_input.Begin(useGamepad: false);
		Sound.Play("ui.popup.message.open");
		EmulatorComponent.Current?.Unload();
		StateHasChanged();
	}

	public void Hide(bool update = true)
	{
		_isVisible = false;
		Mouse.Visibility = MouseVisibility.Hidden;
		if (update)
			StateHasChanged();
	}

	private void ActivateCarouselItem(int index)
	{
		if (IsHomeViewMoreIndex(index))
		{
			SelectCarouselItem(index, _input.UseGamepad);
			OpenAllSoftware();
			return;
		}

		bool shouldLaunch = !_navFocused && index == _selectedIndex;
		SelectCarouselItem(index, _input.UseGamepad);

		if (shouldLaunch && SelectedGame != null)
		{
			if (_homeCarouselSettleRemaining > 0f)
			{
				_queuedHomeLaunchGame = SelectedGame;
				return;
			}

			LaunchGame(SelectedGame);
		}
	}

	private void OpenAddGamesModal(bool useGamepad = false)
	{
		Sound.Play("ui.button.press");
		AddGamesModal addGamesPanel = EnsureAddGamesPanel();
		if (addGamesPanel == null)
			return;

		_addGamesModalOpen = true;
		StateHasChanged();
		addGamesPanel.Open(useGamepad);
	}

	public void OnAddGamesModalClosed(bool useGamepad)
	{
		_addGamesModalOpen = false;
		if (useGamepad)
			_input.ForceGamepadMode();
		else
			_input.ForceMouseMode();

		EnsureAllSoftwareSelectionVisible();
		StateHasChanged();
	}

	private AddGamesModal EnsureAddGamesPanel()
	{
		if (AddGamesModal.Current != null && AddGamesModal.Current.IsValid())
			return AddGamesModal.Current;

		AddGamesModal addGamesPanel = Scene.GetAllComponents<AddGamesModal>().FirstOrDefault(panel => panel.IsValid());
		if (addGamesPanel != null)
			return addGamesPanel;

		return AddComponent<AddGamesModal>();
	}

	private void LaunchGame(GameEntry game)
	{
		var emulator = EmulatorComponent.Current;
		if (!emulator.IsValid())
			return;

		GamePlayHistory.MarkPlayed(game.Path);
		_homeOrderDirty = true;
		_suppressHomeCarouselTransitions = true;
		_pendingHomeSelectionPath = game.Path;

		if (_allSoftwareOpen)
		{
			_allSoftwareOpen = false;
			Sound.Play("ui.button.press");
			Hide();
			emulator.Restart(game.Path);
			return;
		}

		Sound.Play("ui.button.press");
		_suppressHomeCarouselTransitions = true;
		_homeCarouselTransitionSuppressionFrames = 4;
		Panel?.SkipTransitions();
		_homeCarouselRenderVersion++;
		_pendingFrozenHomeLaunchPath = game.Path;
		_pendingFrozenHomeLaunchFrames = 1;
		StateHasChanged();
	}

	private void CompleteFrozenHomeLaunch()
	{
		if (string.IsNullOrWhiteSpace(_pendingFrozenHomeLaunchPath))
			return;

		var path = _pendingFrozenHomeLaunchPath;
		_pendingFrozenHomeLaunchPath = null;
		_pendingFrozenHomeLaunchFrames = 0;

		var emulator = EmulatorComponent.Current;
		if (!emulator.IsValid())
			return;

		Hide(update: false);
		emulator.Restart(path);
	}

	private void RebuildGameLists(bool preserveSelection = true)
	{
		bool hadHomeGames = _homeItems.Count > 0;
		var selectedGame = preserveSelection ? SelectedGame : null;
		bool selectedViewMore = preserveSelection && hadHomeGames && IsHomeViewMoreSelected;

		_allGames = [.._games];
		_homeItems = [.._games
			.OrderByDescending(game => GamePlayHistory.LastPlayedAt(game.Path))
			.ThenBy(game => game.DisplayTitle, StringComparer.OrdinalIgnoreCase)];

		if (!preserveSelection)
		{
			_selectedIndex = 0;
		}
		else if (selectedGame != null && !selectedViewMore)
		{
			int homeIndex = _homeItems.IndexOf(selectedGame);
			if (homeIndex >= 0)
				_selectedIndex = homeIndex;
		}
		else if (selectedViewMore)
		{
			_selectedIndex = HomeViewMoreIndex;
		}

		ClampSelectedIndex();
	}

	private void SelectPendingHomeGame()
	{
		if (string.IsNullOrWhiteSpace(_pendingHomeSelectionPath))
			return;

		int homeIndex = _homeItems.FindIndex(game => string.Equals(game.Path, _pendingHomeSelectionPath, StringComparison.OrdinalIgnoreCase));
		_selectedIndex = homeIndex >= 0 && homeIndex < HomeGameCount ? homeIndex : HomeViewMoreIndex;
	}

	private void ClampSelectedIndex()
	{
		if (HomeEntryCount == 0)
		{
			_selectedIndex = 0;
		}
		else
		{
			_selectedIndex = _selectedIndex.Clamp(0, HomeEntryCount - 1);
		}

		_allSoftwareSelection = _allSoftwareSelection.Clamp(0, AllSoftwareAddIndex);
		EnsureAllSoftwareSelectionVisible();
	}

	private void SetGamepadMode()
	{
		_input.ForceGamepadMode();
	}

	private void SyncVisibilityWithEmulator()
	{
		var emu = EmulatorComponent.Current;
		bool running = emu.IsValid() && emu.IsReady && !string.IsNullOrEmpty(emu.RomPath);
		if (running && _isVisible) Hide();
		else if (!running && !_isVisible) Show();
	}

	private SettingsModal EnsureSettingsPanel()
	{
		if (SettingsModal.Current != null && SettingsModal.Current.IsValid())
			return SettingsModal.Current;

		SettingsModal settingsPanel = Scene.GetAllComponents<SettingsModal>().FirstOrDefault(panel => panel.IsValid());
		if (settingsPanel != null)
			return settingsPanel;

		return AddComponent<SettingsModal>();
	}

	private InputHints EnsureInputHintsPanel()
	{
		if (InputHints.Current != null && InputHints.Current.IsValid())
			return InputHints.Current;

		InputHints inputHintsPanel = Scene.GetAllComponents<InputHints>().FirstOrDefault(panel => panel.IsValid());
		if (inputHintsPanel != null)
			return inputHintsPanel;

		return AddComponent<InputHints>();
	}

	private void OpenSettings(bool useGamepad = false)
	{
		if (useGamepad)
			SetGamepadMode();
		else
			_input.End();

		_navFocused = true;
		_navSelection = 3;
		Sound.Play("ui.button.press");
		SettingsModal settingsPanel = EnsureSettingsPanel();
		settingsPanel?.Show(useGamepad);
	}

	public void FocusHeaderAction(int headerSelection, bool useGamepad)
	{
		_navFocused = true;
		_navSelection = headerSelection.Clamp(0, NavItems.Length - 1);
		if (useGamepad) _input.ForceGamepadMode();
		else _input.End();
		StateHasChanged();
	}

	public void RestoreInputMode(bool useGamepad)
	{
		if (useGamepad) _input.ForceGamepadMode();
		else _input.ForceMouseMode();
		StateHasChanged();
	}

	public void FocusSettingsAction(bool useGamepad)
	{
		FocusHeaderAction(3, useGamepad);
	}

	protected override void OnUpdate()
	{
		SyncVisibilityWithEmulator();
		if (!string.IsNullOrWhiteSpace(_pendingFrozenHomeLaunchPath))
		{
			if (_pendingFrozenHomeLaunchFrames > 0)
			{
				_pendingFrozenHomeLaunchFrames--;
				return;
			}

			CompleteFrozenHomeLaunch();
			return;
		}

		if (_homeCarouselSettleRemaining > 0f)
		{
			_homeCarouselSettleRemaining = MathF.Max(0f, _homeCarouselSettleRemaining - Time.Delta);
			if (_homeCarouselSettleRemaining <= 0f && _queuedHomeLaunchGame != null)
			{
				var queuedLaunch = _queuedHomeLaunchGame;
				_queuedHomeLaunchGame = null;
				LaunchGame(queuedLaunch);
				return;
			}
		}

		if (_isVisible && _suppressHomeCarouselTransitions)
		{
			if (_homeCarouselTransitionSuppressionFrames > 0)
			{
				_homeCarouselTransitionSuppressionFrames--;
			}
			else
			{
				_suppressHomeCarouselTransitions = false;
				StateHasChanged();
			}
		}

		UpdateBackdropTransition();
		UpdatePendingTextureDisposals();
		UpdateToast();

		_pollTimer += Time.Delta;
		if (_pollTimer >= PollInterval)
		{
			_pollTimer = 0f;
			var current = GameEntry.GetInstalledPaths();
			if (!current.SetEquals(_knownPaths))
			{
				_knownPaths = current;
				_games = GameEntry.Discover();
				RebuildGameLists();
				QueueArtworkRefresh();
				StateHasChanged();
			}
		}

		if (!_isVisible) return;
		if (SettingsModal.Current?.IsVisible == true) return;
		if (DetailsModal.Current?.IsVisible == true) return;
		if (AddGamesModal.Current?.IsVisible == true) return;
		if (new Game.Overlay().IsOpen) return;

		UpdateAllSoftwareScrollState();

		var nav = _input.TickRepeating();
		if (nav.Up) { SetGamepadMode(); NavigateUp(); }
		if (nav.Down) { SetGamepadMode(); NavigateDown(); }
		if (nav.Left) { SetGamepadMode(); NavigateLeft(); }
		if (nav.Right) { SetGamepadMode(); NavigateRight(); }

		if (Input.Pressed("GBA_A") || Input.Pressed("GBA_Start"))
		{
			SetGamepadMode();
			if (_allSoftwareOpen) ActivateAllSoftwareItem(_allSoftwareSelection);
			else if (_navFocused) ActivateNavItem(_navSelection, useGamepad: true);
			else ActivateCarouselItem(_selectedIndex);
		}

		if (Input.Pressed("GBA_B") && _allSoftwareOpen)
		{
			SetGamepadMode();
			CloseAllSoftware();
		}

		if (Input.Pressed("GBA_B") && _navFocused)
		{
			SetGamepadMode();
			_navFocused = false;
			Sound.Play("ui.button.over");
		}

		if (Input.Pressed("GBA_Select") && CanOpenDetails)
		{
			SetGamepadMode();
			OpenSelectedDetails();
		}
	}

	private void NavigateUp()
	{
		if (_allSoftwareOpen)
		{
			NavigateAllSoftwareVertical(-1);
			return;
		}

		if (_navFocused)
		{
			_navFocused = false;
			Sound.Play("ui.button.over");
		}
	}

	private void NavigateDown()
	{
		if (_allSoftwareOpen)
		{
			NavigateAllSoftwareVertical(1);
			return;
		}

		if (!_navFocused)
		{
			_navFocused = true;
			Sound.Play("ui.button.over");
		}
	}

	private void NavigateLeft()
	{
		if (_allSoftwareOpen)
		{
			NavigateAllSoftwareHorizontal(-1);
			return;
		}

		if (_navFocused)
		{
			if (_navSelection > 0)
			{
				_navSelection--;
				Sound.Play("ui.button.over");
			}
			return;
		}

		SelectCarouselItem(_selectedIndex - 1, useGamepad: true);
	}

	private void NavigateRight()
	{
		if (_allSoftwareOpen)
		{
			NavigateAllSoftwareHorizontal(1);
			return;
		}

		if (_navFocused)
		{
			if (_navSelection < NavItems.Length - 1)
			{
				_navSelection++;
				Sound.Play("ui.button.over");
			}
			return;
		}

		SelectCarouselItem(_selectedIndex + 1, useGamepad: true);
	}

	private void SelectCarouselItem(int index, bool useGamepad)
	{
		if (HomeEntryCount == 0)
			return;

		var next = index.Clamp(0, HomeEntryCount - 1);
		if (_selectedIndex == next && !_navFocused)
			return;

		_selectedIndex = next;
		_navFocused = false;
		_queuedHomeLaunchGame = null;
		_homeCarouselSettleRemaining = HomeCarouselSettleDuration;
		if (useGamepad)
			SetGamepadMode();

		Sound.Play("ui.button.over");
		QueueArtworkRefresh();
		SyncBackdropTexture();
		StateHasChanged();
	}

	private bool IsHomeViewMoreIndex(int index)
	{
		return index == HomeViewMoreIndex;
	}

	private void UpdateBackdropTransition()
	{
		SyncBackdropTexture();

		if (_previousBackdrop == null)
			return;

		if (_currentBackdrop == null && !_previousBackdropFading)
		{
			_previousBackdropFading = true;
			_backdropVersion++;
			StateHasChanged();
			return;
		}

		_previousBackdropAge += Time.Delta;
		if (_previousBackdropAge < BackdropRetainDuration)
			return;

		_previousBackdrop = null;
		PruneArtworkTextures();
		_backdropVersion++;
		StateHasChanged();
	}

	private void SyncBackdropTexture()
	{
		var next = SelectedBackdrop;
		if (next == _currentBackdrop)
			return;

		if (_currentBackdrop != null)
			_previousBackdrop = _currentBackdrop;

		_currentBackdrop = next;
		_previousBackdropAge = 0f;
		_previousBackdropFading = false;
		_backdropVersion++;
	}

	private void SelectNavItem(int index, bool useGamepad)
	{
		_navSelection = index.Clamp(0, NavItems.Length - 1);
		_navFocused = true;
		if (useGamepad)
			SetGamepadMode();
		else
			_input.ForceMouseMode();
		StateHasChanged();
	}

	private void ActivateNavItem(int index, bool useGamepad)
	{
		SelectNavItem(index, useGamepad);

		switch (index)
		{
			case 0:
				ShowUnavailableToast("#toast.netplay.title", "#toast.netplay.message", "group", NavItems[index].ColorClass);
				break;
			case 1:
				ShowUnavailableToast("#toast.news.title", "#toast.news.message", "newspaper", NavItems[index].ColorClass);
				break;
			case 2:
				ShowUnavailableToast("#toast.cloud.title", "#toast.cloud.message", "cloud", NavItems[index].ColorClass);
				break;
			case 3:
				OpenSettings(useGamepad);
				break;
			case 4:
				Sound.Play("ui.button.press");
				Game.Close();
				break;
		}
	}

	private void ShowUnavailableToast(string title, string message, string icon, string colorClass)
	{
		_toast.Show(title, message, icon, colorClass);
		Sound.Play("ui.button.press");
		StateHasChanged();
	}

	public void ShowToast(string title, string message, string icon = "info", string colorClass = "blue")
	{
		_toast.Show(title, message, icon, colorClass);
		StateHasChanged();
	}

	private void UpdateToast()
	{
		if (!_toast.IsRunning)
			return;

		_toast.Tick(Time.Delta);
		StateHasChanged();
	}

	private void OpenSelectedDetails()
	{
		if (!CanOpenDetails)
			return;

		DetailsModal detailsPanel = EnsureDetailsPanel();
		detailsPanel?.Open(SelectedGame, _input.UseGamepad);
		StateHasChanged();
	}

	public void RefreshControllerHints()
	{
		InputHints.Current?.Refresh();
	}

	private DetailsModal EnsureDetailsPanel()
	{
		if (DetailsModal.Current != null && DetailsModal.Current.IsValid())
			return DetailsModal.Current;

		DetailsModal detailsPanel = Scene.GetAllComponents<DetailsModal>().FirstOrDefault(panel => panel.IsValid());
		if (detailsPanel != null)
			return detailsPanel;

		return AddComponent<DetailsModal>();
	}

	private static bool IsCarouselOffsetMounted(int offset)
	{
		return offset >= -CarouselMountedRange && offset <= CarouselMountedRange;
	}

	protected override int BuildHash()
	{
		if (_isVisible)
			return HashCode.Combine(
				HashCode.Combine(_selectedIndex, _allSoftwareSelection, (int)AllSoftwareScrollY, _allSoftwareOpen),
				HashCode.Combine(AddGamesModalOpen, SettingsModalOpen, _allGames.Count, _homeItems.Count, _suppressHomeCarouselTransitions),
				HashCode.Combine(_homeCarouselRenderVersion, _input.UseGamepad, _artworkVersion, _backdropVersion),
				_allSoftwareTitleDirection,
				HashCode.Combine(_navFocused, _navSelection, _toast.RenderHash));

		return HashCode.Combine(_isVisible);
	}

	protected override void OnDestroy()
	{
		if (Current == this)
			Current = null;
		DisposeArtworkTextures();
		_currentBackdrop = null;
		_previousBackdrop = null;
	}

	private void DisposeArtworkTextures()
	{
		var textures = _boxArt.Values.Concat(_logos.Values).Concat(_snaps.Values).Concat(_titles.Values).Concat(_pendingTextureDisposals.Select(item => item.Texture)).Where(texture => texture != null).ToHashSet();
		foreach (var texture in textures)
			Thumbnails.ReleaseTexture(texture);

		_boxArt.Clear();
		_logos.Clear();
		_snaps.Clear();
		_titles.Clear();
		_pendingTextureDisposals.Clear();
	}
}