using FishNet.Broadcast;
using FishNet.Broadcast.Helping;
using FishNet.Managing.Logging;
using FishNet.Managing.Utility;
using FishNet.Object.Helping;
using FishNet.Serializing;
using FishNet.Serializing.Helping;
using FishNet.Transporting;
using FishNet.Utility.Extension;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEngine;
namespace FishNet.Managing.Client
{
    public sealed partial class ClientManager : MonoBehaviour
    {
        #region Private.
        /// 
        /// Delegate to read received broadcasts.
        /// 
        /// 
        private delegate void ServerBroadcastDelegate(PooledReader reader);
        /// 
        /// Delegates for each key.
        /// 
        private readonly Dictionary> _broadcastHandlers = new Dictionary>();
        /// 
        /// Delegate targets for each key.
        /// 
        private Dictionary> _handlerTargets = new Dictionary>();
        #endregion
        /// 
        /// Registers a method to call when a Broadcast arrives.
        /// 
        /// Type of broadcast being registered.
        /// Method to call.
        public void RegisterBroadcast(Action handler) where T : struct, IBroadcast
        {
            ushort key = typeof(T).FullName.GetStableHash16();
            /* Create delegate and add for
             * handler method. */
            HashSet handlers;
            if (!_broadcastHandlers.TryGetValueIL2CPP(key, out handlers))
            {
                handlers = new HashSet();
                _broadcastHandlers.Add(key, handlers);
            }
            ServerBroadcastDelegate del = CreateBroadcastDelegate(handler);
            handlers.Add(del);
            /* Add hashcode of target for handler.
             * This is so we can unregister the target later. */
            int handlerHashCode = handler.GetHashCode();
            HashSet<(int, ServerBroadcastDelegate)> targetHashCodes;
            if (!_handlerTargets.TryGetValueIL2CPP(key, out targetHashCodes))
            {
                targetHashCodes = new HashSet<(int, ServerBroadcastDelegate)>();
                _handlerTargets.Add(key, targetHashCodes);
            }
            targetHashCodes.Add((handlerHashCode, del));
        }
        /// 
        /// Unregisters a method call from a Broadcast type.
        /// 
        /// Type of broadcast being unregistered.
        /// Method to unregister.
        public void UnregisterBroadcast(Action handler) where T : struct, IBroadcast
        {
            ushort key = BroadcastHelper.GetKey();
            /* If key is found for T then look for
             * the appropriate handler to remove. */
            if (_broadcastHandlers.TryGetValueIL2CPP(key, out HashSet handlers))
            {
                HashSet<(int, ServerBroadcastDelegate)> targetHashCodes;
                if (_handlerTargets.TryGetValueIL2CPP(key, out targetHashCodes))
                {
                    int handlerHashCode = handler.GetHashCode();
                    ServerBroadcastDelegate result = null;
                    foreach ((int targetHashCode, ServerBroadcastDelegate del) in targetHashCodes)
                    {
                        if (targetHashCode == handlerHashCode)
                        {
                            result = del;
                            targetHashCodes.Remove((targetHashCode, del));
                            break;
                        }
                    }
                    //If no more in targetHashCodes then remove from handlerTarget.
                    if (targetHashCodes.Count == 0)
                        _handlerTargets.Remove(key);
                    if (result != null)
                        handlers.Remove(result);
                }
                //If no more in handlers then remove broadcastHandlers.
                if (handlers.Count == 0)
                    _broadcastHandlers.Remove(key);
            }
        }
        /// 
        /// Creates a ServerBroadcastDelegate.
        /// 
        /// 
        /// 
        /// 
        /// 
        private ServerBroadcastDelegate CreateBroadcastDelegate(Action handler)
        {
            void LogicContainer(PooledReader reader)
            {
                T broadcast = reader.Read();
                handler?.Invoke(broadcast);
            }
            return LogicContainer;
        }
        /// 
        /// Parses a received broadcast.
        /// 
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private void ParseBroadcast(PooledReader reader, Channel channel)
        {
            ushort key = reader.ReadUInt16();
            int dataLength = Packets.GetPacketLength((ushort)PacketId.Broadcast, reader, channel);
            // try to invoke the handler for that message
            if (_broadcastHandlers.TryGetValueIL2CPP(key, out HashSet handlers))
            {
                int readerStartPosition = reader.Position;
                /* //muchlater resetting the position could be better by instead reading once and passing in
                 * the object to invoke with. */
                foreach (ServerBroadcastDelegate handler in handlers)
                {
                    reader.Position = readerStartPosition;
                    handler.Invoke(reader);
                }
            }
            else
            {
                reader.Skip(dataLength);
            }
        }
        /// 
        /// Sends a Broadcast to the server.
        /// 
        /// Type of broadcast to send.
        /// Broadcast data being sent; for example: an instance of your broadcast type.
        /// Channel to send on.
        public void Broadcast(T message, Channel channel = Channel.Reliable) where T : struct, IBroadcast
        {
            //Check local connection state.
            if (!Started)
            {
                NetworkManager.LogWarning($"Cannot send broadcast to server because client is not active.");
                return;
            }
            using (PooledWriter writer = WriterPool.GetWriter())
            {
                Broadcasts.WriteBroadcast(writer, message, channel);
                ArraySegment segment = writer.GetArraySegment();
                NetworkManager.TransportManager.SendToServer((byte)channel, segment);
            }
        }
    }
}