using FishNet.Connection;
using FishNet.Transporting;
using FishNet.Utility.Performance;
using System;
using System.Collections.Generic;
using UnityEngine;
//Thanks to TiToMoskito originally creating this as a Transport.
//https://github.com/TiToMoskito/FishyLatency
namespace FishNet.Managing.Transporting
{
    [System.Serializable]
    public class LatencySimulator
    {
        #region Types.
        /// 
        /// A message affected by latency.
        /// 
        private struct Message
        {
            public readonly int ConnectionId;
            public readonly byte[] Data;
            public readonly int Length;
            public readonly float SendTime;
            public Message(int connectionId, ArraySegment segment, float latency)
            {
                this.ConnectionId = connectionId;
                this.SendTime = (Time.unscaledTime + latency);
                this.Length = segment.Count;
                this.Data = ByteArrayPool.Retrieve(this.Length);
                Buffer.BlockCopy(segment.Array, segment.Offset, this.Data, 0, this.Length);
            }
            public ArraySegment GetSegment()
            {
                return new ArraySegment(Data, 0, Length);
            }
        }
        #endregion
        #region Internal.
        /// 
        /// True if latency can be simulated.
        /// 
        internal bool CanSimulate => (GetEnabled() && (GetLatency() > 0 || GetPacketLost() > 0 || GetOutOfOrder() > 0));
        #endregion
        #region Serialized
        [Header("Settings")]
        /// 
        /// 
        /// 
        [Tooltip("True if latency simulator is enabled.")]
        [SerializeField]
        private bool _enabled;
        /// 
        /// Gets the enabled value of simulator.
        /// 
        public bool GetEnabled() => _enabled;
        /// 
        /// Sets the enabled value of simulator.
        /// 
        /// New value.
        public void SetEnabled(bool value)
        {
            if (value == _enabled)
                return;
            _enabled = value;
            Reset();
        }
        /// 
        /// 
        /// 
        [Tooltip("True to add latency on clientHost as well.")]
        [SerializeField]
        private bool _simulateHost = true;
        /// 
        /// Milliseconds to add between packets. When acting as host this value will be doubled. Added latency will be a minimum of tick rate.
        /// 
        [Tooltip("Milliseconds to add between packets. When acting as host this value will be doubled. Added latency will be a minimum of tick rate.")]
        [Range(0, 60000)]
        [SerializeField]
        private long _latency = 0;
        /// 
        /// Gets the latency value.
        /// 
        /// 
        public long GetLatency() => _latency;
        /// 
        /// Sets a new latency value.
        /// 
        /// Latency as milliseconds.
        public void SetLatency(long value) => _latency = value;
        [Header("Unreliable")]
        /// 
        /// Percentage of unreliable packets which should arrive out of order.
        /// 
        [Tooltip("Percentage of unreliable packets which should arrive out of order.")]
        [Range(0f, 1f)]
        [SerializeField]
        private double _outOfOrder = 0;
        /// 
        /// Out of order chance, 1f is a 100% chance to occur.
        /// 
        /// 
        public double GetOutOfOrder() => _outOfOrder;
        /// 
        /// Sets out of order chance. 1f is a 100% chance to occur.
        /// 
        /// New Value.
        public void SetOutOfOrder(double value) => _outOfOrder = value;
        /// 
        /// Percentage of packets which should drop.
        /// 
        [Tooltip("Percentage of packets which should drop.")]
        [Range(0, 1)]
        [SerializeField]
        private double _packetLoss = 0;
        /// 
        /// Gets packet loss chance. 1f is a 100% chance to occur.
        /// 
        /// 
        public double GetPacketLost() => _packetLoss;
        /// 
        /// Sets packet loss chance. 1f is a 100% chance to occur.
        /// 
        /// New Value.
        public void SetPacketLoss(double value) => _packetLoss = value;
        #endregion
        #region Private
        /// 
        /// Transport to send data on.
        /// 
        private Transport _transport;
        /// 
        /// Reliable messages to the server.
        /// 
        private List _toServerReliable = new List();
        /// 
        /// Unreliable messages to the server.
        /// 
        private List _toServerUnreliable = new List();
        /// 
        /// Reliable messages to clients.
        /// 
        private List _toClientReliable = new List();
        /// 
        /// Unreliable messages to clients.
        /// 
        private List _toClientUnreliable = new List();
        /// 
        /// NetworkManager for this instance.
        /// 
        private NetworkManager _networkManager;
        /// 
        /// Used to generate chances of latency.
        /// 
        private readonly System.Random _random = new System.Random();
        #endregion
        #region Initialization and Unity
        public void Initialize(NetworkManager manager, Transport transport)
        {
            _networkManager = manager;
            _transport = transport;
        }
        #endregion        
        /// 
        /// Stops both client and server.
        /// 
        public void Reset()
        {
            bool enabled = GetEnabled();
            if (_transport != null && enabled)
            { 
                IterateAndStore(_toServerReliable);
                IterateAndStore(_toServerUnreliable);
                IterateAndStore(_toClientReliable);
                IterateAndStore(_toClientUnreliable);
            }
            void IterateAndStore(List messages)
            {
                foreach (Message m in messages)
                {
                    _transport.SendToServer((byte)Channel.Reliable, m.GetSegment());
                    ByteArrayPool.Store(m.Data);
                }
            }
            _toServerReliable.Clear();
            _toServerUnreliable.Clear();
            _toClientReliable.Clear();
            _toClientUnreliable.Clear();
        }
        /// 
        /// Removes pending or held packets for a connection.
        /// 
        /// Connection to remove pending packets for.
        public void RemovePendingForConnection(int connectionId)
        {
            RemoveFromCollection(_toServerUnreliable);
            RemoveFromCollection(_toServerUnreliable);
            RemoveFromCollection(_toClientReliable);
            RemoveFromCollection(_toClientUnreliable);
            void RemoveFromCollection(List c)
            {
                for (int i = 0; i < c.Count; i++)
                {
                    if (c[i].ConnectionId == connectionId)
                    {
                        c.RemoveAt(i);
                        i--;
                    }
                }
            }
        }
        #region Simulation
        /// 
        /// Returns long latency as a float.
        /// 
        /// 
        /// 
        private float GetLatencyAsFloat()
        {
            return (float)(_latency / 1000f);
        }
        /// 
        /// Adds a packet for simulation.
        /// 
        public void AddOutgoing(byte channelId, ArraySegment segment, bool toServer = true, int connectionId = -1)
        {
            /* If to not simulate for host see if this packet
             * should be sent normally. */
            if (!_simulateHost && _networkManager != null && _networkManager.IsHost)
            {
                /* If going to the server and is host then
                 * it must be sent from clientHost. */
                if (toServer)
                {
                    _transport.SendToServer(channelId, segment);
                    return;
                }
                //Not to server, see if going to clientHost.
                else
                {
                    //If connId is the same as clientHost id.
                    if (_networkManager.ClientManager.Connection.ClientId == connectionId)
                    {
                        _transport.SendToClient(channelId, segment, connectionId);
                        return;
                    }
                }
            }
            List collection;
            Channel c = (Channel)channelId;
            if (toServer)
                collection = (c == Channel.Reliable) ? _toServerReliable : _toServerUnreliable;
            else
                collection = (c == Channel.Reliable) ? _toClientReliable : _toClientUnreliable;
            float latency = GetLatencyAsFloat();
            //If dropping check to add extra latency if reliable, or discard if not.
            if (DropPacket())
            {
                if (c == Channel.Reliable)
                {
                    latency += (latency * 0.3f); //add extra for resend.
                }
                //If not reliable then return the segment array to pool.
                else
                {
                    return;
                }
            }
            Message msg = new Message(connectionId, segment, latency);
            int count = collection.Count;
            if (c == Channel.Unreliable && count > 0 && OutOfOrderPacket(c))
                collection.Insert(count - 1, msg);
            else
                collection.Add(msg);
        }
        /// 
        /// Simulates pending outgoing packets.
        /// 
        /// True if sending to the server.
        public void IterateOutgoing(bool toServer)
        {
            if (_transport == null)
            {
                Reset();
                return;
            }
            if (toServer)
            {
                IterateCollection(_toServerReliable, Channel.Reliable);
                IterateCollection(_toServerUnreliable, Channel.Unreliable);
            }
            else
            {
                IterateCollection(_toClientReliable, Channel.Reliable);
                IterateCollection(_toClientUnreliable, Channel.Unreliable);
            }
            void IterateCollection(List collection, Channel channel)
            {
                byte cByte = (byte)channel;
                float unscaledTime = Time.unscaledTime;
                int count = collection.Count;
                int iterations = 0;
                for (int i = 0; i < count; i++)
                {
                    Message msg = collection[i];
                    //Not enough time has passed.
                    if (unscaledTime < msg.SendTime)
                        break;
                    if (toServer)
                        _transport.SendToServer(cByte, msg.GetSegment());
                    else
                        _transport.SendToClient(cByte, msg.GetSegment(), msg.ConnectionId);
                    iterations++;
                }
                if (iterations > 0)
                {
                    for (int i = 0; i < iterations; i++)
                        ByteArrayPool.Store(collection[i].Data);
                    collection.RemoveRange(0, iterations);
                }
            }
            _transport.IterateOutgoing(toServer);
        }
        /// 
        /// Returns if a packet should drop.
        /// 
        /// 
        private bool DropPacket()
        {
            return (_packetLoss > 0d && (_random.NextDouble() < _packetLoss));
        }
        /// 
        /// Returns if a packet should be out of order.
        /// 
        /// 
        /// 
        private bool OutOfOrderPacket(Channel c)
        {
            if (c == Channel.Reliable)
                return false;
            return (_outOfOrder > 0d && (_random.NextDouble() < _outOfOrder));
        }
        #endregion
    }
}