Networking/NetworkManager.cs
using Emulotl.Gba;
using System.Collections.Concurrent;
using Sandbox.Network;

namespace Emulotl;

public sealed class NetworkManager : Component, IWirelessNetwork, Component.INetworkListener
{
	public const int MaxWirelessPlayers = 5;
	public const int MaxLinkPlayers = 4;

	private const int BroadcastClientId = 0xFFFF;

	public int PlayerCap => Mode == SessionMode.LinkCable ? MaxLinkPlayers : MaxWirelessPlayers;

	public static NetworkManager Current { get; private set; }

	public event Action JoinRejected;

	public SessionState State { get; private set; } = SessionState.Solo;
	public SessionMode Mode { get; private set; } = SessionMode.LinkCable;
	public SessionVisibility Visibility { get; private set; } = SessionVisibility.InviteOnly;
	public string GameTitle { get; private set; }
	public string GameSha1 { get; private set; }
	public string GameCode { get; private set; }

	public IReadOnlyList<SessionPlayer> Players => _roster.Players;
	public SessionPlayer LocalPlayer => _roster.LocalPlayer;

	public bool IsActive => State != SessionState.Solo;
	public bool IsHost => Networking.IsHost && IsActive;
	public bool IsClient => IsActive && !Networking.IsHost;

	public bool InMultiplayerSession => _roster.PlayerCount > 1;
	public bool AllReady
	{
		get
		{
			var ps = _roster.Players;
			return ps.Count > 1 && ps.All( p => p.Ready || p.IsHost );
		}
	}
	public bool CanStart => IsHost && AllReady && State == SessionState.Hosting;

	private readonly SessionRoster _roster = new( MaxWirelessPlayers );

	private EmulatorComponent _emulator;
	private readonly ConcurrentQueue<(int Slot, byte[] Payload)> _outbox = new();

	public LinkCableMode Link { get; private set; }
	private WirelessAdapterMode _wireless;
	private INetMode ActiveMode => Mode == SessionMode.LinkCable ? Link : _wireless;

	internal SessionRoster Roster => _roster;
	internal EmulatorComponent Emulator => _emulator;
	internal void SetState( SessionState state ) => State = state;
	internal void CommitRoster() => HostCommitRoster();
	internal void BroadcastBeginGame() => RpcBeginGame();

	internal void SendJoinLinkedGame( Connection conn )
	{
		using ( Rpc.FilterInclude( conn ) )
			RpcJoinLinkedGame();
	}

	internal void SendRaw( int slot, byte[] data ) => ((IWirelessNetwork)this).Send( slot, data, data.Length );

	protected override void OnAwake()
	{
		Current = this;
		Link = new LinkCableMode( this );
		_wireless = new WirelessAdapterMode( this );
	}

	protected override void OnStart()
	{
		_emulator = Scene.GetAllComponents<EmulatorComponent>().FirstOrDefault();
	}

	protected override void OnUpdate()
	{
		if ( Networking.IsActive && Networking.IsHost && State == SessionState.Solo )
			State = SessionState.Hosting;

		while ( _outbox.TryDequeue( out var pkt ) )
			DispatchSend( pkt.Slot, pkt.Payload );

		DetectHostLeft();
		HostUpdateMode();

		ActiveMode?.Tick();
	}

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

	private string _modeGameCode;

	private void HostUpdateMode()
	{
		if ( !Networking.IsHost )
			return;

		var code = EmulatorComponent.Current?.GameCode;
		if ( string.IsNullOrEmpty( code ) || code == _modeGameCode )
			return;

		_modeGameCode = code;
		GameCode = code;

		var mode = GameLibrary.DetectMode( code );
		if ( mode == Mode )
			return;

		Mode = mode;
		try { Networking.SetData( LobbyDataKeys.Mode, ((int)mode).ToString() ); }
		catch { }
		RpcSyncMode( (int)mode );
	}

	[Rpc.Broadcast( NetFlags.Reliable | NetFlags.SendImmediate )]
	private void RpcSyncMode( int mode )
	{
		if ( Networking.IsHost )
			return;
		Mode = (SessionMode)mode;
	}

	public bool Host( GameEntry rom, SessionVisibility visibility )
	{
		if ( IsActive )
			return false;
		if ( rom is null )
			return false;

		Visibility = visibility;
		Mode = GameLibrary.DetectMode( rom.GameCode );
		GameTitle = rom.DisplayTitle;
		GameCode = rom.GameCode;
		GameSha1 = GameLibrary.ComputeSha1( rom );

		var config = new LobbyConfig
		{
			MaxPlayers = PlayerCap,
			Name = $"{rom.DisplayTitle}",
			Privacy = ToLobbyPrivacy( visibility ),
			Hidden = visibility == SessionVisibility.InviteOnly,
			DestroyWhenHostLeaves = true,
			AutoSwitchToBestHost = false
		};

		Networking.CreateLobby( config );
		State = SessionState.Hosting;

		Networking.SetData( LobbyDataKeys.GameTitle, GameTitle ?? string.Empty );
		Networking.SetData( LobbyDataKeys.GameCode, GameCode ?? string.Empty );
		Networking.SetData( LobbyDataKeys.GameSha1, GameSha1 ?? string.Empty );
		Networking.SetData( LobbyDataKeys.Visibility, ((int)Visibility).ToString() );
		Networking.SetData( LobbyDataKeys.Mode, ((int)Mode).ToString() );
		Networking.SetData( LobbyDataKeys.HostName, Connection.Local?.DisplayName ?? string.Empty );

		HostCommitRoster();

		return true;
	}

	public bool Join( ulong lobbyId )
	{
		if ( IsActive )
			return false;

		Networking.Connect( lobbyId );
		State = SessionState.Joined;
		return true;
	}

	public void Leave()
	{
		if ( !IsActive )
			return;

		if ( IsHost )
			RpcHostLeaving();

		Link.Exit();
		Networking.Disconnect();
		ClearSessionState();
	}

	[Rpc.Broadcast( NetFlags.Reliable | NetFlags.SendImmediate )]
	private void RpcHostLeaving()
	{
		if ( Networking.IsHost )
			return;

		ActiveMode?.OnHostLeft();
	}

	private void ClearSessionState()
	{
		_roster.Clear();
		State = SessionState.Solo;
		Mode = SessionMode.WirelessAdapter;
		GameTitle = null;
		GameSha1 = null;
		GameCode = null;
		_modeGameCode = null;
		_joinedGameLoaded = false;
		_hadRemoteHost = false;
	}

	private bool _hadRemoteHost;

	private void DetectHostLeft()
	{
		var host = Connection.Host;
		bool hasRemoteHost = Networking.IsActive && host is not null && host != Connection.Local && host.IsActive;

		if ( !_hadRemoteHost )
		{
			_hadRemoteHost = hasRemoteHost;
			return;
		}

		if ( hasRemoteHost )
			return;

		_hadRemoteHost = false;
		ActiveMode?.OnHostLeft();
	}

	public string ResolveSessionRomPath() => GameLibrary.FindMatch( GameSha1, GameCode )?.Path;

	public void ClearSessionAfterHostLost()
	{
		Link.Exit();
		Networking.Disconnect();
		ClearSessionState();
	}

	public void SetReady( bool ready )
	{
		if ( !IsActive )
			return;
		if ( IsHost )
			return;

		RpcSetReady( ready );
	}

	public void StartGame()
	{
		if ( !CanStart )
			return;
		RpcBeginGame();
	}

	public void OnActive( Connection conn )
	{
		if ( !Networking.IsHost && State == SessionState.Solo )
		{
			State = SessionState.Joined;
			LoadSessionFromLobbyData();
		}

		if ( !Networking.IsHost )
			return;

		_roster.AssignSlot( conn, PlayerCap );

		if ( conn is not null && conn != Connection.Local )
		{
			using ( Rpc.FilterInclude( conn ) )
				RpcSyncSession( GameTitle ?? string.Empty, GameSha1 ?? string.Empty, GameCode ?? string.Empty, (int)Visibility, (int)Mode );
		}

		HostCommitRoster();
		ActiveMode?.OnPeerJoined( conn );
	}

	private void LoadSessionFromLobbyData()
	{
		var title = Networking.GetData( LobbyDataKeys.GameTitle );
		var sha1 = Networking.GetData( LobbyDataKeys.GameSha1 );
		var code = Networking.GetData( LobbyDataKeys.GameCode );

		if ( !string.IsNullOrEmpty( title ) )
			GameTitle = title;
		if ( !string.IsNullOrEmpty( sha1 ) )
			GameSha1 = sha1;
		if ( !string.IsNullOrEmpty( code ) )
			GameCode = code;

		var vis = Networking.GetData( LobbyDataKeys.Visibility );
		if ( int.TryParse( vis, out var v ) )
			Visibility = (SessionVisibility)v;

		var modeRaw = Networking.GetData( LobbyDataKeys.Mode );
		if ( int.TryParse( modeRaw, out var m ) )
			Mode = (SessionMode)m;

		TryLoadJoinedGame();
	}

	public void OnDisconnected( Connection conn )
	{
		if ( conn is not null )
			_roster.RemoveReady( conn.Id );

		if ( !Networking.IsHost )
			return;

		int slot = conn is not null && _roster.TryGetSlot( conn.Id, out var s ) ? s : -1;

		_roster.ReleaseSlot( conn );
		HostCommitRoster();
		ActiveMode?.OnPeerLeft( slot, conn );
	}

	private void HostCommitRoster()
	{
		if ( !Networking.IsHost )
			return;

		_roster.EnsureLocal( PlayerCap );
		_roster.RebuildLocalView();
		RpcSyncRoster( _roster.Serialize() );
	}

	[Rpc.Broadcast( NetFlags.Reliable | NetFlags.SendImmediate )]
	private void RpcSyncRoster( byte[] payload )
	{
		if ( Networking.IsHost || payload is null || payload.Length < 4 )
			return;

		_roster.Apply( payload );
	}

	[Rpc.Host( NetFlags.Reliable | NetFlags.SendImmediate )]
	private void RpcSetReady( bool ready )
	{
		var caller = Rpc.Caller;
		if ( caller is null )
			return;

		_roster.SetReady( caller.Id, ready );
		HostCommitRoster();
	}

	[Rpc.Broadcast( NetFlags.Reliable | NetFlags.SendImmediate )]
	private void RpcSyncSession( string gameTitle, string gameSha1, string gameCode, int visibility, int mode )
	{
		if ( Networking.IsHost )
			return;

		if ( !string.IsNullOrEmpty( gameTitle ) )
			GameTitle = gameTitle;
		if ( !string.IsNullOrEmpty( gameSha1 ) )
			GameSha1 = gameSha1;
		if ( !string.IsNullOrEmpty( gameCode ) )
			GameCode = gameCode;
		Visibility = (SessionVisibility)visibility;
		Mode = (SessionMode)mode;
		State = SessionState.Joined;

		TryLoadJoinedGame();
	}

	private bool _joinedGameLoaded;

	private void TryLoadJoinedGame()
	{
		if ( _joinedGameLoaded )
			return;

		if ( string.IsNullOrEmpty( GameSha1 ) && string.IsNullOrEmpty( GameCode ) )
			return;

		var match = GameLibrary.FindMatch( GameSha1, GameCode );
		if ( match is null )
		{
			Log.Warning( $"[Emulotl] Join: no matching game for '{GameTitle}' (code {GameCode}); returning to home." );
			Leave();
			JoinRejected?.Invoke();
			return;
		}

		_joinedGameLoaded = true;

		if ( Mode == SessionMode.LinkCable )
			return;

		EmulatorComponent.Current?.Restart( match.Path );
	}

	[Rpc.Broadcast( NetFlags.Reliable | NetFlags.SendImmediate )]
	private void RpcBeginGame()
	{
		State = SessionState.InGame;
		ActiveMode?.OnGameStarted();
	}

	[Rpc.Broadcast( NetFlags.Reliable | NetFlags.SendImmediate )]
	private void RpcJoinLinkedGame()
	{
		if ( Networking.IsHost )
			return;

		State = SessionState.InGame;
		Link.OnGameStarted();
	}

	private static LobbyPrivacy ToLobbyPrivacy( SessionVisibility v ) => v switch
	{
		SessionVisibility.Public => LobbyPrivacy.Public,
		SessionVisibility.FriendsOnly => LobbyPrivacy.FriendsOnly,
		SessionVisibility.InviteOnly => LobbyPrivacy.Private,
		_ => LobbyPrivacy.Private
	};

	void IWirelessNetwork.Send( int clientId, byte[] data, int length )
	{
		if ( data is null || length <= 0 )
			return;

		var payload = new byte[length];
		Buffer.BlockCopy( data, 0, payload, 0, length );
		_outbox.Enqueue( (clientId, payload) );
	}

	private void DispatchSend( int slot, byte[] payload )
	{
		bool isAv = payload is not null && payload.Length > 0
			&& payload[0] == (byte)LinkCablePacketType.Av;

		if ( slot == BroadcastClientId )
		{
			if ( isAv )
				RpcReceiveAv( payload );
			else
				RpcReceive( payload );
			return;
		}

		var target = _roster.ConnectionForSend( slot );
		if ( target is null || target == Connection.Local )
			return;

		using ( Rpc.FilterInclude( target ) )
		{
			if ( isAv )
				RpcReceiveAv( payload );
			else
				RpcReceive( payload );
		}
	}

	[Rpc.Broadcast( NetFlags.Reliable | NetFlags.SendImmediate )]
	private void RpcReceive( byte[] payload )
	{
		HandleReceive( payload );
	}

	[Rpc.Broadcast( NetFlags.UnreliableNoDelay )]
	private void RpcReceiveAv( byte[] payload )
	{
		HandleReceive( payload );
	}

	private void HandleReceive( byte[] payload )
	{
		if ( Rpc.Caller == Connection.Local )
			return;

		if ( payload is null || payload.Length < 12 )
			return;

		if ( !_roster.TryGetSlot( Rpc.Caller.Id, out var senderIndex ) )
			return;

		ActiveMode?.OnPacket( senderIndex, payload );
	}
}