using FishNet.Connection;
using FishNet.Managing;
using FishNet.Managing.Logging;
using FishNet.Managing.Server;
using FishNet.Object;
using FishNet.Observing;
using FishNet.Utility.Extension;
using FishNet.Utility.Performance;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using UnityEngine;

namespace FishNet.Component.Observing
{
    /// <summary>
    /// When this observer condition is placed on an object, a client must be within the same match to view the object.
    /// </summary>
    [CreateAssetMenu(menuName = "FishNet/Observers/Match Condition", fileName = "New Match Condition")]
    public class MatchCondition : ObserverCondition
    {
        #region Private.
        /// <summary>
        /// 
        /// </summary>
        private static Dictionary<int, HashSet<NetworkConnection>> _matchConnections = new Dictionary<int, HashSet<NetworkConnection>>();
        /// <summary>
        /// Matches and connections in each match.
        /// </summary>
        public static IReadOnlyDictionary<int, HashSet<NetworkConnection>> MatchConnections => _matchConnections;
        /// <summary>
        /// 
        /// </summary>
        /// //todo this needs to hold hashset so conns can be in multiple matches.
        private static Dictionary<NetworkConnection, int> _connectionMatch = new Dictionary<NetworkConnection, int>();
        /// <summary>
        /// Match a connection is in.
        /// </summary>
        public static IReadOnlyDictionary<NetworkConnection, int> ConnectionMatch => _connectionMatch;
        /// <summary>
        /// 
        /// </summary>
        private static Dictionary<int, HashSet<NetworkObject>> _matchObjects = new Dictionary<int, HashSet<NetworkObject>>();
        /// <summary>
        /// Matches and connections in each match.
        /// </summary>
        public static IReadOnlyDictionary<int, HashSet<NetworkObject>> MatchObjects => _matchObjects;
        /// <summary>
        /// 
        /// </summary>
        /// //todo this needs to hold hashset so conns can be in multiple matches.
        private static Dictionary<NetworkObject, int> _objectMatch = new Dictionary<NetworkObject, int>();
        /// <summary>
        /// Match a connection is in.
        /// </summary>
        public static IReadOnlyDictionary<NetworkObject, int> ObjectMatch => _objectMatch;
        #endregion

        public void ConditionConstructor() { }

        #region Add to match NetworkConnection.
        /// <summary>
        /// Adds a connection to a match.
        /// </summary>
        /// <param name="match">Match to add conn to.</param>
        /// <param name="conn">Connection to add to match.</param>
        /// <param name="manager">NetworkManager to rebuild observers on. If null InstanceFinder.NetworkManager will be used.</param>
        /// <param name="replaceMatch">True to replace other matches with the new match.</param>
        public static void AddToMatch(int match, NetworkConnection conn, NetworkManager manager = null, bool replaceMatch = false)
        {
            if (replaceMatch)
                RemoveFromMatchWithoutRebuild(conn, manager);

            HashSet<NetworkConnection> results;
            if (!_matchConnections.TryGetValueIL2CPP(match, out results))
            {
                results = new HashSet<NetworkConnection>();
                _matchConnections.Add(match, results);
            }

            bool r = results.Add(conn);
            _connectionMatch[conn] = match;
            if (r)
                FinalizeChange(match, results, manager);
        }
        /// <summary>
        /// Adds connections to a match.
        /// </summary>
        /// <param name="match">Match to add conns to.</param>
        /// <param name="conns">Connections to add to match.</param>
        /// <param name="manager">NetworkManager to rebuild observers on. If null InstanceFinder.NetworkManager will be used.</param>
        /// <param name="replaceMatch">True to replace other matches with the new match.</param>
        public static void AddToMatch(int match, NetworkConnection[] conns, NetworkManager manager = null, bool replaceMatch = false)
        {
            AddToMatch(match, conns.ToList(), manager, replaceMatch);
        }
        /// <summary>
        /// Adds connections to a match.
        /// </summary>
        /// <param name="match">Match to add conns to.</param>
        /// <param name="conns">Connections to add to match.</param>
        /// <param name="manager">NetworkManager to rebuild observers on. If null InstanceFinder.NetworkManager will be used.</param>
        /// <param name="replaceMatch">True to replace other matches with the new match.</param>
        public static void AddToMatch(int match, List<NetworkConnection> conns, NetworkManager manager = null, bool replaceMatch = false)
        {
            if (replaceMatch)
            {
                foreach (NetworkConnection nc in conns)
                    RemoveFromMatchWithoutRebuild(nc, manager);
            }

            HashSet<NetworkConnection> results;
            if (!_matchConnections.TryGetValueIL2CPP(match, out results))
            {
                results = new HashSet<NetworkConnection>();
                _matchConnections.Add(match, results);
            }

            bool r = false;
            for (int i = 0; i < conns.Count; i++)
            {
                NetworkConnection c = conns[i];
                r |= results.Add(c);
                _connectionMatch[c] = match;
            }

            if (r)
                FinalizeChange(match, results, manager);
        }
        #endregion

        #region Add to match NetworkObject.
        /// <summary>
        /// Adds an object to a match.
        /// </summary>
        /// <param name="match">Match to add conn to.</param>
        /// <param name="nob">Connection to add to match.</param>
        /// <param name="manager">NetworkManager to rebuild observers on. If null InstanceFinder.NetworkManager will be used.</param>
        /// <param name="replaceMatch">True to replace other matches with the new match.</param>
        public static void AddToMatch(int match, NetworkObject nob, NetworkManager manager = null, bool replaceMatch = false)
        {
            if (replaceMatch)
                RemoveFromMatchWithoutRebuild(nob, manager);

            HashSet<NetworkObject> results;
            if (!_matchObjects.TryGetValueIL2CPP(match, out results))
            {
                results = new HashSet<NetworkObject>();
                _matchObjects.Add(match, results);
            }

            bool r = results.Add(nob);
            _objectMatch[nob] = match;

            if (r)
                FinalizeChange(match, results, nob, manager);
        }
        /// <summary>
        /// Adds objects to a match.
        /// </summary>
        /// <param name="match">Match to add conns to.</param>
        /// <param name="nobs">Connections to add to match.</param>
        /// <param name="manager">NetworkManager to rebuild observers on. If null InstanceFinder.NetworkManager will be used.</param>
        /// <param name="replaceMatch">True to replace other matches with the new match.</param>
        public static void AddToMatch(int match, NetworkObject[] nobs, NetworkManager manager = null, bool replaceMatch = false)
        {
            AddToMatch(match, nobs.ToList(), manager, replaceMatch);
        }
        /// <summary>
        /// Adds objects to a match.
        /// </summary>
        /// <param name="match">Match to add conns to.</param>
        /// <param name="nobs">Connections to add to match.</param>
        /// <param name="manager">NetworkManager to rebuild observers on. If null InstanceFinder.NetworkManager will be used.</param>
        /// <param name="replaceMatch">True to replace other matches with the new match.</param>
        public static void AddToMatch(int match, List<NetworkObject> nobs, NetworkManager manager = null, bool replaceMatch = false)
        {
            if (replaceMatch)
            {
                foreach (NetworkObject n in nobs)
                    RemoveFromMatchWithoutRebuild(n, manager);
            }

            HashSet<NetworkObject> results;
            if (!_matchObjects.TryGetValueIL2CPP(match, out results))
            {
                results = new HashSet<NetworkObject>();
                _matchObjects.Add(match, results);
            }

            bool r = false;
            for (int i = 0; i < nobs.Count; i++)
            {
                NetworkObject n = nobs[i];
                r |= results.Add(n);
                _objectMatch[n] = match;
            }

            if (r)
                FinalizeChange(match, results, nobs, manager);
        }
        #endregion

        #region Remove from match NetworkConnection.
        /// <summary>
        /// Removes a connection from any match without rebuilding observers.
        /// </summary>
        /// <param name="conn">Connection to remove from matches.</param>
        /// <param name="manager">NetworkManager connection belongs to. This is not currently used.</param>
        internal static bool RemoveFromMatchWithoutRebuild(NetworkConnection conn, NetworkManager manager)
        {
            bool removed = false;
            //If found to be in a match.
            if (_connectionMatch.TryGetValueIL2CPP(conn, out int match))
            {
                //If match is found.
                if (_matchConnections.TryGetValue(match, out HashSet<NetworkConnection> conns))
                {
                    removed |= conns.Remove(conn);
                    //If no more in hashset remove match.
                    if (conns.Count == 0)
                        _matchConnections.Remove(match);
                }
            }

            //Remove from connectionMatch.
            _connectionMatch.Remove(conn);
            return removed;
        }
        /// <summary>
        /// Removes a connection from all matches.
        /// </summary>
        /// <param name="conn">NetworkConnection to remove.</param>
        /// <param name="manager">NetworkManager to rebuild observers on. If null InstanceFinder.NetworkManager will be used.</param>
        public static void RemoveFromMatch(NetworkConnection conn, NetworkManager manager)
        {
            bool removed = RemoveFromMatchWithoutRebuild(conn, manager);
            if (removed)
                GetServerObjects(manager).RebuildObservers();
        }
        /// <summary>
        /// Removes a connection from a match.
        /// </summary>
        /// <param name="match">Match to remove conn from.</param>
        /// <param name="conn">Connection to remove from match.</param>
        /// <param name="manager">NetworkManager to rebuild observers on. If null InstanceFinder.NetworkManager will be used.</param>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static void RemoveFromMatch(int match, NetworkConnection conn, NetworkManager manager)
        {
            if (_matchConnections.TryGetValueIL2CPP(match, out HashSet<NetworkConnection> results))
            {
                bool r = results.Remove(conn);
                _connectionMatch.Remove(conn);
                if (r)
                    FinalizeChange(match, results, manager);
            }
        }
        /// <summary>
        /// Removes connections from a match.
        /// </summary>
        /// <param name="match">Match to remove conns from.</param>
        /// <param name="conns">Connections to remove from match.</param>
        /// <param name="manager">NetworkManager to rebuild observers on. If null InstanceFinder.NetworkManager will be used.</param>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static void RemoveFromMatch(int match, NetworkConnection[] conns, NetworkManager manager)
        {
            if (_matchConnections.TryGetValueIL2CPP(match, out HashSet<NetworkConnection> results))
            {
                bool r = false;
                for (int i = 0; i < conns.Length; i++)
                {
                    NetworkConnection c = conns[i];
                    r |= results.Remove(c);
                    _connectionMatch.Remove(c);
                }

                if (r)
                    FinalizeChange(match, results, manager);
            }
        }
        /// <summary>
        /// Removes connections from a match.
        /// </summary>
        /// <param name="match">Match to remove conns from.</param>
        /// <param name="conns">Connections to remove from match.</param>
        /// <param name="manager">NetworkManager to rebuild observers on. If null InstanceFinder.NetworkManager will be used.</param>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static void RemoveFromMatch(int match, List<NetworkConnection> conns, NetworkManager manager)
        {
            if (_matchConnections.TryGetValueIL2CPP(match, out HashSet<NetworkConnection> results))
            {
                bool r = false;
                for (int i = 0; i < conns.Count; i++)
                {
                    NetworkConnection c = conns[i];
                    r |= results.Remove(c);
                    _connectionMatch.Remove(c);
                }

                if (r)
                    FinalizeChange(match, results, manager);
            }
        }
        #endregion

        #region Remove from match NetworkObject.
        /// <summary>
        /// Removes a network object from any match without rebuilding observers.
        /// </summary>
        /// <param name="nob">NetworkObject to remove.</param>
        /// <param name="manager">Manager which the network object belongs to. This value is not yet used.</param>
        internal static bool RemoveFromMatchWithoutRebuild(NetworkObject nob, NetworkManager manager = null)
        {
            bool removed = false;
            //If found to be in a match.
            if (_objectMatch.TryGetValueIL2CPP(nob, out int match))
            {
                //If match is found.
                if (_matchObjects.TryGetValue(match, out HashSet<NetworkObject> nobs))
                {
                    removed |= nobs.Remove(nob);
                    //If no more in hashset remove match.
                    if (nobs.Count == 0)
                        _matchObjects.Remove(match);
                }
            }

            //Remove from connectionMatch.
            _objectMatch.Remove(nob);
            return removed;
        }
        /// <summary>
        /// Removes nob from all matches.
        /// </summary>
        /// <param name="nob">NetworkObject to remove.</param>
        /// <param name="manager">NetworkManager to rebuild observers on. If null InstanceFinder.NetworkManager will be used.</param>
        public static void RemoveFromMatch(NetworkObject nob, NetworkManager manager = null)
        {
            bool removed = RemoveFromMatchWithoutRebuild(nob, manager);
            if (removed)
                GetServerObjects(manager).RebuildObservers(nob);
        }
        /// <summary>
        /// Removes a network object from all matches.
        /// </summary>
        /// <param name="nobs">NetworkObjects to remove.</param>
        /// <param name="manager">NetworkManager to rebuild observers on. If null InstanceFinder.NetworkManager will be used.</param>
        public static void RemoveFromMatch(NetworkObject[] nobs, NetworkManager manager = null)
        {
            RemoveFromMatch(nobs.ToList(), manager);
        }
        /// <summary>
        /// Removes network objects from all matches.
        /// </summary>
        /// <param name="nobs">NetworkObjects to remove.</param>
        /// <param name="manager">NetworkManager to rebuild observers on. If null InstanceFinder.NetworkManager will be used.</param>
        public static void RemoveFromMatch(List<NetworkObject> nobs, NetworkManager manager = null)
        {
            bool removed = false;
            foreach (NetworkObject n in nobs)
                removed |= RemoveFromMatchWithoutRebuild(n, manager);

            if (removed)
                GetServerObjects(manager).RebuildObservers(nobs);
        }
        /// <summary>
        /// Removes a network object from a match.
        /// </summary>
        /// <param name="match">Match to remove conn from.</param>
        /// <param name="nob">NetworkObject to remove from match.</param>
        /// <param name="manager">NetworkManager to rebuild observers on. If null InstanceFinder.NetworkManager will be used.</param>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static void RemoveFromMatch(int match, NetworkObject nob, NetworkManager manager = null)
        {
            if (_matchObjects.TryGetValueIL2CPP(match, out HashSet<NetworkObject> results))
            {
                bool r = results.Remove(nob);
                _objectMatch.Remove(nob);
                if (r)
                    FinalizeChange(match, results, nob, manager);
            }
        }
        /// <summary>
        /// Removes network objects from a match.
        /// </summary>
        /// <param name="match">Match to remove conns from.</param>
        /// <param name="nobs">NetworkObjects to remove from match.</param>
        /// <param name="manager">NetworkManager to rebuild observers on. If null InstanceFinder.NetworkManager will be used.</param>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static void RemoveFromMatch(int match, NetworkObject[] nobs, NetworkManager manager = null)
        {
            if (_matchObjects.TryGetValueIL2CPP(match, out HashSet<NetworkObject> results))
            {
                bool r = false;
                for (int i = 0; i < nobs.Length; i++)
                {
                    NetworkObject n = nobs[i];
                    r |= results.Remove(n);
                    _objectMatch.Remove(n);
                }

                if (r)
                    FinalizeChange(match, results, nobs, manager);
            }
        }
        /// <summary>
        /// Removes network objects from a match.
        /// </summary>
        /// <param name="match">Match to remove conns from.</param>
        /// <param name="nobs">NetworkObjects to remove from match.</param>
        /// <param name="manager">NetworkManager to rebuild observers on. If null InstanceFinder.NetworkManager will be used.</param>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static void RemoveFromMatch(int match, List<NetworkObject> nobs, NetworkManager manager = null)
        {
            if (_matchObjects.TryGetValueIL2CPP(match, out HashSet<NetworkObject> results))
            {
                bool r = false;
                for (int i = 0; i < nobs.Count; i++)
                {
                    NetworkObject n = nobs[i];
                    r |= results.Remove(n);
                    _objectMatch.Remove(n);
                }

                if (r)
                    FinalizeChange(match, results, nobs, manager);
            }
        }
        #endregion

        #region FinalizeChange NetworkConnection.
        /// <summary>
        /// Finalizes changes to observers.
        /// </summary>
        private static void FinalizeChange(int match, HashSet<NetworkConnection> remainingConnsInMatch, NetworkManager manager)
        {
            if (remainingConnsInMatch.Count == 0)
                _matchConnections.Remove(match);

            /* Observers on all objects and all conditions have to be rebuilt.
             * This is because the connection changing matches could
             * require the connection to be visible for other players in the match,
             * as well make other connections in the same match visible.
             * But also make all the objects not associated with connections
             * of that match visible. In result to tick all of those boxes
             * all objects need to be rebuilt for all connections. */
            GetServerObjects(manager).RebuildObservers();
        }
        #endregion

        #region FinalizeChange NetworkObject.
        /// <summary>
        /// Finalizes changes to observers.
        /// </summary>
        private static void FinalizeChange(int match, HashSet<NetworkObject> results, List<NetworkObject> nobs, NetworkManager manager)
        {
            ListCache<NetworkObject> cache = ListCaches.GetNetworkObjectCache();
            cache.AddValues(nobs);
            FinalizeChange(match, results, cache, manager);
            ListCaches.StoreCache(cache);
        }
        /// <summary>
        /// Finalizes changes to observers.
        /// </summary>
        private static void FinalizeChange(int match, HashSet<NetworkObject> results, NetworkObject[] nobs, NetworkManager manager)
        {
            ListCache<NetworkObject> cache = ListCaches.GetNetworkObjectCache();
            cache.AddValues(nobs);
            FinalizeChange(match, results, cache, manager);
            ListCaches.StoreCache(cache);
        }
        /// <summary>
        /// Finalizes changes to observers.
        /// </summary>
        private static void FinalizeChange(int match, HashSet<NetworkObject> results, NetworkObject nob, NetworkManager manager)
        {
            ListCache<NetworkObject> cache = ListCaches.GetNetworkObjectCache();
            cache.AddValue(nob);
            FinalizeChange(match, results, cache, manager);
            ListCaches.StoreCache(cache);
        }
        /// <summary>
        /// Finalizes changes to observers.
        /// </summary>
        private static void FinalizeChange(int match, HashSet<NetworkObject> results, ListCache<NetworkObject> nobs, NetworkManager manager)
        {
            if (results.Count == 0)
                _matchConnections.Remove(match);

            GetServerObjects(manager).RebuildObservers(nobs);
        }
        #endregion

        /// <summary>
        /// Returns if the object which this condition resides should be visible to connection.
        /// </summary>
        /// <param name="connection">Connection which the condition is being checked for.</param>
        /// <param name="currentlyAdded">True if the connection currently has visibility of this object.</param>
        /// <param name="notProcessed">True if the condition was not processed. This can be used to skip processing for performance. While output as true this condition result assumes the previous ConditionMet value.</param>
        public override bool ConditionMet(NetworkConnection connection, bool alreadyAdded, out bool notProcessed)
        {
            //If here then checks are being processed.
            notProcessed = false;
            NetworkConnection owner = base.NetworkObject.Owner;
            /* If object is owned then check if owner
            * and connection share a match. */
            if (owner.IsValid)
            {
                //Connection isn't in a match.
                if (!_connectionMatch.TryGetValueIL2CPP(connection, out int match))
                {
                    //Return if this owner is also not in a match.
                    return !_connectionMatch.TryGetValueIL2CPP(owner, out int _);
                }
                //Match isn't found.
                if (!_matchConnections.TryGetValueIL2CPP(match, out HashSet<NetworkConnection> conns))
                    return false;
                //If owner is in same match return true.
                return conns.Contains(owner);
            }
            /* If no owner see if the object is in a match and if so
             * then compare that. */
            else
            {
                //Object isn't in a match.
                if (!_objectMatch.TryGetValueIL2CPP(base.NetworkObject, out int objectMatch))
                    return true;
                /* See if connection is in the same match as the object.
                 * If connection isn't in a match then it fails. */
                if (!_connectionMatch.TryGetValueIL2CPP(connection, out int connectionMatch))
                    return false;
                return (connectionMatch == objectMatch);
            }
        }


        /// <summary>
        /// Returns which ServerObjects to rebuild observers on.
        /// </summary>
        /// <param name="nm"></param>
        /// <returns></returns>
        private static ServerObjects GetServerObjects(NetworkManager manager)
        {
            return (manager == null) ? InstanceFinder.ServerManager.Objects : manager.ServerManager.Objects;
        }


        /* //todo this needs to be passing in the network manager to clear on,
         * otherwise only a single instance of NM is supported.
         * Users are already forced to specify which NM to add
         * matches for but the functionality separating different NMs in relation
         * to such isn't done yet. */
        /// <summary>
        /// Clears all match information without rebuilding.
        /// </summary>
        internal static void ClearMatchesWithoutRebuilding()
        {
            _connectionMatch.Clear();
            _matchConnections.Clear();
            _objectMatch.Clear();
            _matchObjects.Clear();
        }


        /// <summary>
        /// True if the condition requires regular updates.
        /// </summary>
        /// <returns></returns>
        public override bool Timed()
        {
            return false;
        }

        /// <summary>
        /// Clones referenced ObserverCondition. This must be populated with your conditions settings.
        /// </summary>
        /// <returns></returns>
        public override ObserverCondition Clone()
        {
            MatchCondition copy = ScriptableObject.CreateInstance<MatchCondition>();
            copy.ConditionConstructor();
            return copy;
        }
    }
}