using FishNet.Component.Observing;
using FishNet.Managing.Logging;
using FishNet.Object;
using FishNet.Serializing;
using FishNet.Transporting;
using FishNet.Utility.Extension;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEngine;
using UnityEngine.SceneManagement;

namespace FishNet.Managing.Object
{
    public abstract class ManagedObjects
    {
        #region Public.
        /// <summary>
        /// NetworkObjects which are currently active.
        /// </summary>
        public Dictionary<int, NetworkObject> Spawned = new Dictionary<int, NetworkObject>();
        #endregion

        #region Protected.
        /// <summary>
        /// Returns the next ObjectId to use.
        /// </summary>
        protected internal virtual int GetNextNetworkObjectId() { return -1; }
        /// <summary>
        /// NetworkManager handling this.
        /// </summary>
        protected NetworkManager NetworkManager = null;
        /// <summary>
        /// Objects in currently loaded scenes. These objects can be active or inactive.
        /// Key is the objectId while value is the object. Key is not the same as NetworkObject.ObjectId.
        /// </summary>
        protected Dictionary<ulong, NetworkObject> SceneObjects = new Dictionary<ulong, NetworkObject>();
        #endregion

        /// <summary>
        /// Subscribes to SceneManager.SceneLoaded event.
        /// </summary>
        /// <param name="subscribe"></param>
        internal void SubscribeToSceneLoaded(bool subscribe)
        {
            if (subscribe)
                SceneManager.sceneLoaded += SceneManager_sceneLoaded;
            else
                SceneManager.sceneLoaded -= SceneManager_sceneLoaded;
        }

        /// <summary>
        /// Called when a scene is loaded.
        /// </summary>
        /// <param name="s"></param>
        /// <param name="arg1"></param>
        protected internal virtual void SceneManager_sceneLoaded(Scene s, LoadSceneMode arg1) { }

        /// <summary>
        /// Called when a NetworkObject runs Deactivate.
        /// </summary>
        /// <param name="nob"></param>
        internal virtual void NetworkObjectUnexpectedlyDestroyed(NetworkObject nob)
        {
            if (nob == null)
                return;

            RemoveFromSpawned(nob, true);
        }

        /// <summary>
        /// Removes a NetworkedObject from spawned.
        /// </summary>
        /// <param name="nob"></param>
        private void RemoveFromSpawned(NetworkObject nob, bool unexpectedlyDestroyed)
        {
            Spawned.Remove(nob.ObjectId);
            //Do the same with SceneObjects.
            if (unexpectedlyDestroyed && nob.IsSceneObject)
                RemoveFromSceneObjects(nob);
        }

        /// <summary>
        /// Despawns a NetworkObject.
        /// </summary>
        internal virtual void Despawn(NetworkObject nob, bool disableOnDespawn, bool asServer)
        {
            disableOnDespawn = false; //tmp until finished.

            if (nob == null)
            {
                if (NetworkManager.CanLog(LoggingType.Warning))
                    Debug.LogWarning($"Cannot despawn a null NetworkObject.");
                return;
            }

            //True if should be destroyed, false if deactivated.
            bool destroy = false;
            /* Only modify object state if asServer,
             * or !asServer and not host. This is so clients, when acting as
             * host, don't destroy objects they lost observation of. */

            //Nested objects are NEVER destroyed.
            if (nob.IsNested)
                disableOnDespawn = true;

            //Only check potential destroys when not destruction is choosen over disabling.
            if (!disableOnDespawn)
            {
                //If as server.
                if (asServer)
                {
                    //Scene object.
                    if (!nob.IsSceneObject)
                    {
                        /* If client-host has visibility
                         * then disable and wait for client-host to get destroy
                         * message. Otherwise destroy immediately. */
                        if (nob.Observers.Contains(NetworkManager.ClientManager.Connection))
                            NetworkManager.ServerManager.Objects.AddToPending(nob);
                        else
                            destroy = true;
                    }
                }
                //Not as server.
                else
                {
                    bool isServer = NetworkManager.IsServer;
                    //Only check to destroy if not a scene object.
                    if (!nob.IsSceneObject)
                    {
                        /* If was removed from pending then also destroy.
                        * Pending objects are ones that exist on the server
                        * side only to await destruction from client side.
                        * Objects can also be destroyed if server is not
                        * active. */
                        destroy = (!isServer || NetworkManager.ServerManager.Objects.RemoveFromPending(nob.ObjectId));
                    }
                }
            }

            //Deinitialize to invoke callbacks.
            nob.Deinitialize(asServer);
            //Remove from match condition only if server.
            if (asServer)
                MatchCondition.RemoveFromMatchWithoutRebuild(nob, NetworkManager);
            //Remove from spawned collection.
            RemoveFromSpawned(nob, false);

            //If to destroy.
            if (destroy)
            {
                MonoBehaviour.Destroy(nob.gameObject);
            }
            //If to potentially disable.
            else
            {
                //If as server.
                if (asServer)
                {
                    //If not clientHost then the object can be disabled.
                    if (!NetworkManager.IsClient)
                        nob.gameObject.SetActive(false);
                }
                //Not as server.
                else
                {
                    //If the server is not active then the object can be disabled.
                    if (!NetworkManager.IsServer)
                    {
                        nob.gameObject.SetActive(false);
                    }
                    //If also server then checks must be done.
                    else
                    {
                        /* Object is still spawned on the server side. This means
                         * the clientHost likely lost visibility. When this is the case
                         * update clientHost renderers. */
                        if (NetworkManager.ServerManager.Objects.Spawned.ContainsKey(nob.ObjectId))
                            nob.SetRenderersVisible(false);
                        /* No longer spawned on the server, can
                         * deactivate on the client. */
                        else
                            nob.gameObject.SetActive(false);
                    }
                }

                /* Also despawn child objects.
                 * This only must be done when not destroying
                 * as destroying would result in the despawn being
                 * forced. 
                 *
                 * Only run if asServer as well. The server will send
                 * individual despawns for each child. */
                if (asServer)
                {
                    foreach (NetworkObject childNob in nob.ChildNetworkObjects)
                    {
                        if (childNob != null && !childNob.IsDeinitializing)
                            Despawn(childNob, disableOnDespawn, asServer);
                    }
                }
            }

        }


        /// <summary>
        /// Updates NetworkBehaviours on nob.
        /// </summary>
        /// <param name="asServer"></param>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        protected void UpdateNetworkBehaviours(NetworkObject nob, bool asServer)
        {
            //Would have already been done on server side.
            if (!asServer && NetworkManager.IsServer)
                return;

            InitializePrefab(nob, -1);
        }

        /// <summary>
        /// Initializes a prefab, not to be mistaken for initializing a spawned object.
        /// </summary>
        /// <param name="prefab">Prefab to initialize.</param>
        /// <param name="index">Index within spawnable prefabs.</param>
        public static void InitializePrefab(NetworkObject prefab, int index)
        {
            if (prefab == null)
                return;
            /* Only set the Id if not -1. 
             * A value of -1 would indicate it's a scene
             * object. */
            if (index != -1)
                prefab.PrefabId = (short)index;

            byte componentIndex = 0;
            prefab.UpdateNetworkBehaviours(null, ref componentIndex);
        }

        /// <summary>
        /// Despawns Spawned NetworkObjects. Scene objects will be disabled, others will be destroyed.
        /// </summary>
        protected virtual void DespawnSpawnedWithoutSynchronization(bool asServer)
        {
            foreach (NetworkObject nob in Spawned.Values)
                DespawnWithoutSynchronization(nob, asServer);

            Spawned.Clear();
        }

        /// <summary>
        /// Despawns a network object.
        /// </summary>
        /// <param name="nob"></param>
        protected virtual void DespawnWithoutSynchronization(NetworkObject nob, bool asServer)
        {
            //Null can occur when running as host and server already despawns such as wehen stopping.
            if (nob == null)
                return;

            nob.Deinitialize(asServer);
            /* Only run if asServer, or not 
            * asServer and server isn't running. This
            * prevents objects from affecting the server
            * as host* when being modified client side. */
            if (asServer || (!asServer && !NetworkManager.IsServer))
            {
                if (nob.IsSceneObject)
                    nob.gameObject.SetActive(false);
                else
                    MonoBehaviour.Destroy(nob.gameObject);
            }
        }

        /// <summary>
        /// Adds a NetworkObject to Spawned.
        /// </summary>
        /// <param name="nob"></param>
        internal void AddToSpawned(NetworkObject nob, bool asServer)
        {
            Spawned[nob.ObjectId] = nob;

            //If being added as client and is also server.
            if (!asServer && NetworkManager.IsServer)
                nob.SetRenderersVisible(true);
        }

        /// <summary>
        /// Adds a NetworkObject to SceneObjects.
        /// </summary>
        /// <param name="nob"></param>
        protected internal void AddToSceneObjects(NetworkObject nob)
        {
            SceneObjects[nob.SceneId] = nob;
        }

        /// <summary>
        /// Removes a NetworkObject from SceneObjects.
        /// </summary>
        /// <param name="nob"></param>
        protected internal void RemoveFromSceneObjects(NetworkObject nob)
        {
            SceneObjects.Remove(nob.SceneId);
        }

        /// <summary>
        /// Finds a NetworkObject within Spawned.
        /// </summary>
        /// <param name="objectId"></param>
        /// <returns></returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        protected internal NetworkObject GetSpawnedNetworkObject(int objectId)
        {
            NetworkObject r;
            if (!Spawned.TryGetValueIL2CPP(objectId, out r))
            {
                if (NetworkManager.CanLog(LoggingType.Error))
                    Debug.LogError($"Spawned NetworkObject not found for ObjectId {objectId}.");
            }

            return r;
        }

        /// <summary>
        /// Tries to skip data length for a packet.
        /// </summary>
        /// <param name="packetId"></param>
        /// <param name="reader"></param>
        /// <param name="dataLength"></param>
        protected internal void SkipDataLength(ushort packetId, PooledReader reader, int dataLength, int rpcLinkObjectId = -1)
        {
            /* -1 means length wasn't set, which would suggest a reliable packet.
            * Object should never be missing for reliable packets since spawns
            * and despawns are reliable in order. */
            if (dataLength == (int)MissingObjectPacketLength.Reliable)
            {
                string msg;
                bool isRpcLink = (packetId >= NetworkManager.StartingRpcLinkIndex);
                if (isRpcLink)
                {
                    msg = (rpcLinkObjectId == -1) ?
                        $"RPCLink of Id {(PacketId)packetId} could not be found. Remaining data will be purged." :
                        $"ObjectId {rpcLinkObjectId} for RPCLink {(PacketId)packetId} could not be found.";
                }
                else
                {
                    msg = $"NetworkBehaviour could not be found for packetId {(PacketId)packetId}. Remaining data will be purged.";
                }

                /* Default logging for server is errors only. Use error on client and warning
                 * on servers to reduce chances of allocation attacks. */
#if DEVELOPMENT_BUILD || UNITY_EDITOR || !UNITY_SERVER
                if (NetworkManager.CanLog(LoggingType.Error))
                    Debug.LogError(msg);
#else
                if (NetworkManager.CanLog(LoggingType.Warning))
                    Debug.LogWarning(msg);
#endif
                reader.Skip(reader.Remaining);
            }
            /* If length is known then is unreliable packet. It's possible
             * this packetId arrived before or after the object was spawned/destroyed.
             * Skip past the data for this packet and use rest in reader. With non-linked
             * RPCs length is sent before object information. */
            else if (dataLength >= 0)
            {
                reader.Skip(Math.Min(dataLength, reader.Remaining));
            }
            /* -2 indicates the length is very long. Don't even try saving
             * the packet, user shouldn't be sending this much data over unreliable. */
            else if (dataLength == (int)MissingObjectPacketLength.PurgeRemaiming)
            {
                reader.Skip(reader.Remaining);
            }
        }

    }

}
