using Microsoft.EntityFrameworkCore;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace AsbCloudInfrastructure.EfCache
{
#nullable enable
    /// <summary>
    /// Кеширование запросов EF.<br/>
    /// Кеш не отслеживается ChangeTracker.
    /// </summary>
    public static class EfCacheExtensions
    {
        private static readonly Dictionary<string, CacheItem> caches = new(16);
        private static readonly TimeSpan semaphoreTimeout = TimeSpan.FromSeconds(25);
        private static readonly SemaphoreSlim semaphore = new(1);
        private static readonly TimeSpan minCacheTime = TimeSpan.FromSeconds(2);
        private static readonly TimeSpan defaultObsolescence = TimeSpan.FromMinutes(4);

        private class YieldConvertedData<TEntity, TModel> : IEnumerable<TModel>
        {
            private struct ConvertedData
            {
                public TEntity? Entity;
                public TModel? Model;
            }

            ConvertedData[] data;
            public Func<TEntity, TModel> convert { get; }

            public YieldConvertedData(TEntity[] entities, Func<TEntity, TModel> convert)
            {
                data = (entities.Select(x => new ConvertedData { 
                        Entity = x, 
                        Model = default }))
                    .ToArray();
                this.convert = convert;
            }

            class YieldConvertedDataEnumerator : IEnumerator<TModel>
            {
                private readonly ConvertedData[] data;
                private readonly Func<TEntity, TModel> convert;
                private int position = -1;

                public YieldConvertedDataEnumerator(ConvertedData[] data, Func<TEntity, TModel> convert)
                {
                    this.data = data;
                    this.convert = convert;
                }

                public TModel Current
                {
                    get
                    {
                        if (data[position].Entity is TEntity entity)
                        {
                            var dto = convert(entity);
                            data[position].Entity = default;
                            data[position].Model = dto;
                        }
                        return data[position].Model!;
                    }
                }

                object IEnumerator.Current => Current!;

                public void Dispose()
                {
                }

                public bool MoveNext()
                {
                    position++;
                    return (position < data.Length);
                }

                public void Reset()
                { 
                    position = -1; 
                }
            }

            public IEnumerator<TModel> GetEnumerator()
            {
                var result = new YieldConvertedDataEnumerator(data, convert);
                return result;
            }

            IEnumerator IEnumerable.GetEnumerator()
            {
                throw new NotImplementedException();
            }
        }

        private class CacheItem
        {
            internal IEnumerable? Data;
            internal DateTime DateObsolete;
            internal DateTime DateObsoleteTotal;
            internal readonly SemaphoreSlim semaphore = new(1);

            internal IEnumerable<TEntity> GetData<TEntity>()
            {
                if (Data is IEnumerable<TEntity> typedData)
                    return typedData;
                throw new TypeAccessException("Cache data has wrong type. Possible 'tag' is not unique.");
            }

            internal IEnumerable<TModel> GetData<TEntity, TModel>(Func<TEntity, TModel> convert, int attempt = 1)
            {
                if (Data is IEnumerable<TModel> typedData)
                    return typedData;
                if (Data is IEnumerable<TEntity> typedEntityData)
                {
                    if (semaphore.Wait(0))
                    {
                        try
                        {
                            var convertedData = new YieldConvertedData<TEntity, TModel>(typedEntityData.ToArray(), convert);
                            Data = convertedData;
                            return convertedData;
                        }
                        finally
                        {
                            semaphore.Release();
                        }
                    }
                    else
                    {
                        if (semaphore.Wait(semaphoreTimeout))
                        {
                            semaphore.Release();
                        }
                        else
                        {
                            semaphore.Release();
                            throw new TimeoutException("EfCacheL2.GetData. Can't wait too long while converting cache data");
                        }
                    }
                }
                if (attempt > 0)
                    return GetData(convert, --attempt);
                throw new TypeAccessException("Cache data has wrong type. Possible 'tag' is not unique.");
            }
        }

        private static CacheItem GetOrAddCache(string tag, Func<object[]> valueFactory, TimeSpan obsolete)
        {
            CacheItem cache;
            while (!caches.ContainsKey(tag))
            {
                if (semaphore.Wait(0))
                {
                    try
                    {
                        if (!caches.ContainsKey(tag))
                        {
                            cache = new CacheItem();
                            caches.Add(tag, cache);
                        }
                    }
                    finally
                    {
                        semaphore.Release();
                    }
                    break;
                }
                else
                {
                    if (semaphore.Wait(semaphoreTimeout))
                    {
                        semaphore.Release();
                    }
                    else
                    {
                        semaphore.Release();
                        throw new TimeoutException("EfCacheL2.GetOrAddCache. Can't wait too long while getting cache");
                    }
                }
            }

            cache = caches[tag];

            if (cache.DateObsolete < DateTime.Now)
            {
                if (cache.semaphore.Wait(0))
                {
                    try
                    {
                        var dateObsolete = DateTime.Now + obsolete;
                        var dateQueryStart = DateTime.Now;
                        var data = valueFactory();
                        var queryTime = DateTime.Now - dateQueryStart;

                        if (dateObsolete - DateTime.Now < minCacheTime)
                            dateObsolete = DateTime.Now + minCacheTime;

                        cache.Data = data;
                        cache.DateObsolete = dateObsolete;
                        cache.DateObsoleteTotal = dateObsolete + queryTime + minCacheTime;
                    }
                    finally
                    {
                        cache.semaphore.Release();
                    }
                }
                else if (cache.DateObsoleteTotal < DateTime.Now)
                {
                    if (cache.semaphore.Wait(semaphoreTimeout))
                    {
                        cache.semaphore.Release();
                    }
                    else
                    {
                        cache.semaphore.Release();
                        throw new TimeoutException("EfCacheL2.GetOrAddCache. Can't wait too long while getting cache");
                    }
                }
            }
            return cache;
        }

        private static async Task<CacheItem> GetOrAddCacheAsync(string tag, Func<CancellationToken, Task<object[]>> valueFactoryAsync, TimeSpan obsolete, CancellationToken token)
        {
            CacheItem cache;
            while (!caches.ContainsKey(tag))
            {
                if (semaphore.Wait(0))
                {
                    try
                    {
                        if (!caches.ContainsKey(tag))
                        {
                            cache = new CacheItem();
                            caches.Add(tag, cache);
                        }
                    }
                    finally
                    {
                        semaphore.Release();
                    }
                    break;
                }
                else
                {
                    if (await semaphore.WaitAsync(semaphoreTimeout, token))
                    {
                        semaphore.Release();
                    }
                    else
                    {
                        semaphore.Release();
                        throw new TimeoutException("EfCacheL2.GetOrAddCache. Can't wait too long while getting cache");
                    }
                }
            }

            cache = caches[tag];

            if (cache.DateObsolete < DateTime.Now)
            {
                if (cache.semaphore.Wait(0))
                {
                    try
                    {
                        var dateObsolete = DateTime.Now + obsolete;
                        var dateQueryStart = DateTime.Now;
                        var data = await valueFactoryAsync(token);
                        var queryTime = DateTime.Now - dateQueryStart;

                        if (dateObsolete - DateTime.Now < minCacheTime)
                            dateObsolete = DateTime.Now + minCacheTime;

                        cache.Data = data;
                        cache.DateObsolete = dateObsolete;
                        cache.DateObsoleteTotal = dateObsolete + queryTime + minCacheTime;
                    }
                    finally
                    {
                        cache.semaphore.Release();
                    }
                }
                else if (cache.DateObsoleteTotal < DateTime.Now)
                {
                    if (await cache.semaphore.WaitAsync(semaphoreTimeout, token))
                    {
                        cache.semaphore.Release();
                    }
                    else
                    {
                        cache.semaphore.Release();
                        throw new TimeoutException("EfCacheL2.GetOrAddCache. Can't wait too long while getting updated cache");
                    }
                }
            }
            return cache;
        }

        /// <summary>
        /// Кешировать запрос в List&lt;<typeparamref name="TEntity"/>&gt;. Кеш tag = typeof(TEntity).Name
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <param name="query"></param>
        /// <returns></returns>
        public static IEnumerable<TEntity> FromCache<TEntity>(this IQueryable<TEntity> query)
            where TEntity : class
        {
            var tag = typeof(TEntity).Name;
            return FromCache(query, tag, defaultObsolescence);
        }

        /// <summary>
        /// Кешировать запрос в List&lt;<typeparamref name="TEntity"/>&gt;.
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <param name="query"></param>
        /// <param name="tag">Метка кеша</param>
        /// <param name="obsolescence">Период устаревания данных</param>
        /// <returns></returns>
        public static IEnumerable<TEntity> FromCache<TEntity>(this IQueryable<TEntity> query, string tag, TimeSpan obsolescence)
            where TEntity : class
        {
            object[] factory() => query.AsNoTracking().ToArray();
            var cache = GetOrAddCache(tag, factory, obsolescence);
            return cache.GetData<TEntity>();
        }

        /// <summary>
        /// Кешировать запрос с последующим преобразованием из <typeparamref name="TEntity"/> в <typeparamref name="TModel"/>.<br/>
        /// Преобразование выполняется после получения из БД, результат кешируется в List&lt;<typeparamref name="TEntity"/>&gt;.
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <typeparam name="TModel"></typeparam>
        /// <param name="query"></param>
        /// <param name="tag">Метка кеша</param>
        /// <param name="obsolescence">Период устаревания данных</param>
        /// <param name="convert">Преобразование данных БД в DTO</param>
        /// <returns></returns>
        public static IEnumerable<TModel> FromCache<TEntity, TModel>(this IQueryable<TEntity> query, string tag, TimeSpan obsolescence, Func<TEntity, TModel> convert)
            where TEntity : class
        {
            object[] factory() => query.AsNoTracking().ToArray();
            var cache = GetOrAddCache(tag, factory, obsolescence);
            return cache.GetData(convert);
        }

        public static Task<IEnumerable<TEntity>> FromCacheAsync<TEntity>(this IQueryable<TEntity> query, CancellationToken token)
            where TEntity : class
        {
            var tag = typeof(TEntity).Name;
            return FromCacheAsync(query, tag, defaultObsolescence, token);
        }

        /// <summary>
        /// Асинхронно кешировать запрос в List&lt;<typeparamref name="TEntity"/>&gt;.<br/>
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <param name="query"></param>
        /// <param name="tag">Метка кеша</param>
        /// <param name="obsolescence">Период устаревания данных</param>
        /// <param name="token"></param>
        /// <returns></returns>
        public static async Task<IEnumerable<TEntity>> FromCacheAsync<TEntity>(this IQueryable<TEntity> query, string tag, TimeSpan obsolescence, CancellationToken token)
            where TEntity : class
        {
            async Task<object[]> factory(CancellationToken token)
                => await query.AsNoTracking().ToArrayAsync(token);
            var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token);
            return cache.GetData<TEntity>();
        }

        /// <summary>
        /// Асинхронно кешировать запрос с последующим преобразованием из <typeparamref name="TEntity"/> в <typeparamref name="TModel"/>.<br/>
        /// Преобразование выполняется после получения из БД, результат кешируется в List&lt;<typeparamref name="TModel"/>&gt;.
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <typeparam name="TModel"></typeparam>
        /// <param name="query"></param>
        /// <param name="tag">Метка кеша</param>
        /// <param name="obsolescence">Период устаревания данных</param>
        /// <param name="convert">Преобразование данных БД в DTO</param>
        /// <param name="token"></param>
        /// <returns></returns>
        public static async Task<IEnumerable<TModel>> FromCacheAsync<TEntity, TModel>(this IQueryable<TEntity> query, string tag, TimeSpan obsolescence, Func<TEntity, TModel> convert, CancellationToken token)
            where TEntity : class
        {
            async Task<object[]> factory(CancellationToken token)
                => await query.AsNoTracking().ToArrayAsync(token);
            var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token);
            return cache.GetData(convert);
        }

        /// <summary>
        /// drops cache with tag = typeof(T).Name
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="query"></param>
        public static void DropCache<T>(this IQueryable<T> query)
        {
            var tag = typeof(T).Name;
            DropCache(query, tag);
        }

        /// <summary>
        /// Очистить кеш
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="query"></param>
        /// <param name="tag">Метка кеша</param>
        public static void DropCache<T>(this IQueryable<T> query, string tag)
        {
            caches.Remove(tag, out var _);
        }
    }
#nullable disable
}