using System;
using System.Runtime.InteropServices;
using Unity.Networking.Transport;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Networking.Transport.Utilities;

namespace Unity.Networking.Transport.Samples
{
    [StructLayout(LayoutKind.Explicit)]
    public unsafe struct SoakMessage
    {
        public const int Capacity = NetworkParameterConstants.MTU;
        public const int HeaderLength = 6 * sizeof(int) + sizeof(float);

        [FieldOffset(0)] public fixed byte data[Capacity];
        [FieldOffset(0)] public int type;

        [FieldOffset(4)] public int sequence;
        [FieldOffset(8)] public int ack;
        [FieldOffset(12)] public int ackBuffer;

        [FieldOffset(16)] public int id;
        [FieldOffset(20)] public float time;
        [FieldOffset(24)] public int length;

        [FieldOffset(28)] public fixed byte payload[Capacity - HeaderLength];
    }

    public struct SoakJobContext
    {
        public int FrameId;
        public double StartedAt;
        public double Accumulator;
        public double TimeStep;
        public double NextStatsPrint;

        public int Connected;

        public int NextSequenceNumber;
        public int LatestReceivedSequenceNumber;

        public int PacketSize;
        public double SendInterval;
        public int Duration;
        public int Done;
    }

    public struct NetworkDriverStatisticsPoint
    {
        public float Timestamp;

        public float SentPackets;
        public float ReceivedPackets;
        public float DroppedOrStalePackets;
        public float SentBytes;
        public float ReceivedBytes;
    }

    public struct SoakStatisticsPoint
    {
        public int PingTimeMeanCount;
        public float Timestamp;
        public float PingTimeMean;
        public float SentPackets;
        public float ReceivedPackets;
        public float DroppedOrStalePackets;
        public float SentBytes;
        public float ReceivedBytes;

        // Reliability pipeline
        public float ReliableDropped;
        public float ReliableSent;
        public float ReliableReceived;
        public float ReliableResent;
        public float ReliableDuplicate;
        public float ReliableRTT;
        public float ReliableSRTT;
        public float ReliableMaxRTT;
        public float ReliableMaxProcessingTime;
        public float ReliableResendQueue;
        public float ReliableOldestResendPacketAge;
    }

    public class StatisticsReport : IDisposable
    {
        public NativeList<SoakStatisticsPoint> Samples;
        public int BucketSize;

        public void AddSample(SoakStatisticsPoint point, float now)
        {
            point.Timestamp = now;
            Samples.Add(point);
        }

        public StatisticsReport(int bucketSize)
        {
            BucketSize = bucketSize;
            Samples = new NativeList<SoakStatisticsPoint>(Allocator.Persistent);
        }

        public void Dispose()
        {
            if (Samples.IsCreated)
                Samples.Dispose();
        }
    }

    public static class Util
    {
        public static unsafe void DumpReliabilityStatistics(NetworkDriver driver, NetworkPipeline pipeline, NetworkPipelineStageId reliableStageId, NetworkConnection con)
        {
            driver.GetPipelineBuffers(pipeline, reliableStageId, con, out var receiveBuffer, out var sendBuffer, out var sharedBuffer);
            /*var relCtx = (ReliableUtility.SharedContext*)sharedBuffer.GetUnsafeReadOnlyPtr();
            var sendCtx = (ReliableUtility.Context*)sendBuffer.GetUnsafeReadOnlyPtr();
            UnityEngine.Debug.Log("Reliability stats\nPacketsDropped: " + relCtx->stats.PacketsDropped + "\n" +
                    "PacketsDuplicated: " + relCtx->stats.PacketsDuplicated + "\n" +
                    "PacketsOutOfOrder: " + relCtx->stats.PacketsOutOfOrder + "\n" +
                    "PacketsReceived: " + relCtx->stats.PacketsReceived + "\n" +
                    "PacketsResent: " + relCtx->stats.PacketsResent + "\n" +
                    "PacketsSent: " + relCtx->stats.PacketsSent + "\n" +
                    "PacketsStale: " + relCtx->stats.PacketsStale + "\n" +
                    "Last received remote seqId: " + relCtx->ReceivedPackets.Sequence + "\n" +
                    "Last received remote ackMask: " + SequenceHelpers.BitMaskToString(relCtx->ReceivedPackets.AckMask) + "\n" +
                    "Last sent seqId: " + (relCtx->SentPackets.Sequence - 1)+ "\n" +
                    "Last acked seqId: " + relCtx->SentPackets.Acked + "\n" +
                    "Last ackmask: " + SequenceHelpers.BitMaskToString(relCtx->SentPackets.AckMask));*/
        }

        public static unsafe void GatherReliabilityStats(ref SoakStatisticsPoint stats, ref SoakStatisticsPoint lastStats, NetworkDriver driver,
            NetworkPipeline pipeline, NetworkPipelineStageId reliableStageId, NetworkConnection con, long timestamp)
        {
            driver.GetPipelineBuffers(pipeline, reliableStageId, con, out var receiveBuffer, out var sendBuffer, out var sharedBuffer);

            var sharedCtx = (ReliableUtility.SharedContext*)sharedBuffer.GetUnsafeReadOnlyPtr();
            stats.ReliableSent += sharedCtx->stats.PacketsSent - lastStats.ReliableSent;
            //Console.WriteLine("sharedCtx->stats.PacketsSent=" + sharedCtx->stats.PacketsSent + " lastStats.ReliableSent=" + lastStats.ReliableSent + " stats.ReliableSent=" + stats.ReliableSent);
            stats.ReliableResent += sharedCtx->stats.PacketsResent - lastStats.ReliableResent;
            stats.ReliableDropped += sharedCtx->stats.PacketsDropped - lastStats.ReliableDropped;
            stats.ReliableReceived += sharedCtx->stats.PacketsReceived - lastStats.ReliableReceived;
            stats.ReliableDuplicate += sharedCtx->stats.PacketsDuplicated - lastStats.ReliableDuplicate;
            stats.ReliableRTT = sharedCtx->RttInfo.LastRtt;
            stats.ReliableSRTT = sharedCtx->RttInfo.SmoothedRtt;
            int resendQueueSize = 0;
            int oldestResendPacketAge = 0;
            int maxRtt = 0;
            int maxProcessingTime = 0;
            GatherExtraStats(sendBuffer, sharedBuffer, timestamp, ref resendQueueSize, ref oldestResendPacketAge, ref maxRtt, ref maxProcessingTime);
            stats.ReliableResendQueue = resendQueueSize;
            stats.ReliableOldestResendPacketAge = oldestResendPacketAge;
            stats.ReliableMaxRTT = maxRtt;
            stats.ReliableMaxProcessingTime = maxProcessingTime;

            lastStats.ReliableSent = sharedCtx->stats.PacketsSent;
            lastStats.ReliableResent = sharedCtx->stats.PacketsResent;
            lastStats.ReliableDropped = sharedCtx->stats.PacketsDropped;
            lastStats.ReliableReceived = sharedCtx->stats.PacketsReceived;
            lastStats.ReliableDuplicate = sharedCtx->stats.PacketsDuplicated;
        }

        static unsafe void GatherExtraStats(NativeArray<byte> sendBuffer, NativeArray<byte> sharedBuffer, long timestamp, ref int usedCount, ref int oldestAge, ref int maxRtt, ref int maxProcessingTime)
        {
            var ptr = (byte*)sendBuffer.GetUnsafePtr();
            var ctx = (ReliableUtility.Context*)ptr;
            var shared = (ReliableUtility.SharedContext*)sharedBuffer.GetUnsafePtr();

            for (int i = 0; i < shared->WindowSize; i++)
            {
                var seqId = (int*)(ptr + ctx->IndexPtrOffset + i * ctx->IndexStride);
                if (*seqId != -1)
                {
                    usedCount++;
                    var packetInfo = ReliableUtility.GetPacketInformation((byte*)sendBuffer.GetUnsafeReadOnlyPtr(), *seqId);
                    oldestAge = Math.Max(oldestAge, (int)(timestamp - packetInfo->SendTime));
                }

                var timingData = ReliableUtility.GetLocalPacketTimer((byte*)sharedBuffer.GetUnsafeReadOnlyPtr(), (ushort)i);
                if (timingData->SentTime > 0 && timingData->ReceiveTime > 0)
                    maxRtt = Math.Max(maxRtt, (int)(timingData->ReceiveTime - timingData->SentTime - timingData->ProcessingTime));

                maxProcessingTime = Math.Max(maxProcessingTime, timingData->ProcessingTime);
            }
        }
    }
}
