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

namespace AsbCloudInfrastructure.Services.Cache
{
    public class CacheTable<TEntity> : IEnumerable<TEntity>
        where TEntity : class
    {
        private const int semaphoreTimeout = 5_000;
        private static readonly SemaphoreSlim semaphore = new(1);
        private readonly DbContext context;
        private (DateTime refreshDate, IEnumerable entities) data;
        private readonly List<TEntity> cached;
        private readonly DbSet<TEntity> dbSet;

        internal CacheTable(DbContext context, (DateTime refreshDate, IEnumerable entities) data)
        {
            this.context = context;
            this.data = data;
            dbSet = context.Set<TEntity>();
            cached = (List<TEntity>)data.entities;
            if (cached.Count == 0)
                Refresh();
        }

        public TEntity this[int index] { get => cached.ElementAt(index); }

        /// <summary>
        /// Runs action like atomic operation.
        /// wasFree is action argument indicates that semaphore was free.
        /// It may be needed to avoid multiple operations like Refresh().
        /// </summary>
        /// <param name="action">(wasFree) => {...}</param>
        /// <returns>false - semaphore.Wait returned by timeout</returns>
        private static bool Sync(Action<bool> action)
        {
            var wasFree = semaphore.CurrentCount > 0;
            if (!semaphore.Wait(semaphoreTimeout))
                return false;
            try
            {
                action?.Invoke(wasFree);
            }
            catch (Exception ex)
            {
                Trace.WriteLine($"{DateTime.Now:yyyy.MM.dd HH:mm:ss:fff} error in CacheTable<{typeof(TEntity).Name}>.Sync()");
                Trace.WriteLine(ex.Message);
                Trace.WriteLine(ex.StackTrace);
            }
            finally
            {
                semaphore.Release();
            }
            return true;
        }

        /// <summary>
        /// Runs action like atomic operation.
        /// wasFree is action argument indicates that semaphore was free.
        /// It may be needed to avoid multiple operations like Refresh().
        /// </summary>
        /// <param name="action">(wasFree) => {...}</param>
        /// <returns>false - semaphore.Wait returned by timeout</returns>
        private static async Task<bool> SyncAsync(Func<bool, CancellationToken, Task> task, CancellationToken token = default)
        {
            var wasFree = semaphore.CurrentCount > 0;
            if (!await semaphore.WaitAsync(semaphoreTimeout, token).ConfigureAwait(false))
                return false;
            try
            {
                await task?.Invoke(wasFree, token);
            }
            catch (Exception ex)
            {
                Trace.WriteLine($"{DateTime.Now:yyyy.MM.dd HH:mm:ss:fff} error in CacheTable<{typeof(TEntity).Name}>.SyncAsync()");
                Trace.WriteLine(ex.Message);
                Trace.WriteLine(ex.StackTrace);
            }
            finally
            {
                semaphore.Release();
            }
            return true;
        }

        private void InternalRefresh()
        {
            cached.Clear();
            var entities = dbSet.AsNoTracking().ToList();
            cached.AddRange(entities);
            data.refreshDate = DateTime.Now;
        }

        private async Task InternalRefreshAsync(CancellationToken token = default)
        {
            cached.Clear();
            var entities = await context.Set<TEntity>().AsNoTracking()
                .ToListAsync(token).ConfigureAwait(false);
            cached.AddRange(entities);
            data.refreshDate = DateTime.Now;            
        }

        public int Refresh()
        {
            Sync((wasFree) => {
                if (wasFree)
                    InternalRefresh();
                });
            return cached.Count;
        }

        public async Task<int> RefreshAsync(CancellationToken token = default)
        {
            await SyncAsync(async (wasFree, token) => {
                if (wasFree)
                    await InternalRefreshAsync(token).ConfigureAwait(false);
            }, token).ConfigureAwait(false);
            return cached.Count;
        }

        public bool Contains(Func<TEntity, bool> predicate)
            => FirstOrDefault(predicate) != default;

        public async Task<bool> ContainsAsync(Func<TEntity, bool> predicate, CancellationToken token = default)
            => await FirstOrDefaultAsync(predicate, token) != default;

        public TEntity GetOrCreate(Func<TEntity, bool> predicate, Func<TEntity> makeNew)
        {
            TEntity result = default;
            Sync(wasFree => {
                result = cached.FirstOrDefault(predicate);
                if (result != default)
                    return;

                InternalRefresh();                
                result = cached.FirstOrDefault(predicate);
                if (result != default)
                    return;

                var entry = dbSet.Add(makeNew());
                context.SaveChanges();
                InternalRefresh();
                result = entry.Entity;                
            });
            return result;
        }

        public TEntity FirstOrDefault()
        {
            var result = cached.FirstOrDefault();
            if (result != default)
                return result;

            Refresh();
            return cached.FirstOrDefault();
        }

        public async Task<TEntity> FirstOrDefaultAsync(CancellationToken token = default)
        {
            var result = cached.FirstOrDefault();
            if (result != default)
                return result;

            await RefreshAsync(token);
            return cached.FirstOrDefault();            
        }

        public TEntity FirstOrDefault(Func<TEntity, bool> predicate)
        {
            var result = cached.FirstOrDefault(predicate);
            if (result != default)
                return result;

            Refresh();
            return cached.FirstOrDefault(predicate);
        }

        public async Task<TEntity> FirstOrDefaultAsync(Func<TEntity, bool> predicate, CancellationToken token = default)
        {
            var result = cached.FirstOrDefault(predicate);
            if (result != default)
                return result;

            await RefreshAsync(token);
            return cached.FirstOrDefault(predicate);
        }

        public IEnumerable<TEntity> Where(Func<TEntity, bool> predicate = default)
        {
            var result = (predicate != default)
                ? cached.Where(predicate)
                : cached;
            if (result.Any())
                return result;

            Refresh();
            result = (predicate != default)
                ? cached.Where(predicate)
                : cached;
            return result;
        }

        public Task<IEnumerable<TEntity>> WhereAsync(CancellationToken token = default) =>
            WhereAsync(default, token);

        public async Task<IEnumerable<TEntity>> WhereAsync(Func<TEntity, bool> predicate = default,
            CancellationToken token = default)
        {
            var result = (predicate != default)
                ? cached.Where(predicate)
                : cached;
            if (result.Any())
                return result;

            await RefreshAsync(token);
            result = (predicate != default)
                ? cached.Where(predicate)
                : cached;
            return result;
        }

        public void Upsert(TEntity entity)
        {
            if (entity == default)
                return;
            Sync((wasFree) =>
            {
                if (dbSet.Contains(entity))
                    dbSet.Update(entity);
                else
                    dbSet.Add(entity);
                context.SaveChanges();
                InternalRefresh();
            });                
        }

        public Task UpsertAsync(TEntity entity, CancellationToken token = default)
        => SyncAsync(async (wasFree, token) => {
                if (dbSet.Contains(entity))
                    dbSet.Update(entity);
                else
                    dbSet.Add(entity);
                await context.SaveChangesAsync(token).ConfigureAwait(false);
                await InternalRefreshAsync(token).ConfigureAwait(false);
            }, token);

        public void Upsert(IEnumerable<TEntity> entities)
        {
            if (!entities.Any())
                return;

            Sync((wasFree) =>
            {
                foreach (var entity in entities)
                {
                    if (dbSet.Contains(entity)) // TODO: это очень ммедленно
                        dbSet.Update(entity);
                    else
                        dbSet.Add(entity);                 
                }
                context.SaveChanges();
                InternalRefresh();
            });
        }

        public async Task UpsertAsync(IEnumerable<TEntity> entities, CancellationToken token = default)
        {
            if (!entities.Any())
                return;            

            await SyncAsync(async (wasFree, token) => {
                var upsertedEntries = new List<TEntity>(entities.Count());
                foreach (var entity in entities)
                {
                    if (dbSet.Contains(entity))
                        dbSet.Update(entity);
                    else
                        dbSet.Add(entity);
                }
                await context.SaveChangesAsync(token).ConfigureAwait(false);
                await InternalRefreshAsync(token).ConfigureAwait(false);
            }, token);
        }

        public void Remove(Func<TEntity, bool> predicate)
        => Sync(_ =>
            {
                dbSet.RemoveRange(dbSet.Where(predicate));
                context.SaveChanges();
                InternalRefresh();
            });
        
        public Task RemoveAsync(Func<TEntity, bool> predicate, CancellationToken token = default)
            => SyncAsync(async (wasFree, token) => {
                dbSet.RemoveRange(dbSet.Where(predicate));
                await context.SaveChangesAsync(token).ConfigureAwait(false);
                await InternalRefreshAsync(token).ConfigureAwait(false);
            }, token);

        public TEntity Insert(TEntity entity)
        {
            TEntity result = default;
            Sync(_ =>
            {
                var entry = dbSet.Add(entity);
                context.SaveChanges();
                InternalRefresh();
                result = entry.Entity;
            });
            return result;
        }

        public async Task<TEntity> InsertAsync(TEntity entity, CancellationToken token = default)
        {
            TEntity result = default;
            await SyncAsync(async (wasFree, token) =>
            {
                var entry = dbSet.Add(entity);
                await context.SaveChangesAsync(token).ConfigureAwait(false);
                await InternalRefreshAsync(token).ConfigureAwait(false);
                result = entry.Entity;
            }, token);
            return result;
        }

        public int Insert(IEnumerable<TEntity> newEntities)
        {
            int result = 0;
            Sync(_ => {
                dbSet.AddRange(newEntities);
                result = context.SaveChanges();
                InternalRefresh();
            });            
            return result;
        }

        public async Task<int> InsertAsync(IEnumerable<TEntity> newEntities, CancellationToken token = default)
        {
            int result = 0;
            await SyncAsync(async (wasFree, token) => {
                dbSet.AddRange(newEntities);
                result = await context.SaveChangesAsync(token).ConfigureAwait(false);
                await RefreshAsync(token).ConfigureAwait(false);
            }, token);
            return result;
        }

        public IEnumerator<TEntity> GetEnumerator() => Where().GetEnumerator();

        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }
}