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


namespace Wayne.Lib
{
    class ServiceContainer : IServiceContainer
    {
        private readonly IServiceLocator parentServiceLocator;
        private readonly Dictionary<ServiceKey, object> services;
        private bool disposed;
        private List<Func<Type, object>> resolvers = new List<Func<Type, object>>();

        public ServiceContainer()
            : this(null)
        {
        }

        public ServiceContainer(IServiceLocator parentServiceLocator)
        {
            services = new Dictionary<ServiceKey, object>();
            this.parentServiceLocator = parentServiceLocator;
        }


        public void RegisterResolver(Func<Type, object> requestedType)
        {
            resolvers.Add(requestedType);
        }

        public void RegisterService<TServiceImplementation>()
        {
            AssertContainerIsNotDisposed();

            var instance = (TServiceImplementation)TryCreate(typeof(TServiceImplementation));
            RegisterService(instance);
        }

        public void RegisterService<TServiceImplementation>(string serviceId)
        {
            AssertContainerIsNotDisposed();

            var instance = (TServiceImplementation)TryCreate(typeof(TServiceImplementation), serviceId);
            RegisterService(instance, serviceId);
        }






        public void RegisterService<TServiceContract>(TServiceContract serviceInstance)
        {
            RegisterService(serviceInstance, string.Empty);
        }

        public void RegisterService<TServiceContract>(TServiceContract serviceInstance, string serviceId)
        {
            AssertContainerIsNotDisposed();
            services[new ServiceKey(typeof(TServiceContract), serviceId)] = serviceInstance;
        }





        public void RegisterService<TServiceContract>(ObjectConstructor<IServiceLocator, TServiceContract> constructorMethod)
        {
            AssertContainerIsNotDisposed();

            var serviceInstance = constructorMethod.Invoke(this);

            services[new ServiceKey(typeof(TServiceContract))] = serviceInstance;
        }

        public void RegisterService<TServiceContract>(ObjectConstructor<IServiceLocator, TServiceContract> constructorMethod, string serviceId)
        {
            AssertContainerIsNotDisposed();

            var serviceInstance = constructorMethod.Invoke(this);

            services[new ServiceKey(typeof(TServiceContract), serviceId)] = serviceInstance;
        }





        public void RegisterService<TServiceContract, TServiceImplementation>()
            where TServiceImplementation : TServiceContract
            where TServiceContract : class
        {
            AssertContainerIsNotDisposed();

            var instance = (TServiceImplementation)TryCreate(typeof(TServiceImplementation));

            if ((object)instance == null) //Cast to object to get rid of warning
                throw new ServiceContainerException(typeof(TServiceImplementation), "Could not create implementation");

            RegisterService<TServiceContract>(instance);
        }


        public void RegisterService<TServiceContract, TServiceImplementation>(string serviceId)
            where TServiceImplementation : TServiceContract
            where TServiceContract : class
        {
            AssertContainerIsNotDisposed();

            var instance = (TServiceImplementation)TryCreate(typeof(TServiceImplementation), serviceId);

            if ((object)instance == null) //Cast to object to get rid of warning
                throw new ServiceContainerException(typeof(TServiceImplementation), "Could not create implementation");

            RegisterService<TServiceContract>(instance, serviceId);
        }






        public TServiceContract GetService<TServiceContract>()
        {
            return (TServiceContract)GetService(typeof(TServiceContract));
        }

        public TServiceContract GetService<TServiceContract>(string serviceId)
        {
            return (TServiceContract)GetService(typeof(TServiceContract), serviceId);
        }


        public object GetService(Type serviceType)
        {
            return GetService(serviceType, string.Empty);
        }

        public object GetService(Type serviceType, string serviceId)
        {
            AssertContainerIsNotDisposed();

            object service;
            if (services.TryGetValue(new ServiceKey(serviceType, serviceId), out service))
                return service;

            //Try resolvers
            service = resolvers
                .Select(x => x(serviceType))
                .FirstOrDefault(x => x != null);

            if (service != null)
                return service;

            //Try parent container
            if (parentServiceLocator != null)
                return parentServiceLocator.GetService(serviceType, serviceId);

            throw ServiceContainerException.CreateNotFoundException(serviceType);
        }





        public TServiceContract GetServiceOrDefault<TServiceContract>(CreateDefaultService<TServiceContract> func, string serviceId)
        {
            AssertContainerIsNotDisposed();

            object service;
            Type serviceType = typeof(TServiceContract);
            var key = new ServiceKey(serviceType, serviceId);
            if (!services.TryGetValue(key, out service))
            {

                //Try resolvers
                service = resolvers
                    .Select(x => x(serviceType))
                    .FirstOrDefault(x => x != null);

                if (service != null)
                {
                    return (TServiceContract)service;
                }

                if (parentServiceLocator != null)
                    return parentServiceLocator.GetServiceOrDefault(func, serviceId);

                return func();
            }
            return (TServiceContract)service;
        }

        public TServiceContract GetServiceOrDefault<TServiceContract>(CreateDefaultService<TServiceContract> func)
        {
            return GetServiceOrDefault(func, string.Empty);
        }

        public T CreateInstance<T>(params object[] additionalObjects) where T : class
        {
            return (T)CreateInstance(typeof(T), additionalObjects);
        }

        public object CreateInstance(Type typeToInstantiate, params object[] additionalParameter)
        {
            AssertContainerIsNotDisposed();

            object result = TryCreate(typeToInstantiate, additionalParameter);

            if (result == null)
                throw new ServiceContainerException(typeToInstantiate, "Could not create object");

            return result;
        }

        public T CreateInstance<T>(string serviceId, params object[] additionalObjects) where T : class
        {
            return (T)CreateInstance(typeof(T), serviceId, additionalObjects);
        }

        public object CreateInstance(Type typeToInstantiate, string serviceId, params object[] additionalParameter)
        {
            AssertContainerIsNotDisposed();

            object result = TryCreate(typeToInstantiate, serviceId, additionalParameter);

            if (result == null)
                throw new ServiceContainerException(typeToInstantiate, "Could not create object");

            return result;
        }




        private void AssertContainerIsNotDisposed()
        {
            if (disposed)
                throw new ObjectDisposedException(GetType().Name);
        }

        public object TryGetService(Type serviceType)
        {

            return TryGetService(serviceType, string.Empty);
        }

        public object TryGetService(Type serviceType, string serviceId)
        {
            object service;

            if (serviceType == typeof(IServiceLocator) || serviceType == typeof(IServiceContainer))
                return this;

            if (!services.TryGetValue(new ServiceKey(serviceType, serviceId), out service))
            {
                //Try resolvers
                service = resolvers
                    .Select(x => x(serviceType))
                    .FirstOrDefault(x => x != null);

                if (service != null)
                {
                    return service;
                }

                if (parentServiceLocator != null)
                    return parentServiceLocator.TryGetService(serviceType, serviceId);

                return null;
            }
            return service;
        }


        public T TryGetService<T>()
        {
            return TryGetService<T>(string.Empty);
        }

        public T TryGetService<T>(string serviceId)
        {
            Type serviceType = typeof(T);

            object result;

            if (serviceType == typeof(IServiceLocator) || serviceType == typeof(IServiceContainer))
                result = this;
            else
            {
                if (!services.TryGetValue(new ServiceKey(serviceType, serviceId), out result))
                {
                    //Try resolvers
                    result= resolvers
                        .Select(x => x(serviceType))
                        .FirstOrDefault(x => x != null);

                    if (result != null)
                    {
                        return (T) result;
                    }

                    if (parentServiceLocator != null)
                        result = parentServiceLocator.TryGetService(serviceType, serviceId);
                }
            }
            return (T)result;
        }


        private object TryCreate(Type typeToCreate, params object[] additionalObjects)
        {
            return TryCreate(typeToCreate, string.Empty, additionalObjects);
        }

        private object TryCreate(Type typeToCreate, string serviceId, params object[] additionalObjects)
        {
            var constructors = typeToCreate.GetConstructors();

            Array.Reverse(constructors);
            List<object> parameters = new List<object>();
            ConstructorInfo foundConstructor = null;
            foreach (ConstructorInfo constructor in constructors)
            {
                var parameterInfos = constructor.GetParameters();
                parameters.Clear();
                foreach (var parameterInfo in parameterInfos)
                {
                    var dependency = TryGetService(parameterInfo.ParameterType, serviceId);

                    if (dependency != null)
                    {
                        parameters.Add(dependency);
                    }
                    else
                    {
                        int firstMatchingParameterObjectIndex = -1;

                        for (int index = 0; index < additionalObjects.Length; index++)
                        {
                            object additionalObject = additionalObjects[index];
                            if (parameterInfo.ParameterType.IsAssignableFrom(additionalObject.GetType()))
                            {
                                firstMatchingParameterObjectIndex = index;
                                break;
                            }
                        }

                        if (firstMatchingParameterObjectIndex >= 0)
                        {
                            parameters.Add(additionalObjects[firstMatchingParameterObjectIndex]);
                        }
                    }
                }

                if (parameters.Count == parameterInfos.Length)
                {
                    foundConstructor = constructor;
                    break;
                }
            }

            if (foundConstructor != null)
            {
                var stateObject = foundConstructor.Invoke(parameters.ToArray());
                return stateObject;
            }

            return null;
        }


        // Help struct used as key in the dictionary of the container 

        private struct ServiceKey
        {
            private readonly Type _typeOfObject;
            private readonly string _id;

            public ServiceKey(Type typeOfObject, string id)
            {
                _typeOfObject = typeOfObject;
                _id = id;
            }
            public ServiceKey(Type typeOfObject)
            {
                _typeOfObject = typeOfObject;
                _id = string.Empty;
            }


            public override int GetHashCode()
            {

                int hash = 17;
                hash = hash * 31 + _typeOfObject.GetHashCode();
                hash = hash * 31 + _id.GetHashCode();
                return hash;
            }

            public override bool Equals(object obj)
            {
                return obj is ServiceKey && this == (ServiceKey)obj;
            }
            public static bool operator ==(ServiceKey s1, ServiceKey s2)
            {
                return (s1._typeOfObject == s2._typeOfObject) && (s1._id.Equals(s2._id, StringComparison.InvariantCulture));
            }
            public static bool operator !=(ServiceKey s1, ServiceKey s2)
            {
                return !(s1 == s2);
            }

        }

        #region Implementation of IDisposable
        ~ServiceContainer()
        {
            Dispose(false);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        private void Dispose(bool disposing)
        {
            if (disposed) return;
            disposed = true;

            if (disposing)
            {
                foreach (var service in services.Values)
                {
                    var disposable = service as IDisposable;
                    if (disposable != null)
                    {
                        try
                        {
                            disposable.Dispose();
                        }
                        catch
                        {
                            //Ignore errors
                        }
                    }
                }
                services.Clear();
            }
        }
        #endregion

    }
}