using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using LiteDB;
using Dacryptero.Notifications;

namespace Dacryptero.DB
{
    partial class DBSingleController
    {
        private bool indexing = false;

        public async Task RebuildIndex(bool onlyConfidential, ITransmitter? execute,
            CancellationToken cancellationToken
        )
        {
            await mutex.WaitAsync(cancellationToken).ConfigureAwait(false);
            indexing = true;
            try
            {
                if (database is null)
                    return;
                Serilog.Log.Debug("Clearing index of {name}", Config.Name);
                using var job = new JobProgressNotification(
                    execute ?? new GlobalTransmitter(),
                    $"Rebuild Index for {Config.Name}",
                    descriptionPrefix: "Clearing old index",
                    image: "/img/svgrepo/essential-set-2/garbage-51844.svg"
                );
                if (!onlyConfidential)
                {
                    Config.ContainsIndex = false;
                    await Config.SaveAsync().ConfigureAwait(false);
                    database.Index.Clear();
                }
                confidential?.Index.Clear();
                Serilog.Log.Debug("Initiate re-indexing of {name}", Config.Name);
                using var dbIndexLock = database.Index.EnterCacheOnlyRegion(execute);
                using var confIndexLock = confidential?.Index.EnterCacheOnlyRegion(execute);

                job.Image = "/img/svgrepo/essential-set-2/database-135265.svg";
                int count = 0;
                if (!onlyConfidential)
                {
                    Serilog.Log.Verbose("Index normal Files");
                    job.Reset(job.Title, null, "Normal Files: ");
                    foreach (var file in database.File.Query().Where(x => !x.Deleted).ToEnumerable())
                    {
                        if ((++count % 500) == 0)
                            Serilog.Log.Verbose("    file {count:#,#0}...", count);
                        job.Update(count);
                        IndexTokenifyString(database.Index, Data.IndexSource.Files, file.Id, file.Name);
                    }
                }

                if (confidential is not null)
                {
                    Serilog.Log.Verbose("Index confidential Files");
                    job.Reset(job.Title, null, "Conf. Files: ");
                    count = 0;
                    foreach (var file in confidential.File.Query().Where(x => !x.Deleted).ToEnumerable())
                    {
                        if ((++count % 500) == 0)
                            Serilog.Log.Verbose("    file {count:#,#0}...", count);
                        job.Update(count);
                        IndexTokenifyString(confidential.Index, Data.IndexSource.Files, file.Id, file.Name);
                    }
                }

                if (confidential is not null)
                {
                    Serilog.Log.Verbose("Index Interview data");
                    job.Reset(job.Title, null, "Interviews: ");
                    count = 0;
                    foreach (var interview in confidential.Interview.Query().Where(x => !x.Deleted).ToEnumerable())
                    {
                        if ((++count % 500) == 0)
                            Serilog.Log.Verbose("    interview {count:#,#0}...", count);
                        job.Update(count);
                        interview.PerformIndex(this, false, execute);
                    }
                }

                Serilog.Log.Verbose("Index Person data");
                job.Reset(job.Title, null, "Persons: ");
                count = 0;
                foreach (var person in database.Person.Query().Where(x => !x.Deleted).ToEnumerable())
                {
                    if ((++count % 500) == 0)
                        Serilog.Log.Verbose("    person {count:#,#0}...", count);
                    job.Update(count);
                    
                    var conf = confidential?.PersonData.FindById(person.Id);
                    person.ConfData = conf;

                    if (!onlyConfidential)
                        LoadPersonAttribute(person.Attributes, database.Attribute,
                            database.AttributeEntry, true
                        );
                    if (person.ConfData is not null && confidential is not null)
                        LoadPersonAttribute(person.ConfData.Attributes, confidential.Attribute,
                            confidential.AttributeEntry, true
                        );
                    
                    person.PerformIndex(this, false, onlyConfidential, execute);
                }
            
                Serilog.Log.Debug("Indexing is completed in {name}", Config.Name);
                job.DescriptionPrefix = null;
                job.Dispose();
                if (!onlyConfidential)
                {
                    Config.ContainsIndex = true;
                    await Config.SaveAsync().ConfigureAwait(false);
                }

                System.GC.KeepAlive(dbIndexLock);
                System.GC.KeepAlive(confIndexLock);
            }
            catch (Exception e)
            {
                Serilog.Log.Error(e, "Cannot re-index database {name}", Config.Name);
                throw;
            }
            finally
            {
                indexing = false;
                mutex.Release();
            }
        }

        private class EmptyLock : IDisposable
        {
            public void Dispose() {}
        }

        public IDisposable GetIndexLock(bool confidential, ITransmitter? execute)
        {
            if (confidential)
                return this.confidential?.Index.EnterCacheOnlyRegion(execute) ?? new EmptyLock();
            else
                return this.database?.Index.EnterCacheOnlyRegion(execute) ?? new EmptyLock();
        }

        public void SetIndex(Data.IndexSource source, ObjectId id, string value, bool confidential,
            bool remove, ITransmitter? execute
        )
        {
            IndexCache? cache = confidential ? this.confidential?.Index : this.database?.Index;
            if (cache is null)
                return;
            using (var @lock = cache.EnterCacheOnlyRegion(execute))
            {
                if (remove)
                {
                    foreach (var token in Data.Index.Tokenify(value))
                        cache.RemoveRef(token, source, id);
                }
                else
                {
                    foreach (var token in Data.Index.Tokenify(value))
                        cache.AddRef(token, source, id);
                }
            }
        }

        public void ReplaceIndex(Data.IndexSource source, ObjectId id, string old, string @new,
            bool confidential, ITransmitter? execute
        )
        {
            IndexCache? cache = confidential ? this.confidential?.Index : this.database?.Index;
            if (cache is null)
                return;
            using (var @lock = cache.EnterCacheOnlyRegion(execute))
            {
                foreach (var token in Data.Index.Tokenify(old))
                    cache.RemoveRef(token, source, id);
                foreach (var token in Data.Index.Tokenify(@new))
                    cache.AddRef(token, source, id);
            }
        }

        private void IndexTokenifyString(IndexCache index, Data.IndexSource source, ObjectId id,
            string value
        )
        {
            foreach (var token in Data.Index.Tokenify(value))
                index.AddRef(token, source, id);
        }

        public async Task<Search.SearchIndex> GetIndex(string token, Data.IndexSource source, CancellationToken cancellationToken)
        {
            await mutex.WaitAsync(cancellationToken).ConfigureAwait(false);
            try
            {
                var @base = database?.Index.GetSearchIndex(token, source);
                var @conf = confidential?.Index.GetSearchIndex(token, source);
                if (@base is null || @conf is null)
                    return @base ?? @conf ?? Search.SearchIndex.Empty;
                var result = @base.Value;
                result.UnionWith(@conf.Value);
                return result;
            }
            finally
            {
                mutex.Release();
            }
        }
    }
}