Networking/NetworkManager.LinkCable.cs
namespace sGBA;

public sealed partial class NetworkManager
{
	private const int HostClientId = 0;

	public LinkCableSession LinkSession { get; private set; }
	public Action<LinkCableAvPacket> OnRemoteAv;
	public Action<LinkCableStatePacket> OnRemoteState;

	public bool ExternalAvDriver { get; set; }

	public void AttachLinkSession( LinkCableSession session )
	{
		LinkSession = session;
	}

	public void DetachLinkSession()
	{
		LinkSession = null;
	}

	public void BeginLinkedPlay()
	{
		var emu = EmulatorComponent.Current;
		if ( emu == null )
			return;

		if ( IsHost )
			emu.BeginLinkedHost( HostActiveSlots() );
		else if ( IsClient )
			emu.BeginLinkedClient();
	}

	private bool _linkActive;
	private int _linkedMask;
	private int _pendingRosterMask = -1;
	private RealTimeSince _timeSinceRosterChange;
	private const float LinkRosterDebounceSeconds = 1.5f;

	public void UpdateLinkRoster()
	{
		if ( !Networking.IsHost )
			return;

		var emu = EmulatorComponent.Current;
		if ( emu == null )
			return;

		int mask = ActiveSlotMask();

		if ( mask != _pendingRosterMask )
		{
			_pendingRosterMask = mask;
			_timeSinceRosterChange = 0;
			return;
		}
		if ( _timeSinceRosterChange < LinkRosterDebounceSeconds )
			return;
		if ( mask == _linkedMask )
			return;

		int count = CountBits( mask );

		if ( !_linkActive )
		{
			if ( count < 2 )
				return;

			_linkActive = true;
			_linkedMask = mask;
			State = SessionState.InGame;
			RpcBeginGame();
			return;
		}

		if ( count < 2 )
		{
			_linkActive = false;
			_linkedMask = 0;
			ReturnHostToLobby();
			emu.ContinueSoloFromLinkedHost();
			return;
		}

		int additions = mask & ~_linkedMask;
		_linkedMask = mask;
		emu.SyncLinkedHostSlots( HostActiveSlots() );

		for ( int slot = 0; slot < _slotConns.Length; slot++ )
		{
			if ( (additions & (1 << slot)) == 0 )
				continue;
			var conn = _slotConns[slot];
			if ( conn is not null && conn != Connection.Local )
			{
				using ( Rpc.FilterInclude( conn ) )
					RpcJoinLinkedGame();
			}
		}
	}

	private void ReturnHostToLobby()
	{
		var local = Connection.Local;
		for ( int i = 0; i < _slotConns.Length; i++ )
		{
			if ( _slotConns[i] is not null && _slotConns[i] != local )
				_slotConns[i] = null;
		}
		HostCommitRoster();
		State = SessionState.Hosting;
	}

	private int ActiveSlotMask()
	{
		int mask = 0;
		for ( int i = 0; i < _slotConns.Length; i++ )
		{
			if ( _slotConns[i] is not null && _slotConns[i].IsActive )
				mask |= 1 << i;
		}
		return mask;
	}

	private static int CountBits( int mask )
	{
		int n = 0;
		while ( mask != 0 )
		{
			n += mask & 1;
			mask >>= 1;
		}
		return n;
	}

	private void RestartLinkedPlay()
	{
		var emu = EmulatorComponent.Current;
		if ( emu == null )
			return;
		if ( emu.IsLinked )
			emu.EndLinkedMode();
		BeginLinkedPlay();
	}

	public void EndLinkedPlay()
	{
		_linkActive = false;
		_linkedMask = 0;
		_pendingRosterMask = -1;
		EmulatorComponent.Current?.EndLinkedMode();
	}

	public void SendLocalInput( long frame, ushort keys )
	{
		if ( !IsClient )
			return;

		var packet = new LinkCableInputPacket( frame, keys ).Serialize();
		SendLinkPacket( HostClientId, packet );
	}

	public void SendLocalSave( byte[] saveData )
	{
		if ( !IsClient || saveData == null || saveData.Length == 0 )
			return;

		var packet = new LinkCableSaveUploadPacket
		{
			PlayerId = LocalPlayer?.Slot ?? -1,
			SaveData = saveData,
		}.Serialize();
		SendLinkPacket( HostClientId, packet );
	}

	public void PumpLinkCable()
	{
		var session = LinkSession;
		if ( session is null || !IsHost )
			return;

		if ( ExternalAvDriver )
			return;

		var instances = session.Host.Instances;
		for ( int i = 0; i < instances.Count; i++ )
		{
			var inst = instances[i];
			int slot = inst.Slot;
			if ( slot < 0 )
				continue;

			while ( session.TryDequeueAv( inst, out var packet ) )
			{
				var data = packet.Serialize();
				SendLinkPacket( slot, data );
			}
		}
	}

	public void HostSendAv( int slot, LinkCableAvPacket packet )
	{
		if ( !IsHost || packet is null )
			return;

		var data = packet.Serialize();
		SendLinkPacket( slot, data );
	}

	public void HostSendState( int slot, byte[] state )
	{
		if ( !IsHost || state is null || state.Length == 0 )
			return;

		var data = new LinkCableStatePacket { PlayerId = slot, State = state }.Serialize();
		SendLinkPacket( slot, data );
	}

	private void RouteLinkCablePacket( int senderIndex, byte[] payload )
	{
		if ( payload.Length < 1 )
			return;

		switch ( (LinkCablePacketType)payload[0] )
		{
			case LinkCablePacketType.Input:
				RouteInput( senderIndex, payload );
				break;
			case LinkCablePacketType.SaveUpload:
				RouteSaveUpload( senderIndex, payload );
				break;
			case LinkCablePacketType.Av:
				RouteAv( payload );
				break;
			case LinkCablePacketType.State:
				RouteState( payload );
				break;
		}
	}

	private void RouteState( byte[] payload )
	{
		if ( !IsClient )
			return;

		if ( LinkCableStatePacket.TryParse( payload, payload.Length, out var packet ) )
		{
			try { OnRemoteState?.Invoke( packet ); }
			catch ( Exception e ) { Log.Warning( $"[sGBA] OnRemoteState error: {e.Message}" ); }
		}
	}

	private void RouteInput( int senderIndex, byte[] payload )
	{
		if ( !IsHost )
			return;

		var session = LinkSession;
		if ( session is null )
			return;

		if ( LinkCableInputPacket.TryParse( payload, payload.Length, out var packet ) )
			session.SetSlotInput( senderIndex, packet.Keys );
	}

	private void RouteSaveUpload( int senderIndex, byte[] payload )
	{
		if ( !IsHost )
			return;

		var session = LinkSession;
		if ( session is null )
			return;

		if ( LinkCableSaveUploadPacket.TryParse( payload, payload.Length, out var packet ) )
			session.LoadSlotSave( senderIndex, packet.SaveData );
	}

	private void RouteAv( byte[] payload )
	{
		if ( !IsClient )
			return;

		if ( LinkCableAvPacket.TryParse( payload, payload.Length, out var packet ) )
		{
			try { OnRemoteAv?.Invoke( packet ); }
			catch ( Exception e ) { Log.Warning( $"[sGBA] OnRemoteAv error: {e.Message}" ); }
		}
	}

	private void SendLinkPacket( int clientId, byte[] data )
	{
		((IWirelessNetwork)this).Send( clientId, data, data.Length );
	}
}