// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT License.
// See the LICENSE file in the project root for more information. 

using System.Collections.Concurrent;
using System.Reactive.Disposables;
using System.Reactive.Subjects;
using System.Threading;

namespace System.Reactive.Linq.ObservableImpl
{
    internal sealed class RetryWhen<T, U> : IObservable<T>
    {
        private readonly IObservable<T> _source;
        private readonly Func<IObservable<Exception>, IObservable<U>> _handler;

        internal RetryWhen(IObservable<T> source, Func<IObservable<Exception>, IObservable<U>> handler)
        {
            _source = source;
            _handler = handler;
        }

        public IDisposable Subscribe(IObserver<T> observer)
        {
            if (observer == null)
            {
                throw new ArgumentNullException(nameof(observer));
            }

            var errorSignals = new Subject<Exception>();
            
            IObservable<U> redo;

            try
            {
                redo = _handler(errorSignals);

                if (redo == null)
                {
#pragma warning disable CA2201 // (Do not raise reserved exception types.) Backwards compatibility prevents us from complying.
                    throw new NullReferenceException("The handler returned a null IObservable");
#pragma warning restore CA2201
                }
            }
            catch (Exception ex)
            {
                observer.OnError(ex);
                return Disposable.Empty;
            }

            var parent = new MainObserver(observer, _source, new RedoSerializedObserver<Exception>(errorSignals));

            var d = redo.SubscribeSafe(parent.HandlerConsumer);
            parent.HandlerUpstream.Disposable = d;

            parent.HandlerNext();

            return parent;
        }

        private sealed class MainObserver : Sink<T>, IObserver<T>
        {
            private readonly IObservable<T> _source;
            private readonly IObserver<Exception> _errorSignal;

            internal readonly HandlerObserver HandlerConsumer;
            private IDisposable? _upstream;
            internal SingleAssignmentDisposableValue HandlerUpstream;
            private int _trampoline;
            private int _halfSerializer;
            private Exception? _error;

            internal MainObserver(IObserver<T> downstream, IObservable<T> source, IObserver<Exception> errorSignal) : base(downstream)
            {
                _source = source;
                _errorSignal = errorSignal;
                HandlerConsumer = new HandlerObserver(this);
            }

            protected override void Dispose(bool disposing)
            {
                if (disposing)
                {
                    Disposable.Dispose(ref _upstream);
                    HandlerUpstream.Dispose();
                }

                base.Dispose(disposing);
            }

            public void OnCompleted()
            {
                HalfSerializer.ForwardOnCompleted(this, ref _halfSerializer, ref _error);
            }

            public void OnError(Exception error)
            {
                if (Disposable.TrySetSerial(ref _upstream, null))
                {
                    _errorSignal.OnNext(error);
                }
            }

            public void OnNext(T value)
            {
                HalfSerializer.ForwardOnNext(this, value, ref _halfSerializer, ref _error);
            }

            private void HandlerError(Exception error)
            {
                HalfSerializer.ForwardOnError(this, error, ref _halfSerializer, ref _error);
            }

            private void HandlerComplete()
            {
                HalfSerializer.ForwardOnCompleted(this, ref _halfSerializer, ref _error);
            }

            internal void HandlerNext()
            {
                if (Interlocked.Increment(ref _trampoline) == 1)
                {
                    do
                    {
                        var sad = new SingleAssignmentDisposable();
                        if (Disposable.TrySetSingle(ref _upstream, sad) != TrySetSingleResult.Success)
                        {
                            return;
                        }

                        sad.Disposable = _source.SubscribeSafe(this);
                    }
                    while (Interlocked.Decrement(ref _trampoline) != 0);
                }
            }

            internal sealed class HandlerObserver : IObserver<U>
            {
                private readonly MainObserver _main;

                internal HandlerObserver(MainObserver main)
                {
                    _main = main;
                }

                public void OnCompleted()
                {
                    _main.HandlerComplete();
                }

                public void OnError(Exception error)
                {
                    _main.HandlerError(error);
                }

                public void OnNext(U value)
                {
                    _main.HandlerNext();
                }
            }
        }
    }

    internal sealed class RedoSerializedObserver<X> : IObserver<X>
    {
#pragma warning disable CA2201 // (Do not raise reserved exception types.) This is a sentinel, and is not thrown, so there's no need for it to be anything else.
        private static readonly Exception SignaledIndicator = new();
#pragma warning restore CA2201

        private readonly IObserver<X> _downstream;
        private readonly ConcurrentQueue<X> _queue;

        private int _wip;
        private Exception? _terminalException;

        internal RedoSerializedObserver(IObserver<X> downstream)
        {
            _downstream = downstream;
            _queue = new ConcurrentQueue<X>();
        }

        public void OnCompleted()
        {
            if (Interlocked.CompareExchange(ref _terminalException, ExceptionHelper.Terminated, null) == null)
            {
                Drain();
            }
        }

        public void OnError(Exception error)
        {
            if (Interlocked.CompareExchange(ref _terminalException, error, null) == null)
            {
                Drain();
            }
        }

        public void OnNext(X value)
        {
            _queue.Enqueue(value);
            Drain();
        }

        private void Clear()
        {
            while (_queue.TryDequeue(out _))
            {
            }
        }

        private void Drain()
        {
            if (Interlocked.Increment(ref _wip) != 1)
            {
                return;
            }

            var missed = 1;

            for (; ; )
            {
                var ex = Volatile.Read(ref _terminalException);
                if (ex != null)
                {
                    if (ex != SignaledIndicator)
                    {
                        Interlocked.Exchange(ref _terminalException, SignaledIndicator);
                        if (ex != ExceptionHelper.Terminated)
                        {
                            _downstream.OnError(ex);
                        }
                        else
                        {
                            _downstream.OnCompleted();
                        }
                    }
                    Clear();
                }
                else
                {
                    while (_queue.TryDequeue(out var item))
                    {
                        _downstream.OnNext(item);
                    }
                }


                missed = Interlocked.Add(ref _wip, -missed);
                if (missed == 0)
                {
                    break;
                }
            }
        }
    }
}
