UI/Screen/MapChooser.razor
@using Sandbox;
@using Sandbox.Network;
@using Sandbox.UI;
@using System.Threading.Tasks;
@using System
@inherits PanelComponent
@namespace Sandbox

<root>
	<div class="header">
		<h1>Choose a Map</h1>
		<div class="subtitle">Pick a map, set lobby options, then launch.</div>
	</div>

	<div class="content">
		<div class="map-list">
			<div class="section-title">Maps</div>
			<div class="map-entries">
				@foreach (var map in Maps)
				{
					var pkg = GetPackage( map );
					<button class="@GetMapClass(map)" onclick="@(() => SelectMap(map))">
						<div class="thumb" style="@GetThumbStyle(pkg)"></div>
						<div class="text">
							<div class="title">@GetPackageTitle(map, pkg)</div>
							<div class="ident">@map</div>
						</div>
					</button>
				}
			</div>
		</div>

		<div class="settings">
			<div class="section-title">Lobby Settings</div>

			<div class="field">
				<label>Lobby Name</label>
				<TextEntry @ref="LobbyNameEntry" placeholder="Skater Crew"></TextEntry>
			</div>

			<div class="field split">
				<div class="field">
					<label>Min Players</label>
					<TextEntry @ref="MinPlayersEntry" placeholder="1"></TextEntry>
				</div>
				<div class="field">
					<label>Max Players</label>
					<TextEntry @ref="MaxPlayersEntry" placeholder="8"></TextEntry>
				</div>
			</div>

			<div class="field">
				<label>Privacy</label>
				<div class="privacy">
					<button class="@GetPrivacyClass( LobbyPrivacy.Public )" onclick="@(() => SetPrivacy( LobbyPrivacy.Public ))">Public</button>
					<button class="@GetPrivacyClass( LobbyPrivacy.FriendsOnly )" onclick="@(() => SetPrivacy( LobbyPrivacy.FriendsOnly ))">Friends</button>
					<button class="@GetPrivacyClass( LobbyPrivacy.Private )" onclick="@(() => SetPrivacy( LobbyPrivacy.Private ))">Private</button>
				</div>
			</div>

			<div class="selected-map">
				<div class="label">Selected Map</div>
				<div class="value">@(_selectedMapIdent ?? "None")</div>
			</div>

			<button class="launch @GetLaunchClass()" onclick="@StartGame">Launch</button>
		</div>
	</div>
</root>

@code
{

	[Property, TextArea] public string MyStringValue { get; set; } = "Hello World!";

	/// <summary>
	/// the hash determines if the system should be rebuilt. If it changes, it will be rebuilt
	/// </summary>
	protected override int BuildHash() => System.HashCode.Combine(
		MyStringValue,
		_selectedMapIdent,
		_privacy,
		Maps?.Count ?? 0
	);

	[Property, MapAssetPath] public List<string> Maps { get; set; } = new();
	private string _selectedMapIdent;
	private LobbyPrivacy _privacy = LobbyPrivacy.Public;

	private TextEntry LobbyNameEntry;
	private TextEntry MinPlayersEntry;
	private TextEntry MaxPlayersEntry;
	private readonly Dictionary<string, Package> _packageCache = new( StringComparer.OrdinalIgnoreCase );
	private readonly HashSet<string> _pendingFetch = new( StringComparer.OrdinalIgnoreCase );
	private bool _fetching;
	private int _lastMapCount;

	protected override void OnStart()
	{
		QueueFetch();
	}

	protected override void OnUpdate()
	{
		var count = Maps?.Count ?? 0;
		if ( count != _lastMapCount )
		{
			_lastMapCount = count;
			QueueFetch();
		}
	}

	private void SelectMap( string map )
	{
		_selectedMapIdent = map;
		StateHasChanged();
	}

	private void SetPrivacy( LobbyPrivacy privacy )
	{
		_privacy = privacy;
		StateHasChanged();
	}

	private string GetPrivacyClass( LobbyPrivacy privacy )
	{
		return _privacy == privacy ? "active" : string.Empty;
	}

	private string GetLaunchClass()
	{
		return string.IsNullOrWhiteSpace( _selectedMapIdent ) ? "disabled" : string.Empty;
	}

	private void StartGame()
	{
		if ( string.IsNullOrWhiteSpace( _selectedMapIdent ) )
			return;

		var lobbyName = LobbyNameEntry?.Text?.Trim();
		var minPlayers = ParsePositiveInt( MinPlayersEntry?.Text, 1 );
		var maxPlayers = ParsePositiveInt( MaxPlayersEntry?.Text, 8 );
		if ( maxPlayers < minPlayers )
			maxPlayers = minPlayers;

		LaunchArguments.Map = _selectedMapIdent;
		LaunchArguments.MaxPlayers = maxPlayers;
		LaunchArguments.Privacy = _privacy;
		LaunchArguments.ServerName = string.IsNullOrWhiteSpace( lobbyName ) ? $"{Utility.Steam.PersonaName}'s Skate Server" : lobbyName;


		var options = new SceneLoadOptions();
		options.SetScene( "scenes/system.scene" );
		Game.ChangeScene( options );
	}

	private static int ParsePositiveInt( string value, int fallback )
	{
		if ( int.TryParse( value, out var parsed ) && parsed > 0 )
			return parsed;

		return fallback;
	}

	private Package GetPackage( string map )
	{
		if ( string.IsNullOrWhiteSpace( map ) )
			return null;

		_packageCache.TryGetValue( map, out var pkg );
		return pkg;
	}

	private string GetPackageTitle( string map, Package package )
	{
		if ( package is not null && !string.IsNullOrWhiteSpace( package.Title ) )
			return package.Title;

		return map;
	}

	private string GetThumbStyle( Package package )
	{
		var url = package?.Thumb;
		if ( string.IsNullOrWhiteSpace( url ) )
			return string.Empty;

		return $"background-image: url('{url}')";
	}

	private void QueueFetch()
	{
		if ( Maps is null )
			return;

		foreach ( var map in Maps )
		{
			if ( string.IsNullOrWhiteSpace( map ) )
				continue;

			if ( _packageCache.ContainsKey( map ) || _pendingFetch.Contains( map ) )
				continue;

			_pendingFetch.Add( map );
		}

		if ( _pendingFetch.Count == 0 )
			return;

		if ( _fetching )
			return;

		_ = FetchPackagesAsync();
	}

	private async Task FetchPackagesAsync()
	{
		if ( _fetching )
			return;

		_fetching = true;

		try
		{
			foreach ( var map in _pendingFetch.ToArray() )
			{
				var pkg = await Package.FetchAsync( map, partial: true );
				if ( pkg is not null )
					_packageCache[map] = pkg;

				_pendingFetch.Remove( map );
				StateHasChanged();
			}
		}
		finally
		{
			_fetching = false;
		}
	}
	private string GetMapClass( string map )
	{
		return map == _selectedMapIdent ? "active" : string.Empty;
	}
}