using System.Collections;
using Prexonite.Commands.List;
using Prexonite.Compiler.Cil;
using Prexonite.Concurrency;

namespace Prexonite.Commands.Concurrency;

public class AsyncSeq : CoroutineCommand, ICilCompilerAware
{
    #region Singleton pattern

    AsyncSeq() { }

    public static AsyncSeq Instance { get; } = new();

    #endregion

    #region Overrides of CoroutineCommand

    protected override IEnumerable<PValue> CoroutineRun(ContextCarrier sctxCarrier, PValue[] args)
    {
        return CoroutineRunStatically(sctxCarrier, args);
    }

    [SuppressMessage(
        "Microsoft.Naming",
        "CA1704:IdentifiersShouldBeSpelledCorrectly",
        MessageId = nameof(Coroutine)
    )]
    protected static IEnumerable<PValue> CoroutineRunStatically(
        ContextCarrier sctxCarrier,
        PValue[] args
    )
    {
        if (sctxCarrier == null)
            throw new ArgumentNullException(nameof(sctxCarrier));
        if (args == null)
            throw new ArgumentNullException(nameof(args));

        if (args.Length < 1)
            throw new PrexoniteException(
                "async_seq requires one parameter: The sequence to be computed."
            );

        return new ChannelEnumerable(sctxCarrier, args[0]);
    }

    //Contains all the logic for spawing the background worker
    class ChannelEnumerable : IEnumerable<PValue>
    {
        readonly ContextCarrier _sctxCarrier;
        readonly PValue _arg;

        public ChannelEnumerable(ContextCarrier sctxCarrier, PValue arg)
        {
            _sctxCarrier = sctxCarrier;
            _arg = arg;
        }

        #region Implementation of IEnumerable

        [SuppressMessage("ReSharper", "AccessToDisposedClosure")]
        public IEnumerator<PValue> GetEnumerator()
        {
            var peek = new Channel();
            var data = new Channel();
            var rset = new Channel();
            var disp = new Channel();

            #region Producer

            Func<PValue> producer = () =>
            {
                using var e = Map._ToEnumerable(_sctxCarrier.StackContext, _arg).GetEnumerator();
                var doCont = true;
                var doDisp = false;

                cont:
                if (e.MoveNext())
                {
                    peek.Send(true);
                    data.Send(e.Current);
                }
                else
                {
                    peek.Send(false);
                    //doCont = false;
                    goto shutDown;
                }

                wait:
                Select.RunStatically(
                    _sctxCarrier.StackContext,
                    [
                        new KeyValuePair<Channel?, PValue>(
                            rset,
                            pfunc(
                                (_, _) =>
                                {
                                    doCont = true;
                                    try
                                    {
                                        e.Reset();
                                    }
                                    catch (Exception exc)
                                    {
                                        rset.Send(PType.Object.CreatePValue(exc));
                                    }
                                    rset.Send(PType.Null);
                                    return PType.Null;
                                }
                            )
                        ),
                        new KeyValuePair<Channel?, PValue>(
                            disp,
                            pfunc(
                                (_, _) =>
                                {
                                    doCont = false;
                                    doDisp = true;
                                    return PType.Null;
                                }
                            )
                        ),
                        new KeyValuePair<Channel?, PValue>(null, PType.Null),
                    ],
                    false
                );

                //We loop until the dispose command is explicitly given.
                //  -> This way, a reset command can be issued after
                //  the complete enumeration has been computed
                //  without the enumerator being disposed of prematurely
                if (doCont)
                    goto cont;
                else if (!doDisp)
                    goto wait;

                //Ignored (part of CallAsync interface)
                shutDown:
                return PType.Null;
            };

            #endregion

            return new ChannelEnumerator(peek, data, rset, disp, producer, _sctxCarrier);
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        #endregion
    }

    //The channel enumerator is a proxy that translates method calls into messages
    //  to our producer
    class ChannelEnumerator : IEnumerator<PValue>
    {
        public ChannelEnumerator(
            Channel peek,
            Channel data,
            Channel rset,
            Channel disp,
            Func<PValue> produce,
            ContextCarrier sctxCarrier
        )
        {
            _peek = peek;
            _sctxCarrier = sctxCarrier;
            _produce = produce;
            _data = data;
            _rset = rset;
            _disp = disp;

            //The producer runs on a separate thread and communicates
            //  with this thread via 4 channels, one for each method
        }

        readonly Channel _peek;
        readonly Channel _data;
        readonly Channel _rset;
        readonly Channel _disp;
        readonly Func<PValue> _produce;
        readonly ContextCarrier _sctxCarrier;
        PValue? _current;

        #region Implementation of IDisposable

        public void Dispose()
        {
            _disp.Send(PType.Null);
        }

        #endregion

        #region Implementation of IEnumerator

        public bool MoveNext()
        {
            if (_current == null)
            {
                //The producer runs on a separate thread and communicates
                //  with this thread via 4 channels, one for each method
                CallAsync.RunAsync(_sctxCarrier.StackContext, _produce);
                Current = PType.Null;
            }

            if ((bool)_peek.Receive().Value!)
            {
                Current = _data.Receive();
                return true;
            }
            else
            {
                return false;
            }
        }

        public void Reset()
        {
            _rset.Send(PType.Null);
            var p = _rset.Receive();
            if (!p.IsNull)
            {
                if (p.Value is Exception exc)
                    throw exc;
                else
                    throw new PrexoniteException("Cannot reset async enumerator: " + p.Value);
            }
        }

        public PValue Current
        {
            get => _current ?? PType.Null;
            private set => _current = value;
        }

        object IEnumerator.Current => Current;

        #endregion
    }

    //Makes working with select from managed code easier
    class PFunc(PFuncImpl f) : IIndirectCall
    {
        #region Implementation of IIndirectCall

        public PValue IndirectCall(StackContext sctx, params ReadOnlySpan<PValue> args) =>
            f(sctx, args);

        #endregion

        public static implicit operator PValue(PFunc f) => PType.Object.CreatePValue(f);
    }

    static PValue pfunc(PFuncImpl f) => new PFunc(f);

    delegate PValue PFuncImpl(StackContext sctx, ReadOnlySpan<PValue> args);

    public static PValue RunStatically(StackContext sctx, PValue[] args)
    {
        var carrier = new ContextCarrier();
        var corctx = new CoroutineContext(sctx, CoroutineRunStatically(carrier, args));
        carrier.StackContext = corctx;
        return sctx.CreateNativePValue(new Coroutine(corctx));
    }

    #endregion

    #region Implementation of ICilCompilerAware

    CompilationFlags ICilCompilerAware.CheckQualification(Instruction ins)
    {
        return CompilationFlags.PrefersRunStatically;
    }

    void ICilCompilerAware.ImplementInCil(CompilerState state, Instruction ins)
    {
        throw new NotSupportedException();
    }

    #endregion
}
