using System.Collections.Generic;

namespace HeuristicLab.Problems.ProgramSynthesis {
  using System;
  using System.Collections.Concurrent;
  using System.IO;
  using System.Reflection;
  using System.Runtime.Serialization.Formatters.Binary;

  using Microsoft.IO;

  public interface IManagedPool<T> : IDisposable where T : class, IPooledObject {
    T Get(bool reset = true);
    void Release();
  }

  public class ManagedPoolProvider<T> where T : class, IPooledObject {
    private readonly ConcurrentStack<T[]> partitions = new ConcurrentStack<T[]>();
    private readonly ObjectPool<IManagedPool<T>> managedPools;
    private readonly BinaryFormatter binaryFormatter = new BinaryFormatter();
    private readonly RecyclableMemoryStreamManager recyclableMemoryStreamManager = new RecyclableMemoryStreamManager();
    private byte[] dummyPartition;
    private volatile object dummyCreationLockObject = new object();

    private static readonly FieldInfo InternalListArrayProperty = typeof(List<T[]>).GetField(
      "_items",
      BindingFlags.NonPublic | BindingFlags.Instance);

    private readonly Func<T> factory;

    public readonly int PartitionSize;
    public readonly int MaxPartitionCount;
    public const int DefaultMaxInstanceCount = 65536;

    public ManagedPoolProvider(int partitionSize, Func<T> factory, int? maxPartitionCount = null) {
      PartitionSize = partitionSize;
      MaxPartitionCount = maxPartitionCount ?? DefaultMaxInstanceCount / PartitionSize;
      this.factory = factory;

      managedPools = new ObjectPool<IManagedPool<T>>(() => new ManagedPool(this));
    }
    public int InstanceCount { get { return partitions.Count * PartitionSize; } }

    public void Clear() {
      dummyPartition = null;
      managedPools.Clear();
      partitions.Clear();
    }

    public void ReleasePartitions(List<T[]> releasedPartitions) {
      if (partitions.Count < MaxPartitionCount)
        partitions.PushRange((T[][])InternalListArrayProperty.GetValue(releasedPartitions), 0, releasedPartitions.Count);
    }

    private T[] GetPartition() {
      T[] partition;
      return partitions.TryPop(out partition) ? partition : CloneDummyPartition();
    }

    private T[] CloneDummyPartition() {
      // init dummy partition
      if (dummyPartition == null) {
        lock (dummyCreationLockObject) {
          if (dummyPartition == null) {
            var temp = new T[PartitionSize];

            for (var i = 0u; i < PartitionSize; i++) {
              temp[i] = factory();
            }

            using (var ms = recyclableMemoryStreamManager.GetStream("dummyPartition")) {
              binaryFormatter.Serialize(ms, temp);
              dummyPartition = ms.ToArray();
            }
          }
        }
      }

      using (var ms = recyclableMemoryStreamManager.GetStream("dummyPartition", dummyPartition, 0, dummyPartition.Length)) {
        ms.Seek(0, SeekOrigin.Begin);
        var result = (T[])binaryFormatter.Deserialize(ms);
        return result;
      }
    }

    public IManagedPool<T> CreatePool() {
      return managedPools.Allocate();
    }

    private class ManagedPool : IManagedPool<T> {
      private readonly ManagedPoolProvider<T> provider;
      private readonly List<T[]> partitions = new List<T[]>();
      private T[] currentPartition;
      private int entryIndex;

      public ManagedPool(ManagedPoolProvider<T> provider) {
        this.provider = provider;
        entryIndex = provider.PartitionSize;
      }

      public T Get(bool resetEntry = true) {
        if (entryIndex == provider.PartitionSize) {
          currentPartition = provider.GetPartition();
          partitions.Add(currentPartition);
          entryIndex = 0;
        }

        var entry = currentPartition[entryIndex++];
        if (resetEntry) entry.Reset();
        return entry;
      }

      public void Release() {
        if (partitions.Count > 0) {
          provider.ReleasePartitions(partitions);
          partitions.Clear();
          currentPartition = null;
          entryIndex = provider.PartitionSize;
        }
      }

      public void Dispose() {
        Release();

        provider.managedPools.Free(this);
      }
    }
  }
}