using System;

namespace ServiceStack.Text.Pools;

/// <summary>
/// Courtesy of @marcgravell
/// https://github.com/mgravell/protobuf-net/blob/master/src/protobuf-net/BufferPool.cs
/// </summary>
public sealed class BufferPool
{
    public static void Flush()
    {
        lock (Pool)
        {
            for (var i = 0; i < Pool.Length; i++)
                Pool[i] = null;
        }
    }

    private BufferPool() { }
    private const int POOL_SIZE = 20;
    public const int BUFFER_LENGTH = 1450; //<= MTU - DJB
    private static readonly CachedBuffer[] Pool = new CachedBuffer[POOL_SIZE];

    public static byte[] GetBuffer()
    {
        return GetBuffer(BUFFER_LENGTH);
    }

    public static byte[] GetBuffer(int minSize)
    {
        byte[] cachedBuff = GetCachedBuffer(minSize);
        return cachedBuff ?? new byte[minSize];
    }

    public static byte[] GetCachedBuffer(int minSize)
    {
        lock (Pool)
        {
            var bestIndex = -1;
            byte[] bestMatch = null;
            for (var i = 0; i < Pool.Length; i++)
            {
                var buffer = Pool[i];
                if (buffer == null || buffer.Size < minSize)
                {
                    continue;
                }
                if (bestMatch != null && bestMatch.Length < buffer.Size)
                {
                    continue;
                }

                var tmp = buffer.Buffer;
                if (tmp == null)
                {
                    Pool[i] = null;
                }
                else
                {
                    bestMatch = tmp;
                    bestIndex = i;
                }
            }

            if (bestIndex >= 0)
            {
                Pool[bestIndex] = null;
            }

            return bestMatch;
        }
    }

    /// <remarks>
    /// https://docs.microsoft.com/en-us/dotnet/framework/configure-apps/file-schema/runtime/gcallowverylargeobjects-element
    /// </remarks>
    private const int MaxByteArraySize = int.MaxValue - 56;

    public static void ResizeAndFlushLeft(ref byte[] buffer, int toFitAtLeastBytes, int copyFromIndex, int copyBytes)
    {
        Helpers.DebugAssert(buffer != null);
        Helpers.DebugAssert(toFitAtLeastBytes > buffer.Length);
        Helpers.DebugAssert(copyFromIndex >= 0);
        Helpers.DebugAssert(copyBytes >= 0);

        int newLength = buffer.Length * 2;
        if (newLength < 0)
        {
            newLength = MaxByteArraySize;
        }

        if (newLength < toFitAtLeastBytes) newLength = toFitAtLeastBytes;

        if (copyBytes == 0)
        {
            ReleaseBufferToPool(ref buffer);
        }

        var newBuffer = GetCachedBuffer(toFitAtLeastBytes) ?? new byte[newLength];

        if (copyBytes > 0)
        {
            Buffer.BlockCopy(buffer, copyFromIndex, newBuffer, 0, copyBytes);
            ReleaseBufferToPool(ref buffer);
        }

        buffer = newBuffer;
    }

    public static void ReleaseBufferToPool(ref byte[] buffer)
    {
        if (buffer == null) return;

        lock (Pool)
        {
            var minIndex = 0;
            var minSize = int.MaxValue;
            for (var i = 0; i < Pool.Length; i++)
            {
                var tmp = Pool[i];
                if (tmp == null || !tmp.IsAlive)
                {
                    minIndex = 0;
                    break;
                }
                if (tmp.Size < minSize)
                {
                    minIndex = i;
                    minSize = tmp.Size;
                }
            }

            Pool[minIndex] = new CachedBuffer(buffer);
        }

        buffer = null;
    }

    private class CachedBuffer
    {
        private readonly WeakReference _reference;

        public int Size { get; }

        public bool IsAlive => _reference.IsAlive;
        public byte[] Buffer => (byte[])_reference.Target;

        public CachedBuffer(byte[] buffer)
        {
            Size = buffer.Length;
            _reference = new WeakReference(buffer);
        }
    }
}

internal sealed class Helpers
{
    private Helpers() { }
        
    [System.Diagnostics.Conditional("DEBUG")]
    internal static void DebugAssert(bool condition)
    {
#if DEBUG   
        if (!condition && System.Diagnostics.Debugger.IsAttached) 
            System.Diagnostics.Debugger.Break();
        System.Diagnostics.Debug.Assert(condition);
#endif
    }
        
}