using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using DatabaseEditor.BL.Interfaces;

namespace DatabaseEditor.BL
{
    public class VirtualizingCollection<T> : IList<T>, IList
    {
        private int _count = -1;

        private readonly Dictionary<int, List<T>> _pages = new Dictionary<int, List<T>>();
        private readonly Dictionary<int, DateTime> _pageTouchTimes = new Dictionary<int, DateTime>();
        private readonly Measurer _measurer = new Measurer();

        /// <summary>
        /// Creates instance of <see cref="VirtualizingCollection{T}"/>.
        /// </summary>
        /// <param name="itemsProvider"></param>
        /// <param name="pageSize"></param>
        /// <param name="pageTimeout"></param>
        public VirtualizingCollection(IItemsProvider<T> itemsProvider, int pageSize = 100, int pageTimeout = 10000)
        {
            ItemsProvider = itemsProvider;
            PageSize = pageSize;
            PageTimeout = pageTimeout;
        }

        public int PageSize { get; }

        public long PageTimeout { get; }

        public IItemsProvider<T> ItemsProvider { get; }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        /// <summary>
        /// Not recommended to use with large sets of data.
        /// </summary>
        /// <returns></returns>
        public IEnumerator<T> GetEnumerator()
        {
            // I didn't find a way to avoid calling this method
            yield break;
        }

        #region NotReadOnly

        public void Add(T item)
        {
            throw new System.NotImplementedException();
        }

        public int Add(object value)
        {
            throw new NotImplementedException();
        }

        public void Clear()
        {
            throw new System.NotImplementedException();
        }

        public bool Contains(T item)
        {
            return IndexOf(item) != -1;
        }

        public bool Contains(object value)
        {
            return Contains((T) value);
        }

        public void CopyTo(T[] array, int arrayIndex)
        {
            throw new System.NotImplementedException();
        }

        public void CopyTo(Array array, int index)
        {
            throw new NotImplementedException();
        }

        public bool Remove(T item)
        {
            return ItemsProvider.DeleteItem(item);
        }

        public void Remove(object value)
        {
            Remove((T) value);
        }

        public void RemoveAt(int index)
        {
            throw new System.NotImplementedException();
        }

        public int IndexOf(T item)
        {
            foreach (var page in _pages)
            {
                int itemIndex;
                if ((itemIndex = page.Value.IndexOf(item)) != -1)
                {
                    return page.Key * PageSize + itemIndex;
                }
            }
            return -1;
        }

        public int IndexOf(object value)
        {
            return IndexOf((T) value);
        }

        public void Insert(int index, T item)
        {
            throw new System.NotImplementedException();
        }

        public void Insert(int index, object value)
        {
            throw new NotImplementedException();
        }

        #endregion

        public object SyncRoot => this;

        public bool IsSynchronized => false;

        public bool IsFixedSize => false;

        public bool IsReadOnly => true;

        public T this[int index]
        {
            get
            {
                if (index >= Count) return default(T);

                // determine which page and offset within page
                int pageIndex = index / PageSize;
                int pageOffset = index % PageSize;

                // request primary page
                RequestPage(pageIndex);

                // if accessing upper 50% then request next page
                if (pageOffset > PageSize / 2 && pageIndex < Count / PageSize)
                    RequestPage(pageIndex + 1);

                // if accessing lower 50% then request prev page
                if (pageOffset < PageSize / 2 && pageIndex > 0)
                    RequestPage(pageIndex - 1);

                // remove stale pages
                CleanUpPages();

                // defensive check in case of async load
                if (_pages[pageIndex] == null)
                    return default(T);

                // return requested item
                var item = _pages[pageIndex][pageOffset];
                return item;
            }
            set { throw new NotImplementedException(); }
        }

        object IList.this[int index]
        {
            get { return this[index]; }
            set { throw new NotImplementedException(); }
        }

        /// <summary>
        /// Number of items in database.
        /// </summary>
        public virtual int Count
        {
            get
            {
                if (_count == -1)
                {
                    LoadCount();
                }
                return _count;
            }
            protected set { _count = value; }
        }

        /// <summary>
        /// Populates <see cref="Count"/> property.
        /// </summary>
        protected virtual void LoadCount()
        {
            Count = FetchCount();
        }

        /// <summary>
        /// Fetches number of items in database.
        /// </summary>
        /// <returns></returns>
        public int FetchCount()
        {
            return ItemsProvider.FetchCount();
        }

        /// <summary>
        /// Fetches <paramref name="count"/> items from database starting by <paramref name="startIndex"/>.
        /// </summary>
        /// <param name="startIndex"></param>
        /// <param name="count"></param>
        /// <returns></returns>
        public IList<T> FetchRange(int startIndex, int count)
        {
            return ItemsProvider.FetchRange(startIndex, count);
        }

        /// <summary>
        /// Removes stale pages.
        /// </summary>
        public void CleanUpPages()
        {
            List<int> keys = new List<int>(_pageTouchTimes.Keys);

            foreach (int key in keys)
            {
                // page 0 is a special case, since WPF ItemsControl access the first item frequently
                if (key != 0 && (DateTime.Now - _pageTouchTimes[key]).TotalMilliseconds > PageTimeout)
                {
                    RemovePage(key);
                }
            }
        }

        private void RemovePage(int key)
        {
            Trace.WriteLine($"Freeing page {key}");
            _pages.Remove(key);
            _pageTouchTimes.Remove(key);
        }

        protected void RequestPage(int pageIndex)
        {
            if (!_pages.ContainsKey(pageIndex))
            {
                _pages.Add(pageIndex, null);
                _pageTouchTimes.Add(pageIndex, DateTime.Now);
                LoadPage(pageIndex);
            }
            else
            {
                _pageTouchTimes[pageIndex] = DateTime.Now;
            }
        }

        protected void LoadPage(int pageIndex)
        {
            Stopwatch stopwatch = Stopwatch.StartNew();
            PopulatePage(pageIndex, FetchPage(pageIndex));
            stopwatch.Stop();
            _measurer.AddTime(stopwatch.Elapsed);
            Trace.WriteLine($"Fetching time - AVG: {_measurer.AverageTime().TotalMilliseconds} ms, MIN: { _measurer.MaxTime.TotalMilliseconds}, MAX: {_measurer.MinTime.TotalMilliseconds}");
        }

        /// <summary>
        /// Populates the page within <see cref="_pages"/> dictionary.
        /// </summary>
        /// <param name="pageIndex"></param>
        /// <param name="page"></param>
        protected void PopulatePage(int pageIndex, List<T> page)
        {
            if (_pages.ContainsKey(pageIndex))
            {
                _pages[pageIndex] = page;
            }
        }

        /// <summary>
        /// Fetches the requested page from <see cref="ItemsProvider"/>.
        /// </summary>
        /// <param name="pageIndex">Index of page to be fetched</param>
        /// <returns></returns>
        protected List<T> FetchPage(int pageIndex)
        {
            Trace.WriteLine($"Fetching page {pageIndex}");
            return ItemsProvider.FetchRange(pageIndex * PageSize, PageSize).ToList();
        }
    }
}