using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Threading;

namespace Dacryptero
{
    /// <summary>
    /// This allows to iterate through an <see cref="IAsyncEnumerable&lt;out T&gt;" />, execute some
    /// custom code with all of these items and yield specific items to the user. The execution is
    /// locked with a <see cref="SemaphoreSlim" /> as such as only one instance can iterate at the
    /// same time. There are handlers for async creation of the client <see
    /// cref="IAsyncEnumerable&lt;out T&gt;" /> and custom disposing before the lock is released.
    /// </summary>
    /// <typeparam name="T">The input type</typeparam>
    /// <typeparam name="U">the output type</typeparam>
    /// <example> How the usage of this class could look like:
    /// <code>
    /// return new LockFilterEnumerable&lt;TInput, TOutput&gt;( semaphore, _ =&gt;
    /// GetClientAsyncCollection(), (item, content) =&gt; { content.Content = await Operate(item);
    /// return true; }, () =&gt; Finish() );
    /// </code></example>
    /// <example> How this code whould normally look like if c# works like intended:
    /// <code>
    /// await semaphore.WaitOne(cancellationToken); try { await foreach (var item in
    /// GetClientAsyncCollection() .WithCancellation(cancellationToken)) { yield return await
    /// Operate(item); } } finally { try { Finish(); } finally { semaphore.Release(); } }
    /// </code></example>
    /// <remarks>
    /// This class has one crucial benefit against normal inline style: You can cancel your
    /// enumeration safely and release your locks and resources at any time. If you use the
    /// statement in example 2 the compiler will generate code that behave weird if you want to
    /// cancel your operations. Most times your locks are not even released after execution.
    /// </remarks>
    public class LockFilterEnumerable<T, U> : IAsyncEnumerable<U>
        where T : notnull
        where U : notnull
    {
        public interface IContent
        {
            U Content { get; set; }
        }

        class Executor : IAsyncEnumerator<U>, IContent
        {
            readonly LockFilterEnumerable<T, U> parent;
            readonly CancellationToken cancellationToken;

            private U? content;

            public U Content
            {
                get => content ?? throw new NotSupportedException("value wasn't set");
                set => content = value;
            }

            public U Current => content ?? throw new NotSupportedException("value doesn't exists");

            public Executor(LockFilterEnumerable<T, U> parent, CancellationToken cancellationToken)
            {
                this.parent = parent;
                this.cancellationToken = cancellationToken;
            }

            public async ValueTask DisposeAsync()
            {
                if (disposed)
                    return;
                disposed = true;
                try
                {
                    if (enumerator is not null)
                    {
                        await enumerator.DisposeAsync();
                        enumerator = null;
                    }
                }
                catch (OperationCanceledException)
                {}
                catch (Exception e)
                {
                    Serilog.Log.Error(e, "Unexpected error in disposing LockFilterEnumerable");
                }
                finally
                {
                    try
                    {
                        if (parent.customDisposer is not null)
                            await parent.customDisposer();
                    }
                    catch (OperationCanceledException)
                    {}
                    catch (Exception e)
                    {
                        Serilog.Log.Error(e, "Unexpected error in disposing LockFilterEnumerable");
                    }
                    finally
                    {
                        if (parentLockHeld)
                        {
                            parent.@lock.Release();
                            parentLockHeld = false;
                        }
                    }
                }

                lockCreation.Dispose();
            }

            IAsyncEnumerator<T>? enumerator;
            bool parentLockHeld = false;
            bool disposed = false;
            readonly SemaphoreSlim lockCreation = new SemaphoreSlim(1, 1);

            public async ValueTask<bool> MoveNextAsync()
            {
                if (disposed)
                    return false;
                
                if (enumerator is null)
                {
                    await lockCreation.WaitAsync(cancellationToken);
                    try
                    {
                        if (enumerator is null)
                        {
                            await parent.@lock.WaitAsync(cancellationToken);
                            parentLockHeld = true;
                            var enumFactory = await parent.clientEnumerable(cancellationToken);
                            enumerator = enumFactory.GetAsyncEnumerator(cancellationToken);
                        }
                    }
                    finally
                    {
                        lockCreation.Release();
                    }
                }

                while (await enumerator.MoveNextAsync() && !cancellationToken.IsCancellationRequested)
                {
                    if (await parent.filter(enumerator.Current, this) && content is not null)
                        return true;
                }
                await DisposeAsync();
                cancellationToken.ThrowIfCancellationRequested();
                return false;
            }
        }

        readonly Func<CancellationToken, Task<IAsyncEnumerable<T>>> clientEnumerable;
        readonly SemaphoreSlim @lock;
        readonly Func<T, IContent, ValueTask<bool>> filter;
        readonly Func<ValueTask>? customDisposer;

        public LockFilterEnumerable(SemaphoreSlim @lock,
            Func<CancellationToken, Task<IAsyncEnumerable<T>>> enumerable,
            Func<T, IContent, ValueTask<bool>> filter,
            Func<ValueTask>? disposer = null
        )
        {
            this.clientEnumerable = enumerable;
            this.@lock = @lock;
            this.filter = filter;
            this.customDisposer = disposer;
        }

        public LockFilterEnumerable(SemaphoreSlim @lock,
            Func<CancellationToken, IAsyncEnumerable<T>> enumerable,
            Func<T, IContent, ValueTask<bool>> filter,
            Func<ValueTask>? disposer = null
        )
            : this(@lock, c => Task.FromResult(enumerable(c)), filter, disposer)
        {}

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