Stages/TitleStage.cs
namespace BlockParty;

/// <summary>
/// Title screen: the game's entry point. Port of the original <c>TitleStage</c> menu —
/// "BLOCK PARTY" with PLAY / DAILY CHALLENGE / LEADERBOARDS / REPLAYS / EDITOR / OPTIONS / QUIT.
/// The title name is drawn from the original CP437 bitmap font (see <see cref="TitleScreen"/>);
/// the options are a horizontal row of square icon buttons sized by importance, peaking at the
/// centred PLAY (see <see cref="DisplayOrder"/>), with a text label under the row naming the selection.
/// Navigation logic lives here; the panel is a thin view bound to <c>SelectedIndex</c> /
/// <c>FadeAlpha</c>.
/// </summary>
public sealed class TitleStage : MenuStageBase
{
	public readonly string[] Options = { "PLAY", "DAILY CHALLENGE", "LEADERBOARDS", "REPLAYS", "EDITOR", "OPTIONS", "QUIT", "WORKSHOP & EDITOR" };
	public int SelectedIndex { get; private set; }

	public const int PlayOptionIndex = 0;
	public const int DailyOptionIndex = 1;
	public const int LeaderboardsOptionIndex = 2;
	public const int LocalReplaysOptionIndex = 3;
	public const int EditorOptionIndex = 4;
	public const int SettingsOptionIndex = 5;
	public const int QuitOptionIndex = 6;
	public const int WorkshopOptionIndex = 7;

	/// <summary>Left-to-right on-screen button order: PLAY (the largest) dead centre,
	/// WORKSHOP+DAILY at its left hand, LEADERBOARDS/REPLAYS at its right, OPTIONS alone on the
	/// far-left wing and QUIT alone on the far-right one. EDITOR left this row — it lives on the
	/// workshop screen now (creations are made to be published). The two sides mirror in total width
	/// (360px each around PLAY) and in tier: DAILY/LEADERBOARDS both primary, OPTIONS/QUIT tertiary
	/// with the same 24px group gap (see the SCSS), so the hero sits on the exact centreline.
	/// Keyboard nav and the razor's render loop both walk THIS order, not the Options index
	/// order.</summary>
	public static readonly int[] DisplayOrder =
	{
		SettingsOptionIndex,
		WorkshopOptionIndex, DailyOptionIndex, PlayOptionIndex, LeaderboardsOptionIndex, LocalReplaysOptionIndex,
		QuitOptionIndex,
	};

	/// <summary>Whether today's daily challenge has never been launched — LOCAL attempts file only
	/// (one launched run counts, even with attempts left). Drives the button's attention flash.</summary>
	public bool DailyUnattempted => !DailyChallengeProgress.IsAttempted( DailyChallenge.TodayId );

	/// <summary>Which of the calendar icon's 16 date dots carries today's unattempted mark: row-major
	/// cell (top-left → bottom-right), advancing one per day and wrapping, so the mark walks the whole
	/// calendar over 16 days. Derived from <see cref="DailyChallenge.TodayId"/> so it uses the daily's
	/// own UTC day boundary and every player sees the mark on the same dot. Day count comes from
	/// Ticks — the sandbox whitelist doesn't allow DateOnly.</summary>
	public static int DailyMarkCell => DailyChallenge.TryParseId( DailyChallenge.TodayId, out var date )
		? (int)(date.Ticks / System.TimeSpan.TicksPerDay % 16)
		: 0;

	// Hold-to-repeat nav, shared cadence with the lists/options. The options are a horizontal row
	// of image buttons (no selector cube), so the primary axis is A/D — see Tick.
	private readonly NavRepeater _nav = new();

	private TitleScreen _screen;

	public TitleStage( GameManager manager ) : base( manager )
	{
	}

	protected override void OnEnter()
	{
		Audio.PlayMusic();
		SpawnBackgroundBlocks();

		var go = CreateUiRoot();
		_screen = go.Components.Create<TitleScreen>();
		_screen.Stage = this;
	}

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

		// Ignore navigation once we've committed to a stage change (matches original's exit lock).
		if ( !IsFadingOut )
		{
			// The buttons are a left-to-right row: A/D is the natural axis, but W/S keeps working
			// (old muscle memory from the text list) — up = previous, down = next. Both hold-to-repeat.
			int step = _nav.Tick( dt, InputState.Left || InputState.NavUp, InputState.Right || InputState.NavDown );
			if ( step != 0 )
			{
				Navigate( step );

				// Landed on (or tapped against) the end of the row: stop the held repeat so holding
				// toward no button doesn't keep re-firing the nav blip (same rule as the level map's
				// dead ends). A release or reversal re-arms it.
				int pos = Array.IndexOf( DisplayOrder, SelectedIndex );
				if ( pos + step < 0 || pos + step >= DisplayOrder.Length )
					_nav.SuppressRepeat();
			}
			if ( InputState.ConfirmJust ) Activate();
		}
	}

	/// <summary>Keyboard/pad navigation: step through the on-screen (DisplayOrder) positions, not
	/// the Options indices — left/right must match what the eye sees. Blips either way; no cube.</summary>
	private void Navigate( int delta )
	{
		int pos = Array.IndexOf( DisplayOrder, SelectedIndex );
		SelectedIndex = DisplayOrder[MenuNav.Step( pos, delta, DisplayOrder.Length )];
	}

	/// <summary>Called by the panel on hover/click to sync the highlighted option.</summary>
	public void SelectOption( int index )
	{
		// Ignore hover once we've committed to a stage change (same exit lock as keyboard nav in
		// Tick) — mousing across rows during the fade-out would move the highlight and play blips.
		if ( IsFadingOut )
			return;

		int next = Math.Clamp( index, 0, Options.Length - 1 );
		if ( next == SelectedIndex )
			return;

		SelectedIndex = next;
		Audio.PlaySfx( SfxType.MenuBlip );
	}

	/// <summary>Activate the current option (Confirm key or click).</summary>
	public void Activate()
	{
		if ( IsFadingOut )
			return;

		Audio.PlaySfx( SfxType.MenuStart );
		Haptics.MenuConfirm();

		switch ( SelectedIndex )
		{
			case PlayOptionIndex: FadeToStage( new LevelSelectStage( Manager ) { FadeInMusic = true } ); break;
			case DailyOptionIndex: FadeToStage( new DailyChallengeStage( Manager ) ); break;
			case LeaderboardsOptionIndex: FadeToStage( new HighscoreStage( Manager ) ); break;
			case LocalReplaysOptionIndex: FadeToStage( new LocalReplaysStage( Manager ) ); break;
			case EditorOptionIndex: LevelEditorStage.OpenLevelEditor(); break;
			case SettingsOptionIndex: FadeToStage( new OptionsStage( Manager ) ); break;
			case QuitOptionIndex: Game.Close(); break;
			case WorkshopOptionIndex: FadeToStage( new WorkshopStage( Manager ) ); break;
		}
	}

	/// <summary>Editor button (bottom-left letterbox, editor-only): open the level editor.
	/// Locked during the exit wipe like every other input path.</summary>
	public void ClickEditor()
	{
		if ( IsFadingOut )
			return;

		Audio.PlaySfx( SfxType.MenuStart, 0.7f );
		LevelEditorStage.OpenLevelEditor( "" );
	}
}