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

namespace InABox.Core
{
    public interface ILookupDefinition<TLookup> where TLookup : BaseObject, new()
    {
        Filter<TLookup>? DefineFilter();

        Columns<TLookup> DefineColumns();

        Columns<TLookup> RequiredColumns();

        SortOrder<TLookup> DefineSortOrder();

        string FormatLookup(Dictionary<string, object> values, IEnumerable<string> exclude);
    }

    public interface ILookupDefinition<TLookup, TEntity> where TLookup : Entity
    {
        Filter<TLookup> DefineFilter(TEntity[] items);

        /// <summary>
        /// Define the columns required for the <c>items</c> parameter of <see cref="DefineFilter(TEntity[])"/>.
        /// </summary>
        /// <returns></returns>
        Columns<TEntity> DefineFilterColumns();
    }

    public interface IStaticLookupDefinition<TEntity>
    {
        Dictionary<object, object> DefineLookups(PropertyInfo property);
    }

    public static class LookupFactory
    {
        #region LookupCache

        private class LookupCacheEntry
        {
            public LookupCacheEntry(object factory)
            {
                DefaultFactory = factory;
                CustomFactories = new Dictionary<Type, object>();
                StaticFactory = null;
            }

            public object DefaultFactory { get; }

            public Dictionary<Type, object> CustomFactories { get; }

            public object StaticFactory { get; set; }

            public object GetFactory(Type type = null)
            {
                if (type == null)
                    return DefaultFactory;
                if (CustomFactories.ContainsKey(type))
                    return CustomFactories[type];
                return DefaultFactory;
            }
        }

        private class LookupCache
        {
            private static Dictionary<Type, LookupCacheEntry> _cache;

            public LookupCache()
            {
                _cache = new Dictionary<Type, LookupCacheEntry>();


                // Load up the default types
                var defaulttypes = CoreUtils.TypeList(
                    AppDomain.CurrentDomain.GetAssemblies(),
                    x => !x.IsAbstract &&
                         x.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition().Equals(typeof(ILookupDefinition<>))
                         )
                );
                foreach (var type in defaulttypes)
                    try
                    {
                        var intfs = type.GetInterfaces().Where(i =>
                            i.IsGenericType
                            && i.GetGenericTypeDefinition().Equals(typeof(ILookupDefinition<>))
                        );
                        foreach (var intf in intfs)
                            _cache[intf.GenericTypeArguments.First()] = new LookupCacheEntry(Activator.CreateInstance(type));
                    }
                    catch (Exception e)
                    {
                        Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace));
                    }

                // Now load up the specific types
                var specifictypes = CoreUtils.TypeList(
                    AppDomain.CurrentDomain.GetAssemblies(),
                    x =>
                        !x.IsAbstract &&
                        x.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition().Equals(typeof(ILookupDefinition<,>))
                        )
                );
                foreach (var type in specifictypes)
                {
                    var intfs = type.GetInterfaces().Where(i =>
                        i.IsGenericType
                        && i.GetGenericTypeDefinition().Equals(typeof(ILookupDefinition<,>))
                    );
                    foreach (var intf in intfs)
                    {
                        var lookuptype = intf.GenericTypeArguments.First();
                        var entitytype = intf.GenericTypeArguments.Last();
                        if (!_cache.ContainsKey(lookuptype))
                            _cache[lookuptype] = new LookupCacheEntry(null);
                        _cache[lookuptype].CustomFactories[entitytype] = Activator.CreateInstance(type);
                    }
                }


                // Load up the static lookups
                var staticlookups = CoreUtils.TypeList(
                    AppDomain.CurrentDomain.GetAssemblies(),
                    x =>
                        !x.IsAbstract &&
                        x.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition().Equals(typeof(IStaticLookupDefinition<>))
                        )
                );
                foreach (var type in staticlookups)
                {
                    var intf = type.GetInterfaces().First(i =>
                        i.IsGenericType
                        && i.GetGenericTypeDefinition().Equals(typeof(IStaticLookupDefinition<>))
                    );
                    var iType = intf.GenericTypeArguments.First();
                    if (!_cache.ContainsKey(iType))
                        _cache[iType] = new LookupCacheEntry(null);
                    _cache[iType].StaticFactory = Activator.CreateInstance(type);
                }
            }

            public object GetFactory(Type lookup, Type entity = null)
            {
                if (!_cache.ContainsKey(lookup))
                    return null;

                if (entity == null)
                    return _cache[lookup].DefaultFactory;

                if (_cache[lookup].CustomFactories.ContainsKey(entity))
                    return _cache[lookup].CustomFactories[entity];
                return null;
            }

            public object GetStaticFactory(Type lookup)
            {
                if (!_cache.ContainsKey(lookup))
                    return null;
                return _cache[lookup].StaticFactory;
            }
        }

        private static readonly LookupCache _cache = new LookupCache();

        #endregion

        #region General Factory Methods

        private static object? DoInvoke(Type TLookup, Type TResult, string Method)
        {
            var factory = _cache.GetFactory(TLookup);
            if (factory != null)
            {
                var filtertype = TResult.MakeGenericType(TLookup);
                var method = factory.GetType().GetMethods().FirstOrDefault(m => 
                    m.Name.Equals(Method) 
                    && !m.GetParameters().Any() 
                    && m.ReturnType == filtertype
                );
                if (method != null)
                    try
                    {
                        object[] parameters = { };
                        return method.Invoke(factory, parameters);
                    }
                    catch (Exception e)
                    {
                        Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace));
                    }
            }

            return null;
        }

        // If a specific Entity/Lookup match can't be found,
        // this will fall back to the global Lookup filter (if there is one)
        // Otherwise, it will return null
        private static object? DoInvoke(Type TLookup, Type TEntity, IEnumerable items, Type TResult, string Method)
        {
            var factory = _cache.GetFactory(TLookup, TEntity);
            if (factory != null)
            {
                var filtertype = TResult.MakeGenericType(TLookup);
                var method = factory.GetType().GetMethods().FirstOrDefault(m => 
                    m.Name.Equals(Method) 
                    && m.GetParameters().Any() 
                    && m.ReturnType == filtertype 
                    && m.GetParameters().First().ParameterType == items.GetType()
                );

                if (method != null)
                    try
                    {
                        object[] parameters = { items };
                        return method.Invoke(factory, parameters);
                    }
                    catch (Exception e)
                    {
                        Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace));
                    }
            }

            return DoInvoke(TLookup, TResult, Method);
        }
        private static object? DoInvoke(Type TLookup, Type TEntity, Type TResult, string Method)
        {
            var factory = _cache.GetFactory(TLookup, TEntity);
            if (factory != null)
            {
                var method = factory.GetType().GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).FirstOrDefault(m => 
                    m.Name.Split('.').Last().Equals(Method) // For explicit implementations, whose name is ILookupDefinition<TLookup,TEntity>.Method
                    && !m.GetParameters().Any() 
                    && m.ReturnType == TResult
                );

                if (method != null)
                    try
                    {
                        return method.Invoke(factory, Array.Empty<object>());
                    }
                    catch (Exception e)
                    {
                        Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace));
                    }
            }

            return null;
        }

        private static Dictionary<object, object> DoInvoke(Type TLookup, PropertyInfo property, string Method)
        {
            var factory = _cache.GetStaticFactory(TLookup);
            if (factory != null)
            {
                var method = factory.GetType().GetMethods().Where(m =>
                    m.Name.Equals(Method) &&
                    m.GetParameters().SingleOrDefault() != null &&
                    m.ReturnType.Equals(typeof(Dictionary<object, object>))
                ).FirstOrDefault();
                if (method != null)
                    try
                    {
                        object[] parameters = { property };
                        return method.Invoke(factory, parameters) as Dictionary<object, object>;
                    }
                    catch (Exception e)
                    {
                        Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace));
                    }
            }

            return null;
        }

        private static object DoInvoke(Type TLookup, Type TReturn, string Method, params object[] args)
        {
            var factory = _cache.GetFactory(TLookup);
            if (factory != null)
            {
                var method = factory.GetType().GetMethods().Where(m =>
                    m.Name.Equals(Method) &&
                    m.GetParameters().Length == args.Length &&
                    m.ReturnType.Equals(TReturn)
                ).FirstOrDefault();
                if (method != null)
                    try
                    {
                        return method.Invoke(factory, args);
                    }
                    catch (Exception e)
                    {
                        Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace));
                    }
            }

            return null;
        }

        #endregion

        #region Formats

        public static string DefaultFormatLookup(Dictionary<string, object> values, IEnumerable<string> exclude)
        {
            return string.Join(": ",
                values.Where(x => x.Value != null && x.Value.GetType() != typeof(Guid) && (exclude == null || !exclude.Contains(x.Key)))
                    .Select(p => p.Value));
        }

        public static string FormatLookup(Type TLookup, Dictionary<string, object> values, IEnumerable<string> exclude)
        {
            var result = DoInvoke(TLookup, typeof(string), "FormatLookup", values, exclude) as string;
            if(result is null)
            {
                return DefaultFormatLookup(values, exclude);
            }
            return result;
        }

        public static string FormatLookup<TLookup>(Dictionary<string, object> values, IEnumerable<string> exclude)
        {
            return FormatLookup(typeof(TLookup), values, exclude);
        }

        #endregion

        #region Filters

        public static IFilter? DefineFilter(Type TLookup)
        {
            return DoInvoke(TLookup, typeof(Filter<>), "DefineFilter") as IFilter;

            //var factory =_cache.GetFactory(TLookup);
            //if (factory != null)
            //{

            //    Type filtertype = typeof(Filter<>).MakeGenericType(TLookup);
            //    var method = factory.GetType().GetMethods().Where(m =>
            //        m.Name.Equals("DefineFilter") &&
            //        m.GetParameters().Any() &&
            //        m.GetParameters().First().ParameterType.IsByRef &&
            //        m.GetParameters().First().ParameterType.GetElementType().Equals(filtertype)).FirstOrDefault();
            //    if (method != null)
            //    {
            //        try
            //        {
            //            object[] parameters = new object[] { null };
            //            method.Invoke(factory, parameters);
            //            return parameters[0];
            //        }
            //        catch (Exception e)
            //        {

            //        }
            //    }
            //}
            //return null;
        }

        public static Filter<TLookup>? DefineFilter<TLookup>()
        {
            return DefineFilter(typeof(TLookup)) as Filter<TLookup>;
        }

        public static IFilter? DefineFilter(Type TLookup, Type TEntity, IEnumerable items)
        {
            return DoInvoke(TLookup, TEntity, items, typeof(Filter<>), "DefineFilter") as IFilter;
        }

        public static IFilter? DefineFilter<TEntity>(IEnumerable<TEntity> items, Type TLookup)
        {
            return DoInvoke(TLookup, typeof(TEntity), items, typeof(Filter<>), "DefineFilter") as IFilter;
        }

        public static Filter<TLookup>? DefineFilter<TEntity, TLookup>(TEntity[] items) where TEntity : Entity where TLookup : Entity
        {
            return DefineFilter(items, typeof(TLookup)) as Filter<TLookup>;
        }
        
        #endregion

        #region Columns

        public static IColumns DefineColumns(Type TLookup)
        {
            var result = DoInvoke(TLookup, typeof(Columns<>), "DefineColumns") as IColumns;
            if (result == null)
            {
                var type = typeof(Columns<>).MakeGenericType(TLookup);
                result = Activator.CreateInstance(type) as IColumns;
                result = result.DefaultColumns();
            }

            return result;
        }

        public static Columns<TLookup> DefineColumns<TLookup>()
        {
            return DefineColumns(typeof(TLookup)) as Columns<TLookup>;
        }

        public static IColumns DefineFilterColumns(Type TLookup, Type TEntity, IEnumerable items)
        {
            var result = DoInvoke(TLookup, TEntity, typeof(Columns<>).MakeGenericType(TEntity), "DefineFilterColumns") as IColumns;
            if (result == null)
            {
                var type = typeof(Columns<>).MakeGenericType(TLookup);
                result = Activator.CreateInstance(type) as IColumns;
                result = result.DefaultColumns();
            }
            return result;
        }

        public static IColumns DefineFilterColumns<TEntity>(Type TLookup)
        {
            var result = DoInvoke(TLookup, typeof(TEntity), typeof(Columns<>).MakeGenericType(typeof(TEntity)), "DefineFilterColumns") as IColumns;
            if (result == null)
            {
                result = Columns.Create(TLookup);
            }
            return result;
        }

        public static Columns<TLookup> DefineFilterColumns<TEntity, TLookup>() where TEntity : Entity where TLookup : Entity
        {
            return DefineFilterColumns<TEntity>(typeof(TLookup)) as Columns<TLookup>;
        }

        #endregion

        #region RequiredColumns

        public static IColumns DefaultRequiredColumns(Type TLookup)
        {
            var result = Columns.Create(TLookup);
            var props = DatabaseSchema.Properties(TLookup).Where(x => x.Required);

            foreach (var prop in props)
                result.Add(prop.Name);

            return result;
        }
        public static Columns<TLookup> DefaultRequiredColumns<TLookup>()
            => (DefaultRequiredColumns(typeof(TLookup)) as Columns<TLookup>)!;

        public static IColumns RequiredColumns(Type TLookup)
        {
            var result = DoInvoke(TLookup, typeof(Columns<>), "RequiredColumns") as IColumns;
            if (result == null)
            {
                result = DefaultRequiredColumns(TLookup);
            }

            return result;
        }

        public static Columns<TLookup> RequiredColumns<TLookup>()
        {
            return RequiredColumns(typeof(TLookup)) as Columns<TLookup>;
        }
        
        #endregion

        #region SortOrder

        public static ISortOrder DefineSort(Type TLookup)
        {
            return DoInvoke(TLookup, typeof(SortOrder<>), "DefineSortOrder") as ISortOrder;
        }

        public static SortOrder<TLookup> DefineSort<TLookup>()
        {
            return DefineSort(typeof(TLookup)) as SortOrder<TLookup>;
        }
        
        #endregion

        #region Static Lookups

        public static Dictionary<object, object> DefineLookups(Type type, PropertyInfo property)
        {
            return DoInvoke(type, property, "DefineLookups");
        }

        public static Dictionary<object, object> DefineLookups<TLookup>(Expression<Func<TLookup, object>> property)
        {
            return DefineLookups(typeof(TLookup), CoreUtils.GetPropertyFromExpression(property));
        }

        #endregion
    }

    public abstract class BaseObjectLookup<TLookup> : ILookupDefinition<TLookup> where TLookup : BaseObject, new()
    {
        public virtual string FormatLookup(Dictionary<string, object> values, IEnumerable<string> exclude)
        {
            var filtered = new Dictionary<string, object>();
            var cols = DefineColumns();
            foreach (var col in cols.Items)
                if (values.ContainsKey(col.Property) && values[col.Property] != null && values[col.Property].GetType() != typeof(Guid))
                    filtered[col.Property] = values[col.Property];
            return LookupFactory.DefaultFormatLookup(filtered, exclude);
        }

        public abstract Columns<TLookup> DefineColumns();

        public abstract Columns<TLookup> RequiredColumns();

        public abstract Filter<TLookup>? DefineFilter();

        public abstract SortOrder<TLookup> DefineSortOrder();
    }

    public abstract class EntityLookup<TLookup> : BaseObjectLookup<TLookup> where TLookup : Entity, new()
    {
        public override Columns<TLookup> DefineColumns()
        {
            return new Columns<TLookup>(x => x.ID);
        }

        public override Columns<TLookup> RequiredColumns()
        {
            return LookupFactory.DefaultRequiredColumns<TLookup>();
        }
    }
}