#if CSHARP_7_OR_LATER || (UNITY_2018_3_OR_NEWER && (NET_STANDARD_2_0 || NET_4_6))
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member

using System;
using System.Threading;

namespace UniRx.Async.Internal
{
    // Same interface as System.Buffers.ArrayPool<T> but only provides Shared.

    internal sealed class ArrayPool<T>
    {
        // Same size as System.Buffers.DefaultArrayPool<T>
        const int DefaultMaxNumberOfArraysPerBucket = 50;

        static readonly T[] EmptyArray = new T[0];

        public static readonly ArrayPool<T> Shared = new ArrayPool<T>();

        readonly MinimumQueue<T[]>[] buckets;
        readonly SpinLock[] locks;

        ArrayPool()
        {
            // see: GetQueueIndex
            buckets = new MinimumQueue<T[]>[18];
            locks = new SpinLock[18];
            for (int i = 0; i < buckets.Length; i++)
            {
                buckets[i] = new MinimumQueue<T[]>(4);
                locks[i] = new SpinLock(false);
            }
        }

        public T[] Rent(int minimumLength)
        {
            if (minimumLength < 0)
            {
                throw new ArgumentOutOfRangeException("minimumLength");
            }
            else if (minimumLength == 0)
            {
                return EmptyArray;
            }

            var size = CalculateSize(minimumLength);
            var index = GetQueueIndex(size);
            if (index != -1)
            {
                var q = buckets[index];
                var lockTaken = false;
                try
                {
                    locks[index].Enter(ref lockTaken);

                    if (q.Count != 0)
                    {
                        return q.Dequeue();
                    }
                }
                finally
                {
                    if (lockTaken) locks[index].Exit(false);
                }
            }

            return new T[size];
        }

        public void Return(T[] array, bool clearArray = false)
        {
            if (array == null || array.Length == 0)
            {
                return;
            }

            var index = GetQueueIndex(array.Length);
            if (index != -1)
            {
                if (clearArray)
                {
                    Array.Clear(array, 0, array.Length);
                }

                var q = buckets[index];
                var lockTaken = false;

                try
                {
                    locks[index].Enter(ref lockTaken);

                    if (q.Count > DefaultMaxNumberOfArraysPerBucket)
                    {
                        return;
                    }

                    q.Enqueue(array);
                }
                finally
                {
                    if (lockTaken) locks[index].Exit(false);
                }
            }
        }

        static int CalculateSize(int size)
        {
            size--;
            size |= size >> 1;
            size |= size >> 2;
            size |= size >> 4;
            size |= size >> 8;
            size |= size >> 16;
            size += 1;

            if (size < 8)
            {
                size = 8;
            }

            return size;
        }

        static int GetQueueIndex(int size)
        {
            switch (size)
            {
                case 8: return 0;
                case 16: return 1;
                case 32: return 2;
                case 64: return 3;
                case 128: return 4;
                case 256: return 5;
                case 512: return 6;
                case 1024: return 7;
                case 2048: return 8;
                case 4096: return 9;
                case 8192: return 10;
                case 16384: return 11;
                case 32768: return 12;
                case 65536: return 13;
                case 131072: return 14;
                case 262144: return 15;
                case 524288: return 16;
                case 1048576: return 17; // max array length
                default:
                    return -1;
            }
        }
    }
}
#endif