using System;
using System.Collections.Generic;
using System.Linq;
using RebootKit.Engine.Extensions;
using RebootKit.Engine.Foundation;
using RebootKit.Engine.Main;
using RebootKit.Engine.Network;
using Unity.Collections;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.Assertions;
using UnityEngine.ResourceManagement.AsyncOperations;
using Logger = RebootKit.Engine.Foundation.Logger;
using Object = UnityEngine.Object;

namespace RebootKit.Engine.Simulation {
    // @TODO:
    // - Actors States might be packed into chunks to reduce the number of RPCs sent.
    // - Release addressables when they are no longer needed.
    public class ActorsManager : IDisposable {
        static readonly Logger s_Logger = new Logger(nameof(ActorsManager));

        readonly NetworkSystem m_Network;

        internal readonly List<Actor> InSceneActors = new List<Actor>();
        internal readonly List<Actor> SpawnedActors = new List<Actor>();

        // @NOTE: 0 is reserved for no actor so we should start assigning IDs from 1.
        ushort m_ActorIDCounter;
        List<ushort> m_ActorIDFreeList = new List<ushort>(ushort.MaxValue);

        public ushort InSceneActorsCount { get { return (ushort) InSceneActors.Count; } }
        public ushort SpawnedActorsCount { get { return (ushort) SpawnedActors.Count; } }
        public int TotalActorsCount { get { return InSceneActorsCount + SpawnedActorsCount; } }

        public ActorsManager(NetworkSystem networkSystem) {
            m_Network = networkSystem;
        }

        public void Dispose() { }

        //
        // @MARK: Update
        //
        public void Tick(float deltaTime) {
            foreach (Actor actor in InSceneActors) {
                actor.OnClientTick(deltaTime);
            }

            foreach (Actor actor in SpawnedActors) {
                actor.OnClientTick(deltaTime);
            }
        }

        //
        // @MARK: Server-side logic
        //
        public void ServerTick(float dt) {
            if (!RR.IsServer()) {
                return;
            }

            TickActorsList(InSceneActors, dt);
            TickActorsList(SpawnedActors, dt);
        }

        void TickActorsList(List<Actor> actors, float deltaTime) {
            foreach (Actor actor in actors) {
                actor.OnServerTick(deltaTime);

                if (actor.IsDataDirty) {
                    actor.IsDataDirty = false;

                    if (actor.Data.GetMaxBytes() > 0) {
                        foreach (NetworkClientState client in m_Network.Clients.Values) {
                            if (client.IsServer) {
                                continue;
                            }

                            m_Network.WriteActorState(client.ClientID, actor.ActorID, actor.Data);
                        }
                    }
                }

                if (actor.IsCoreStateDirty) {
                    actor.IsCoreStateDirty = false;

                    foreach (NetworkClientState client in m_Network.Clients.Values) {
                        if (client.IsServer) {
                            continue;
                        }

                        m_Network.WriteActorCoreState(client.ClientID, actor.GetCoreStateSnapshot());
                    }
                }
            }

            foreach (NetworkClientState client in m_Network.Clients.Values) {
                if (client.IsServer) {
                    continue;
                }

                foreach (Actor actor in actors.OrderBy(t => t.GetLastSyncTick(client.ClientID))) {
                    if (!actor.syncTransform || actor.MasterActor != null) {
                        continue;
                    }

                    ActorTransformSyncData syncData = actor.GetTransformSyncDataForClient(client.ClientID);
                    if (syncData.SyncMode == ActorTransformSyncMode.None) {
                        continue;
                    }

                    if (m_Network.WriteActorTransformState(client.ClientID, syncData)) {
                        actor.UpdateClientState(client.ClientID, RR.Network.TickCount);
                    } else {
                        // @NOTE: We ran out of space in the packet
                        break;
                    }
                }
            }
        }

        //
        // @MARK: Server API
        //
        public Actor SpawnActor(AssetReferenceGameObject assetReference, Vector3 position, Quaternion rotation) {
            if (!RR.IsServer()) {
                s_Logger.Error("Only the server can spawn actors.");
                return null;
            }

            if (!assetReference.RuntimeKeyIsValid()) {
                s_Logger.Error("Trying to spawn an actor with an invalid asset reference.");
                return null;
            }

            if (!TryGenerateNextActorID(out ushort actorID)) {
                s_Logger.Error("Cannot spawn actor: Failed to generate next actor ID.");
                return null;
            }

            Actor actor = InstantiateActor(assetReference.AssetGUID,
                                           actorID,
                                           position,
                                           rotation,
                                           null,
                                           default);
            actor.IsLocalOnly = false;
            SpawnedActors.Add(actor);

            foreach (NetworkClientState client in m_Network.Clients.Values) {
                if (client.IsServer) {
                    continue;
                }

                m_Network.SendSpawnActor(client.ClientID,
                                         assetReference.AssetGUID,
                                         actor.GetCoreStateSnapshot(),
                                         actor.Data);
            }

            return actor;
        }

        bool TryGenerateNextActorID(out ushort actorID) {
            if (m_ActorIDFreeList.Count > 0) {
                actorID = m_ActorIDFreeList[0];
                m_ActorIDFreeList.RemoveAtSwapBack(0);
                return true;
            }

            if (m_ActorIDCounter >= ushort.MaxValue) {
                s_Logger.Error("Reached maximum number of actor ids which is: " + ushort.MaxValue);
                actorID = 0;
                return false;
            }

            m_ActorIDCounter += 1;
            actorID = m_ActorIDCounter;
            return true;
        }

        internal void AssignActorsIDs() {
            if (!RR.IsServer()) {
                s_Logger.Info("Only the server can assign actors IDs.");
                return;
            }

            m_ActorIDCounter = 0;
            m_ActorIDFreeList.Clear();

            foreach (Actor actor in InSceneActors) {
                if (!TryGenerateNextActorID(out ushort actorID)) {
                    s_Logger.Error("Failed to generate actor ID. Probably reached the limit of 65535 actors.");
                    return;
                }

                actor.ActorID = actorID;
            }
        }

        public void DestroyActor(Actor actor) {
            if (actor.IsLocalOnly) {
                Object.Destroy(actor.gameObject);
                return;
            }

            if (!RR.IsServer()) {
                s_Logger.Error("Only the server can destroy non-local only actors");
                return;
            }

            if (InSceneActors.Contains(actor)) {
                s_Logger.Error("InScene actors cannot be destroyed!");
                return;
            }

            for (int i = SpawnedActors.Count - 1; i >= 0; --i) {
                if (SpawnedActors[i] == actor) {
                    m_ActorIDFreeList.Add(actor.ActorID);
                    return;
                }
            }

            s_Logger.Error($"Failed to destroy an actor(name: {actor.ActorName}, id: {actor.ActorID})");
        }

        //
        // @MARK: Common API
        //
        public void RegisterInSceneActor(Actor actor) {
            if (actor.Data == null) {
                actor.Data = actor.InternalCreateActorData();
            }

            actor.Manager = this;

            InSceneActors.Add(actor);
        }

        Actor FindInSceneActorWithStaticID(ulong staticID) {
            foreach (Actor actor in InSceneActors) {
                if (actor.ActorStaticID == staticID) {
                    return actor;
                }
            }

            return null;
        }

        public Actor FindActorByID(ushort actorID) {
            foreach (Actor actor in InSceneActors) {
                if (actor.ActorID == actorID) {
                    return actor;
                }
            }

            foreach (Actor actor in SpawnedActors) {
                if (actor.ActorID == actorID) {
                    return actor;
                }
            }

            return null;
        }

        public void CleanUp() {
            InSceneActors.Clear();

            foreach (Actor actor in SpawnedActors) {
                if (actor.OrNull() != null) {
                    Object.Destroy(actor.gameObject);
                }
            }

            SpawnedActors.Clear();
        }

        public Actor SpawnLocalOnlyActor(AssetReferenceGameObject assetReference, Vector3 position, Quaternion rotation) {
            if (!assetReference.RuntimeKeyIsValid()) {
                s_Logger.Error("Trying to spawn an actor with an invalid asset reference.");
                return null;
            }

            AsyncOperationHandle<GameObject> handle = Addressables.InstantiateAsync(assetReference, position, rotation);
            GameObject actorObject = handle.WaitForCompletion();
            Actor actor = actorObject.GetComponent<Actor>();
            if (actor == null) {
                s_Logger.Error($"GameObject {actorObject.name} does not have an Actor component.");
                handle.Release();
                return null;
            }

            actor.Manager = this;
            actor.AssetHandle = handle;
            actor.IsLocalOnly = true;
            actor.SourceActorPath = assetReference.AssetGUID;
            actor.ActorID = 0;
            actor.Data = actor.InternalCreateActorData();

            return actor;
        }

        /// 
        /// @MARK: Network Data Handling
        ///
        internal void ProcessActorEvent(in ActorEvent actorEvent) {
            Actor actor = FindActorByID(actorEvent.ActorID);
            if (actor == null) {
                s_Logger.Error($"Failed to find actor with ID {actorEvent.ActorID} for event handling.");
                return;
            }

            actor.HandleActorEvent(actorEvent);
        }

        internal void ProcessActorCommand(ulong senderID, in ActorCommand actorCommand) {
            if (!RR.IsServer()) {
                s_Logger.Error("Received ActorCommand on client, but this should only be handled on the server.");
                return;
            }

            Actor actor = FindActorByID(actorCommand.ActorID);
            if (actor == null) {
                s_Logger.Error($"Failed to find actor with ID {actorCommand.ActorID} for command handling.");
                return;
            }

            actor.HandleActorCommand(senderID, actorCommand);
        }

        internal void ProcessActorCoreState(in ActorCoreStateSnapshot coreState) {
            if (RR.IsServer()) {
                s_Logger.Error("Received ActorCoreState on server, but this should only be handled on the client.");
                return;
            }

            Actor actor = FindActorByID(coreState.ActorID);
            if (actor == null) {
                s_Logger.Error($"Failed to find actor with ID {coreState.ActorID} for core state update.");
                return;
            }

            actor.RestoreCoreState(coreState);
        }

        internal void ProcessActorState(ushort actorID, NativeSlice<byte> stateData) {
            if (RR.IsServer()) {
                s_Logger.Error("Received ActorState on server, but this should only be handled on the client.");
                return;
            }

            Actor actor = FindActorByID(actorID);
            if (actor == null) {
                s_Logger.Error($"Failed to find actor with ID {actorID} for state update.");
                return;
            }

            DataSerializationUtils.Deserialize(stateData, ref actor.Data);
        }

        internal void ProcessActorTransformState(in ActorTransformSyncData transformData) {
            if (RR.IsServer()) {
                s_Logger.Error("Received ActorTransformSync on server, but this should only be handled on the client.");
                return;
            }

            Actor actor = FindActorByID(transformData.ActorID);
            if (actor == null) {
                s_Logger.Error($"Failed to find actor with ID {transformData.ActorID} for transform state update.");
                return;
            }

            actor.RestoreTransformState(transformData);
        }

        internal void ProcessSpawnActor(in FixedString64Bytes assetGUID,
                                        in ActorCoreStateSnapshot coreState,
                                        in NativeSlice<byte> stateData) {
            if (RR.IsServer()) {
                s_Logger.Error("Received SpawnActor on server, but this should only be handled on the client.");
                return;
            }

            if (assetGUID.IsEmpty) {
                s_Logger.Error("Received SpawnActor with empty asset GUID, this should not happen.");
                return;
            }

            InstantiateActor(assetGUID.ToString(),
                             coreState.ActorID,
                             coreState.Position,
                             coreState.Rotation,
                             coreState,
                             stateData);
        }

        Actor InstantiateActor(string guid,
                               ushort actorID,
                               Vector3 position,
                               Quaternion rotation,
                               ActorCoreStateSnapshot? coreStateSnapshot,
                               NativeSlice<byte> stateData) {
            AssetReferenceGameObject assetReference = new AssetReferenceGameObject(guid);
            if (!assetReference.RuntimeKeyIsValid()) {
                s_Logger.Error($"Invalid asset reference for actor with GUID {guid}");
                return null;
            }

            AsyncOperationHandle<GameObject> handle = Addressables.InstantiateAsync(assetReference, position, rotation);
            GameObject actorObject = handle.WaitForCompletion();

            Actor actor = actorObject.GetComponent<Actor>();
            if (actor == null) {
                s_Logger.Error($"GameObject {actorObject.name} does not have an Actor component.");
                Addressables.ReleaseInstance(handle);
                return null;
            }

            actor.Manager = this;
            actor.AssetHandle = handle;
            actor.SourceActorPath = guid;
            actor.ActorID = actorID;
            actor.Data = actor.InternalCreateActorData();

            if (!RR.IsServer()) {
                actor.InitializeOnClient();
            }

            if (coreStateSnapshot.HasValue) {
                actor.RestoreCoreState(coreStateSnapshot.Value);
            }

            if (stateData.Length > 0) {
                DataSerializationUtils.Deserialize(stateData, ref actor.Data);
            }

            return actor;
        }

        //
        // @MARK: Actor Commands and Events
        //
        internal void SendActorEvent(ActorEvent actorEvent) {
            if (!RR.IsServer()) {
                s_Logger.Error("Only the server can send actor events.");
                return;
            }

            foreach (ulong clientID in m_Network.Clients.Keys) {
                m_Network.WriteActorEvent(clientID, actorEvent);
            }
        }

        internal void SendActorCommand(ActorCommand actorCommand) {
            m_Network.WriteActorCommand(actorCommand);
        }

        ///
        /// @MARK: Synchronization Helpers
        ///

        // @TODO: We might want to sending this in chunks because it probably wont fit into a single packet in real game.
        internal void WriteInSceneActorsStates(NetworkBufferWriter writer) {
            writer.Write((ushort) InSceneActorsCount);

            foreach (Actor actor in InSceneActors) {
                writer.Write(actor.ActorStaticID);
                writer.Write(actor.ActorID);
                actor.GetCoreStateSnapshot().Serialize(writer);

                writer.Write((byte) actor.Data.GetMaxBytes());
                actor.Data.Serialize(writer);
            }
        }

        internal bool ReadInSceneActorsStates(NetworkBufferReader reader) {
            reader.Read(out ushort count);

            for (int i = 0; i < count; ++i) {
                reader.Read(out ulong actorStaticID);
                reader.Read(out ushort actorID);

                s_Logger.Info($"Reading actor with StaticID {actorStaticID} and ID {actorID} during synchronization.");

                Actor actor = FindInSceneActorWithStaticID(actorStaticID);
                if (actor == null) {
                    s_Logger.Error($"Failed to find actor with ID {actorID} during synchronization.");
                    return false;
                }

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

                reader.Read(out byte actorDataSize);
                reader.Read(out NativeSlice<byte> stateData, actorDataSize);

                actor.RestoreCoreState(coreState);
                if (stateData.Length > 0) {
                    DataSerializationUtils.Deserialize(stateData, ref actor.Data);
                }

                s_Logger.Info($"Assigning StaticID {actorStaticID} and ID {actorID}");
                actor.ActorID = actorID;
                s_Logger.Info("Actor id set to " + actor.ActorID);
            }

            foreach (Actor inSceneActor in InSceneActors) {
                s_Logger.Info($"InSceneActor: StaticID={inSceneActor.ActorStaticID}, ID={inSceneActor.ActorID}, Path={inSceneActor.SourceActorPath}");
            }

            return true;
        }

        internal void SpawnDynamicActorsForClient(ulong clientID) {
            if (!RR.IsServer()) {
                s_Logger.Error("Only the server can spawn dynamic actors for clients.");
                return;
            }

            foreach (Actor actor in SpawnedActors) {
                if (actor.OrNull() == null) {
                    continue;
                }

                ActorCoreStateSnapshot coreState = actor.GetCoreStateSnapshot();
                m_Network.SendSpawnActor(clientID,
                                         actor.SourceActorPath,
                                         coreState,
                                         actor.Data);
            }

        }
    }

}