using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Timers;
using InABox.Core;
using IQueryProvider = InABox.Core.IQueryProvider;

namespace InABox.Clients
{
    public enum SerializerProtocol
    {
        Rest,
        RPC
    }

    public class QueryMultipleResults
    {
        public Dictionary<string, CoreTable> Results { get; private set; }

        internal QueryMultipleResults(Dictionary<string, CoreTable> results)
        {
            Results = results;
        }

        public CoreTable this[string name] => Results[name];

        public CoreTable Get<T>() => Results[typeof(T).Name];

        /// <summary>
        /// Like <see cref="Get{T}"/>, but calls <see cref="CoreTable.ToObjects{T}"/> on the table.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public IEnumerable<T> GetObjects<T>()
            where T: BaseObject, new() 
            => Results[typeof(T).Name].ToObjects<T>();

        /// <summary>
        /// Like <see cref="Get{T}"/>, but calls <see cref="CoreTable.ToArray{T}"/> on the table.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public T[] GetArray<T>()
            where T: BaseObject, new() 
            => Results[typeof(T).Name].ToArray<T>();

        /// <summary>
        /// Like <see cref="Get{T}"/>, but calls <see cref="CoreTable.ToList{T}"/> on the table.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public List<T> GetList<T>()
            where T: BaseObject, new() 
            => Results[typeof(T).Name].ToList<T>();

        public CoreTable Get(string name) => Results[name];

        public CoreTable GetOrDefault(string name) => Results.GetValueOrDefault(name);
    }

    public class ClientQueryProvider<TEntity> : IQueryProvider<TEntity>
        where TEntity : Entity, IRemotable, new()
    {
        
        public bool ExcludeCustomProperties { get; set; }
        
        #region Non-generic

        public CoreTable Query(IFilter? filter = null, IColumns? columns = null, ISortOrder? sort = null, CoreRange? range = null)
        {
            return new Client<TEntity>().Query(filter, columns, sort, range);
        }

        #endregion

        public CoreTable Query(Filter<TEntity>? filter = null, Columns<TEntity>? columns = null, SortOrder<TEntity>? sort = null, CoreRange? range = null)
        {
            return Client.Query(filter, columns, sort, range);
        }

        public void Query(Filter<TEntity>? filter, Columns<TEntity>? columns, SortOrder<TEntity>? sort, CoreRange? range, Action<CoreTable?, Exception?> action)
        {
            Client.Query(filter, columns, sort, range, action);
        }

        public void Save(TEntity entity, string auditNote)
        {
            Client.Save(entity, auditNote);
        }

        public void Save(IEnumerable<TEntity> entities, string auditNote)
        {
            Client.Save(entities, auditNote);
        }

        public void Save(TEntity entity, string auditnote, Action<TEntity, Exception?> callback)
        {
            Client.Save(entity, auditnote, callback);
        }

        public void Save(IEnumerable<TEntity> entities, string auditnote, Action<IEnumerable<TEntity>, Exception?> callback)
        {
            Client.Save(entities, auditnote, callback);
        }

        public void Delete(TEntity entity, string auditNote)
        {
            Client.Delete(entity, auditNote);
        }

        public void Delete(IEnumerable<TEntity> entities, string auditNote)
        {
            Client.Delete(entities, auditNote);
        }

        public void Delete(TEntity entity, string auditnote, Action<TEntity, Exception?> callback)
        {
            Client.Delete(entity, auditnote, callback);
        }

        public void Delete(IEnumerable<TEntity> entities, string auditnote, Action<IList<TEntity>, Exception?> callback)
        {
            Client.Delete(entities, auditnote, callback);
        }
    }

    public abstract class Client
    {
        #region IQueryProvider Factory

        private class _Factory : IQueryProviderFactory
        {

            public bool ExcludeCustomProperties => false;
            
            public IQueryProvider Create(Type T)
            {
                var type = typeof(ClientQueryProvider<>).MakeGenericType(T);
                var result = (Activator.CreateInstance(type) as IQueryProvider)!;
                result.ExcludeCustomProperties = ExcludeCustomProperties;
                return result;
            }
        }

        public static IQueryProviderFactory Factory { get; } = new _Factory();

        #endregion

        #region Abstract Methods

        public abstract CoreTable Query(IFilter? filter = null, IColumns? columns = null, ISortOrder? sortOrder = null, CoreRange? range = null);
        public abstract void Save(Entity entity, string auditNote);
        public abstract void Save(IEnumerable<Entity> entity, string auditNote);

        #endregion

        private static IClient CheckClient()
        {
            return ClientFactory.CreateClient<User>();
        }

        public static void EnsureColumns<TEntity>(TEntity entity, Columns<TEntity> columns)
            where TEntity : Entity, IRemotable, new()
        {
            var newColumns = Columns.None<TEntity>()
                .AddRange(columns.Where(x => !entity.HasColumn(x.Property)));
            if (newColumns.Count > 0)
            {
                var row = Query(new Filter<TEntity>(x => x.ID).IsEqualTo(entity.ID), newColumns).Rows.FirstOrDefault();
                row?.FillObject(entity);
            }
        }
        public static void EnsureColumns<TEntity>(ICollection<TEntity> entities, Columns<TEntity> columns)
            where TEntity : Entity, IRemotable, new()
        {
            var newColumns = Columns.None<TEntity>()
                .AddRange(columns.Where(x => entities.Any(entity => !entity.HasColumn(x.Property))));
            if (newColumns.Count > 0)
            {
                newColumns.Add(x => x.ID);
                var table = Query(new Filter<TEntity>(x => x.ID).InList(entities.Select(x => x.ID).ToArray()), newColumns);
                foreach(var row in table.Rows)
                {
                    var id = row.Get<TEntity, Guid>(x => x.ID);
                    var entity = entities.FirstOrDefault(x => x.ID == id);
                    if(entity is null)
                    {
                        // Shouldn't happen, but just in case.
                        continue;
                    }
                    row?.FillObject(entity);
                }
            }
        }

        #region Query

        public static CoreTable Query<TEntity>(Filter<TEntity>? filter = null, Columns<TEntity>? columns = null, SortOrder<TEntity>? orderby = null, CoreRange? range = null)
            where TEntity : Entity, IRemotable, new()
        {
            return new Client<TEntity>().Query(filter, columns, orderby, range);
        }

        public static Task<CoreTable> QueryAsync<TEntity>(Filter<TEntity>? filter = null, Columns<TEntity>? columns = null, SortOrder<TEntity>? orderby = null, CoreRange? range = null)
            where TEntity : Entity, IRemotable, new()
        {
            return Task.Run(() => new Client<TEntity>().Query(filter, columns, orderby, range));
        }

        public static void Query<TEntity>(Filter<TEntity>? filter, Columns<TEntity>? columns, SortOrder<TEntity>? orderby, CoreRange? range, Action<CoreTable?, Exception?> callback)
            where TEntity : Entity, IRemotable, new()
        {
            new Client<TEntity>().Query(filter, columns, orderby, range, callback);
        }

        public static void Query<TEntity>(Filter<TEntity>? filter, Columns<TEntity>? columns, SortOrder<TEntity>? orderby, Action<CoreTable?, Exception?> callback)
            where TEntity : Entity, IRemotable, new()
        {
            new Client<TEntity>().Query(filter, columns, orderby, null, callback);
        }

        #endregion

        #region Save

        public static void Save<TEntity>(TEntity entity, string auditNote)
            where TEntity : Entity, IRemotable, new()
        {
            new Client<TEntity>().Save(entity, auditNote);
        }
        public static void Save<TEntity>(IEnumerable<TEntity> entities, string auditNote)
            where TEntity : Entity, IRemotable, new()
        {
            new Client<TEntity>().Save(entities, auditNote);
        }

        public static Task SaveAsync<TEntity>(TEntity entity, string auditNote)
            where TEntity : Entity, IRemotable, new()
        {
            return Task.Run(() => new Client<TEntity>().Save(entity, auditNote));
        }
        public static Task SaveAsync<TEntity>(IEnumerable<TEntity> entities, string auditNote)
            where TEntity : Entity, IRemotable, new()
        {
            return Task.Run(() => new Client<TEntity>().Save(entities, auditNote));
        }

        public static void Save<TEntity>(TEntity entity, string auditNote, Action<TEntity, Exception?> callback)
            where TEntity : Entity, IRemotable, new()
        {
            new Client<TEntity>().Save(entity, auditNote, callback);
        }
        public static void Save<TEntity>(IEnumerable<TEntity> entities, string auditNote, Action<IEnumerable<TEntity>, Exception?> callback)
            where TEntity : Entity, IRemotable, new()
        {
            new Client<TEntity>().Save(entities, auditNote, callback);
        }

        #endregion

        #region Delete

        public static void Delete<TEntity>(TEntity entity, string auditNote)
            where TEntity : Entity, IRemotable, new()
        {
            new Client<TEntity>().Delete(entity, auditNote);
        }

        public static void Delete<TEntity>(TEntity entity, string auditNote, Action<TEntity, Exception?> callback)
            where TEntity : Entity, IRemotable, new()
        {
            new Client<TEntity>().Delete(entity, auditNote, callback);
        }

        public static Task DeleteAsync<TEntity>(TEntity entity, string auditNote)
            where TEntity : Entity, IRemotable, new()
        {
            return Task.Run(() => new Client<TEntity>().Delete(entity, auditNote));
        }
        public static Task DeleteAsync<TEntity>(IEnumerable<TEntity> entities, string auditNote)
            where TEntity : Entity, IRemotable, new()
        {
            return Task.Run(() => new Client<TEntity>().Delete(entities, auditNote));
        }

        public static void Delete<TEntity>(IEnumerable<TEntity> entities, string auditNote)
            where TEntity : Entity, IRemotable, new()
        {
            new Client<TEntity>().Delete(entities, auditNote);
        }

        public static void Delete<TEntity>(IEnumerable<TEntity> entities, string auditNote, Action<IList<TEntity>, Exception?> callback)
            where TEntity : Entity, IRemotable, new()
        {
            new Client<TEntity>().Delete(entities, auditNote, callback);
        }

        #endregion

        #region Query Multiple

        public static void QueryMultiple(
            Action<Dictionary<string, CoreTable>?, Exception?> callback,
            Dictionary<string, IQueryDef> queries)
        {
            try
            {
                using var timer = new Profiler(false);
                CheckClient().QueryMultiple((result, e) =>
                {
                    timer.Dispose(result != null ? result.Sum(x => x.Value.Rows.Count) : -1);
                    callback?.Invoke(result, e);
                }, queries);
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public static QueryMultipleResults QueryMultiple(params IKeyedQueryDef[] queries) =>
            new QueryMultipleResults(QueryMultiple(queries.ToDictionary(x => x.Key, x => x as IQueryDef)));

        public static void QueryMultiple(Action<QueryMultipleResults?, Exception?> callback, params IKeyedQueryDef[] queries) =>
            QueryMultiple((results, e) =>
            {
                if (results != null)
                {
                    callback?.Invoke(new QueryMultipleResults(results), e);
                }
                else
                {
                    callback?.Invoke(null, e);
                }
            }, queries.ToDictionary(x => x.Key, x => x as IQueryDef));

        public static QueryMultipleResults QueryMultiple(IEnumerable<IKeyedQueryDef> queries) =>
            new QueryMultipleResults(QueryMultiple(queries.ToDictionary(x => x.Key, x => x as IQueryDef)));

        public static void QueryMultiple(Action<QueryMultipleResults?, Exception?> callback, IEnumerable<IKeyedQueryDef> queries) =>
            QueryMultiple((results, e) =>
            {
                if(results != null)
                {
                    callback?.Invoke(new QueryMultipleResults(results), e);
                }
                else
                {
                    callback?.Invoke(null, e);
                }
            }, queries.ToDictionary(x => x.Key, x => x as IQueryDef));

        public static async Task<QueryMultipleResults> QueryMultipleAsync(IEnumerable<IKeyedQueryDef> queries)
        {
            return new QueryMultipleResults(await QueryMultipleAsync(queries.ToDictionary(x => x.Key, x => x as IQueryDef)));
        }

        public static Dictionary<string, CoreTable> QueryMultiple(Dictionary<string, IQueryDef> queries)
        {
            try
            {
                using var timer = new Profiler(false);
                var result = CheckClient().QueryMultiple(queries);
                timer.Log(result.Sum(x => x.Value.Rows.Count));
                return result;
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public static Task<Dictionary<string, CoreTable>> QueryMultipleAsync(Dictionary<string, IQueryDef> queries)
        {
            return Task.Run(() =>
            {
                try
                {
                    using var timer = new Profiler(false);
                    var result = CheckClient().QueryMultiple(queries);
                    timer.Log(result.Sum(x => x.Value.Rows.Count));
                    return result;
                }
                catch (RequestException e)
                {
                    ClientFactory.RaiseRequestError(e);
                    throw;
                }
            });
        }

        #endregion


        public static IValidationData Validate(Guid session)
        {
            try
            {
                using (new Profiler(true))
                    return CheckClient().Validate(session);
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public static IValidationData Validate(string pin, Guid session = default)
        {
            try
            {
                using (new Profiler(true))
                    return CheckClient().Validate(pin, session);
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public static IValidationData Validate(string userid, string password, Guid session = default)
        {
            try
            {
                using (new Profiler(true))
                    return CheckClient().Validate(userid, password, session);
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public static bool Check2FA(string code, Guid? session = null)
        {
            try
            {
                using (new Profiler(true))
                    return CheckClient().Check2FA(code, session);
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public static bool Ping()
        {
            try
            {
                return CheckClient().Ping();
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public static DatabaseInfo? Info()
        {
            try
            {
                using (new Profiler(true))
                    return CheckClient().Info();
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public static string Version()
        {
            try
            {
                using (new Profiler(true))
                    return CheckClient().Version();
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public static string ReleaseNotes()
        {
            try
            {
                using (new Profiler(true))
                    return CheckClient().ReleaseNotes();
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public static byte[]? Installer()
        {
            try
            {
                using (new Profiler(true))
                    return CheckClient().Installer();
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public static Client Create(Type TEntity) => 
            (Activator.CreateInstance(typeof(Client<>).MakeGenericType(TEntity)) as Client)!;

    }

    public class Client<TEntity> : Client, IDisposable where TEntity : Entity, IRemotable, new()
    {
        #region IQueryProvider

        public static IQueryProvider<TEntity> Provider { get; private set; } = new ClientQueryProvider<TEntity>();

        #endregion

        private IClient<TEntity> _client;

        public Client()
        {
            _client = ClientFactory.CreateClient<TEntity>();
        }

        public void Dispose()
        {
        }


        private void CheckSupported()
        {
            if (!ClientFactory.IsSupported<TEntity>())
                throw new NotSupportedException(string.Format("{0} is not supported in this context", typeof(TEntity).EntityName()));
        }

        public CoreTable Query(Filter<TEntity>? filter = null, Columns<TEntity>? columns = null, SortOrder<TEntity>? orderby = null, CoreRange? range = null)
        {
            try
            {
                using var timer = new Profiler<TEntity>(false);
                CheckSupported();
                var result = _client.Query(filter, columns, orderby, range);
                timer.Log(result.Rows.Count);
                return result;
            }
            catch(RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }
        public override CoreTable Query(IFilter? filter = null, IColumns? columns = null, ISortOrder? sortOrder = null, CoreRange? range = null)
        {
            return Query(filter as Filter<TEntity>, columns as Columns<TEntity>, sortOrder as SortOrder<TEntity>, range);
        }

        public void Query(Filter<TEntity>? filter, Columns<TEntity>? columns, SortOrder<TEntity>? sort, CoreRange? range, Action<CoreTable?, Exception?>? callback)
        {
            try
            {
                var timer = new Profiler<TEntity>(false);
                CheckSupported();
                _client.Query(filter, columns, sort, range, (c, e) =>
                {
                    timer.Log(c != null ? c.Rows.Count : -1);
                    callback?.Invoke(c, e);
                });
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public TEntity[] Load(Filter<TEntity>? filter = null, SortOrder<TEntity>? sort = null, CoreRange? range = null)
        {
            try
            {

                using (var timer = new Profiler<TEntity>(false))
                {
                    CheckSupported();
                    var result = _client.Load(filter, sort, range);
                    foreach (var entity in result)
                        entity.CommitChanges();
                    timer.Log(result.Length);
                    return result;
                }
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public void Load(Filter<TEntity> filter, SortOrder<TEntity> sort, CoreRange? range, Action<TEntity[]?, Exception?>? callback)
        {
            try
            {
                var timer = new Profiler<TEntity>(false);
                CheckSupported();
                _client.Load(filter, sort, range,(i, e) =>
                {
                    timer.Dispose(i != null ? i.Length : -1);
                    callback?.Invoke(i, e);
                });
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public override void Save(Entity entity, string auditNote)
        {
            try
            {
                Save((entity as TEntity)!, auditNote);
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }
        public override void Save(IEnumerable<Entity> entities, string auditNote)
        {
            try
            {
                Save(entities.Cast<TEntity>(), auditNote);
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }
        public void Save(TEntity entity, string auditnote)
        {
            try
            {

                using (new Profiler<TEntity>(true))
                {
                    CheckSupported();
                    entity.LastUpdate = DateTime.Now;
                    entity.LastUpdateBy = ClientFactory.UserID;
                    _client.Save(entity, auditnote);
                    entity.CommitChanges();
                }
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public void Save(TEntity entity, string auditnote, Action<TEntity, Exception?> callback)
        {
            try
            {
                var timer = new Profiler<TEntity>(false);
                CheckSupported();
                _client.Save(entity, auditnote, (i, c) =>
                {
                    timer.Dispose();
                    callback?.Invoke(i, c);
                });
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public void Save(IEnumerable<TEntity> entities, string auditnote)
        {
            try
            {
                using var timer = new Profiler<TEntity>(false);
                CheckSupported();
                var items = entities.AsArray();
                if (items.Any())
                    _client.Save(items, auditnote);
                timer.Log(items.Length);
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public void Save(IEnumerable<TEntity> entities, string auditnote, Action<IEnumerable<TEntity>, Exception?> callback)
        {
            try
            {
                var timer = new Profiler<TEntity>(false);
                CheckSupported();
                var items = entities.AsArray();
                if (items.Any())
                {
                    _client.Save(items, auditnote, (i, e) =>
                    {
                        timer.Dispose(i.Count());
                        callback?.Invoke(i, e);
                    });
                }
                else
                {
                    timer.Dispose(0);
                    callback?.Invoke(items, null);
                }
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }


        public void Delete(TEntity entity, string auditnote)
        {
            try
            {
                using (new Profiler<TEntity>(true))
                {
                    CheckSupported();
                    _client.Delete(entity, auditnote);
                }
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public void Delete(TEntity entity, string auditnote, Action<TEntity, Exception?> callback)
        {
            try
            {
                var timer = new Profiler<TEntity>(true);
                CheckSupported();
                _client.Delete(entity, auditnote, (i, e) =>
                {
                    timer.Dispose();
                    callback?.Invoke(i, e);
                });
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public void Delete(IEnumerable<TEntity> entities, string auditnote)
        {
            try
            {
                using var timer = new Profiler<TEntity>(false);
                CheckSupported();
                var items = entities.AsArray();
                _client.Delete(items, auditnote);
                timer.Log(items.Length);
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public void Delete(IEnumerable<TEntity> entities, string auditnote, Action<IList<TEntity>, Exception?> callback)
        {
            try
            {
                var timer = new Profiler<TEntity>(false);
                CheckSupported();
                var items = entities.AsArray();
                _client.Delete(items, auditnote, (i, e) =>
                {
                    timer.Dispose(i.Count);
                    callback?.Invoke(i, e);
                });
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public IEnumerable<string> SupportedTypes()
        {
            try
            {
                using (new Profiler(true))
                    return _client.SupportedTypes();
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }

        public new DatabaseInfo Info()
        {
            try
            {
                using (new Profiler(true))
                    return _client.Info();
            }
            catch (RequestException e)
            {
                ClientFactory.RaiseRequestError(e);
                throw;
            }
        }
    }

    public static class ClientExtensions
    {
        /// <summary>
        /// Load the properties of any <see cref="EntityLink{T}"/>s on this <typeparamref name="T"/> where the <see cref="IEntityLink.ID"/> is not <see cref="Guid.Empty"/>.
        /// This allows us to populate columns of transient objects, as long as they are linked by the ID. What this actually then does is query each
        /// linked table with the required columns.
        /// </summary>
        /// <param name="columns"></param>
        public static void LoadForeignProperties<T>(this IEnumerable<T> items, Columns<T> columns)
            where T : BaseObject, new()
        {
            // Lists of properties that we need, arranged by the entity link property which is their parent.
            // LinkIDProperty : (Type, Properties: [(columnName, property)], Objects)
            var newData = new Dictionary<IProperty, Tuple<Type, List<Tuple<string, IProperty>>, HashSet<T>>>();

            foreach (var column in columns)
            {
                var property = DatabaseSchema.Property(typeof(T), column.Property);
                if (property?.GetOuterParent(x => x.IsEntityLink) is IProperty linkProperty)
                {
                    var remaining = column.Property[(linkProperty.Name.Length + 1)..];
                    if (remaining.Equals(nameof(IEntityLink.ID)))
                    {
                        // This guy isn't foreign, so we don't pull him.
                        continue;
                    }

                    var idProperty = DatabaseSchema.Property(typeof(T), linkProperty.Name + "." + nameof(IEntityLink.ID))!;

                    var linkType = linkProperty.PropertyType.GetInterfaceDefinition(typeof(IEntityLink<>))!.GenericTypeArguments[0];
                    if (!newData.TryGetValue(idProperty, out var data))
                    {
                        data = new Tuple<Type, List<Tuple<string, IProperty>>, HashSet<T>>(
                            linkType,
                            new List<Tuple<string, IProperty>>(),
                            new HashSet<T>());
                        newData.Add(idProperty, data);
                    }

                    var any = false;
                    foreach (var item in items)
                    {
                        if (!item.LoadedColumns.Contains(column.Property))
                        {
                            var linkID = (Guid)idProperty.Getter()(item);
                            if (linkID != Guid.Empty)
                            {
                                any = true;
                                data.Item3.Add(item);
                            }
                        }
                    }
                    if (any)
                    {
                        data.Item2.Add(new Tuple<string, IProperty>(remaining, property));
                    }
                }
            }

            var queryDefs = new List<IKeyedQueryDef>();
            foreach (var (prop, data) in newData)
            {
                if (data.Item2.Count != 0)
                {
                    var ids = data.Item3.Select(prop.Getter()).Cast<Guid>().ToArray();
                    queryDefs.Add(new KeyedQueryDef(prop.Name, data.Item1,
                        Filter.Create<Entity>(data.Item1, x => x.ID).InList(ids),
                        Columns.None(data.Item1)
                            .Add(data.Item2.Select(x => x.Item1))
                            .Add<Entity>(x => x.ID)));
                }
            }
            var results = Client.QueryMultiple(queryDefs);
            foreach(var (prop, data) in newData)
            {
                var table = results.GetOrDefault(prop.Name);
                if(table is null)
                {
                    continue;
                }
                var keyCol = table.GetColumnIndex<Entity, Guid>(x => x.ID);
                var dict = table.Rows.ToDictionary(x => x.Get<Guid>(keyCol));
                foreach (var entity in data.Item3)
                {
                    var linkID = (Guid)prop.Getter()(entity);
                    if (dict.TryGetValue(linkID, out var row))
                    {
                        foreach (var (name, property) in data.Item2)
                        {
                            if (!entity.LoadedColumns.Contains(property.Name))
                            {
                                property.Setter()(entity, row[name]);
                                entity.LoadedColumns.Add(property.Name);
                            }
                        }
                    }
                }
            }
        }
    }
}