using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Dacryptero.Search;
using Dacryptero.Search.AST;
using Dacryptero.Search.Parser;

namespace Dacryptero.DB
{
    partial class DBSingleController
    {
        class SearchHandler<T> : IAsyncEnumerable<SearchResult>
            where T : ISearchable
        {
            readonly DBSingleController database;  
            readonly AstBase ast;
            readonly SearchResultType resultType;
            readonly Func<AstBase, CancellationToken, Task<IAsyncEnumerable<T>>> entries;
            readonly Search.Sorting.SortConfig sortConfig;

            class Executor : IAsyncEnumerator<SearchResult>
            {
                readonly SearchHandler<T> parent;
                readonly CancellationToken cancellationToken;
                readonly AstPostTransformer postTransformer;
                readonly AstTransformer transformer;
                readonly Dictionary<string, AstBase> roleAsts;
                readonly AstBase ast;
                IAsyncEnumerator<T>? @enum;

                public Executor(SearchHandler<T> parent, CancellationToken cancellationToken)
                {
                    this.parent = parent;
                    this.cancellationToken = cancellationToken;
                    postTransformer = new AstPostTransformer();
                    transformer = new AstTransformer { ExtendedTransformation = true };
                    roleAsts = new Dictionary<string, AstBase>();
                    ast = transformer.Transform(postTransformer.Transform(parent.ast, parent.resultType, null));
#if DEBUG
                    Serilog.Log.Debug("Search AST for {type} in {name}",
                        parent.resultType, parent.database.Config.Name
                    );
                    AstBase.Log(1, ast);
#endif
                }

                private SearchResult? current;
                public SearchResult Current => current ?? 
                    throw new NotSupportedException($"value is not set, item={current}, executed={executedNext}, returne={lastReturn}, disposed={isDisposed}");
                bool isDisposed = false;
                bool executedNext = false;
                bool lastReturn = false;

                readonly SemaphoreSlim creationLock = new SemaphoreSlim(1, 1);

                public async ValueTask DisposeAsync()
                {
                    if (isDisposed)
                        return;
                    isDisposed = true;
                    if (@enum is not null)
                        try { await @enum.DisposeAsync().ConfigureAwait(false); }
                        catch (NotSupportedException)
                        {
                            // huh, unexpected but okay...
                        }
                    creationLock.Dispose();
                }

                public async ValueTask<bool> MoveNextAsync()
                {
                    if (isDisposed)
                        return lastReturn = false;
                        
                    if (@enum is null)
                    {
                        await creationLock.WaitAsync();
                        try
                        {
                            if (@enum is null)
                                @enum = (await parent.entries(ast, cancellationToken)
                                    .ConfigureAwait(false)
                                ).GetAsyncEnumerator(cancellationToken);
                        }
                        catch (Exception e)
                        {
                            Serilog.Log.Error(e, "Cannot itterate to the next item");
                            return false;
                        }
                        finally
                        {
                            creationLock.Release();
                        }
                    }
                    
                    while (true)
                    {
                        cancellationToken.ThrowIfCancellationRequested();
                        // try to get the next item from the client collection. If not return false
                        try
                        {
                            if (!await @enum!.MoveNextAsync().ConfigureAwait(false))
                            {
#if DEBUG
                                Serilog.Log.Verbose("Searching finished for {type}", parent.resultType);
#endif
                                await DisposeAsync();
                                return lastReturn = false;
                            }
                        }
                        // catch (OperationCanceledException)
                        // {
                        //     throw;
                        // }
                        catch (Exception e)
                        {
                            Serilog.Log.Error(e, "cannot iterate to the next search item");
                            throw;
                        }
                        cancellationToken.ThrowIfCancellationRequested();
                        // try to filter this item. If this is a match, set the current item and return true
                        try
                        {
                            var entry = @enum.Current;
                            var role = entry.GetRole();
                            var useAst = ast;
                            if (role is not null)
                            {
                                if (!roleAsts.TryGetValue(role, out useAst))
                                    roleAsts.Add(role, useAst = transformer.Transform(
                                        postTransformer.Transform(ast, parent.resultType, role)
                                    ));
                            }
                            var result = new SearchResult(parent.database, entry.Id, parent.resultType,
                                role, entry.GetName(), entry.GetInfo
                            );
                            foreach (var part in entry.GetSearchParts())
                            {
                                cancellationToken.ThrowIfCancellationRequested();
                                var ctx = new SearchContext(entry.ResultType, part);
                                var progress = useAst.Check(ctx);
                                if (!progress.IsSuccess)
                                    continue;
                                result.Previews.AddRange(progress.Previews);
                                result.Score += progress.Score;
                            }
                            if (result.Score >= 1e-10)
                            {
                                var sorting = Search.Sorting.SortInfo.FromSearchable(entry,
                                    parent.sortConfig, result.Score
                                );
                                result.Sorting = sorting;
                                current = result;
                                return lastReturn = true;
                            }
                        }
                        catch (Exception e)
                        {
                            Serilog.Log.Error(e, "cannot filter search item");
                            throw;
                        }
                    }
                }
            }

            public SearchHandler(DBSingleController database, AstBase ast,
                SearchResultType resultType,
                Func<AstBase, CancellationToken, Task<IAsyncEnumerable<T>>> entries,
                Search.Sorting.SortConfig sortConfig
            )
            {
                this.database = database;
                this.ast = ast;
                this.resultType = resultType;
                this.entries = entries;
                this.sortConfig = sortConfig;
            }

            public IAsyncEnumerator<SearchResult> GetAsyncEnumerator(CancellationToken cancellationToken = default)
            {
                return new Executor(this, cancellationToken);
            }
        }
    }
}