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

namespace InABox.Core
{
    internal static class _AutoDataModel
    {
        private static Type[]? _allo2mtypes;

        public static Type[] GetOneOrManyToManyTypes()
        {
            _allo2mtypes ??= CoreUtils.Entities.Where(x => x.HasInterface(typeof(IOneToMany<>)) || x.HasInterface(typeof(IManyToMany<,>))).ToArray();
            return _allo2mtypes;
        }
    }

    public class AutoDataModel<T> : DataModel<T> where T : Entity, IPersistent, IRemotable, new()
    {
        private static bool _loaded = false;

        // Method; Parent Column; Child Column; Columns expression; Table Name
        private static readonly List<Tuple<MethodInfo, Expression, Expression, IColumns, string>> children =
            new List<Tuple<MethodInfo, Expression, Expression, IColumns, string>>();

        // Method; Parent Column; Child Column; Table Name
        private static readonly List<Tuple<MethodInfo, Expression, Expression, string>> lookups =
            new List<Tuple<MethodInfo, Expression, Expression, string>>();

        // 
        private static readonly List<Tuple<MethodInfo, Expression, Expression, IColumns, string>> manyToManys0 =
            new List<Tuple<MethodInfo, Expression, Expression, IColumns, string>>();

        private static readonly List<Tuple<MethodInfo, Expression, Expression, string, string>> manyToManys1 =
            new List<Tuple<MethodInfo, Expression, Expression, string, string>>();

        //private List<Tuple<String,Type>> _lookuptables = new List<Tuple<String,Type>>();
        private readonly List<Tuple<Type, string, bool>> _childtables = new List<Tuple<Type, string, bool>>();

        public AutoDataModel(Filter<T>? filter, Columns<T>? columns, SortOrder<T>? sort) : base(filter, columns, sort)
        {
            //var props = CoreUtils.PropertyList(typeof(T), x => x.PropertyType.GetInterfaces().Contains(typeof(IEntityLink)));
            //foreach (var prop in props)
            //{
            //    var lookuptype = prop.PropertyType.GetInheritedGenericTypeArguments().FirstOrDefault();
            //    if (lookuptype != null)
            //    {
            //        MethodInfo method = this.GetType().GetMethod("AddLookupTable").MakeGenericMethod(typeof(T), lookuptype);
            //        var source = CoreUtils.GetPropertyExpression(typeof(T), prop.Name + ".ID", typeof(object));
            //        var target = CoreUtils.GetPropertyExpression(lookuptype, "ID", typeof(object));
            //        method.Invoke(this, new object[] { source, target, false, null, prop.Name });
            //        _lookuptables.Add(new Tuple<String,Type>(prop.Name,lookuptype));
            //    }
            //}

            var headName = TableName<T>();

            if (!_loaded)
            {
                var maps = _AutoDataModel.GetOneOrManyToManyTypes()
                    .Where(x => x.GetInterfaces(typeof(IOneToMany<>)).Any(x => x.GenericTypeArguments[0] == typeof(T)) && !x.HasInterface<ISkipLoad>())
                    .OrderBy(x => x.Name);
                var childMethod = GetType().GetMethods().Where(x => string.Equals(x.Name, nameof(AddChildTable)) && x.IsGenericMethod).FirstOrDefault();
                var lookupMethod = GetType().GetMethods().Where(x => string.Equals(x.Name, nameof(AddLookupTable)) && x.IsGenericMethod).FirstOrDefault();
                foreach (var map in maps)
                {
                    var method = childMethod.MakeGenericMethod(typeof(T), map);
                    var source = CoreUtils.GetPropertyExpression(typeof(T), "ID", typeof(object));
                    var prop = map.GetProperties().FirstOrDefault(
                        x => x.PropertyType.GetInterfaces().Contains(typeof(IEntityLink)) &&
                             !x.CustomAttributes.Any(y => y.AttributeType == typeof(ObsoleteAttribute)) &&
                             x.PropertyType.GetInheritedGenericTypeArguments().FirstOrDefault() == typeof(T));
                    var target = CoreUtils.GetPropertyExpression(map, prop.Name + ".ID", typeof(object));

                    var cols = Core.Columns.None(map);
                    var columnList = CoreUtils.GetColumnNames(map, x => x.GetOuterParent(x => x.IsEntityLink)?.Name != prop.Name);
                    foreach (var col in columnList) cols.Add(col);
                    cols.Add(prop.Name + ".ID");

                    children.Add(new Tuple<MethodInfo, Expression, Expression, IColumns, string>(method, source, target, cols,
                        headName + "_" + TableName(map)));
                    //method.Invoke(this, new object[] { source, target, null, columns, false, null, headName + "_" + TableName(map) });
                    //_childtables.Add(new Tuple<Type, String, bool>(map, prop.Name, false));
                }

                var properties = typeof(T).GetProperties().Where(
                    x => x.PropertyType.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEntityLink<>))
                         && !x.CustomAttributes.Any(y => y.AttributeType == typeof(ObsoleteAttribute)));
                foreach (var property in properties)
                {
                    var propType = property.PropertyType;
                    var otherType = propType.GetInterfaces().Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEntityLink<>))
                        .FirstOrDefault().GenericTypeArguments[0];
                    if (!typeof(ISkipLoad).IsAssignableFrom(otherType))
                    {
                        var method = lookupMethod.MakeGenericMethod(typeof(T), otherType);

                        var source = CoreUtils.GetPropertyExpression(typeof(T), property.Name + ".ID", typeof(object));
                        var target = CoreUtils.GetPropertyExpression(otherType, "ID", typeof(object));

                        var tableNameAttribute = property.GetCustomAttribute<DataModelTableNameAttribute>();
                        var tableName = tableNameAttribute?.TableName ?? TableName(otherType);

                        lookups.Add(new Tuple<MethodInfo, Expression, Expression, string>(method, source, target,
                            headName + "_" + tableName));
                        //method.Invoke(this, new object[] { source, target, null, null, false, null, headName + "_" + TableName(otherType) });
                        //_childtables.Add(new Tuple<Type, String, bool>(otherType, property.Name, true));
                    }
                }

                var mapsLookup = _AutoDataModel.GetOneOrManyToManyTypes()
                    .Where(x => x.GetInterfaces(typeof(IManyToMany<,>)).Any(x => x.GenericTypeArguments.Contains(typeof(T))))
                    .OrderBy(x => x.EntityName());
                foreach (var map in mapsLookup)
                {
                    var method = childMethod.MakeGenericMethod(typeof(T), map);
                    var source = CoreUtils.GetPropertyExpression(typeof(T), "ID", typeof(object));
                    var prop = map.GetProperties().FirstOrDefault(
                        x => x.PropertyType.GetInterfaces().Contains(typeof(IEntityLink))
                             && !x.CustomAttributes.Any(y => y.AttributeType == typeof(ObsoleteAttribute))
                             && x.PropertyType.GetInheritedGenericTypeArguments().FirstOrDefault() == typeof(T));
                    var target = CoreUtils.GetPropertyExpression(map, prop.Name + ".ID", typeof(object));

                    var lookupAlias = headName + "_" + TableName(map);

                    var cols = Core.Columns.None(map);
                    var columnList = CoreUtils.GetColumnNames(map, x => x.GetOuterParent(x => x.IsEntityLink)?.Name != prop.Name);
                    foreach (var col in columnList) cols.Add(col);
                    cols.Add(prop.Name + ".ID");

                    manyToManys0.Add(new Tuple<MethodInfo, Expression, Expression, IColumns, string>(method, source, target, cols, lookupAlias));
                    //method.Invoke(this, new object[] { source, target, null, columns, false, null, lookupAlias });
                    //_childtables.Add(new Tuple<Type, String, bool>(map, prop.Name, false));

                    var otherType = map.GetInterfaces().Where(
                            x => x.IsGenericType
                                 && x.GetGenericTypeDefinition() == typeof(IManyToMany<,>))
                        .FirstOrDefault().GenericTypeArguments.Where(x => x != typeof(T)).FirstOrDefault();
                    if (!typeof(ISkipLoad).IsAssignableFrom(otherType))
                    {
                        method = lookupMethod.MakeGenericMethod(map, otherType);
                        prop = map.GetProperties().FirstOrDefault(
                            x => x.PropertyType.GetInterfaces().Contains(typeof(IEntityLink))
                                 && !x.CustomAttributes.Any(y => y.AttributeType == typeof(ObsoleteAttribute))
                                 && x.PropertyType.GetInheritedGenericTypeArguments().FirstOrDefault() == otherType);
                        source = CoreUtils.GetPropertyExpression(map, prop.Name + ".ID", typeof(object));
                        target = CoreUtils.GetPropertyExpression(otherType, "ID", typeof(object));
                        manyToManys1.Add(new Tuple<MethodInfo, Expression, Expression, string, string>(method, source, target, lookupAlias,
                            lookupAlias + "_" + TableName(otherType)));
                        //method.Invoke(this, new object[] { source, target, null, null, false, lookupAlias, lookupAlias + "_" + TableName(otherType) });
                        //_childtables.Add(new Tuple<Type, String, bool>(map, prop.Name, false));
                    }
                }
                _loaded = true;
            }

            foreach (var child in children)
                child.Item1.Invoke(this, new object?[] { child.Item2, child.Item3, null, child.Item4, false, null, child.Item5 });
            foreach (var lookup in lookups)
                lookup.Item1.Invoke(this, new object?[] { lookup.Item2, lookup.Item3, null, null, false, null, lookup.Item4 });
            foreach (var manyToMany0 in manyToManys0)
                manyToMany0.Item1.Invoke(this,
                    new object?[] { manyToMany0.Item2, manyToMany0.Item3, null, manyToMany0.Item4, false, null, manyToMany0.Item5 });
            foreach (var manyToMany1 in manyToManys1)
                manyToMany1.Item1.Invoke(this,
                    new object?[] { manyToMany1.Item2, manyToMany1.Item3, null, null, false, manyToMany1.Item4, manyToMany1.Item5 });
        }

        public AutoDataModel(Filter<T>? filter): this(filter, null, null) { }

        public override string Name => typeof(T).EntityName().Split('.').Last();

    }
}