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

namespace LichPhatSong.Scraper.Helpers
{
    public static class ReflectionHelper
    {
        private static readonly ILog _log = LogManager.GetLogger(typeof(ReflectionHelper));

        public static IEnumerable<Type> TypesImplementingInterface<T>() where T : class
        {
            return TypesImplementingInterface<T>(Enumerable.Empty<Type>());
        }

        public static IEnumerable<Type> TypesImplementingInterface<T>(IEnumerable<Type> except) where T : class
        {
            if (except == null)
                throw new ArgumentNullException("except");

            return TypesImplementingInterface<T>(AppDomain.CurrentDomain, except);
        }

        public static IEnumerable<Type> TypesImplementingInterface<T>(AppDomain appDomain, IEnumerable<Type> except) where T : class
        {
            if (appDomain == null)
                throw new ArgumentNullException("appDomain");

            if (except == null)
                throw new ArgumentNullException("except");

            return appDomain
                .GetAssemblies()
                .SelectMany(assembly => TypesImplementingInterface<T>(assembly, except));
        }

        public static IEnumerable<Type> TypesImplementingInterface<T>(Assembly assembly, IEnumerable<Type> except) where T : class
        {
            if (assembly == null)
                throw new ArgumentNullException("assembly");

            if (except == null)
                throw new ArgumentNullException("except");

            return assembly
                .GetTypes()
                .Where(type => typeof(T).IsAssignableFrom(type) && IsRealClass(type) && !except.Any(t => t.IsAssignableFrom(type))).ToList();
        }

        public static bool IsRealClass(Type testType)
        {
            if (testType == null)
                throw new ArgumentNullException("testType");

            return testType.IsAbstract == false
                && testType.IsGenericTypeDefinition == false
                && testType.IsInterface == false;
        }

        public static IEnumerable<T> GetInstances<T>() where T : class
        {
            return GetInstances<T>(Enumerable.Empty<Type>());
        }

        public static IEnumerable<T> GetInstances<T>(AppDomain appDomain) where T : class
        {
            if (appDomain == null)
                throw new ArgumentNullException("appDomain");

            return GetInstances<T>(appDomain, Enumerable.Empty<Type>());
        }

        public static IEnumerable<T> GetInstances<T>(IEnumerable<Type> except) where T : class
        {
            if (except == null)
                throw new ArgumentNullException("except");

            return TypesImplementingInterface<T>(except).Select(CreateInstance<T>).Where(i => i != null).ToList();
        }

        public static IEnumerable<T> GetInstances<T>(AppDomain appDomain, IEnumerable<Type> except) where T : class
        {
            if (appDomain == null)
                throw new ArgumentNullException("appDomain");

            if (except == null)
                throw new ArgumentNullException("except");

            return TypesImplementingInterface<T>(appDomain, except).Select(CreateInstance<T>).Where(i => i != null).ToList();
        }

        public static IEnumerable<T> GetInstances<T>(Assembly assembly, IEnumerable<Type> except) where T : class
        {
            if (assembly == null)
                throw new ArgumentNullException("assembly");

            if (except == null)
                throw new ArgumentNullException("except");

            return TypesImplementingInterface<T>(assembly, except).Select(CreateInstance<T>).Where(i => i != null).ToList();
        }

        public static T CreateInstance<T>(Type type) where T : class
        {
            if (type == null)
                throw new ArgumentNullException("type");

            var constructor = type.GetConstructor(Type.EmptyTypes);

            try
            {
                if (constructor != null)
                    return constructor.Invoke(null) as T;
            }
            catch (Exception ex)
            {
                _log.Error("Unable to create instance of the specified type.", ex);
            }

            return null;
        }

        public static int LoadAssembliesFromDir(AppDomain targetAppDomain)
        {
            if (targetAppDomain == null)
                throw new ArgumentNullException("targetAppDomain");

            return LoadAssembliesFromDir(targetAppDomain, "*.dll");
        }

        public static int LoadAssembliesFromDir(AppDomain targetAppDomain, string searchPattern)
        {
            if (targetAppDomain == null)
                throw new ArgumentNullException("targetAppDomain");

            if (String.IsNullOrEmpty(searchPattern))
                throw new ArgumentException("Search pattern must be not null or empty.", "searchPattern");

            try
            {
                var currentDir = new DirectoryInfo(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));

                return LoadAssembliesFromDir(targetAppDomain, currentDir, searchPattern);
            }
            catch (Exception ex)
            {
                _log.Error("Unable to load assemblies.", ex);

                return 0;
            }
        }

        public static int LoadAssembliesFromDir(AppDomain targetAppDomain, DirectoryInfo directory, string searchPattern)
        {
            if (targetAppDomain == null)
                throw new ArgumentNullException("targetAppDomain");

            if (directory == null)
                throw new ArgumentNullException("directory");

            if (String.IsNullOrEmpty(searchPattern))
                throw new ArgumentException("Search pattern must be not null or empty.", "searchPattern");

            int counter = 0;

            try
            {
                FileInfo[] files = directory.GetFiles(searchPattern, SearchOption.TopDirectoryOnly);

                foreach (FileInfo file in files)
                {
                    try
                    {
                        // Load the file into the application domain.
                        AssemblyName assemblyName = AssemblyName.GetAssemblyName(file.FullName);
                        targetAppDomain.Load(assemblyName);

                        counter++;
                    }
                    catch (Exception ex)
                    {
                        _log.Error("Unable to load assembly '" + file.FullName + "'. Skipping.", ex);
                    }
                }
            }
            catch (Exception ex)
            {
                _log.Error("Unable to load assemblies.", ex);
            }

            return counter;
        }
    }
}