using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
namespace MECF.Framework.Common.Communications.Tcp.Buffer
{
    /// 
    /// A manager to handle buffers for the socket connections.
    /// 
    /// 
    /// When used in an async call a buffer is pinned. Large numbers of pinned buffers
    /// cause problem with the GC (in particular it causes heap fragmentation).
    /// This class maintains a set of large segments and gives clients pieces of these
    /// segments that they can use for their buffers. The alternative to this would be to
    /// create many small arrays which it then maintained. This methodology should be slightly
    /// better than the many small array methodology because in creating only a few very
    /// large objects it will force these objects to be placed on the LOH. Since the
    /// objects are on the LOH they are at this time not subject to compacting which would
    /// require an update of all GC roots as would be the case with lots of smaller arrays
    /// that were in the normal heap.
    /// 
    public class SegmentBufferManager : ISegmentBufferManager
    {
        private const int TrialsCount = 100;
        private static SegmentBufferManager _defaultBufferManager;
        private readonly int _segmentChunks;
        private readonly int _chunkSize;
        private readonly int _segmentSize;
        private readonly bool _allowedToCreateMemory;
        private readonly ConcurrentStack> _buffers = new ConcurrentStack>();
        private readonly List _segments;
        private readonly object _creatingNewSegmentLock = new object();
        public static SegmentBufferManager Default
        {
            get
            {
                // default to 1024 1kb buffers if people don't want to manage it on their own;
                if (_defaultBufferManager == null)
                    _defaultBufferManager = new SegmentBufferManager(1024, 1024, 1);
                return _defaultBufferManager;
            }
        }
        public static void SetDefaultBufferManager(SegmentBufferManager manager)
        {
            if (manager == null)
                throw new ArgumentNullException("manager");
            _defaultBufferManager = manager;
        }
        public int ChunkSize
        {
            get { return _chunkSize; }
        }
        public int SegmentsCount
        {
            get { return _segments.Count; }
        }
        public int SegmentChunksCount
        {
            get { return _segmentChunks; }
        }
        public int AvailableBuffers
        {
            get { return _buffers.Count; }
        }
        public int TotalBufferSize
        {
            get { return _segments.Count * _segmentSize; }
        }
        public SegmentBufferManager(int segmentChunks, int chunkSize)
            : this(segmentChunks, chunkSize, 1) { }
        public SegmentBufferManager(int segmentChunks, int chunkSize, int initialSegments)
            : this(segmentChunks, chunkSize, initialSegments, true) { }
        /// 
        /// Constructs a new  object
        /// 
        /// The number of chunks to create per segment
        /// The size of a chunk in bytes
        /// The initial number of segments to create
        /// If false when empty and checkout is called an exception will be thrown
        public SegmentBufferManager(int segmentChunks, int chunkSize, int initialSegments, bool allowedToCreateMemory)
        {
            if (segmentChunks <= 0)
                throw new ArgumentException("segmentChunks");
            if (chunkSize <= 0)
                throw new ArgumentException("chunkSize");
            if (initialSegments < 0)
                throw new ArgumentException("initialSegments");
            _segmentChunks = segmentChunks;
            _chunkSize = chunkSize;
            _segmentSize = _segmentChunks * _chunkSize;
            _segments = new List();
            _allowedToCreateMemory = true;
            for (int i = 0; i < initialSegments; i++)
            {
                CreateNewSegment(true);
            }
            _allowedToCreateMemory = allowedToCreateMemory;
        }
        private void CreateNewSegment(bool forceCreation)
        {
            if (!_allowedToCreateMemory)
                throw new UnableToCreateMemoryException();
            lock (_creatingNewSegmentLock)
            {
                if (!forceCreation && _buffers.Count > _segmentChunks / 2)
                    return;
                var bytes = new byte[_segmentSize];
                _segments.Add(bytes);
                for (int i = 0; i < _segmentChunks; i++)
                {
                    var chunk = new ArraySegment(bytes, i * _chunkSize, _chunkSize);
                    _buffers.Push(chunk);
                }
            }
        }
        public ArraySegment BorrowBuffer()
        {
            int trial = 0;
            while (trial < TrialsCount)
            {
                ArraySegment result;
                if (_buffers.TryPop(out result))
                    return result;
                CreateNewSegment(false);
                trial++;
            }
            throw new UnableToAllocateBufferException();
        }
        public IEnumerable> BorrowBuffers(int count)
        {
            var result = new ArraySegment[count];
            var trial = 0;
            var totalReceived = 0;
            try
            {
                while (trial < TrialsCount)
                {
                    ArraySegment piece;
                    while (totalReceived < count)
                    {
                        if (!_buffers.TryPop(out piece))
                            break;
                        result[totalReceived] = piece;
                        ++totalReceived;
                    }
                    if (totalReceived == count)
                        return result;
                    CreateNewSegment(false);
                    trial++;
                }
                throw new UnableToAllocateBufferException();
            }
            catch
            {
                if (totalReceived > 0)
                    ReturnBuffers(result.Take(totalReceived));
                throw;
            }
        }
        public void ReturnBuffer(ArraySegment buffer)
        {
            if (ValidateBuffer(buffer))
            {
                _buffers.Push(buffer);
            }
        }
        public void ReturnBuffers(IEnumerable> buffers)
        {
            if (buffers == null)
                throw new ArgumentNullException("buffers");
            foreach (var buf in buffers)
            {
                if (ValidateBuffer(buf))
                {
                    _buffers.Push(buf);
                }
            }
        }
        public void ReturnBuffers(params ArraySegment[] buffers)
        {
            if (buffers == null)
                throw new ArgumentNullException("buffers");
            foreach (var buf in buffers)
            {
                if (ValidateBuffer(buf))
                {
                    _buffers.Push(buf);
                }
            }
        }
        private bool ValidateBuffer(ArraySegment buffer)
        {
            if (buffer.Array == null || buffer.Count == 0 || buffer.Array.Length < buffer.Offset + buffer.Count)
                return false;
            if (buffer.Count != _chunkSize)
                return false;
            return true;
        }
    }
}