// 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.Diagnostics;
using System.Reactive.Concurrency;
using System.Reactive.Disposables;

namespace System.Reactive.Linq.ObservableImpl
{
    internal static class AppendPrepend<TSource>
    {
        internal interface IAppendPrepend : IObservable<TSource>
        {
            IAppendPrepend Append(TSource value);
            IAppendPrepend Prepend(TSource value);
            IScheduler Scheduler { get; }
        }

        internal abstract class SingleBase<TSink> : Producer<TSource, TSink>, IAppendPrepend
            where TSink : IDisposable
        {
            protected readonly IObservable<TSource> _source;
            protected readonly TSource _value;
            protected readonly bool _append;

            public abstract IScheduler Scheduler { get; }

            public SingleBase(IObservable<TSource> source, TSource value, bool append)
            {
                _source = source;
                _value = value;
                _append = append;
            }

            public IAppendPrepend Append(TSource value)
            {
                var prev = new Node<TSource>(_value);

                Node<TSource> appendNode;
                Node<TSource>? prependNode = null;

                if (_append)
                {
                    appendNode = new Node<TSource>(prev, value);
                }
                else
                {
                    prependNode = prev;
                    appendNode = new Node<TSource>(value);
                }

                return CreateAppendPrepend(prependNode, appendNode);
            }

            public IAppendPrepend Prepend(TSource value)
            {
                var prev = new Node<TSource>(_value);

                Node<TSource>? appendNode = null;
                Node<TSource> prependNode;

                if (_append)
                {
                    prependNode = new Node<TSource>(value);
                    appendNode = prev;
                }
                else
                {
                    prependNode = new Node<TSource>(prev, value);
                }

                return CreateAppendPrepend(prependNode, appendNode);
            }

            private IAppendPrepend CreateAppendPrepend(Node<TSource>? prepend, Node<TSource>? append)
            {
                if (Scheduler is ISchedulerLongRunning longRunning)
                {
                    return new LongRunning(_source, prepend, append, Scheduler, longRunning);
                }

                return new Recursive(_source, prepend, append, Scheduler);
            }
        }


        internal sealed class SingleValue : SingleBase<SingleValue._>
        {
            public override IScheduler Scheduler { get; }

            public SingleValue(IObservable<TSource> source, TSource value, IScheduler scheduler, bool append)
                : base (source, value, append)
            {
                Scheduler = scheduler;
            }

            protected override _ CreateSink(IObserver<TSource> observer) => new(this, observer);

            protected override void Run(_ sink) => sink.Run();

            internal sealed class _ : IdentitySink<TSource>
            {
                private readonly IObservable<TSource> _source;
                private readonly TSource _value;
                private readonly IScheduler _scheduler;
                private readonly bool _append;
                private SingleAssignmentDisposableValue _schedulerDisposable;

                public _(SingleValue parent, IObserver<TSource> observer)
                    : base(observer)
                {
                    _source = parent._source;
                    _value = parent._value;
                    _scheduler = parent.Scheduler;
                    _append = parent._append;
                }

                public void Run()
                {
                    var disp = _append
                        ? _source.SubscribeSafe(this)
                        : _scheduler.ScheduleAction(this, PrependValue);

                    SetUpstream(disp);
                }

                private static IDisposable PrependValue(_ sink)
                {
                    sink.ForwardOnNext(sink._value);
                    return sink._source.SubscribeSafe(sink);
                }

                public override void OnCompleted()
                {
                    if (_append)
                    {
                        var disposable = _scheduler.ScheduleAction(this, AppendValue);
                        _schedulerDisposable.Disposable = disposable;
                    }
                    else
                    {
                        ForwardOnCompleted();
                    }
                }

                private static void AppendValue(_ sink)
                {
                    sink.ForwardOnNext(sink._value);
                    sink.ForwardOnCompleted();
                }

                protected override void Dispose(bool disposing)
                {
                    if (disposing)
                    {
                        _schedulerDisposable.Dispose();
                    }
                    base.Dispose(disposing);
                }
            }
        }

        private sealed class Recursive : Producer<TSource, Recursive._>, IAppendPrepend
        {
            private readonly IObservable<TSource> _source;
            private readonly Node<TSource>? _appends;
            private readonly Node<TSource>? _prepends;

            public IScheduler Scheduler { get; }

            public Recursive(IObservable<TSource> source, Node<TSource>? prepends, Node<TSource>? appends, IScheduler scheduler)
            {
                _source = source;
                _appends = appends;
                _prepends = prepends;
                Scheduler = scheduler;
            }

            public IAppendPrepend Append(TSource value)
            {
                return new Recursive(_source,
                    _prepends, new Node<TSource>(_appends, value), Scheduler);
            }

            public IAppendPrepend Prepend(TSource value)
            {
                return new Recursive(_source,
                    new Node<TSource>(_prepends, value), _appends, Scheduler);
            }

            protected override _ CreateSink(IObserver<TSource> observer) => new(this, observer);

            protected override void Run(_ sink) => sink.Run();

            // The sink is based on the sink of the ToObervalbe class and does basically
            // the same twice, once for the append list and once for the prepend list.
            // Inbetween it forwards the values of the source class.
            //
            internal sealed class _ : IdentitySink<TSource>
            {
                private readonly IObservable<TSource> _source;
                private readonly Node<TSource>? _appends;
                private readonly IScheduler _scheduler;

                private Node<TSource>? _currentPrependNode;
                private TSource[]? _appendArray;
                private int _currentAppendIndex;
                private volatile bool _disposed;

                public _(Recursive parent, IObserver<TSource> observer)
                    : base(observer)
                {
                    _source = parent._source;
                    _scheduler = parent.Scheduler;
                    _currentPrependNode = parent._prepends;
                    _appends = parent._appends;
                }

                public void Run()
                {
                    if (_currentPrependNode == null)
                    {
                        SetUpstream(_source.SubscribeSafe(this));
                    }
                    else
                    {
                        //
                        // We never allow the scheduled work to be cancelled. Instead, the _disposed flag
                        // is used to have PrependValues() bail out.
                        //
                        _scheduler.Schedule(this, static (innerScheduler, @this) => @this.PrependValues(innerScheduler));
                    }
                }

                public override void OnCompleted()
                {
                    if (_appends == null)
                    {
                        ForwardOnCompleted();
                    }
                    else
                    {
                        _appendArray = _appends.ToReverseArray();

                        //
                        // We never allow the scheduled work to be cancelled. Instead, the _disposed flag
                        // is used to have `AppendValues` bail out.
                        //
                        _scheduler.Schedule(this, static (innerScheduler, @this) => @this.AppendValues(innerScheduler));
                    }
                }

                protected override void Dispose(bool disposing)
                {
                    if (disposing)
                    {
                        _disposed = true;
                    }

                    base.Dispose(disposing);
                }

                private IDisposable PrependValues(IScheduler scheduler)
                {
                    if (_disposed)
                    {
                        return Disposable.Empty;
                    }

                    Debug.Assert(_currentPrependNode != null);

                    var current = _currentPrependNode!.Value;
                    ForwardOnNext(current);

                    _currentPrependNode = _currentPrependNode.Parent;
                    if (_currentPrependNode == null)
                    {
                        SetUpstream(_source.SubscribeSafe(this));
                    }
                    else
                    { 
                        //
                        // We never allow the scheduled work to be cancelled. Instead, the _disposed flag
                        // is used to have PrependValues() bail out.
                        //
                        scheduler.Schedule(this, static (innerScheduler, @this) => @this.PrependValues(innerScheduler));
                    }

                    return Disposable.Empty;
                }

                private IDisposable AppendValues(IScheduler scheduler)
                {
                    if (_disposed)
                    {
                        return Disposable.Empty;
                    }

                    Debug.Assert(_appendArray != null);

                    var current = _appendArray![_currentAppendIndex];
                    ForwardOnNext(current);

                    _currentAppendIndex++;

                    if (_currentAppendIndex == _appendArray.Length)
                    {
                        ForwardOnCompleted();
                    }
                    else
                    { 
                        //
                        // We never allow the scheduled work to be cancelled. Instead, the _disposed flag
                        // is used to have AppendValues() bail out.
                        //
                        scheduler.Schedule(this, static (innerScheduler, @this) => @this.AppendValues(innerScheduler));
                    }

                    return Disposable.Empty;
                }
            }
        }

        private sealed class LongRunning : Producer<TSource, LongRunning._>, IAppendPrepend
        {
            private readonly IObservable<TSource> _source;
            private readonly Node<TSource>? _appends;
            private readonly Node<TSource>? _prepends;
            private readonly ISchedulerLongRunning _longRunningScheduler;

            public IScheduler Scheduler { get; }

            public LongRunning(IObservable<TSource> source, Node<TSource>? prepends, Node<TSource>? appends, IScheduler scheduler, ISchedulerLongRunning longRunningScheduler)
            {
                _source = source;
                _appends = appends;
                _prepends = prepends;
                Scheduler = scheduler;
                _longRunningScheduler = longRunningScheduler;
            }

            public IAppendPrepend Append(TSource value)
            {
                return new LongRunning(_source,
                    _prepends, new Node<TSource>(_appends, value), Scheduler, _longRunningScheduler);
            }

            public IAppendPrepend Prepend(TSource value)
            {
                return new LongRunning(_source,
                    new Node<TSource>(_prepends, value), _appends, Scheduler, _longRunningScheduler);
            }

            protected override _ CreateSink(IObserver<TSource> observer) => new(this, observer);

            protected override void Run(_ sink) => sink.Run();

            // The sink is based on the sink of the ToObervalbe class and does basically
            // the same twice, once for the append list and once for the prepend list.
            // Inbetween it forwards the values of the source class.
            //
            internal sealed class _ : IdentitySink<TSource>
            {
                private readonly IObservable<TSource> _source;
                private readonly Node<TSource>? _prepends; 
                private readonly Node<TSource>? _appends;
                private readonly ISchedulerLongRunning _scheduler;

                private SerialDisposableValue _schedulerDisposable;

                public _(LongRunning parent, IObserver<TSource> observer)
                    : base(observer)
                {
                    _source = parent._source;
                    _scheduler = parent._longRunningScheduler;
                    _prepends = parent._prepends;
                    _appends = parent._appends;
                }

                public void Run()
                {
                    if (_prepends == null)
                    {
                        SetUpstream(_source.SubscribeSafe(this));
                    }
                    else
                    {
                        var disposable = _scheduler.ScheduleLongRunning(this, static (@this, cancel) => @this.PrependValues(cancel));
                        _schedulerDisposable.TrySetFirst(disposable);
                    }
                }

                public override void OnCompleted()
                {
                    if (_appends == null)
                    {
                        ForwardOnCompleted();
                    }
                    else
                    {
                        var disposable = _scheduler.ScheduleLongRunning(this, static (@this, cancel) => @this.AppendValues(cancel));
                        _schedulerDisposable.Disposable = disposable;
                    }
                }

                protected override void Dispose(bool disposing)
                {
                    if (disposing)
                    {
                        _schedulerDisposable.Dispose();
                    }

                    base.Dispose(disposing);
                }

                private void PrependValues(ICancelable cancel)
                {
                    Debug.Assert(_prepends != null);

                    var current = _prepends!;

                    while (!cancel.IsDisposed)
                    {
                        ForwardOnNext(current.Value);
                        current = current.Parent;

                        if (current == null)
                        {
                            SetUpstream(_source.SubscribeSafe(this));
                            break;
                        }
                    }
                }

                private void AppendValues(ICancelable cancel)
                {
                    Debug.Assert(_appends != null);

                    var array = _appends!.ToReverseArray();
                    var i = 0;

                    while (!cancel.IsDisposed)
                    {
                        ForwardOnNext(array[i]);
                        i++;

                        if (i == array.Length)
                        {
                            ForwardOnCompleted();
                            break;
                        }
                    }
                }
            }
        }

        private sealed class Node<T>
        {
            public readonly Node<T>? Parent;
            public readonly T Value;
            public readonly int Count;

            public Node(T value)
                : this(null, value)
            {
            }

            public Node(Node<T>? parent, T value)
            {
                Parent = parent;
                Value = value;

                if (parent == null)
                {
                    Count = 1;
                }
                else
                {
                    if (parent.Count == int.MaxValue)
                    {
                        throw new NotSupportedException($"Consecutive appends or prepends with a count of more than int.MaxValue ({int.MaxValue}) are not supported.");
                    }

                    Count = parent.Count + 1;
                }
            }

            public T[] ToReverseArray()
            {
                var array = new T[Count];
                var current = this;
                for (var i = Count - 1; i >= 0; i--)
                {
                    array[i] = current!.Value; // NB: Count property ensures non-nullability.
                    current = current.Parent;
                }
                return array;
            }
        }

        internal sealed class SingleImmediate : SingleBase<SingleImmediate._>
        {
            public override IScheduler Scheduler => ImmediateScheduler.Instance;

            public SingleImmediate(IObservable<TSource> source, TSource value, bool append)
                : base(source, value, append)
            {
            }

            protected override _ CreateSink(IObserver<TSource> observer) => new(this, observer);

            protected override void Run(_ sink) => sink.Run();

            internal sealed class _ : IdentitySink<TSource>
            {
                private readonly IObservable<TSource> _source;
                private readonly TSource _value;
                private readonly bool _append;

                public _(SingleImmediate parent, IObserver<TSource> observer)
                    : base(observer)
                {
                    _source = parent._source;
                    _value = parent._value;
                    _append = parent._append;
                }

                public void Run()
                {
                    if (!_append)
                    {
                        ForwardOnNext(_value);
                    }
                    Run(_source);
                }

                public override void OnCompleted()
                {
                    if (_append)
                    {
                        ForwardOnNext(_value);
                    }
                    ForwardOnCompleted();
                }
            }
        }
    }
}
