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

namespace Dacryptero.Search
{
    public class SearchTask : IDisposable
    {
        private static SemaphoreSlim lockTasks = new SemaphoreSlim(1, 1);

        private static List<SearchTask> searchTasks = new List<SearchTask>();

        public Web.WebSocketConnection Connection { get; }

        public string? UiId { get; }

        public string Query { get; }

        private CancellationTokenSource tokenSource { get; }

        private SearchTask(Web.WebSocketConnection connection, string? uiId, string query)
        {
            Connection = connection;
            UiId = uiId;
            Query = query;
            tokenSource = new CancellationTokenSource();
        }

        public static async Task SearchAsync(Web.WebSocketConnection connection, string? uiId, string query)
        {
            using var job = new SearchTask(connection, uiId, query);
            await lockTasks.WaitAsync().ConfigureAwait(false);
            try
            {
                searchTasks.Add(job);
            }
            finally
            {
                lockTasks.Release();
            }
            try
            {
                await job.ExecuteAsync().ConfigureAwait(false);
            }
            catch (TaskCanceledException)
            {}
            await lockTasks.WaitAsync().ConfigureAwait(false);
            try
            {
                searchTasks.Remove(job);
            }
            finally
            {
                lockTasks.Release();
            }
        }

        public static async Task CancelAsync(Web.WebSocketConnection connection, string? uiId)
        {
            await lockTasks.WaitAsync().ConfigureAwait(false);
            try
            {
                var job = searchTasks.FirstOrDefault(x => x.Connection == connection && x.UiId == uiId);
                job?.tokenSource.Cancel();
            }
            finally
            {
                lockTasks.Release();
            }
        }

        private async Task ExecuteAsync()
        {
            // create transformer classes
            var watch = new System.Diagnostics.Stopwatch();
            watch.Start();
            var lexer = new Lexer(Query);
            var grouper = new Grouper();
            var builder = new AstBuilder();
            var transformer = new AstTransformer();

            // transform search query in a usable structure
            AstBase ast;
            try
            {
                ast = transformer.Transform(
                    builder.Build(
                        Query,
                        grouper.GroupTokens(
                            Query,
                            lexer.Tokenize()
                        )
                    )
                );
            }
            catch (Exception e)
            {
                Serilog.Log.Warning(e, "unable to parse query {query}", Query);
                return;
            }

            var sorting = ast.GetSortConfig();
            sorting.Optimize();
            Serilog.Log.Verbose("Sorting: {sorting}", sorting);

            // now perform the long running job and execute this search
            int order = 0;
            var @enum = Connection.Session.DoSearch(ast, sorting, tokenSource.Token)
                // .Take(100)
                .Chunks(10, tokenSource.Token)
                .GetAsyncEnumerator(tokenSource.Token);
            try
            {
                while (await @enum.MoveNextAsync().ConfigureAwait(false))
                {
                    await Connection.Send(new Web.Events.Send.SearchResponse(
                        Query, UiId, order++, false, watch.Elapsed.TotalSeconds, sorting,
                        @enum.Current
                    )).ConfigureAwait(false);
                }
            }
            catch (OperationCanceledException)
            {}
            catch (Exception e)
            {
                Serilog.Log.Error(e, "unable to execute query {query}", Query);
                return;
            }
            finally
            {
                await @enum.DisposeAsync().ConfigureAwait(false);
            }

            // search is finished
            await Connection.Send(new Web.Events.Send.SearchResponse(
                Query, UiId, order++, true, watch.Elapsed.TotalSeconds, sorting,
                ReadOnlyMemory<Search.SearchResult>.Empty
            )).ConfigureAwait(false);
        }

        public void Dispose()
        {
            if (!tokenSource.IsCancellationRequested)
                tokenSource.Dispose();
        }
    }
}