using System.Collections.Concurrent;
using LiteDB;
using Dacryptero.Data;
using Dacryptero.Notifications;

namespace Dacryptero.DB
{
    public class IndexCache
    {
        private readonly ILiteCollection<Index> collection;

        private readonly ConcurrentDictionary<(string, IndexSource), Index> cached
            = new ConcurrentDictionary<(string, IndexSource), Index>();
        
        private readonly ConcurrentDictionary<ObjectId, Index> cachedById
            = new ConcurrentDictionary<ObjectId, Index>();
        
        private int cacheOnlyCounter = 0;

        /// <summary>
        /// Get if this index is in cache only mode and will no longer store any changed data to the
        /// disk.
        /// </summary>
        public bool CacheOnly => cacheOnlyCounter > 0;

        public IndexCache(ILiteCollection<Index> collection)
        {
            this.collection = collection;
        }

        public void Clear()
        {
            cached.Clear();
            collection.DeleteAll();
        }

        public Index? GetIndex(string token, IndexSource source, bool createIfMissing = true)
        {
            // get token from cache
            if (cached.TryGetValue((token, source), out Index? index))
                return index;
            // get token from database
            try
            {
                index = collection.Query()
                    .Where(x => x.Token == token)
                    .Where(x => x.Source == source)
                    .FirstOrDefault();
            }
            catch (System.Exception e)
            {
                Serilog.Log.Error(e, "Cannot fetch index from db: {token}", token);
            }
            if (index is not null)
            {
                cached.TryAdd((token, source), index);
                cachedById.TryAdd(index.Id, index);
                return index;
            }
            if (!createIfMissing)
                return null;
            // this token doesn't exists in the index - create it
            index = new Index
            {
                Source = source,
                Token = token,
            };
            collection.Insert(index);
            cached.TryAdd((token, source), index);
            cachedById.TryAdd(index.Id, index);
            // now add this token to all parent tokens
            foreach (var partial in Index.GetPartialTokens(token, 3))
            {
                if (partial != token)
                    GetIndex(partial, source)!.ChildTokens.Add(index.Id);
            }
            // return it
            return index;
        }

        public Index? GetIndex(ObjectId id)
        {
            // get token from cache
            if (cachedById.TryGetValue(id, out Index? index))
                return index;
            // get token from database
            index = collection.Query()
                .Where(x => x.Id == id)
                .FirstOrDefault();
            if (index is not null)
            {
                cached.TryAdd((index.Token, index.Source), index);
                cachedById.TryAdd(index.Id, index);
                return index;
            }
            // index does not exists
            return null;
        }

        public void AddRef(string token, IndexSource source, ObjectId id)
        {
            var index = GetIndex(token, source)!;
            lock (index)
            {
                if (!index.Refs.TryGetValue(id, out int old))
                    old = 0;
                index.Refs[id] = old + 1;
            }
            if (!CacheOnly)
                collection.Update(index);
        }

        public void RemoveRef(string token, IndexSource source, ObjectId id)
        {
            var index = GetIndex(token, source)!;
            lock (index)
            {
                if (index.Refs.TryGetValue(id, out int old))
                {
                    if (old > 1)
                        index.Refs[id] = old - 1;
                    else index.Refs.Remove(id);
                }
            }
            if (!CacheOnly)
                collection.Update(index);
        }

        public Search.SearchIndex GetSearchIndex(string token, IndexSource source)
        {
            var index = GetIndex(token, source, false);
            if (index is null)
                return Search.SearchIndex.Empty;
            var search = new Search.SearchIndex(false, index.Refs.Keys);
            foreach (var client in index.ChildTokens)
            {
                var clientIndex = GetIndex(client);
                if (clientIndex is not null)
                    search.UnionWith(new Search.SearchIndex(false, clientIndex.Refs.Keys));
            }
            return search;
        }

        private class CacheOnlyDisabler : System.IDisposable
        {
            public IndexCache Cache { get; }

            public ITransmitter? Execute { get; }

            public CacheOnlyDisabler(IndexCache cache, ITransmitter? execute)
            {
                Cache = cache;
                Execute = execute;
            }

            public void Dispose()
            {
                lock (Cache)
                {
                    Cache.cacheOnlyCounter--;
                    if (Cache.cacheOnlyCounter > 0)
                        return;
                }
                var job = new JobProgressNotification(
                    Execute ?? new GlobalTransmitter(),
                    "Writing updated Index",
                    limit: Cache.cached.Count
                );
                Serilog.Log.Verbose("Write back {count} cached index entries", Cache.cached.Count);
                int count = 0;
                foreach (var (_, entry) in Cache.cached)
                {
                    if ((++count % 2500) == 0)
                        Serilog.Log.Verbose("    index {count:#,#0}...", count);
                    job.Update(count);
                    try { Cache.collection.Update(entry); }
                    catch (System.Exception e)
                    {
                        Serilog.Log.Error(e, "unable to update cache for {entry}", entry.Token);
                        Execute?.SendGlobalNotification(Notification.Error(
                            "Error: Corrupted Index",
                            "Cannot update index. This leads to a corrupt index. See logs for more details."
                        ));
                    }
                }
                job.Dispose(
                    title: "Update Index Finished",
                    description: $"{Cache.cached.Count:#,#0} items written"
                );
            }
        }

        /// <summary>
        /// Enter a cache only region. This will prevent saving every change when they happen. This
        /// is suitable if you intent to make many changes on the index. It is required to dispose
        /// the returned object after you are finished with your work to store all cached objects
        /// at once.
        /// </summary>
        /// <param name="execute">the optional notification client</param>
        /// <returns>The object to dispose after you are finished with your work</returns>
        public System.IDisposable EnterCacheOnlyRegion(ITransmitter? execute)
        {
            lock (this)
                cacheOnlyCounter++;
            return new CacheOnlyDisabler(this, execute);
        }
    }
}