UI/Modals/NetplayModal.razor
@using System.Threading.Tasks
@using Sandbox.Network

@namespace Emulotl

@inherits PanelComponent

<root class="@(IsVisible ? "visible" : "")">
	<div class="modal-backdrop" onclick=@CloseWithMouse />

	<div class="netplay-modal">
		<div class="netplay-title">#netplay.title</div>
		<div class="netplay-subtitle">#netplay.sub</div>

		<div class="netplay-tabs">
			@foreach (var tab in Tabs)
			{
				var t = tab.Index;
				<div class="netplay-tab @(_tab == t ? "selected" : "")" onclick=@(() => ClickTab(t))>
					@if (RingActive(IsTabFocused(t)))
					{
						<TabRing class="netplay-tab-ring" />
					}
					@if (_tab == t)
					{
						<div class="netplay-tab-line" />
					}
					<div class="netplay-tab-label">@tab.Label</div>
				</div>
			}
		</div>

		<div class="dialog-body">
			@if (_tab == TabBrowse)
			{
				<div class="netplay-list fill">
					@if (_loading)
					{
						<div class="netplay-empty">#netplay.loading</div>
					}
					else if (_lobbies.Count == 0)
					{
						<div class="netplay-empty">#netplay.empty</div>
					}
					else
					{
						<div class="netplay-scroll">
							@for (int i = 0; i < _lobbies.Count; i++)
							{
								var idx = i;
								var lobby = _lobbies[idx];
								@if (idx > 0)
								{
									<div class="netplay-row-divider" />
								}
								<div class="netplay-lobby @(FocusClass(IsLobbyFocused(idx)))" onmouseenter=@(() => HoverLobby(idx)) onclick=@(() => ClickLobby(idx))>
									<SelectionRing Active=@(RingActive(IsLobbyFocused(idx))) StrokeWidth=@(8f) CornerRadius=@(8f) Gap=@(0f) Alpha=@_ringAlpha class="netplay-row-ring" />
									<div class="netplay-lobby-text">
										<div class="netplay-lobby-name">@lobby.GetGameTitle()</div>
										<div class="netplay-lobby-sub">
											<span>@lobby.GetHostName()</span>
											<span class="sep">·</span>
											<span>@ModeLabel(lobby)</span>
										</div>
									</div>
									<div class="netplay-lobby-count">@lobby.Members / @lobby.MaxMembers</div>
								</div>
							}
						</div>
					}
				</div>
			}
			else
			{
				<div class="netplay-list">
					<NetplayCycleRow Label="#netplay.game" Value=@HostGameLabel Active=@(RingActive(IsGameRowFocused)) RingAlpha=@_ringAlpha Hovered=@HoverGameRow Cycled=@CycleGameWithMouse />

					<div class="netplay-row-divider" />

					<NetplayCycleRow Label="#netplay.visibility" Value=@VisibilityLabel Active=@(RingActive(IsVisFocused)) RingAlpha=@_ringAlpha Hovered=@HoverVis Cycled=@CycleVisWithMouse />
				</div>
			}
		</div>

		<div class="netplay-rule" />

		<div class="dialog-actions">
			<div class="action-btn @(IsActionDisabled ? "disabled" : "") @(FocusClass(IsActionFocused))" onmouseenter=@HoverAction onclick=@ClickAction>
				<SelectionRing Active=@(RingActive(IsActionFocused)) StrokeWidth=@(8f) CornerRadius=@(43f) Gap=@(0f) FlatTop=@true Alpha=@_ringAlpha class="action-btn-ring" />
				<div class="action-btn-fill" />
				<div class="action-btn-label">@ActionLabel</div>
			</div>
		</div>
	</div>
</root>

@code
{
	private const int TabBrowse = 0;
	private const int TabHost = 1;

	private const int HostGameRow = 0;
	private const int HostVisRow = 1;
	private const int HostActionRow = 2;

	public static NetplayModal Current { get; private set; }
	public bool IsVisible { get; private set; }

	private readonly record struct TabInfo(int Index, string Label);
	private static readonly TabInfo[] Tabs =
	[
		new(TabBrowse, "#netplay.tab.browse"),
		new(TabHost, "#netplay.tab.host"),
	];

	private readonly record struct VisInfo(SessionVisibility Value, string Label);
	private static readonly VisInfo[] VisOptions =
	[
		new(SessionVisibility.Public, "#netplay.vis.public"),
		new(SessionVisibility.FriendsOnly, "#netplay.vis.friends"),
		new(SessionVisibility.InviteOnly, "#netplay.vis.invite"),
	];

	private int _tab = TabBrowse;
	private bool _onTabs = true;
	private int _focus;
	private float _ringAlpha;
	private bool _openedThisFrame;
	private readonly FocusInput _input = new();

	private readonly List<LobbyInformation> _lobbies = new();
	private bool _loading;
	private int _queryVersion;

	private List<GameEntry> _games = new();
	private int _hostGame = -1;
	private SessionVisibility _visibility = SessionVisibility.Public;

	private int ActionFocusIndex => _tab == TabBrowse ? _lobbies.Count : HostActionRow;
	private int ContentMax => ActionFocusIndex;
	private bool CanHost => _hostGame >= 0 && _hostGame < _games.Count;
	private bool IsActionDisabled => _tab == TabHost && !CanHost;
	private string ActionLabel => _tab == TabBrowse ? "#netplay.refresh" : "#netplay.hostbtn";

	private bool IsTabFocused(int t) => _onTabs && _tab == t;
	private bool IsLobbyFocused(int i) => !_onTabs && _tab == TabBrowse && _focus == i;
	private bool IsGameRowFocused => !_onTabs && _tab == TabHost && _focus == HostGameRow;
	private bool IsVisFocused => !_onTabs && _tab == TabHost && _focus == HostVisRow;
	private bool IsActionFocused => !_onTabs && _focus == ActionFocusIndex;

	private bool RingActive(bool focused) => focused && _input.UseGamepad;
	private string FocusClass(bool focused) => focused && _input.UseGamepad ? "focus" : string.Empty;

	private string HostGameLabel => CanHost ? _games[_hostGame].DisplayTitle : "#netplay.nogames";

	private string VisibilityLabel
	{
		get
		{
			int index = Array.FindIndex(VisOptions, o => o.Value == _visibility);
			return VisOptions[Math.Max(0, index)].Label;
		}
	}

	protected override void OnTreeFirstBuilt() => Current = this;
	protected override void OnDestroy() { if (Current == this) Current = null; }

	public void Show(bool useGamepad = false)
	{
		Current = this;
		IsVisible = true;
		_tab = TabBrowse;
		_onTabs = true;
		_focus = 0;
		_ringAlpha = 0f;
		_openedThisFrame = true;
		_input.Begin(useGamepad);
		Sound.Play("ui.popup.message.open");
		LoadGames();
		_ = RefreshLobbies();
		HomeScreen.Current?.RefreshControllerHints();
		StateHasChanged();
	}

	public void Hide()
	{
		if (!IsVisible)
			return;

		IsVisible = false;
		var wasGamepad = _input.UseGamepad;
		_input.End();
		HomeScreen.Current?.RestoreInputMode(wasGamepad);
		Sound.Play("ui.popup.message.close");
		HomeScreen.Current?.RefreshControllerHints();
		StateHasChanged();
	}

	private void LoadGames()
	{
		try { _games = GameEntry.Discover(); }
		catch { _games = new(); }

		if (_hostGame < 0 && _games.Count > 0)
			_hostGame = 0;
		if (_hostGame >= _games.Count)
			_hostGame = _games.Count - 1;
	}

	private async Task RefreshLobbies()
	{
		_loading = true;
		int version = ++_queryVersion;
		StateHasChanged();

		List<LobbyInformation> result;
		try { result = await Networking.QueryLobbies(); }
		catch { result = new(); }

		if (version != _queryVersion)
			return;

		_lobbies.Clear();
		if (result != null)
			_lobbies.AddRange(result);
		_loading = false;
		_focus = Math.Min(_focus, ContentMax);
		StateHasChanged();
	}

	private void SetTab(int tab)
	{
		if (_tab == tab)
			return;

		_tab = tab;
		_focus = 0;
		Sound.Play("ui.button.over");

		if (tab == TabBrowse)
			_ = RefreshLobbies();
		else
			LoadGames();

		StateHasChanged();
	}

	private void CycleGame(int dir)
	{
		if (_games.Count == 0)
			return;

		_hostGame = (_hostGame + dir + _games.Count) % _games.Count;
		Sound.Play("ui.button.over");
		StateHasChanged();
	}

	private void CycleVis(int dir)
	{
		int current = Array.FindIndex(VisOptions, o => o.Value == _visibility);
		if (current < 0)
			current = 0;
		current = (current + dir + VisOptions.Length) % VisOptions.Length;
		_visibility = VisOptions[current].Value;
		Sound.Play("ui.button.over");
		StateHasChanged();
	}

	private void JoinLobby(int index)
	{
		if (index < 0 || index >= _lobbies.Count)
			return;

		var net = NetworkManager.Current;
		if (net == null)
			return;

		Sound.Play("ui.button.press");
		net.Join(_lobbies[index].LobbyId);
		Hide();
	}

	private void DoHost()
	{
		if (!CanHost)
			return;

		var net = NetworkManager.Current;
		var emu = EmulatorComponent.Current;
		if (net == null || !emu.IsValid())
			return;

		var game = _games[_hostGame];
		Sound.Play("ui.button.press");
		net.Host(game, _visibility);
		emu.Restart(game.Path);
		Hide();
	}

	protected override void OnUpdate()
	{
		if (!IsVisible)
			return;
		if (new Game.Overlay().IsOpen)
			return;

		if (Input.EscapePressed)
		{
			Input.EscapePressed = false;
			Hide();
			return;
		}

		if (!_openedThisFrame)
		{
			bool wasGamepad = _input.UseGamepad;
			var nav = _input.TickRepeating();
			if (wasGamepad != _input.UseGamepad)
			{
				HomeScreen.Current?.RefreshControllerHints();
				StateHasChanged();
			}

			if (nav.Up) NavUp();
			if (nav.Down) NavDown();
			if (nav.Left) NavLeft();
			if (nav.Right) NavRight();

			if (Input.Pressed("UI_Accept")) Activate();
			if (Input.Pressed("UI_Back")) { _input.ForceGamepadMode(); Hide(); }
		}
		else
		{
			_openedThisFrame = false;
		}

		float target = _input.UseGamepad ? 1f : 0f;
		float next = MathX.Lerp(_ringAlpha, target, MathF.Min(1f, Time.Delta / 0.12f));
		if (MathF.Abs(next - target) < 0.01f) next = target;
		if (MathF.Abs(next - _ringAlpha) > 0.001f)
		{
			_ringAlpha = next;
			StateHasChanged();
		}
	}

	private void NavUp()
	{
		_input.ForceGamepadMode();
		if (_onTabs)
			return;

		if (_focus <= 0)
			_onTabs = true;
		else
			_focus--;

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

	private void NavDown()
	{
		_input.ForceGamepadMode();
		if (_onTabs)
		{
			_onTabs = false;
			_focus = 0;
		}
		else
		{
			_focus = Math.Min(_focus + 1, ContentMax);
		}

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

	private void NavLeft()
	{
		_input.ForceGamepadMode();
		if (_onTabs)
			SetTab(TabBrowse);
		else if (IsGameRowFocused)
			CycleGame(-1);
		else if (IsVisFocused)
			CycleVis(-1);
	}

	private void NavRight()
	{
		_input.ForceGamepadMode();
		if (_onTabs)
			SetTab(TabHost);
		else if (IsGameRowFocused)
			CycleGame(1);
		else if (IsVisFocused)
			CycleVis(1);
	}

	private void Activate()
	{
		_input.ForceGamepadMode();

		if (_onTabs)
		{
			_onTabs = false;
			_focus = 0;
			Sound.Play("ui.button.press");
			StateHasChanged();
			return;
		}

		if (_focus == ActionFocusIndex)
		{
			ActivateAction();
			return;
		}

		if (_tab == TabBrowse)
			JoinLobby(_focus);
		else if (_focus == HostGameRow)
			CycleGame(1);
		else if (_focus == HostVisRow)
			CycleVis(1);
	}

	private void ActivateAction()
	{
		if (IsActionDisabled)
			return;

		if (_tab == TabBrowse)
		{
			Sound.Play("ui.button.press");
			_ = RefreshLobbies();
		}
		else
		{
			DoHost();
		}
	}

	private void CloseWithMouse() { _input.ForceMouseMode(); Hide(); }
	private void ClickTab(int t) { _input.ForceMouseMode(); SetTab(t); }
	private void HoverLobby(int i) { _input.ForceMouseMode(); _onTabs = false; _focus = i; StateHasChanged(); }
	private void ClickLobby(int i) { _input.ForceMouseMode(); JoinLobby(i); }
	private void HoverGameRow() { _input.ForceMouseMode(); _onTabs = false; _focus = HostGameRow; StateHasChanged(); }
	private void HoverVis() { _input.ForceMouseMode(); _onTabs = false; _focus = HostVisRow; StateHasChanged(); }
	private void CycleGameWithMouse(int dir) { _input.ForceMouseMode(); CycleGame(dir); }
	private void CycleVisWithMouse(int dir) { _input.ForceMouseMode(); CycleVis(dir); }
	private void HoverAction() { _input.ForceMouseMode(); _onTabs = false; _focus = ActionFocusIndex; StateHasChanged(); }

	private void ClickAction()
	{
		_input.ForceMouseMode();
		ActivateAction();
	}

	private static string ModeLabel(LobbyInformation lobby) =>
		lobby.GetMode() == SessionMode.LinkCable ? "#netplay.mode.link" : "#netplay.mode.wireless";

	protected override int BuildHash()
	{
		int hash = HashCode.Combine(IsVisible, _tab, _onTabs, _focus, _loading, _hostGame);
		hash = HashCode.Combine(hash, (int)_visibility, _games.Count, _lobbies.Count, _input.UseGamepad, _ringAlpha);
		foreach (var lobby in _lobbies)
			hash = HashCode.Combine(hash, lobby.LobbyId, lobby.Members);
		return hash;
	}
}