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.
        /// <summary>
        /// A message affected by latency.
        /// </summary>
        private struct Message
        {
            public readonly int ConnectionId;
            public readonly byte[] Data;
            public readonly int Length;
            public readonly float SendTime;

            public Message(int connectionId, ArraySegment<byte> 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<byte> GetSegment()
            {
                return new ArraySegment<byte>(Data, 0, Length);
            }
        }
        #endregion

        #region Internal.
        /// <summary>
        /// True if latency can be simulated.
        /// </summary>
        internal bool CanSimulate => (GetEnabled() && (GetLatency() > 0 || GetPacketLost() > 0 || GetOutOfOrder() > 0));
        #endregion

        #region Serialized
        [Header("Settings")]
        /// <summary>
        /// 
        /// </summary>
        [Tooltip("True if latency simulator is enabled.")]
        [SerializeField]
        private bool _enabled;
        /// <summary>
        /// Gets the enabled value of simulator.
        /// </summary>
        public bool GetEnabled() => _enabled;
        /// <summary>
        /// Sets the enabled value of simulator.
        /// </summary>
        /// <param name="value">New value.</param>
        public void SetEnabled(bool value)
        {
            if (value == _enabled)
                return;

            _enabled = value;
            Reset();
        }
        /// <summary>
        /// 
        /// </summary>
        [Tooltip("True to add latency on clientHost as well.")]
        [SerializeField]
        private bool _simulateHost = true;
        /// <summary>
        /// Milliseconds to add between packets. When acting as host this value will be doubled. Added latency will be a minimum of tick rate.
        /// </summary>
        [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;
        /// <summary>
        /// Gets the latency value.
        /// </summary>
        /// <returns></returns>
        public long GetLatency() => _latency;
        /// <summary>
        /// Sets a new latency value.
        /// </summary>
        /// <param name="value">Latency as milliseconds.</param>
        public void SetLatency(long value) => _latency = value;

        [Header("Unreliable")]
        /// <summary>
        /// Percentage of unreliable packets which should arrive out of order.
        /// </summary>
        [Tooltip("Percentage of unreliable packets which should arrive out of order.")]
        [Range(0f, 1f)]
        [SerializeField]
        private double _outOfOrder = 0;
        /// <summary>
        /// Out of order chance, 1f is a 100% chance to occur.
        /// </summary>
        /// <returns></returns>
        public double GetOutOfOrder() => _outOfOrder;
        /// <summary>
        /// Sets out of order chance. 1f is a 100% chance to occur.
        /// </summary>
        /// <param name="value">New Value.</param>
        public void SetOutOfOrder(double value) => _outOfOrder = value;
        /// <summary>
        /// Percentage of packets which should drop.
        /// </summary>
        [Tooltip("Percentage of packets which should drop.")]
        [Range(0, 1)]
        [SerializeField]
        private double _packetLoss = 0;
        /// <summary>
        /// Gets packet loss chance. 1f is a 100% chance to occur.
        /// </summary>
        /// <returns></returns>
        public double GetPacketLost() => _packetLoss;
        /// <summary>
        /// Sets packet loss chance. 1f is a 100% chance to occur.
        /// </summary>
        /// <param name="value">New Value.</param>
        public void SetPacketLoss(double value) => _packetLoss = value;
        #endregion

        #region Private
        /// <summary>
        /// Transport to send data on.
        /// </summary>
        private Transport _transport;
        /// <summary>
        /// Reliable messages to the server.
        /// </summary>
        private List<Message> _toServerReliable = new List<Message>();
        /// <summary>
        /// Unreliable messages to the server.
        /// </summary>
        private List<Message> _toServerUnreliable = new List<Message>();
        /// <summary>
        /// Reliable messages to clients.
        /// </summary>
        private List<Message> _toClientReliable = new List<Message>();
        /// <summary>
        /// Unreliable messages to clients.
        /// </summary>
        private List<Message> _toClientUnreliable = new List<Message>();
        /// <summary>
        /// NetworkManager for this instance.
        /// </summary>
        private NetworkManager _networkManager;
        /// <summary>
        /// Used to generate chances of latency.
        /// </summary>
        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        

        /// <summary>
        /// Stops both client and server.
        /// </summary>
        public void Reset()
        {
            bool enabled = GetEnabled();
            if (_transport != null && enabled)
            { 
                IterateAndStore(_toServerReliable);
                IterateAndStore(_toServerUnreliable);
                IterateAndStore(_toClientReliable);
                IterateAndStore(_toClientUnreliable);
            }

            void IterateAndStore(List<Message> 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();
        }

        /// <summary>
        /// Removes pending or held packets for a connection.
        /// </summary>
        /// <param name="conn">Connection to remove pending packets for.</param>
        public void RemovePendingForConnection(int connectionId)
        {
            RemoveFromCollection(_toServerUnreliable);
            RemoveFromCollection(_toServerUnreliable);
            RemoveFromCollection(_toClientReliable);
            RemoveFromCollection(_toClientUnreliable);

            void RemoveFromCollection(List<Message> c)
            {
                for (int i = 0; i < c.Count; i++)
                {
                    if (c[i].ConnectionId == connectionId)
                    {
                        c.RemoveAt(i);
                        i--;
                    }
                }
            }
        }

        #region Simulation
        /// <summary>
        /// Returns long latency as a float.
        /// </summary>
        /// <param name="ms"></param>
        /// <returns></returns>
        private float GetLatencyAsFloat()
        {
            return (float)(_latency / 1000f);
        }

        /// <summary>
        /// Adds a packet for simulation.
        /// </summary>
        public void AddOutgoing(byte channelId, ArraySegment<byte> 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<Message> 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);
        }

        /// <summary>
        /// Simulates pending outgoing packets.
        /// </summary>
        /// <param name="toServer">True if sending to the server.</param>
        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<Message> 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);
        }

        /// <summary>
        /// Returns if a packet should drop.
        /// </summary>
        /// <returns></returns>
        private bool DropPacket()
        {
            return (_packetLoss > 0d && (_random.NextDouble() < _packetLoss));
        }

        /// <summary>
        /// Returns if a packet should be out of order.
        /// </summary>
        /// <param name="c"></param>
        /// <returns></returns>
        private bool OutOfOrderPacket(Channel c)
        {
            if (c == Channel.Reliable)
                return false;

            return (_outOfOrder > 0d && (_random.NextDouble() < _outOfOrder));
        }
        #endregion
    }
}