UI/CharacterPicker.razor
@using System
@using Sandbox
@using Sandbox.UI
@namespace BlockParty
@inherits Panel
@attribute [StyleSheet( "CharacterPicker.razor.scss" )]

<root class="@(LeaderboardMode ? "leaderboard" : "") @LockedBounceClass" style="@TriggerRootStyle()">
	<button class="picker-trigger @(TriggerLocked ? "locked" : "") @LockedVisualsClass @LockedShakeClass" title="@(TriggerLocked ? "Character locked for this challenge" : "Choose character")" style="@TriggerStyle()" onclick=@Open onmouseover=@TriggerTip>
		<div class="preview @(Current.Partner is null ? "single" : "paired") @(IsOverall ? "overall" : "")" style="@TriggerViewportStyle()">
			@if ( !IsOverall && Current.Partner is CharacterDef currentPartner )
			{
				<div class="sprite-layer rear" style="@TriggerLayerStyle( currentPartner )"></div>
			}
			<div class="sprite-layer front" style="@TriggerLayerStyle( Current )"></div>
		</div>
		@if ( TriggerLocked )
		{
			<div class="trigger-lock"></div>
		}
		else if ( !LeaderboardMode && NewCharacterCount > 0 )
		{
			<div class="new-badge"><label>@NewCharacterCount</label></div>
		}
	</button>
	@if ( LeaderboardMode && !IsOverall )
	{
		<button class="clear-selection" title="Show all characters" onclick=@ClearSelection onmouseover=@ClearTip>
			<div class="icon">close</div>
		</button>
	}

	@if ( _open )
	{
		<div class="overlay" style="@OverlayStyle()">
			<div class="dismiss" onclick=@Close></div>
			<div class="panel" style="@PanelStyle()">
				<label class="title">@Title</label>
				<div class="character-grid">
					@for ( int row = 0; row < RowCount; row++ )
					{
						<div class="character-row" style="@CharacterRowStyle()">
							@for ( int column = 0; column < Columns; column++ )
							{
								int index = row * Columns + column;
								if ( index < Characters.All.Count )
								{
									var character = Characters.All[index];
									bool unlocked = CharacterProgress.IsUnlocked( character.Id );
									bool isNew = CharacterProgress.IsNew( character.Id );
									<button class="character @(unlocked ? "unlocked" : "locked") @(character.Id == Selected.Id ? "selected" : "")" style="@CharacterButtonStyle()"
										onclick=@(() => Pick( character )) onrightclick=@(() => ToggleLock( character ))>
										<div class="character-sprite @(character.Partner is null ? "single" : "paired") @(isNew ? "new-character" : "")" style="@PreviewViewportStyle( GridPreviewWidth, GridPreviewHeight )">
											@if ( unlocked )
											{
												<div class="icon-glow"></div>
											}
											@if ( character.Partner is CharacterDef partner )
											{
												<div class="sprite-layer rear" style="@PreviewLayerStyle( partner, GridPreviewWidth, GridPreviewHeight )"></div>
											}
											<div class="sprite-layer front" style="@PreviewLayerStyle( character, GridPreviewWidth, GridPreviewHeight )"></div>
										</div>
										@if ( unlocked )
										{
											<label class="character-name">@character.Name</label>
										}
									</button>
								}
							}
						</div>
					}
				</div>
			</div>
		</div>
	}
</root>

@code
{
	private const int Columns = 4;
	[Parameter] public Func<string> GetSelectedId { get; set; }
	[Parameter] public Action<string> OnPick { get; set; }
	[Parameter] public Action OnClear { get; set; }

	/// <summary>Hover handlers for the trigger / clear-filter buttons, so a host can show its own
	/// tooltip bubble (the layer belongs to the SCREEN — see LeaderboardRow.WatchTip). Null = none.</summary>
	[Parameter] public Action<PanelEvent> TriggerTip { get; set; }
	[Parameter] public Action<PanelEvent> ClearTip { get; set; }
	[Parameter] public Action<bool> OnOpenChanged { get; set; }
	[Parameter] public string Title { get; set; } = "WHO ARE YOU?";
	[Parameter] public bool LeaderboardMode { get; set; }
	[Parameter] public bool CenterOverlayOnScreen { get; set; }
	[Parameter] public string DisplayCharacterId { get; set; }
	[Parameter] public bool TriggerLocked { get; set; }
	[Parameter] public bool LockedVisualsVisible { get; set; } = true;
	[Parameter] public bool LockedEnterWithoutFade { get; set; }
	[Parameter] public float ScreenBottom { get; set; } = 60f;
	[Parameter] public float HostMarginRight { get; set; } = 600f;

	private bool _open;
	private int _lockedShakeVersion;
	private int RowCount => (Characters.All.Count + Columns - 1) / Columns;
	private string SelectedId => GetSelectedId is not null ? GetSelectedId() : Settings.Current.SelectedCharacterId;
	private bool IsOverall => LeaderboardMode && string.IsNullOrEmpty( SelectedId );
	private CharacterDef Selected => CharacterProgress.ResolveSelectable( SelectedId );
	private CharacterDef Current => Characters.TryGet( DisplayCharacterId, out var display ) ? display : Selected;
	private string LockedVisualsClass => !TriggerLocked || !LockedVisualsVisible
		? ""
		: LockedEnterWithoutFade ? "locked-visuals" : "locked-visuals locked-fade-in";
	private string LockedBounceClass => TriggerLocked && LockedVisualsVisible ? "locked-entry-bounce" : "";
	private string LockedShakeClass => !TriggerLocked || _lockedShakeVersion == 0
		? ""
		: _lockedShakeVersion % 2 == 0 ? "locked-shake-a" : "locked-shake-b";
	private int NewCharacterCount => CharacterProgress.NewCharacterCount;
	private const float GridPreviewWidth = 110f;
	private const float GridPreviewHeight = 120f;

	private const float TriggerVisiblePixels = 12f;
	private float TriggerPreviewSize => LeaderboardMode ? 108f : 88f;
	private float TriggerPixelSize => TriggerPreviewSize / MathF.Max( Player.ART_SIZE.x, Player.ART_SIZE.y );
	private float TriggerViewportSize => TriggerVisiblePixels * TriggerPixelSize;
	private float TriggerWidth => TriggerViewportSize + 12f;
	private float MaxPreviewScale
	{
		get
		{
			float maximum = 1f;
			foreach ( var character in Characters.All )
			{
				maximum = MathF.Max( maximum, character.PreviewScale );
				if ( character.Partner is not null )
					maximum = MathF.Max( maximum, character.Partner.PreviewScale );
			}
			return maximum;
		}
	}

	private void Open()
	{
		if ( TriggerLocked )
		{
			_lockedShakeVersion++;
			Audio.PlaySfx( SfxType.StickyBlocked );
			return;
		}

		_open = true;
		Audio.PlaySfx( SfxType.MenuStart );
		if ( LeaderboardMode )
			Haptics.MenuBlip();
		OnOpenChanged?.Invoke( true );
	}

	private void Close()
	{
		_open = false;
		OnOpenChanged?.Invoke( false );
	}

	public void RequestClose() => Close();

	private void Pick( CharacterDef character )
	{
		if ( !CharacterProgress.IsUnlocked( character.Id ) )
		{
			Audio.PlaySfx( SfxType.StickyBlocked );
			return;
		}

		SetSelection( character.Id );
		Audio.PlaySfx( SfxType.MenuBlip );
		Haptics.MenuBlip();
		Close();
	}

	private void ClearSelection()
	{
		OnClear?.Invoke();
		Audio.PlaySfx( SfxType.MenuBlip );
		Haptics.MenuBlip();
	}

	private void ToggleLock( CharacterDef character )
	{
		if ( !Game.IsEditor )
			return;

		// if ( !CharacterProgress.ToggleUnlocked( character.Id ) )
		// {
		// 	Audio.PlaySfx( SfxType.StickyBlocked );
		// 	return;
		// }

		// if ( !IsOverall && !CharacterProgress.IsUnlocked( SelectedId ) )
		// 	SetSelection( CharacterProgress.ResolveSelectable( SelectedId ).Id );

		// Audio.PlaySfx( SfxType.MenuBlip );
	}

	private void SetSelection( string characterId )
	{
		if ( OnPick is not null )
			OnPick( characterId );
		else
		{
			Settings.Current.SelectedCharacterId = characterId;
			Settings.Save();
		}

		// Only a real play-selection consumes a character's "new" status; filtering the
		// leaderboard by character must not clear the badge/glow nudge to try them.
		if ( !LeaderboardMode )
			CharacterProgress.MarkSelected( characterId );
	}

	private string TriggerRootStyle()
		=> $"width:{Px( TriggerWidth )};height:{Px( TriggerWidth )};";

	private string TriggerStyle()
		=> $"width:{Px( TriggerWidth )};height:{Px( TriggerWidth )};";

	private string TriggerViewportStyle()
		=> $"width:{Px( TriggerViewportSize )};height:{Px( TriggerViewportSize )};";

	private string TriggerLayerStyle( CharacterDef character )
	{
		float width = character.ArtSize.x * TriggerPixelSize;
		float height = character.ArtSize.y * TriggerPixelSize;
		return $"left:{Px( (TriggerViewportSize - width) * 0.5f )};top:{Px( (TriggerViewportSize - height) * 0.5f )};"
			+ $"width:{Px( width )};height:{Px( height )};background-image:url( {character.PreviewImage} );";
	}

	private string PreviewViewportStyle( float baseWidth, float baseHeight )
		=> $"width:{Px( baseWidth * MaxPreviewScale )};height:{Px( baseHeight * MaxPreviewScale )};";

	private string PreviewLayerStyle( CharacterDef character, float baseWidth, float baseHeight )
	{
		float width = baseWidth * character.PreviewScale;
		float height = baseHeight * character.PreviewScale;
		float left = (baseWidth * MaxPreviewScale - width) * 0.5f;
		float top = (baseHeight * MaxPreviewScale - height) * 0.5f;
		return $"left:{Px( left )};top:{Px( top )};width:{Px( width )};height:{Px( height )};"
			+ $"background-image:url( {character.PreviewImage} );";
	}

	private string CharacterButtonStyle()
	{
		float extraWidth = GridPreviewWidth * (MaxPreviewScale - 1f);
		float extraHeight = GridPreviewHeight * (MaxPreviewScale - 1f);
		return $"width:{Px( 118f + extraWidth )};height:{Px( 180f + extraHeight )};";
	}

	private string CharacterRowStyle()
	{
		float extraWidth = GridPreviewWidth * (MaxPreviewScale - 1f);
		float extraHeight = GridPreviewHeight * (MaxPreviewScale - 1f);
		return $"width:{Px( 620f + Columns * extraWidth )};height:{Px( 185f + extraHeight )};";
	}

	private string PanelStyle()
	{
		float extra = MaxPreviewScale - 1f;
		float paddingY = MathF.Max( 16f, 42f - 78f * extra );
		return $"height:{Px( 900f + RowCount * GridPreviewHeight * extra )};"
			+ $"padding-top:{Px( paddingY )};padding-bottom:{Px( paddingY )};";
	}

	protected override int BuildHash()
		=> HashCode.Combine( SelectedId, DisplayCharacterId, TriggerLocked, LockedVisualsVisible,
			HashCode.Combine( LockedEnterWithoutFade, _lockedShakeVersion,
			HashCode.Combine( _open, CharacterProgress.Version, Characters.All.Count,
			HashCode.Combine( ScreenBottom, HostMarginRight, Title, LeaderboardMode, CenterOverlayOnScreen,
				CenterOverlayOnScreen ? (int)Box.Rect.Left : 0,
				CenterOverlayOnScreen ? (int)Box.Rect.Top : 0 ) ) ) );

	private string OverlayStyle()
	{
		if ( !CenterOverlayOnScreen )
			return $"bottom:{Px( -ScreenBottom )};left:calc( -50vw + {Px( HostMarginRight + TriggerWidth )} );";

		float scale = ScaleFromScreen;
		return $"left:{Px( -Box.Rect.Left * scale )};top:{Px( -Box.Rect.Top * scale )};";
	}

	private static string Px( float value ) => $"{value:0.##}px";
}