using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEngine;

namespace Mirror
{
    public class NetworkConnectionToClient : NetworkConnection
    {
        // rpcs are collected in a buffer, and then flushed out together.
        // this way we don't need one NetworkMessage per rpc.
        // => prepares for LocalWorldState as well.
        // ensure max size when adding!
        readonly NetworkWriter reliableRpcs = new NetworkWriter();
        readonly NetworkWriter unreliableRpcs = new NetworkWriter();

        public virtual string address => Transport.active.ServerGetClientAddress(connectionId);

        /// <summary>NetworkIdentities that this connection can see</summary>
        // TODO move to server's NetworkConnectionToClient?
        public readonly HashSet<NetworkIdentity> observing = new HashSet<NetworkIdentity>();

        // unbatcher
        public Unbatcher unbatcher = new Unbatcher();

        // server runs a time snapshot interpolation for each client's local time.
        // this is necessary for client auth movement to still be smooth on the
        // server for host mode.
        // TODO move them along server's timeline in the future.
        //      perhaps with an offset.
        //      for now, keep compatibility by manually constructing a timeline.
        ExponentialMovingAverage driftEma;
        ExponentialMovingAverage deliveryTimeEma; // average delivery time (standard deviation gives average jitter)
        public double remoteTimeline;
        public double remoteTimescale;
        double bufferTimeMultiplier = 2;
        double bufferTime => NetworkServer.sendInterval * bufferTimeMultiplier;

        // <clienttime, snaps>
        readonly SortedList<double, TimeSnapshot> snapshots = new SortedList<double, TimeSnapshot>();

        // Snapshot Buffer size limit to avoid ever growing list memory consumption attacks from clients.
        public int snapshotBufferSizeLimit = 64;

        // ping for rtt (round trip time)
        // useful for statistics, lag compensation, etc.
        double lastPingTime = 0;
        internal ExponentialMovingAverage _rtt = new ExponentialMovingAverage(NetworkTime.PingWindowSize);

        /// <summary>Round trip time (in seconds) that it takes a message to go server->client->server.</summary>
        public double rtt => _rtt.Value;

        public NetworkConnectionToClient(int networkConnectionId)
            : base(networkConnectionId)
        {
            // initialize EMA with 'emaDuration' seconds worth of history.
            // 1 second holds 'sendRate' worth of values.
            // multiplied by emaDuration gives n-seconds.
            driftEma = new ExponentialMovingAverage(NetworkServer.sendRate * NetworkClient.snapshotSettings.driftEmaDuration);
            deliveryTimeEma = new ExponentialMovingAverage(NetworkServer.sendRate * NetworkClient.snapshotSettings.deliveryTimeEmaDuration);

            // buffer limit should be at least multiplier to have enough in there
            snapshotBufferSizeLimit = Mathf.Max((int)NetworkClient.snapshotSettings.bufferTimeMultiplier, snapshotBufferSizeLimit);
        }

        public void OnTimeSnapshot(TimeSnapshot snapshot)
        {
            // protect against ever growing buffer size attacks
            if (snapshots.Count >= snapshotBufferSizeLimit) return;

            // (optional) dynamic adjustment
            if (NetworkClient.snapshotSettings.dynamicAdjustment)
            {
                // set bufferTime on the fly.
                // shows in inspector for easier debugging :)
                bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment(
                    NetworkServer.sendInterval,
                    deliveryTimeEma.StandardDeviation,
                    NetworkClient.snapshotSettings.dynamicAdjustmentTolerance
                );
                // Debug.Log($"[Server]: {name} delivery std={serverDeliveryTimeEma.StandardDeviation} bufferTimeMult := {bufferTimeMultiplier} ");
            }

            // insert into the server buffer & initialize / adjust / catchup
            SnapshotInterpolation.InsertAndAdjust(
                snapshots,
                NetworkClient.snapshotSettings.bufferLimit,
                snapshot,
                ref remoteTimeline,
                ref remoteTimescale,
                NetworkServer.sendInterval,
                bufferTime,
                NetworkClient.snapshotSettings.catchupSpeed,
                NetworkClient.snapshotSettings.slowdownSpeed,
                ref driftEma,
                NetworkClient.snapshotSettings.catchupNegativeThreshold,
                NetworkClient.snapshotSettings.catchupPositiveThreshold,
                ref deliveryTimeEma
            );
        }

        public void UpdateTimeInterpolation()
        {
            // timeline starts when the first snapshot arrives.
            if (snapshots.Count > 0)
            {
                // progress local timeline.
                SnapshotInterpolation.StepTime(Time.unscaledDeltaTime, ref remoteTimeline, remoteTimescale);

                // progress local interpolation.
                // TimeSnapshot doesn't interpolate anything.
                // this is merely to keep removing older snapshots.
                SnapshotInterpolation.StepInterpolation(snapshots, remoteTimeline, out _, out _, out _);
                // Debug.Log($"NetworkClient SnapshotInterpolation @ {localTimeline:F2} t={t:F2}");
            }
        }

        // Send stage three: hand off to transport
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        protected override void SendToTransport(ArraySegment<byte> segment, int channelId = Channels.Reliable) =>
            Transport.active.ServerSend(connectionId, segment, channelId);

        protected virtual void UpdatePing()
        {
            // localTime (double) instead of Time.time for accuracy over days
            if (NetworkTime.localTime >= lastPingTime + NetworkTime.PingInterval)
            {
                // TODO it would be safer for the server to store the last N
                // messages' timestamp and only send a message number.
                // This way client's can't just modify the timestamp.
                // predictedTime parameter is 0 because the server doesn't predict.
                NetworkPingMessage pingMessage = new NetworkPingMessage(NetworkTime.localTime, 0);
                Send(pingMessage, Channels.Unreliable);
                lastPingTime = NetworkTime.localTime;
            }
        }

        internal override void Update()
        {
            UpdatePing();
            base.Update();
        }

        /// <summary>Disconnects this connection.</summary>
        public override void Disconnect()
        {
            // set not ready and handle clientscene disconnect in any case
            // (might be client or host mode here)
            isReady = false;
            reliableRpcs.Position = 0;
            unreliableRpcs.Position = 0;
            Transport.active.ServerDisconnect(connectionId);

            // IMPORTANT: NetworkConnection.Disconnect() is NOT called for
            // voluntary disconnects from the other end.
            // -> so all 'on disconnect' cleanup code needs to be in
            //    OnTransportDisconnect, where it's called for both voluntary
            //    and involuntary disconnects!
        }

        internal void AddToObserving(NetworkIdentity netIdentity)
        {
            observing.Add(netIdentity);

            // spawn identity for this conn
            NetworkServer.ShowForConnection(netIdentity, this);
        }

        internal void RemoveFromObserving(NetworkIdentity netIdentity, bool isDestroyed)
        {
            observing.Remove(netIdentity);

            if (!isDestroyed)
            {
                // hide identity for this conn
                NetworkServer.HideForConnection(netIdentity, this);
            }
        }

        internal void RemoveFromObservingsObservers()
        {
            foreach (NetworkIdentity netIdentity in observing)
            {
                netIdentity.RemoveObserver(this);
            }
            observing.Clear();
        }

        internal void AddOwnedObject(NetworkIdentity obj)
        {
            owned.Add(obj);
        }

        internal void RemoveOwnedObject(NetworkIdentity obj)
        {
            owned.Remove(obj);
        }

        internal void DestroyOwnedObjects()
        {
            // create a copy because the list might be modified when destroying
            HashSet<NetworkIdentity> tmp = new HashSet<NetworkIdentity>(owned);
            foreach (NetworkIdentity netIdentity in tmp)
            {
                if (netIdentity != null)
                {
                    // unspawn scene objects, destroy instantiated objects.
                    // fixes: https://github.com/MirrorNetworking/Mirror/issues/3538
                    if (netIdentity.sceneId != 0)
                        NetworkServer.UnSpawn(netIdentity.gameObject);
                    else
                        NetworkServer.Destroy(netIdentity.gameObject);
                }
            }

            // clear the hashset because we destroyed them all
            owned.Clear();
        }
    }
}