#nullable enable
using System;
using System.Collections.Generic;

namespace IGP.UnitySDK.Network
{
    internal sealed class IGPBoundedPacketQueue<T>
    {
        private readonly Queue<Entry> queue = new Queue<Entry>();
        private readonly Func<T, int> measureBytes;
        private readonly int maxCount;
        private readonly long maxBytes;

        private long queuedBytes;
        private long totalDroppedCount;
        private long totalDroppedBytes;
        private long intervalDroppedCount;
        private long intervalDroppedBytes;

        public IGPBoundedPacketQueue(int maxCount, long maxBytes, Func<T, int> measureBytes)
        {
            if (maxCount <= 0)
            {
                throw new ArgumentOutOfRangeException(nameof(maxCount));
            }

            if (maxBytes <= 0)
            {
                throw new ArgumentOutOfRangeException(nameof(maxBytes));
            }

            this.maxCount = maxCount;
            this.maxBytes = maxBytes;
            this.measureBytes = measureBytes ?? throw new ArgumentNullException(nameof(measureBytes));
        }

        public int Count => queue.Count;

        public long QueuedBytes => queuedBytes;

        public long TotalDroppedCount => totalDroppedCount;

        public long TotalDroppedBytes => totalDroppedBytes;

        public void Enqueue(T item)
        {
            int bytes = Math.Max(0, measureBytes(item));
            queue.Enqueue(new Entry(item, bytes));
            queuedBytes += bytes;
            TrimToLimits();
        }

        public bool TryPeek(out T item)
        {
            if (queue.Count == 0)
            {
                item = default!;
                return false;
            }

            item = queue.Peek().Item;
            return true;
        }

        public bool TryDequeue(out T item)
        {
            if (queue.Count == 0)
            {
                item = default!;
                return false;
            }

            var entry = queue.Dequeue();
            queuedBytes -= entry.Bytes;
            item = entry.Item;
            return true;
        }

        public DroppedSnapshot ConsumeDroppedSnapshot()
        {
            var snapshot = new DroppedSnapshot(intervalDroppedCount, intervalDroppedBytes);
            intervalDroppedCount = 0;
            intervalDroppedBytes = 0;
            return snapshot;
        }

        public void Clear()
        {
            queue.Clear();
            queuedBytes = 0;
            totalDroppedCount = 0;
            totalDroppedBytes = 0;
            intervalDroppedCount = 0;
            intervalDroppedBytes = 0;
        }

        private void TrimToLimits()
        {
            while (queue.Count > 0 && (queue.Count > maxCount || queuedBytes > maxBytes))
            {
                var dropped = queue.Dequeue();
                queuedBytes -= dropped.Bytes;
                totalDroppedCount++;
                totalDroppedBytes += dropped.Bytes;
                intervalDroppedCount++;
                intervalDroppedBytes += dropped.Bytes;
            }
        }

        private readonly struct Entry
        {
            public Entry(T item, int bytes)
            {
                Item = item;
                Bytes = bytes;
            }

            public T Item { get; }

            public int Bytes { get; }
        }

        public readonly struct DroppedSnapshot
        {
            public DroppedSnapshot(long count, long bytes)
            {
                Count = count;
                Bytes = bytes;
            }

            public long Count { get; }

            public long Bytes { get; }
        }
    }
}
