using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using Cysharp.Threading.Tasks;
using RebootKit.Engine.Extensions;
using RebootKit.Engine.Foundation;
using RebootKit.Engine.Main;
using RebootKit.Engine.Simulation;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine.Assertions;
using Logger = RebootKit.Engine.Foundation.Logger;

namespace RebootKit.Engine.Network {
    enum NetworkClientSyncState {
        NotReady,
        Synchronizing,
        Ready
    }

    class NetworkClientState : IDisposable {
        public NetworkSystem NetworkSystem;

        public ulong ClientID;
        public NetworkClientSyncState SyncState;

        public NetworkMessageBuffer ActorsEvents;
        public NetworkMessageBuffer ActorsCoreStatesUpdates;
        public NetworkMessageBuffer ActorsStatesUpdates;
        public NetworkMessageBuffer ActorsTransformUpdates;

        public Queue<NetworkMessageBuffer> ReliableMessagesQueue = new Queue<NetworkMessageBuffer>(64);
        public Queue<NetworkMessageBuffer> UnreliableMessagesQueue = new Queue<NetworkMessageBuffer>(64);

        public bool IsReady {
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            get {
                return SyncState == NetworkClientSyncState.Ready;
            }
        }

        public bool IsServer { get; private set; }

        public NetworkClientState(NetworkSystem networkSystem, ulong clientID, bool isServer) {
            NetworkSystem = networkSystem;
            ClientID = clientID;
            SyncState = NetworkClientSyncState.NotReady;
            IsServer = isServer;

            ResetMessageBuffers();
        }

        public void Dispose() {
            while (ReliableMessagesQueue.Count > 0) {
                NetworkMessageBuffer message = ReliableMessagesQueue.Dequeue();
                NetworkSystem.ReturnMessageBufferToFreeList(message);
            }
            
            while (UnreliableMessagesQueue.Count > 0) {
                NetworkMessageBuffer message = UnreliableMessagesQueue.Dequeue();
                NetworkSystem.ReturnMessageBufferToFreeList(message);
            }

            if (ActorsEvents != null) {
                NetworkSystem.ReturnMessageBufferToFreeList(ActorsEvents);
            }

            if (ActorsCoreStatesUpdates != null) {
                NetworkSystem.ReturnMessageBufferToFreeList(ActorsCoreStatesUpdates);
            }

            if (ActorsStatesUpdates != null) {
                NetworkSystem.ReturnMessageBufferToFreeList(ActorsStatesUpdates);
            }

            if (ActorsTransformUpdates != null) {
                NetworkSystem.ReturnMessageBufferToFreeList(ActorsTransformUpdates);
            }

            ActorsEvents = null;
            ActorsCoreStatesUpdates = null;
            ActorsStatesUpdates = null;
            ActorsTransformUpdates = null;
        }

        public void ResetMessageBuffers() {
            if (ActorsEvents == null) {
                ActorsEvents = NetworkSystem.GetFreeMessageBuffer();
            }
            ActorsEvents.Reset();
            ActorsEvents.WriteHeader(NetworkMessageType.ActorsEventsList, RR.NetworkProtocolVersion);
            ActorsEvents.Writer.Write((byte) 0);

            if (ActorsCoreStatesUpdates == null) {
                ActorsCoreStatesUpdates = NetworkSystem.GetFreeMessageBuffer();
            }
            ActorsCoreStatesUpdates.Reset();
            ActorsCoreStatesUpdates.WriteHeader(NetworkMessageType.ActorsCoreStatesUpdateList,
                                                RR.NetworkProtocolVersion);
            ActorsCoreStatesUpdates.Writer.Write((byte) 0);

            if (ActorsStatesUpdates == null) {
                ActorsStatesUpdates = NetworkSystem.GetFreeMessageBuffer();
            }
            ActorsStatesUpdates.Reset();
            ActorsStatesUpdates.WriteHeader(NetworkMessageType.ActorsStatesUpdateList, RR.NetworkProtocolVersion);
            ActorsStatesUpdates.Writer.Write((byte) 0);

            if (ActorsTransformUpdates == null) {
                ActorsTransformUpdates = NetworkSystem.GetFreeMessageBuffer();
            }
            ActorsTransformUpdates.Reset();
            ActorsTransformUpdates.WriteHeader(NetworkMessageType.ActorsTransformUpdateList, RR.NetworkProtocolVersion);
            ActorsTransformUpdates.Writer.Write((byte) 0);
        }

        public void ScheduleMessages() {
            if (ActorsEvents.GetMessageContentSize() > sizeof(byte)) {
                ReliableMessagesQueue.Enqueue(ActorsEvents);
                ActorsEvents = null;
            }

            if (ActorsCoreStatesUpdates.GetMessageContentSize() > sizeof(byte)) {
                ReliableMessagesQueue.Enqueue(ActorsCoreStatesUpdates);
                ActorsCoreStatesUpdates = null;
            }

            if (ActorsStatesUpdates.GetMessageContentSize() > sizeof(byte)) {
                ReliableMessagesQueue.Enqueue(ActorsStatesUpdates);
                ActorsStatesUpdates = null;
            }

            if (ActorsTransformUpdates.GetMessageContentSize() > sizeof(byte)) {
                UnreliableMessagesQueue.Enqueue(ActorsTransformUpdates);
                ActorsTransformUpdates = null;
            }

            ResetMessageBuffers();
        }
    }

    public class NetworkSystem : INetworkManagerDelegate, IDisposable {
        [ConfigVar("sv.tick_rate", 30, "Server tick rate in Hz", CVarFlags.Server)]
        public static ConfigVar TickRate;

        static readonly Logger s_Logger = new Logger(nameof(NetworkSystem));

        public ActorsManager Actors { get; private set; }

        internal readonly Dictionary<ulong, NetworkClientState> Clients = new Dictionary<ulong, NetworkClientState>();

        NetworkMessageBuffer m_ActorsCommandsMessage;

        List<NetworkMessageBuffer> m_MessagesFreeList = new List<NetworkMessageBuffer>();

        FixedString64Bytes m_WorldID = new FixedString64Bytes("");
        public FixedString64Bytes WorldID {
            get {
                return m_WorldID;
            }
        }

        bool m_IsChangingWorld = false;

        CancellationTokenSource m_WorldChangeCancellationTokenSource = new CancellationTokenSource();

        float m_TickTimer;

        public INetworkManager Manager { get; private set; }

        public ulong TickCount { get; private set; }
        public event Action<ulong> ServerTick = delegate { };

        public ulong LocalClientID {
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            get {
                return Manager.LocalClientID;
            }
        }

        public int LastTickPacketsSentCount { get; private set; }

        public NetworkSystem(INetworkManager manager) {
            Manager = manager;
            Manager.Delegate = this;
            Actors = new ActorsManager(this);

            m_ActorsCommandsMessage = new NetworkMessageBuffer(Allocator.Persistent);
            m_ActorsCommandsMessage.WriteHeader(NetworkMessageType.ActorsCommandsList, RR.EngineConfig.protocolVersion);
            m_ActorsCommandsMessage.Writer.Write((byte) 0);
        }

        public void Dispose() {
            foreach (NetworkClientState clientState in Clients.Values) {
                clientState.Dispose();
            }
            Clients.Clear();

            Manager.Dispose();
            Manager = null;

            m_ActorsCommandsMessage.Dispose();
            m_ActorsCommandsMessage = null;

            foreach (NetworkMessageBuffer networkMessageBuffer in m_MessagesFreeList) {
                networkMessageBuffer.Dispose();
            }
            m_MessagesFreeList.Clear();
        }

        public void Tick(float deltaTime) {
            Manager.Tick();

            if (!Manager.IsServer() && !Manager.IsClient()) {
                return;
            }

            Actors.Tick(deltaTime);

            float tickDeltaTime = 1.0f / TickRate.IndexValue;
            m_TickTimer += deltaTime;

            while (m_TickTimer >= tickDeltaTime) {
                m_TickTimer -= tickDeltaTime;

                LastTickPacketsSentCount = 0;

                if (RR.IsServer()) {
                    Actors.ServerTick(tickDeltaTime);

                    ServerTick?.Invoke(TickCount);
                    TickCount++;

                    FlushServerPackets();
                }

                if (RR.IsClient()) {
                    if (!RR.IsServer()) {
                        TickCount++;
                    }

                    FlushClientPackets();
                }
            }
        }

        //
        // @MARK: INetworkManagerDelegate
        //
        public void OnServerStarted() {
            TickCount = 0;

            RR.OnServerStarted();
        }

        public void OnServerStopped() {
            RR.OnServerStopped();
        }

        public void OnClientStarted() {
            RR.OnClientStarted();
        }

        public void OnClientStopped() {
            RR.OnClientStopped();
        }

        public void OnClientConnected(ulong clientID) {
            s_Logger.Info($"OnClientConnected: {clientID}");

            if (RR.IsServer()) {
                NetworkClientState newClientState = new NetworkClientState(this,
                                                                           clientID,
                                                                           clientID == Manager.LocalClientID);
                Clients.Add(clientID, newClientState);

                if (!WorldID.IsEmpty && !newClientState.IsServer) {
                    s_Logger.Info($"Synchronizing world load for client {clientID} with world ID '{WorldID}'");
                    SendSynchronizeGameState(clientID);
                }
            }

            if (RR.GameInstance != null) {
                RR.GameInstance.OnClientConnected(clientID);
            }
        }

        public void OnClientDisconnected(ulong clientID) {
            s_Logger.Info($"OnClientDisconnected: {clientID}");

            if (RR.IsServer()) {
                Clients.Remove(clientID);
            }

            if (RR.GameInstance != null) {
                RR.GameInstance.OnClientDisconnected(clientID);
            }
        }

        public void OnMessageReceived(ulong senderID, NativeArray<byte> data) {
            OnReceivedNetworkMessage(senderID, data);
        }

        //
        // @MARK: Server API
        //
        public void SetCurrentWorld(string worldID) {
            if (!Manager.IsServer()) {
                s_Logger.Error("Only server can set the current world.");
                return;
            }

            if (m_IsChangingWorld) {
                s_Logger.Error($"Already changing world to '{WorldID}'. Please wait until the current world change is complete.");
                return;
            }

            WorldConfigAsset worldConfigAsset = RR.GetWorldConfigAsset(worldID);
            if (worldConfigAsset is null) {
                s_Logger.Error($"Failed to set current world: World config asset for '{worldID}' not found.");
                return;
            }

            m_WorldID = worldID;

            foreach ((ulong _, NetworkClientState clientState) in Clients.ToList()) {
                clientState.SyncState = NetworkClientSyncState.Synchronizing;
            }

            m_WorldChangeCancellationTokenSource.Cancel();
            m_WorldChangeCancellationTokenSource.Dispose();
            m_WorldChangeCancellationTokenSource = new CancellationTokenSource();

            ServerLoadWorldAsync(worldConfigAsset, m_WorldChangeCancellationTokenSource.Token).Forget();
        }

        async UniTask ServerLoadWorldAsync(WorldConfigAsset asset, CancellationToken cancellationToken) {
            s_Logger.Info($"ServerLoadWorldAsync: {asset.Config.name}");

            m_IsChangingWorld = true;

            RR.World.Unload();
            RR.CloseMainMenu();

            await RR.World.LoadAsync(asset.Config, cancellationToken);
            m_IsChangingWorld = false;

            await UniTask.Yield(cancellationToken);
            Actors.AssignActorsIDs();

            if (!TryGetClientState(LocalClientID, out NetworkClientState localClientState)) {
                s_Logger.Error($"Local client state not found for client ID {LocalClientID.ToString()}.");
                RR.Disconnect();
                return;
            }

            localClientState.SyncState = NetworkClientSyncState.Ready;
            RR.GameInstance.PlayerBecameReady(localClientState.ClientID);

            foreach (ulong clientID in Clients.Keys) {
                if (clientID == LocalClientID) {
                    continue;
                }

                SendSynchronizeGameState(clientID);
            }
        }

        void SendSynchronizeGameState(ulong clientID) {
            if (!RR.IsServer()) {
                s_Logger.Error("Only server can send synchronize game state.");
                return;
            }

            using NetworkMessageBuffer message = new NetworkMessageBuffer(Allocator.Temp);
            message.WriteHeader(NetworkMessageType.SynchronizeGameState, RR.EngineConfig.protocolVersion);
            message.Writer.Write(WorldID);
            Actors.WriteInSceneActorsStates(message.Writer);

            unsafe {
                Manager.Send(clientID,
                             (byte*) message.GetDataSlice().GetUnsafePtr(),
                             message.Writer.Position,
                             SendMode.Reliable);
            }
        }

        //
        // @MARK: Internal
        //
        internal bool TryGetClientState(ulong clientID, out NetworkClientState clientState) {
            return Clients.TryGetValue(clientID, out clientState);
        }

        internal void ClientSynchronized(ulong clientID) {
            if (TryGetClientState(clientID, out NetworkClientState state)) {
                state.SyncState = NetworkClientSyncState.Ready;
                s_Logger.Info($"Client synchronized: {clientID.ToString()}");

                Actors.SpawnDynamicActorsForClient(clientID);
                RR.GameInstance.PlayerBecameReady(clientID);
            } else {
                s_Logger.Error($"Client state for {clientID} not found.");
            }
        }

        internal int GetReadyClientsCount() {
            int count = 0;
            foreach (NetworkClientState clientState in Clients.Values) {
                if (clientState.IsReady) {
                    count++;
                }
            }
            return count;
        }

        //
        // @MARK: Network packets
        //
        internal void SendPossessedActor(ulong clientID, ushort actorID) {
            if (!RR.IsServer()) {
                s_Logger.Error("Only server can send possessed actor message.");
                return;
            }

            if (TryGetClientState(clientID, out NetworkClientState clientState)) {
                using NetworkMessageBuffer messageBuffer = new NetworkMessageBuffer(Allocator.Temp);
                messageBuffer.WriteHeader(NetworkMessageType.PossessActor, RR.EngineConfig.protocolVersion);
                messageBuffer.Writer.Write(actorID);

                LastTickPacketsSentCount += 1;
                Manager.Send(clientID, messageBuffer.GetDataSlice(), SendMode.Reliable);
                return;
            }

            s_Logger.Error($"Failed to find client state for client ID {clientID}. Cannot send possess actor message.");
        }

        internal bool WriteActorState(ulong clientID, ushort actorID, IActorData actorData) {
            if (!RR.IsServer()) {
                s_Logger.Error("WriteActorState can only be called on the server.");
                return false;
            }
            Assert.IsTrue(actorData.GetMaxBytes() <= byte.MaxValue, "Actor data must not exceed 255 bytes.");

            if (TryGetClientState(clientID, out NetworkClientState clientState)) {
                NetworkMessageBuffer messageBuffer = clientState.ActorsStatesUpdates;

                int dataSizeToFit = actorData.GetMaxBytes() + sizeof(ushort) + sizeof(byte);
                if (!messageBuffer.Writer.WillFit(dataSizeToFit)) {
                    return false;
                }

                // Increment count of entities in the message
                int savedPosition = messageBuffer.Writer.Position;
                using NetworkBufferReader reader = new NetworkBufferReader(messageBuffer.GetDataSlice(),
                                                                           NetworkMessageHeader.k_HeaderSize);
                reader.Read(out byte count);
                if (count >= byte.MaxValue) {
                    return false;
                }
                messageBuffer.Writer.Position = NetworkMessageHeader.k_HeaderSize;
                messageBuffer.Writer.Write((byte) (count + 1));
                messageBuffer.Writer.Position = savedPosition;

                // Actual data writing
                messageBuffer.Writer.Write(actorID);
                messageBuffer.Writer.Write((byte) actorData.GetMaxBytes());
                actorData.Serialize(messageBuffer.Writer);
                return true;
            }

            return false;
        }

        internal bool WriteActorTransformState(ulong clientID, ActorTransformSyncData transformData) {
            if (!RR.IsServer()) {
                s_Logger.Error("WriteActorTransformState can only be called on the server.");
                return false;
            }

            if (TryGetClientState(clientID, out NetworkClientState clientState)) {
                int dataSizeToFit = transformData.GetMaxBytes();
                NetworkMessageBuffer messageBuffer = clientState.ActorsTransformUpdates;

                if (!messageBuffer.Writer.WillFit(dataSizeToFit)) {
                    return false;
                }

                // Increment count of entities in the message
                int savedPosition = messageBuffer.Writer.Position;
                using NetworkBufferReader reader = new NetworkBufferReader(messageBuffer.GetDataSlice(),
                                                                           NetworkMessageHeader.k_HeaderSize);
                reader.Read(out byte count);
                if (count >= byte.MaxValue) {
                    return false;
                }
                messageBuffer.Writer.Position = NetworkMessageHeader.k_HeaderSize;
                messageBuffer.Writer.Write((byte) (count + 1));
                messageBuffer.Writer.Position = savedPosition;

                // Actual data writing
                transformData.Serialize(messageBuffer.Writer);
                return true;
            }

            return false;
        }

        internal bool WriteActorCoreState(ulong clientID, ActorCoreStateSnapshot coreData) {
            if (!RR.IsServer()) {
                s_Logger.Error("WriteActorCoreState can only be called on the server.");
                return false;
            }

            if (TryGetClientState(clientID, out NetworkClientState clientState)) {
                int dataSizeToFit = coreData.GetMaxBytes();
                NetworkMessageBuffer messageBuffer = clientState.ActorsCoreStatesUpdates;

                if (!messageBuffer.Writer.WillFit(dataSizeToFit)) {
                    return false;
                }

                // Increment count of entities in the message
                int savedPosition = messageBuffer.Writer.Position;
                using NetworkBufferReader reader = new NetworkBufferReader(messageBuffer.GetDataSlice(),
                                                                           NetworkMessageHeader.k_HeaderSize);
                reader.Read(out byte count);
                if (count >= byte.MaxValue) {
                    return false;
                }
                messageBuffer.Writer.Position = NetworkMessageHeader.k_HeaderSize;
                messageBuffer.Writer.Write((byte) (count + 1));
                messageBuffer.Writer.Position = savedPosition;

                // Actual data writing
                coreData.Serialize(messageBuffer.Writer);
                return true;
            }

            return false;
        }

        internal bool SendSpawnActor(ulong clientID,
                                     FixedString64Bytes assetGUID,
                                     ActorCoreStateSnapshot coreState,
                                     IActorData actorData) {
            if (!RR.IsServer()) {
                s_Logger.Error("WriteSpawnActor can only be called on the server.");
                return false;
            }

            if (TryGetClientState(clientID, out NetworkClientState clientState)) {
                int dataSizeToFit = sizeof(byte) * 64 + // assetGUID
                                    coreState.GetMaxBytes() +
                                    sizeof(byte) + // actor data size
                                    actorData.GetMaxBytes();
                using NetworkMessageBuffer messageBuffer = new NetworkMessageBuffer(Allocator.Temp);
                messageBuffer.WriteHeader(NetworkMessageType.SpawnActor, RR.NetworkProtocolVersion);

                if (!messageBuffer.Writer.WillFit(dataSizeToFit)) {
                    s_Logger.Error($"WriteSpawnActor failed: message buffer for client {clientID} is full. " +
                                   $"Data size to fit: {dataSizeToFit}");
                    return false;
                }

                messageBuffer.Writer.Write(assetGUID);
                coreState.Serialize(messageBuffer.Writer);
                messageBuffer.Writer.Write((byte) actorData.GetMaxBytes());
                actorData.Serialize(messageBuffer.Writer);

                LastTickPacketsSentCount += 1;
                Manager.Send(clientID, messageBuffer.GetDataSlice(), SendMode.Reliable);

                return true;
            }

            return false;
        }

        internal bool WriteActorEvent(ulong clientID,
                                      ActorEvent actorEvent) {
            if (!RR.IsServer()) {
                s_Logger.Error("WriteActorEvent can only be called on the server.");
                return false;
            }

            if (TryGetClientState(clientID, out NetworkClientState clientState)) {
                int dataSizeToFit = actorEvent.GetMaxBytes();
                NetworkMessageBuffer messageBuffer = clientState.ActorsEvents;

                if (!messageBuffer.Writer.WillFit(dataSizeToFit)) {
                    return false;
                }

                // Increment count of entities in the message
                int savedPosition = messageBuffer.Writer.Position;
                using NetworkBufferReader reader = new NetworkBufferReader(messageBuffer.GetDataSlice(),
                                                                           NetworkMessageHeader.k_HeaderSize);
                reader.Read(out byte count);
                if (count >= byte.MaxValue) {
                    return false;
                }

                messageBuffer.Writer.Position = NetworkMessageHeader.k_HeaderSize;
                messageBuffer.Writer.Write((byte) (count + 1));
                messageBuffer.Writer.Position = savedPosition;

                // Actual data writing
                actorEvent.Serialize(messageBuffer.Writer);
                return true;
            }

            return false;
        }

        // @TODO: When buffer is overloaded we might want to flush it immediately instead of returning false.
        internal bool WriteActorCommand(ActorCommand actorCommand) {
            if (actorCommand.ActorID == 0) {
                s_Logger.Error("Trying to write an actor command with ActorID 0. " +
                               "This is likely a bug in the code that generates actor commands.");
            }

            NetworkMessageBuffer messageBuffer = m_ActorsCommandsMessage;
            if (!messageBuffer.Writer.WillFit(actorCommand.GetMaxBytes())) {
                return false;
            }

            // Increment count of entities in the message
            using NetworkBufferReader reader = new NetworkBufferReader(messageBuffer.GetDataSlice(),
                                                                       NetworkMessageHeader.k_HeaderSize);
            reader.Read(out byte count);
            if (count >= byte.MaxValue) {
                return false;
            }

            int savedPosition = messageBuffer.Writer.Position;
            messageBuffer.Writer.Position = NetworkMessageHeader.k_HeaderSize;
            messageBuffer.Writer.Write((byte) (count + 1));
            messageBuffer.Writer.Position = savedPosition;

            // Actual data writing
            actorCommand.Serialize(messageBuffer.Writer);
            return true;
        }

        void FlushClientPackets() {
            if (!RR.IsClient()) {
                return;
            }

            if (m_ActorsCommandsMessage.GetMessageContentSize() > sizeof(byte)) {
                if (RR.IsServer()) {
                    using NativeArray<byte> messageData = m_ActorsCommandsMessage.GetDataCopy(Allocator.Temp);
                    ResetClientsMessages();
                    OnReceivedNetworkMessage(Manager.LocalClientID, messageData);
                } else {
                    LastTickPacketsSentCount += 1;
                    Manager.Send(0, m_ActorsCommandsMessage.GetDataSlice(), SendMode.Reliable);
                    ResetClientsMessages();
                }
            }
        }

        void ResetClientsMessages() {
            m_ActorsCommandsMessage.Reset();
            m_ActorsCommandsMessage.WriteHeader(NetworkMessageType.ActorsCommandsList, RR.EngineConfig.protocolVersion);
            m_ActorsCommandsMessage.Writer.Write((byte) 0);
        }

        void FlushServerPackets() {
            if (!RR.IsServer()) {
                return;
            }

            foreach (NetworkClientState client in RR.Network.Clients.Values) {
                bool isClientServer = client.ClientID == Manager.LocalClientID;

                client.ScheduleMessages();

                if (isClientServer) {
                    while (client.ReliableMessagesQueue.Count > 0) {
                        NetworkMessageBuffer message = client.ReliableMessagesQueue.Dequeue();
                        OnMessageReceived(LocalClientID, message.GetDataCopy(Allocator.Temp));
                    }

                    while (client.UnreliableMessagesQueue.Count > 0) {
                        NetworkMessageBuffer message = client.UnreliableMessagesQueue.Dequeue();
                        OnMessageReceived(LocalClientID, message.GetDataCopy(Allocator.Temp));
                    }
                } else {
                    while (client.ReliableMessagesQueue.Count > 0) {
                        NetworkMessageBuffer message = client.ReliableMessagesQueue.Dequeue();
                        LastTickPacketsSentCount += 1;
                        Manager.Send(client.ClientID, message.GetDataSlice(), SendMode.Reliable);
                    }

                    while (client.UnreliableMessagesQueue.Count > 0) {
                        NetworkMessageBuffer message = client.UnreliableMessagesQueue.Dequeue();
                        LastTickPacketsSentCount += 1;
                        Manager.Send(client.ClientID, message.GetDataSlice(), SendMode.Unreliable);
                    }
                }

                foreach (NetworkMessageBuffer messageToFree in client.ReliableMessagesQueue) {
                    ReturnMessageBufferToFreeList(messageToFree);
                }
                client.ReliableMessagesQueue.Clear();

                foreach (NetworkMessageBuffer messageToFree in client.UnreliableMessagesQueue) {
                    ReturnMessageBufferToFreeList(messageToFree);
                }
                client.UnreliableMessagesQueue.Clear();
            }
        }

        void OnReceivedNetworkMessage(ulong senderID, NativeArray<byte> data) {
            if (!data.IsCreated) {
                s_Logger.Error("Received empty network message data.");
                return;
            }

            NetworkMessageHeader messageHeader = new NetworkMessageHeader();

            using (NetworkBufferReader reader = new NetworkBufferReader(data)) {
                messageHeader.Deserialize(reader);
            }
            Assert.IsTrue(messageHeader.MagicNumber == RConsts.k_NetworkMessageMagic,
                          "Received packet with invalid magic number.");

            NativeSlice<byte> messageContent = data.Slice(NetworkMessageHeader.k_HeaderSize);

            switch (messageHeader.MessageType) {
            // @MARK: Server to client messages
            case NetworkMessageType.SpawnActor: {
                using NetworkBufferReader contentReader = new NetworkBufferReader(messageContent);

                contentReader.Read(out FixedString64Bytes actorAssetGUID);

                ActorCoreStateSnapshot coreState = new ActorCoreStateSnapshot();
                coreState.Deserialize(contentReader);

                contentReader.Read(out byte dataSize);
                contentReader.Read(out NativeSlice<byte> actorDataContent, dataSize);

                Actors.ProcessSpawnActor(actorAssetGUID, coreState, actorDataContent);
                break;
            }

            case NetworkMessageType.PossessActor: {
                using NetworkBufferReader contentReader = new NetworkBufferReader(messageContent);
                contentReader.Read(out ushort actorID);

                RR.GameInstance.LocalPlayerController.SetPossessedActor(actorID);
                break;
            }

            case NetworkMessageType.ActorsEventsList: {
                using NetworkBufferReader contentReader = new NetworkBufferReader(messageContent);
                contentReader.Read(out byte count);

                for (int i = 0; i < count; ++i) {
                    ActorEvent actorEvent = new ActorEvent();
                    actorEvent.Deserialize(contentReader);

                    Actors.ProcessActorEvent(actorEvent);
                }
                break;
            }

            case NetworkMessageType.ActorsCoreStatesUpdateList: {
                using NetworkBufferReader contentReader = new NetworkBufferReader(messageContent);
                contentReader.Read(out byte count);

                for (int i = 0; i < count; ++i) {
                    ActorCoreStateSnapshot coreState = new ActorCoreStateSnapshot();
                    coreState.Deserialize(contentReader);

                    Actors.ProcessActorCoreState(coreState);
                }
                break;
            }

            case NetworkMessageType.ActorsStatesUpdateList: {
                using NetworkBufferReader contentReader = new NetworkBufferReader(messageContent);
                contentReader.Read(out byte count);

                for (int i = 0; i < count; ++i) {
                    contentReader.Read(out ushort actorID);
                    contentReader.Read(out byte dataSize);
                    contentReader.Read(out NativeSlice<byte> actorDataContent, dataSize);

                    Actors.ProcessActorState(actorID, actorDataContent);
                }
                break;
            }

            case NetworkMessageType.ActorsTransformUpdateList: {
                using NetworkBufferReader contentReader = new NetworkBufferReader(messageContent);
                contentReader.Read(out byte count);

                for (int i = 0; i < count; ++i) {
                    ActorTransformSyncData transformData = new ActorTransformSyncData();
                    transformData.Deserialize(contentReader);

                    Actors.ProcessActorTransformState(transformData);
                }
                break;
            }

            case NetworkMessageType.SynchronizeGameState: {
                ProcessSynchronizeGameStateMessage(messageContent);
                break;
            }

            // @MARK: Client to server messages
            case NetworkMessageType.ClientSynchronizedGameState: {
                s_Logger.Info("On Player Synchronized Game State: " + senderID);
                ClientSynchronized(senderID);
                break;
            }

            case NetworkMessageType.ActorsCommandsList: {
                using NetworkBufferReader contentReader = new NetworkBufferReader(messageContent);
                contentReader.Read(out byte count);

                for (int i = 0; i < count; ++i) {
                    ActorCommand actorCommand = new ActorCommand();
                    actorCommand.Deserialize(contentReader);

                    Actors.ProcessActorCommand(senderID, actorCommand);
                }
                break;
            }

            default:
                s_Logger.Error($"Received packet with unsupported message type: {messageHeader.MessageType.ToString()}");
                break;
            }
        }

        void ProcessSynchronizeGameStateMessage(NativeSlice<byte> data) {
            if (RR.IsServer()) {
                s_Logger.Error("Server should not receive SynchronizeGameState message.");
                return;
            }

            // @TODO: Handle cancellation token properly in this method.
            ProcessSynchronizeGameStateMessageAsync(data, CancellationToken.None).Forget();
        }

        async UniTask ProcessSynchronizeGameStateMessageAsync(NativeSlice<byte> data,
                                                              CancellationToken cancellationToken) {
            using NativeArray<byte> dataArray = new NativeArray<byte>(data.Length, Allocator.Persistent);
            data.CopyTo(dataArray);
            using NetworkBufferReader reader = new NetworkBufferReader(dataArray);

            reader.Read(out m_WorldID);

            WorldConfigAsset worldConfigAsset = RR.GetWorldConfigAsset(m_WorldID.ToString());
            if (worldConfigAsset is null) {
                s_Logger.Error($"Failed to synchronize game state: World config asset for '{m_WorldID}' not found.");
                RR.Disconnect();
                return;
            }

            RR.World.Unload();
            RR.CloseMainMenu();

            await RR.World.LoadAsync(worldConfigAsset.Config, cancellationToken);
            if (!Actors.ReadInSceneActorsStates(reader)) {
                s_Logger.Error("Failed to read in-scene actors states during game state synchronization.");
                RR.Disconnect();
                return;
            }

            NotifyServerClientSynchronizedGameState();
        }

        void NotifyServerClientSynchronizedGameState() {
            s_Logger.Info("Sending ClientSynchronizedGameState message to server");

            using NetworkMessageBuffer message = new NetworkMessageBuffer(Allocator.Temp);
            message.WriteHeader(NetworkMessageType.ClientSynchronizedGameState, RR.EngineConfig.protocolVersion);
            message.Writer.Write(m_WorldID);

            Manager.Send(0, message.GetDataSlice(), SendMode.Reliable);
        }

        //
        // @MARK: Messages free list
        //
        internal NetworkMessageBuffer GetFreeMessageBuffer() {
            if (m_MessagesFreeList.Count > 0) {
                int index = m_MessagesFreeList.Count - 1;
                NetworkMessageBuffer messageBuffer = m_MessagesFreeList[index];
                m_MessagesFreeList.RemoveAt(index);
                return messageBuffer;
            }

            return new NetworkMessageBuffer(Allocator.Persistent);
        }

        internal void ReturnMessageBufferToFreeList(NetworkMessageBuffer messageBuffer) {
            if (m_MessagesFreeList.Contains(messageBuffer)) {
                s_Logger.Error("Trying to return a message buffer that is already in the free list.");
                return;
            }

            m_MessagesFreeList.Add(messageBuffer);
        }
    }
}