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

namespace System.Reactive.Linq.ObservableImpl
{
    internal static class Timer
    {
        internal abstract class Single : Producer<long, Single._>
        {
            private readonly IScheduler _scheduler;

            protected Single(IScheduler scheduler)
            {
                _scheduler = scheduler;
            }

            internal sealed class Relative : Single
            {
                private readonly TimeSpan _dueTime;

                public Relative(TimeSpan dueTime, IScheduler scheduler)
                    : base(scheduler)
                {
                    _dueTime = dueTime;
                }

                protected override _ CreateSink(IObserver<long> observer) => new(observer);

                protected override void Run(_ sink) => sink.Run(this, _dueTime);
            }

            internal sealed class Absolute : Single
            {
                private readonly DateTimeOffset _dueTime;

                public Absolute(DateTimeOffset dueTime, IScheduler scheduler)
                    : base(scheduler)
                {
                    _dueTime = dueTime;
                }

                protected override _ CreateSink(IObserver<long> observer) => new(observer);

                protected override void Run(_ sink) => sink.Run(this, _dueTime);
            }

            internal sealed class _ : IdentitySink<long>
            {
                public _(IObserver<long> observer)
                    : base(observer)
                {
                }

                public void Run(Single parent, DateTimeOffset dueTime)
                {
                    SetUpstream(parent._scheduler.ScheduleAction(this, dueTime, state => state.Invoke()));
                }

                public void Run(Single parent, TimeSpan dueTime)
                {
                    SetUpstream(parent._scheduler.ScheduleAction(this, dueTime, state => state.Invoke()));
                }

                private void Invoke()
                {
                    ForwardOnNext(0);
                    ForwardOnCompleted();
                }
            }
        }

        internal abstract class Periodic : Producer<long, Periodic._>
        {
            private readonly TimeSpan _period;
            private readonly IScheduler _scheduler;

            protected Periodic(TimeSpan period, IScheduler scheduler)
            {
                _period = period;
                _scheduler = scheduler;
            }

            internal sealed class Relative : Periodic
            {
                private readonly TimeSpan _dueTime;

                public Relative(TimeSpan dueTime, TimeSpan period, IScheduler scheduler)
                    : base(period, scheduler)
                {
                    _dueTime = dueTime;
                }

                protected override _ CreateSink(IObserver<long> observer) => new(_period, observer);

                protected override void Run(_ sink) => sink.Run(this, _dueTime);
            }

            internal sealed class Absolute : Periodic
            {
                private readonly DateTimeOffset _dueTime;

                public Absolute(DateTimeOffset dueTime, TimeSpan period, IScheduler scheduler)
                    : base(period, scheduler)
                {
                    _dueTime = dueTime;
                }

                protected override _ CreateSink(IObserver<long> observer) => new(_period, observer);

                protected override void Run(_ sink) => sink.Run(this, _dueTime);
            }

            internal sealed class _ : IdentitySink<long>
            {
                private readonly TimeSpan _period;
                private long _index;

                public _(TimeSpan period, IObserver<long> observer)
                    : base(observer)
                {
                    _period = period;
                }

                public void Run(Periodic parent, DateTimeOffset dueTime)
                {
                    SetUpstream(parent._scheduler.Schedule(this, dueTime, static (innerScheduler, @this) => @this.InvokeStart(innerScheduler)));
                }

                public void Run(Periodic parent, TimeSpan dueTime)
                {
                    //
                    // Optimize for the case of Observable.Interval.
                    //
                    if (dueTime == _period)
                    {
                        SetUpstream(parent._scheduler.SchedulePeriodic(this, _period, static @this => @this.Tick()));
                    }
                    else
                    {
                        SetUpstream(parent._scheduler.Schedule(this, dueTime, static (innerScheduler, @this) => @this.InvokeStart(innerScheduler)));
                    }
                }

                //
                // BREAKING CHANGE v2 > v1.x - No more correction for time drift based on absolute time. This
                //                             didn't work for large period values anyway; the fractional
                //                             error exceeded corrections. Also complicated dealing with system
                //                             clock change conditions and caused numerous bugs.
                //
                // - For more precise scheduling, use a custom scheduler that measures TimeSpan values in a
                //   better way, e.g. spinning to make up for the last part of the period. Whether or not the
                //   values of the TimeSpan period match NT time or wall clock time is up to the scheduler.
                //
                // - For more accurate scheduling wrt the system clock, use Generate with DateTimeOffset time
                //   selectors. When the system clock changes, intervals will not be the same as diffs between
                //   consecutive absolute time values. The precision will be low (1s range by default).
                //
                private void Tick()
                {
                    var count = _index;
                    _index = unchecked(count + 1);

                    ForwardOnNext(count);
                }

                private int _pendingTickCount;
                private IDisposable? _periodic;

                private IDisposable InvokeStart(IScheduler self)
                {
                    //
                    // Notice the first call to OnNext will introduce skew if it takes significantly long when
                    // using the following naive implementation:
                    //
                    //    Code:  base._observer.OnNext(0L);
                    //           return self.SchedulePeriodicEmulated(1L, _period, (Func<long, long>)Tick);
                    //
                    // What we're saying here is that Observable.Timer(dueTime, period) is pretty much the same
                    // as writing Observable.Timer(dueTime).Concat(Observable.Interval(period)).
                    //
                    //    Expected:  dueTime
                    //                  |
                    //                  0--period--1--period--2--period--3--period--4--...
                    //                  |
                    //                  +-OnNext(0L)-|
                    //    
                    //    Actual:    dueTime
                    //                  |
                    //                  0------------#--period--1--period--2--period--3--period--4--...
                    //                  |
                    //                  +-OnNext(0L)-|
                    //
                    // Different solutions for this behavior have different problems:
                    //
                    // 1. Scheduling the periodic job first and using an AsyncLock to serialize the OnNext calls
                    //    has the drawback that InvokeStart may never return. This happens when every callback
                    //    doesn't meet the period's deadline, hence the periodic job keeps queueing stuff up. In
                    //    this case, InvokeStart stays the owner of the AsyncLock and the call to Wait will never
                    //    return, thus not allowing any interleaving of work on this scheduler's logical thread.
                    //
                    // 2. Scheduling the periodic job first and using a (blocking) synchronization primitive to
                    //    signal completion of the OnNext(0L) call to the Tick call requires quite a bit of state
                    //    and careful handling of the case when OnNext(0L) throws. What's worse is the blocking
                    //    behavior inside Tick.
                    //
                    // In order to avoid blocking behavior, we need a scheme much like SchedulePeriodic emulation
                    // where work to dispatch OnNext(n + 1) is delegated to a catch up loop in case OnNext(n) was
                    // still running. Because SchedulePeriodic emulation exhibits such behavior in all cases, we
                    // only need to deal with the overlap of OnNext(0L) with future periodic OnNext(n) dispatch
                    // jobs. In the worst case where every callback takes longer than the deadline implied by the
                    // period, the periodic job will just queue up work that's dispatched by the tail-recursive
                    // catch up loop. In the best case, all work will be dispatched on the periodic scheduler.
                    //

                    //
                    // We start with one tick pending because we're about to start doing OnNext(0L).
                    //
                    _pendingTickCount = 1;

                    var d = new SingleAssignmentDisposable();
                    _periodic = d;
                    _index = 1;
                    d.Disposable = self.SchedulePeriodic(this, _period, static @this => @this.Tock());

                    try
                    {
                        ForwardOnNext(0L);
                    }
                    catch (Exception e)
                    {
                        d.Dispose();
                        e.Throw();
                    }

                    //
                    // If the periodic scheduling job already ran before we finished dispatching the OnNext(0L)
                    // call, we'll find pendingTickCount to be > 1. In this case, we need to catch up by dispatching
                    // subsequent calls to OnNext as fast as possible, but without running a loop in order to ensure
                    // fair play with the scheduler. So, we run a tail-recursive loop in CatchUp instead.
                    //
                    if (Interlocked.Decrement(ref _pendingTickCount) > 0)
                    {
                        var c = self.Schedule((@this: this, index: 1L), static (tuple, action) => tuple.@this.CatchUp(tuple.index, action));

                        return StableCompositeDisposable.Create(d, c);
                    }

                    return d;
                }

                private void Tock()
                {
                    //
                    // Notice the handler for (emulated) periodic scheduling is non-reentrant.
                    //
                    // When there's no overlap with the OnNext(0L) call, the following code will cycle through
                    // pendingTickCount 0 -> 1 -> 0 for the remainder of the timer's execution.
                    //
                    // If there's overlap with the OnNext(0L) call, pendingTickCount will increase to record
                    // the number of catch up OnNext calls required, which will be dispatched by the recursive
                    // scheduling loop in CatchUp (which quits when it reaches 0 pending ticks).
                    //
                    if (Interlocked.Increment(ref _pendingTickCount) == 1)
                    {
                        var count = _index;
                        _index = unchecked(count + 1);

                        ForwardOnNext(count);
                        Interlocked.Decrement(ref _pendingTickCount);
                    }
                }

                private void CatchUp(long count, Action<(_, long)> recurse)
                {
                    try
                    {
                        ForwardOnNext(count);
                    }
                    catch (Exception e)
                    {
                        _periodic!.Dispose(); // NB: _periodic is assigned before this runs.
                        e.Throw();
                    }

                    //
                    // We can simply bail out if we decreased the tick count to 0. In that case, the Tock
                    // method will take over when it sees the 0 -> 1 transition.
                    //
                    if (Interlocked.Decrement(ref _pendingTickCount) > 0)
                    {
                        recurse((this, unchecked(count + 1)));
                    }
                }
            }
        }
    }
}
