Networking/SessionRoster.cs
namespace Emulotl;

public sealed class SessionRoster
{
	private readonly Connection[] _slots;
	private readonly Dictionary<Guid, int> _slotByConn = [];
	private readonly List<SessionPlayer> _players;
	private readonly Dictionary<Guid, bool> _ready = [];
	private int _localSlot = -1;

	public SessionRoster( int capacity )
	{
		_slots = new Connection[capacity];
		_players = new List<SessionPlayer>( capacity );
	}

	public int SlotCount => _slots.Length;
	public int PlayerCount => _players.Count;
	public int LocalSlot => _localSlot;
	public IReadOnlyList<SessionPlayer> Players => _players;
	public SessionPlayer LocalPlayer => _localSlot >= 0 ? _players.FirstOrDefault( p => p.Slot == _localSlot ) : null;

	public Connection ConnectionAt( int slot ) => slot >= 0 && slot < _slots.Length ? _slots[slot] : null;
	public bool TryGetSlot( Guid connId, out int slot ) => _slotByConn.TryGetValue( connId, out slot );

	public void SetReady( Guid connId, bool ready ) => _ready[connId] = ready;
	public void RemoveReady( Guid connId ) => _ready.Remove( connId );

	public int AssignSlot( Connection conn, int cap )
	{
		if ( conn is null )
			return -1;

		for ( int i = 0; i < _slots.Length; i++ )
		{
			if ( _slots[i] is not null && _slots[i].Id == conn.Id )
				return i;
		}

		if ( conn == Connection.Local && _slots[0] is null )
		{
			_slots[0] = conn;
			return 0;
		}

		int start = conn.IsHost ? 0 : 1;
		for ( int i = start; i < cap && i < _slots.Length; i++ )
		{
			if ( _slots[i] is null )
			{
				_slots[i] = conn;
				return i;
			}
		}
		return -1;
	}

	public void ReleaseSlot( Connection conn )
	{
		if ( conn is null )
			return;

		for ( int i = 0; i < _slots.Length; i++ )
		{
			if ( _slots[i] is not null && _slots[i].Id == conn.Id )
				_slots[i] = null;
		}
	}

	public void ReleaseRemoteSlots()
	{
		var local = Connection.Local;
		for ( int i = 0; i < _slots.Length; i++ )
		{
			if ( _slots[i] is not null && _slots[i] != local )
				_slots[i] = null;
		}
	}

	public void EnsureLocal( int cap )
	{
		var local = Connection.Local;
		if ( local is null )
			return;

		for ( int i = 0; i < _slots.Length; i++ )
		{
			if ( _slots[i] is not null && _slots[i].Id == local.Id )
				return;
		}
		AssignSlot( local, cap );
	}

	public void RebuildLocalView()
	{
		_slotByConn.Clear();
		_players.Clear();
		_localSlot = -1;

		var localId = Connection.Local?.Id ?? Guid.Empty;
		for ( int i = 0; i < _slots.Length; i++ )
		{
			var conn = _slots[i];
			if ( conn is null )
				continue;

			_slotByConn[conn.Id] = i;
			var ready = conn.IsHost || _ready.GetValueOrDefault( conn.Id );
			_players.Add( new SessionPlayer( conn, i ) { Ready = ready } );
			if ( conn.Id == localId )
				_localSlot = i;
		}
	}

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

	public List<int> ActiveSlots()
	{
		var slots = new List<int>( _slots.Length );
		for ( int i = 0; i < _slots.Length; i++ )
		{
			if ( _slots[i] is not null && _slots[i].IsActive )
				slots.Add( i );
		}
		return slots;
	}

	public Connection ConnectionForSend( int slot )
	{
		if ( slot < 0 || slot >= _slots.Length )
			return null;

		var conn = _slots[slot];
		if ( conn is not null )
			return conn;

		if ( slot == 0 && !Networking.IsHost )
		{
			var all = Connection.All;
			if ( all is not null )
			{
				for ( int i = 0; i < all.Count; i++ )
				{
					if ( all[i] is not null && all[i].IsHost )
						return all[i];
				}
			}
		}
		return null;
	}

	public byte[] Serialize()
	{
		int count = 0;
		for ( int i = 0; i < _slots.Length; i++ )
		{
			if ( _slots[i] is not null )
				count++;
		}

		using var ms = new System.IO.MemoryStream();
		using var w = new System.IO.BinaryWriter( ms );
		w.Write( count );
		for ( int i = 0; i < _slots.Length; i++ )
		{
			var conn = _slots[i];
			if ( conn is null )
				continue;

			w.Write( i );
			w.Write( conn.Id.ToByteArray() );
			w.Write( conn.SteamId );
			w.Write( conn.IsHost );
			w.Write( conn.IsHost || _ready.GetValueOrDefault( conn.Id ) );
			w.Write( conn.DisplayName ?? string.Empty );
		}
		return ms.ToArray();
	}

	public void Apply( byte[] payload )
	{
		Array.Clear( _slots, 0, _slots.Length );
		_slotByConn.Clear();
		_players.Clear();
		_localSlot = -1;

		var localId = Connection.Local?.Id ?? Guid.Empty;

		try
		{
			using var ms = new System.IO.MemoryStream( payload );
			using var r = new System.IO.BinaryReader( ms );
			int count = r.ReadInt32();
			for ( int n = 0; n < count; n++ )
			{
				int slot = r.ReadInt32();
				var id = new Guid( r.ReadBytes( 16 ) );
				ulong steamId = r.ReadUInt64();
				bool isHost = r.ReadBoolean();
				bool ready = r.ReadBoolean();
				string name = r.ReadString();

				if ( slot < 0 || slot >= _slots.Length )
					continue;

				var player = new SessionPlayer( slot, id, steamId, name, isHost, ready );
				_slots[slot] = player.Connection;
				_slotByConn[id] = slot;
				_players.Add( player );
				if ( id == localId )
					_localSlot = slot;
			}
		}
		catch ( Exception e )
		{
			Log.Warning( $"[Emulotl] roster parse failed: {e.Message}" );
		}
	}

	public void Clear()
	{
		Array.Clear( _slots, 0, _slots.Length );
		_slotByConn.Clear();
		_players.Clear();
		_ready.Clear();
		_localSlot = -1;
	}
}