using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using UnityEngine;

namespace UnityGameEngine.Utilities
{
    public class ObjectPoolWithConstructorInternal<T>
    {
        public int ObjectsCreated { get; internal set; }

        public int InUse { get; internal set; }

        public int Available { get { return ObjectsCreated - InUse; } }

        private Type objectType;
        private ConstructorInfo constructor;
        private int numConstructorArgs = 0;

        private Queue<T> availableQueue = new Queue<T>();
        private HashSet<T> availableObjects = new HashSet<T>();

        public ObjectPoolWithConstructorInternal(Type objectType, Type[] constructorParams)
        {
            this.objectType = objectType;
            ConstructorInfo constructor = objectType.GetConstructor(constructorParams);

            if (!typeof(T).IsAssignableFrom(objectType))
            {
                Debug.LogError("Can't create an ObjectPool<" + typeof(T).Name + "> with a constructor from " + objectType.Name);
            }
            else if (constructor == null)
            {
                string err = "Can't construct an ObjectPool<" + objectType.Name + "> with constructor params (";
                err += String.Join(", ", constructorParams.Select(t => t.Name).ToArray()) + "): No such constructor exists!";
                Debug.LogError(err);
            }
            else
            {
                this.constructor = constructor;
                numConstructorArgs = constructorParams.Length;
            }
        }

        public ObjectPoolWithConstructorInternal(Type[] constructorParams) : this(typeof(T), constructorParams) { }

        protected bool HasAvailableObject { get { return availableObjects.Count > 0; } }

        protected T GetObjectIfAvailable()
        {
            InUse++;
            if (availableObjects.Count > 0)
            {
                T obj = availableQueue.Dequeue();
                while (!availableObjects.Remove(obj)) obj = availableQueue.Dequeue();
                return obj;
            }
            return default(T);
        }

        protected T GetNewObject(object[] constructorArgs)
        {
            ObjectsCreated++;
            InUse++;

            if (constructorArgs.Length != numConstructorArgs)
            {
                Debug.LogError("ObjectPool<" + objectType.Name + ">: not enough arguments provided!");
                return default(T);
            }

            bool valid = true;
            object obj = null;
            if (constructor == null) return default(T);
            try
            {
                obj = constructor.Invoke(constructorArgs);
                if (!(obj is T))
                {
                    valid = false;
                }
            } catch (Exception e)
            {
                valid = false;                
            }
            if (!valid)
            {
                Debug.LogError("ObjectPool<" + objectType.Name + ">: Incorrect constructor parameters passsed!");
                return default(T);
            }
            return (T)obj;
        }

        public void ReturnObject(T obj)
        {
            if (!availableObjects.Contains(obj))
            {
                availableQueue.Enqueue(obj);
                availableObjects.Add(obj);
                InUse--;
            }
        }

        public int AvailableObjectsRemoved { get; private set; }
        public T RemoveAvailable()
        {
            if (availableObjects.Count > 0)
            {
                T obj = availableQueue.Dequeue();
                while (!availableObjects.Remove(obj)) obj = availableQueue.Dequeue();
                ObjectsCreated--;
                AvailableObjectsRemoved++;
                return obj;
            }
            return default(T);
        }
        public void RemoveAvailable(T obj)
        {
            if (availableObjects.Contains(obj))
            {
                availableObjects.Remove(obj);
                ObjectsCreated--;
                AvailableObjectsRemoved++;
            }
        }
    }

    public abstract class ObjectPoolBase<T>
    {
        public int ObjectsCreated { get; internal set; }

        public int InUse { get; internal set; }

        public int Available { get { return ObjectsCreated - InUse; } }

        private Queue<T> availableQueue = new Queue<T>();
        private HashSet<T> availableObjects = new HashSet<T>();
        private ConstructorInfo constructor;

        public ObjectPoolBase(Type newType = null)
        {
            if (newType != null)
            {
                constructor = newType.GetConstructor(Type.EmptyTypes);
            }
        }

        protected abstract T CreateNewObject();

        public T GetNextObject()
        {
            InUse++;

            T obj = default(T);
            if (availableObjects.Count > 0)
            {
                obj = availableQueue.Dequeue();
                while (!availableObjects.Remove(obj)) obj = availableQueue.Dequeue();
            }
            else
            {
                ObjectsCreated++;

                obj = CreateNewObject();
            }

            return obj;
        }

        public void ReturnObject(T obj)
        {
            if (!availableObjects.Contains(obj))
            {
                InUse--;
                availableObjects.Add(obj);
                availableQueue.Enqueue(obj);
            }
        }

        public void ReturnObject(object obj)
        {
            if (obj is T)
            {
                if (!availableObjects.Contains((T)obj))
                {
                    InUse--;
                    availableObjects.Add((T)obj);
                    availableQueue.Enqueue((T)obj);
                }
            }
        }

        public int AvailableObjectsRemoved { get; private set; }
        public T RemoveAvailable()
        {
            if (availableObjects.Count > 0)
            {
                T obj = availableQueue.Dequeue();
                while (!availableObjects.Remove(obj)) obj = availableQueue.Dequeue();
                ObjectsCreated--;
                AvailableObjectsRemoved++;
                return obj;
            }
            return default(T);
        }
        public void RemoveAvailable(T obj)
        {
            if (availableObjects.Contains(obj))
            {
                availableObjects.Remove(obj);
                ObjectsCreated--;
                AvailableObjectsRemoved++;
            }
        }
    }

    public class ObjectPool<T> : ObjectPoolBase<T> where T : new()
    {
        protected override T CreateNewObject()
        {
            return new T();
        }
    }

    public class ObjectPoolBaseType<T> : ObjectPoolBase<T>
    {
        private ConstructorInfo constructor;
        public ObjectPoolBaseType(Type type)
        {
            constructor = type.GetConstructor(Type.EmptyTypes);
        }

        protected override T CreateNewObject()
        {
            return (T)constructor.Invoke(null);
        }
    }

    public class ObjectPool<T, TConstructArg1> : ObjectPoolWithConstructorInternal<T>
    {
        public ObjectPool() : base(new Type[] { typeof(TConstructArg1) }) { }

        public ObjectPool(Type objectType) : base(objectType, new Type[] { typeof(TConstructArg1) }) { }

        public T GetNextObject(TConstructArg1 arg1)
        {
            if (HasAvailableObject)
            {
                return GetObjectIfAvailable();
            }
            return GetNewObject(new object[] { arg1 });
        }
    }

    public class ObjectPool<T, TConstructArg1, TConstructArg2> : ObjectPoolWithConstructorInternal<T>
    {
        public ObjectPool() : base(new Type[] { typeof(TConstructArg1), typeof(TConstructArg2) }) { }

        public ObjectPool(Type objectType) : base(objectType, new Type[] { typeof(TConstructArg1), typeof(TConstructArg2) }) { }

        public T GetNextObject(TConstructArg1 arg1, TConstructArg2 arg2)
        {
            if (HasAvailableObject)
            {
                return GetObjectIfAvailable();
            }
            return GetNewObject(new object[] { arg1, arg2 });
        }
    }

    public class ObjectPool<T, TConstructArg1, TConstructArg2, TConstructArg3> : ObjectPoolWithConstructorInternal<T>
    {
        public ObjectPool() : base(new Type[] { typeof(TConstructArg1), typeof(TConstructArg2), typeof(TConstructArg3) }) { }

        public ObjectPool(Type objectType) : base(objectType, new Type[] { typeof(TConstructArg1), typeof(TConstructArg2), typeof(TConstructArg3) }) { }

        public T GetNextObject(TConstructArg1 arg1, TConstructArg2 arg2, TConstructArg3 arg3)
        {
            if (HasAvailableObject)
            {
                return GetObjectIfAvailable();
            }
            return GetNewObject(new object[] { arg1, arg2, arg3 });
        }
    }
}
