Stages/HighscoreStage.cs
namespace BlockParty;

/// <summary>
/// High-score screen. Port of the original <c>HighscoreStage</c>, but instead of a locally
/// stored top-10 with 3-letter initials it shows the online sbox leaderboard — top 100
/// real players with Steam name + avatar (see <see cref="Leaderboard"/>). Reached from the
/// title's LEADERBOARD option and the level-select footer's trophy button (finished runs
/// return straight to the map).
/// </summary>
public sealed class HighscoreStage : MenuStageBase
{
	/// <summary>Row to focus when the board opens, e.g. the run we just watched (-1 = default/BACK).</summary>
	public int ReturnSelection { get; }

	/// <summary>True when BACK should return to level select instead of the title screen.</summary>
	public bool BackToLevelSelect { get; }

	/// <summary>True when BACK should return to the workshop browser (a workshop level's board).</summary>
	public bool BackToWorkshop { get; }

	/// <summary>Set when this board was opened from a run's score tally (its trophy button): BACK — and
	/// a replay launched from here — rebuilds that tally, already counted out. Null otherwise.</summary>
	public ScoreReturn ReturnScore { get; }

	/// <summary>Set when this stage shows a WORKSHOP level's board: the screen reads its level from
	/// here instead of the persisted last-viewed filter (a ws id must never become that — the normal
	/// leaderboard's level picker can't select it back off). Null on every normal board.</summary>
	public string WorkshopLevelId { get; }

	private HighscoreScreen _screen;

	public HighscoreStage( GameManager manager, string levelId = null, int returnSelection = -1, bool backToLevelSelect = false, bool backToWorkshop = false, ScoreReturn returnScore = null ) : base( manager )
	{
		ReturnSelection = returnSelection;
		BackToLevelSelect = backToLevelSelect;
		BackToWorkshop = backToWorkshop;
		ReturnScore = returnScore;

		if ( WorkshopLevels.IsWorkshopId( levelId ) )
		{
			WorkshopLevelId = levelId;

			// Same rule as the explicit-level arrival below: opening a level's board opens its OVERALL
			// board. Without this, a character filter left on the normal leaderboard silently applies
			// here — typically "NO ENTRIES YET" on a level that has scores.
			if ( Settings.Current.LeaderboardCharacterId is not null )
			{
				Settings.Current.LeaderboardCharacterId = null;
				Settings.Save();
			}
			return;
		}

		// Arriving with a level (the level-select footer's trophy button): open the OVERALL board
		// for that level, not a character-filtered one. These become the last-viewed filters.
		if ( levelId is not null
			&& (Settings.Current.LeaderboardLevelId != levelId
				|| Settings.Current.LeaderboardCharacterId is not null) )
		{
			Settings.Current.LeaderboardLevelId = levelId;
			Settings.Current.LeaderboardCharacterId = null;
			Settings.Save();
		}
	}

	/// <summary>The leaderboard sits on the dark transition fill (carried over from the score tally /
	/// grown over the menu), so the wipe square appears to persist as the background here.</summary>
	public override bool CoveredBackground => true;

	protected override void OnEnter()
	{
		var go = CreateUiRoot();
		_screen = go.Components.Create<HighscoreScreen>();
		_screen.Stage = this;
	}

	public override void Tick( float dt )
	{
		base.Tick( dt );

		if ( _screen?.ModalOpen == true )
		{
			if ( InputState.BackJust )
				_screen.CloseModal();
			return;
		}

		// Space/Confirm watches the selected run's replay; W/S (Up/Down) selection + key-repeat
		// is handled in the screen itself. Back returns to the screen that opened this board.
		if ( InputState.ConfirmJust )
			_screen?.LaunchSelected();

		if ( InputState.BackJust )
			GoBack();
	}

	public void GoBack() => FadeToStage(
		ReturnScore is not null ? ReturnScore.Create( Manager )
		: BackToWorkshop ? new WorkshopStage( Manager )
		: BackToLevelSelect ? new LevelSelectStage( Manager )
		: new TitleStage( Manager ) );
}