using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using LiteDB;
using async_enumerable_dotnet;
using System.Runtime.CompilerServices;

namespace Dacryptero.DB
{
    partial class DBSingleController
    {
        public async Task<DBValue<Data.File>?> GetFileAsync(ObjectId id,
            CancellationToken cancellationToken)
        {
            await mutex.WaitAsync(cancellationToken).ConfigureAwait(false);
            try
            {
                if (database is null)
                    return null;
                Data.File? file = database.File.Query()
                    .Where(x => x.Id == id)
                    .Include(x => x.Entries)
                    .FirstOrDefault();
                if (file is not null)
                    return new DBValue<Data.File>(this, file);
                cancellationToken.ThrowIfCancellationRequested();

                if (confidential is null)
                    return null;
                file = confidential.File.Query()
                    .Where(x => x.Id == id)
                    .Include(x => x.Entries)
                    .FirstOrDefault();
                if (file is not null)
                {
                    file.Confidential = true;
                    return new DBValue<Data.File>(this, file);
                }
                else return null;
            }
            finally
            {
                mutex.Release();
            }
        }

        public async Task<DBValue<Data.FileEntry>?> GetFileEntryAsync(ObjectId id,
            CancellationToken cancellationToken)
        {
            await mutex.WaitAsync(cancellationToken).ConfigureAwait(false);
            try
            {
                if (database is null)
                    return null;
                Data.FileEntry? file = database.FileEntry.FindById(id);
                if (file is not null)
                    return new DBValue<Data.FileEntry>(this, file);
                cancellationToken.ThrowIfCancellationRequested();

                if (confidential is null)
                    return null;
                file = confidential.FileEntry.FindById(id);
                if (file is not null)
                    return new DBValue<Data.FileEntry>(this, file);
                else return null;
            }
            finally
            {
                mutex.Release();
            }
        }

        public IAsyncEnumerable<DBValue<Data.File>> EnumerateFilesAsync(
            Search.SearchIndex index,
            CancellationToken cancellationToken)
        {
            var empty = AsyncEnumerable.Empty<DBValue<Data.File>>();
            return AsyncEnumerable.Concat(
                database is null ? empty :
                    new LockFilterEnumerable<Data.File, DBValue<Data.File>>(
                        mutex,
                        _ => new ThreadSafeEnumerable<Data.File>(
                            () => database.File.Query()
                                .Where(x => !x.Deleted)
                                .Where(index)
                                .ToEnumerable()
                        ),
                        (file, content) =>
                        {
                            content.Content = new DBValue<Data.File>(this, file);
                            return ValueTask.FromResult(true);
                        }
                    ),
                confidential is null ? empty :
                    new LockFilterEnumerable<Data.File, DBValue<Data.File>>(
                        mutex,
                        _ => new ThreadSafeEnumerable<Data.File>(
                            () => confidential.File.Query()
                                .Where(x => !x.Deleted)
                                .Where(index)
                                .ToEnumerable()
                        ),
                        (file, content) =>
                        {
                            file.Confidential = true;
                            content.Content = new DBValue<Data.File>(this, file);
                            return ValueTask.FromResult(true);
                        }
                    )
            );
        }
        
        public async IAsyncEnumerable<DBValue<Data.File>> EnumerateFilesAsync(ObjectId person,
            ObjectId? root, [EnumeratorCancellation] CancellationToken cancellationToken)
        {
            if (database is null)
                yield break;
            await mutex.WaitAsync(cancellationToken).ConfigureAwait(false);
            try
            {
                await foreach (var file in new ThreadSafeEnumerable<Data.File>(
                    () => database.File.Query()
                        .Where(x => x.Person.Id == person)
                        .Where(x => x.ParentId == root)
                        .ToEnumerable()
                    )
                    .WithCancellation(cancellationToken)
                    .ConfigureAwait(false)
                )
                {
                    yield return new DBValue<Data.File>(this, file);
                }

                cancellationToken.ThrowIfCancellationRequested();
                if (confidential is null)
                    yield break;
                await foreach (var file in new ThreadSafeEnumerable<Data.File>(
                    () => confidential.File.Query()
                        .Where(x => x.Person.Id == person)
                        .Where(x => x.ParentId == root)
                        .ToEnumerable()
                    )
                    .WithCancellation(cancellationToken)
                    .ConfigureAwait(false)
                )
                {
                    file.Confidential = true;
                    yield return new DBValue<Data.File>(this, file);
                }
            }
            finally
            {
                mutex.Release();
            }
        }

        public async Task<DBValue<bool>?> UpdateFile(Data.File file, CancellationToken cancellationToken)
        {
            await mutex.WaitAsync(cancellationToken).ConfigureAwait(false);
            try
            {
                var success = database?.File.Update(file) ?? false;
                if (success)
                    return new DBValue<bool>(this, true);
                success = confidential?.File.Update(file) ?? false;
                if (success)
                    return new DBValue<bool>(this, true);
                else return null;
            }
            finally
            {
                mutex.Release();
            }
        }

        public async Task<DBValue<List<Data.File>>?> UpdateFileConfidential(Data.File file, bool confidential, CancellationToken cancellationToken)
        {
            if (database is null || this.confidential is null)
                return null;
            await mutex.WaitAsync(cancellationToken).ConfigureAwait(false);
            try
            {
                var (sourceFile, sourceFileEntry) = confidential 
                    ? (database.File, database.FileEntry)
                    : (this.confidential.File, this.confidential.FileEntry);
                var (targetFile, targetFileEntry) = confidential 
                    ? (this.confidential.File, this.confidential.FileEntry)
                    : (database.File, database.FileEntry);

                var stack = new Stack<Data.File>();
                stack.Push(file);
                var changed = new List<Data.File>();

                while (stack.Count > 0)
                {
                    file = stack.Pop();

                    // if we change to confidential we need to make every child item to be
                    // confidential too. Otherwise this will make this item impossible to
                    // access if we remove the confidential database.
                    if (confidential)
                    {
                        foreach (var child in sourceFile.Query()
                            .Where(x => x.ParentId == file.Id)
                            .Include(x => x.Entries)
                            .ToEnumerable()
                        )
                            stack.Push(child);
                    }
                    // if we change to non-confidential we need to make the path to root to be
                    // non-confidential too. Otherwise the current item will be no longer accessible
                    // if we remove the confidential database.
                    else if (file.ParentId is not null)
                    {
                        var parent = sourceFile.Query()
                            .Where(x => x.Id == file.ParentId)
                            .Include(x => x.Entries)
                            .FirstOrDefault();
                        if (parent is not null)
                            stack.Push(parent);
                    }
                
                    foreach (var entry in file.Entries)
                        targetFileEntry.Upsert(entry);
                    targetFile.Upsert(file);

                    foreach (var entry in file.Entries)
                        sourceFileEntry.Delete(entry.Id);
                    sourceFile.Delete(file.Id);

                    file.Confidential = confidential;
                    changed.Add(file);
                }

                return new DBValue<List<Data.File>>(this, changed);
            }
            finally
            {
                mutex.Release();
            }
        }

        public async Task<DBValue<bool>?> AddFile(Data.File file, CancellationToken cancellationToken)
        {
            await mutex.WaitAsync(cancellationToken).ConfigureAwait(false);
            try
            {
                var (fileDB, fileEntryDB) = file.Confidential
                    ? (confidential?.File, confidential?.FileEntry)
                    : (database?.File, database?.FileEntry);
                fileEntryDB?.InsertBulk(file.Entries);
                fileDB?.Insert(file);
                if (fileDB is not null)
                    return new DBValue<bool>(this, true);
                else return null;
            }
            finally
            {
                mutex.Release();
            }
        }
    }
}