using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using InABox.Clients;

namespace InABox.Core
{
    public class MultiQuery
    {
        private readonly List<InternalQueryDef> _queries = new List<InternalQueryDef>();

        public int Count => _queries.Count;

        public Dictionary<object, IQueryDef> Definitions()
        {
            Dictionary<object, IQueryDef> result = new Dictionary<object, IQueryDef>();
            foreach (var query in _queries)
                result[query.Key] = query.Query;
            return result;
        }
        
        public Dictionary<object, CoreTable> Results()
        {
            Dictionary<object, CoreTable> result = new Dictionary<object, CoreTable>();
            foreach (var query in _queries)
                result[query.Key] = query.Result;
            return result;
        }

        public void Clear()
        {
            _queries.Clear();
        }

        public void Add(IQueryDef query, object key)
        {
            if (key == null)
                throw new Exception("MultiQuery Key many not be null");

            if (_queries.Any(x => Equals(x.Key, key)))
                throw new Exception("Key already exists");
            _queries.Add(new InternalQueryDef
            {
                Key = key,
                Query = query
            });
        }


        public void Query(Action<MultiQuery> callback = null)
        {
            var tasks = new Dictionary<Task<CoreTable>, object>();

            foreach (var query in _queries)
            {
                var task = Task.Run(
                    () =>
                    {
                        var client = ClientFactory.CreateClient(query.Query.Type);
                        return client.Query(query.Query.Filter, query.Query.Columns, query.Query.SortOrder);
                    }
                );
                tasks[task] = query.Key;
            }

            if (callback == null)
            {
                Task.WaitAll(tasks.Keys.ToArray());

                foreach (var task in tasks.Keys)
                {
                    var query = _queries.FirstOrDefault(x => Equals(x.Key, tasks[task]));
                    query.Result = task.Result;
                }
            }
            else
            {
                Task.WhenAll(tasks.Keys.ToArray()).ContinueWith(t =>
                {
                    foreach (var task in tasks.Keys)
                    {
                        var query = _queries.FirstOrDefault(x => Equals(x.Key, tasks[task]));
                        query.Result = task.Result;
                    }

                    callback.Invoke(this);
                });
            }
        }

        public bool Contains(object key)
        {
            return _queries.Any(x => Equals(x.Key, key));
        }

        public CoreTable Get(object key)
        {
            var query = _queries.FirstOrDefault(x => Equals(x.Key, key));
            if (query == null)
                throw new Exception("Key does not exist");
            if (query.Result == null)
                throw new Exception("No result available");
            return query.Result;
        }

        public void Set(object key, CoreTable table)
        {
            var query = _queries.FirstOrDefault(x => Equals(x.Key, key));
            if (query == null)
                throw new Exception("Key does not exist");
            query.Result = table;
        }
        
        private class InternalQueryDef
        {
            public object Key { get; set; }
            public IQueryDef Query { get; set; }
            public CoreTable Result { get; set; }
            public Entity[] Updates { get; set; }
        }


        #region Simplified Generic Helpers

        public void Add<T>(Filter<T>? filter = null, Columns<T>? columns = null, SortOrder<T>? sort = null)
            where T : Entity, IRemotable, IPersistent, new()
        {
            Add(new QueryDef<T>(filter, columns, sort), typeof(T));
        }

        public CoreTable Get<T>()
        {
            return Get(typeof(T));
        }

        public bool Contains<T>()
        {
            return Contains(typeof(T));
        }

        #endregion
    }
}