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

namespace System.Reactive.Concurrency
{
    public static partial class Scheduler
    {
        /// <summary>
        /// Schedules a periodic piece of work by dynamically discovering the scheduler's capabilities.
        /// If the scheduler supports periodic scheduling, the request will be forwarded to the periodic scheduling implementation.
        /// If the scheduler provides stopwatch functionality, the periodic task will be emulated using recursive scheduling with a stopwatch to correct for time slippage.
        /// Otherwise, the periodic task will be emulated using recursive scheduling.
        /// </summary>
        /// <typeparam name="TState">The type of the state passed to the scheduled action.</typeparam>
        /// <param name="scheduler">The scheduler to run periodic work on.</param>
        /// <param name="state">Initial state passed to the action upon the first iteration.</param>
        /// <param name="period">Period for running the work periodically.</param>
        /// <param name="action">Action to be executed, potentially updating the state.</param>
        /// <returns>The disposable object used to cancel the scheduled recurring action (best effort).</returns>
        /// <exception cref="ArgumentNullException"><paramref name="scheduler"/> or <paramref name="action"/> is <c>null</c>.</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="period"/> is less than <see cref="TimeSpan.Zero"/>.</exception>
        public static IDisposable SchedulePeriodic<TState>(this IScheduler scheduler, TState state, TimeSpan period, Func<TState, TState> action)
        {
            if (scheduler == null)
            {
                throw new ArgumentNullException(nameof(scheduler));
            }

            if (period < TimeSpan.Zero)
            {
                throw new ArgumentOutOfRangeException(nameof(period));
            }

            if (action == null)
            {
                throw new ArgumentNullException(nameof(action));
            }

            return SchedulePeriodic_(scheduler, state, period, action);
        }

        /// <summary>
        /// Schedules a periodic piece of work by dynamically discovering the scheduler's capabilities.
        /// If the scheduler supports periodic scheduling, the request will be forwarded to the periodic scheduling implementation.
        /// If the scheduler provides stopwatch functionality, the periodic task will be emulated using recursive scheduling with a stopwatch to correct for time slippage.
        /// Otherwise, the periodic task will be emulated using recursive scheduling.
        /// </summary>
        /// <typeparam name="TState">The type of the state passed to the scheduled action.</typeparam>
        /// <param name="scheduler">Scheduler to execute the action on.</param>
        /// <param name="state">State passed to the action to be executed.</param>
        /// <param name="period">Period for running the work periodically.</param>
        /// <param name="action">Action to be executed.</param>
        /// <returns>The disposable object used to cancel the scheduled recurring action (best effort).</returns>
        /// <exception cref="ArgumentNullException"><paramref name="scheduler"/> or <paramref name="action"/> is <c>null</c>.</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="period"/> is less than <see cref="TimeSpan.Zero"/>.</exception>
        public static IDisposable SchedulePeriodic<TState>(this IScheduler scheduler, TState state, TimeSpan period, Action<TState> action)
        {
            if (scheduler == null)
            {
                throw new ArgumentNullException(nameof(scheduler));
            }

            if (period < TimeSpan.Zero)
            {
                throw new ArgumentOutOfRangeException(nameof(period));
            }

            if (action == null)
            {
                throw new ArgumentNullException(nameof(action));
            }

            return SchedulePeriodic_(scheduler, (state, action), period, static t => { t.action(t.state); return t; });
        }

        /// <summary>
        /// Schedules a periodic piece of work by dynamically discovering the scheduler's capabilities.
        /// If the scheduler supports periodic scheduling, the request will be forwarded to the periodic scheduling implementation.
        /// If the scheduler provides stopwatch functionality, the periodic task will be emulated using recursive scheduling with a stopwatch to correct for time slippage.
        /// Otherwise, the periodic task will be emulated using recursive scheduling.
        /// </summary>
        /// <param name="scheduler">Scheduler to execute the action on.</param>
        /// <param name="period">Period for running the work periodically.</param>
        /// <param name="action">Action to be executed.</param>
        /// <returns>The disposable object used to cancel the scheduled recurring action (best effort).</returns>
        /// <exception cref="ArgumentNullException"><paramref name="scheduler"/> or <paramref name="action"/> is <c>null</c>.</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="period"/> is less than <see cref="TimeSpan.Zero"/>.</exception>
        public static IDisposable SchedulePeriodic(this IScheduler scheduler, TimeSpan period, Action action)
        {
            if (scheduler == null)
            {
                throw new ArgumentNullException(nameof(scheduler));
            }

            if (period < TimeSpan.Zero)
            {
                throw new ArgumentOutOfRangeException(nameof(period));
            }

            if (action == null)
            {
                throw new ArgumentNullException(nameof(action));
            }

            return SchedulePeriodic_(scheduler, action, period, static a => { a(); return a; });
        }

        /// <summary>
        /// Starts a new stopwatch object by dynamically discovering the scheduler's capabilities.
        /// If the scheduler provides stopwatch functionality, the request will be forwarded to the stopwatch provider implementation.
        /// Otherwise, the stopwatch will be emulated using the scheduler's notion of absolute time.
        /// </summary>
        /// <param name="scheduler">Scheduler to obtain a stopwatch for.</param>
        /// <returns>New stopwatch object; started at the time of the request.</returns>
        /// <exception cref="ArgumentNullException"><paramref name="scheduler"/> is <c>null</c>.</exception>
        /// <remarks>The resulting stopwatch object can have non-monotonic behavior.</remarks>
        public static IStopwatch StartStopwatch(this IScheduler scheduler)
        {
            if (scheduler == null)
            {
                throw new ArgumentNullException(nameof(scheduler));
            }

            //
            // All schedulers deriving from LocalScheduler will automatically pick up this
            // capability based on a local stopwatch, typically using QueryPerformanceCounter
            // through the System.Diagnostics.Stopwatch class.
            //
            // Notice virtual time schedulers do implement this facility starting from Rx v2.0,
            // using subtraction of their absolute time notion to compute elapsed time values.
            // This is fine because those schedulers do not allow the clock to go back in time.
            //
            // For schedulers that don't have a stopwatch, we have to pick some fallback logic
            // here. We could either dismiss the scheduler's notion of time and go for the CAL's
            // stopwatch facility, or go with a stopwatch based on "scheduler.Now", which has
            // the drawback of potentially going back in time:
            //
            //   - Using the CAL's stopwatch facility causes us to abandon the scheduler's
            //     potentially virtualized notion of time, always going for the local system
            //     time instead.
            //
            //   - Using the scheduler's Now property for calculations can break monotonicity,
            //     and there's no right answer on how to deal with jumps back in time.
            //
            // However, even the built-in stopwatch in the BCL can potentially fall back to
            // subtraction of DateTime values in case no high-resolution performance counter is
            // available, causing monotonicity to break down. We're not trying to solve this
            // problem there either (though we could check IsHighResolution and smoothen out
            // non-monotonic points somehow), so we pick the latter option as the lesser of
            // two evils (also because it should occur rarely).
            //
            // Users of the stopwatch retrieved by this method could detect nonsensical data
            // revealing a jump back in time, or implement custom fallback logic like the one
            // shown below.
            //
            var swp = scheduler.AsStopwatchProvider();
            if (swp != null)
            {
                return swp.StartStopwatch();
            }

            return new EmulatedStopwatch(scheduler);
        }

        private static IDisposable SchedulePeriodic_<TState>(IScheduler scheduler, TState state, TimeSpan period, Func<TState, TState> action)
        {
            //
            // Design rationale:
            //
            //   In Rx v1.x, we employed recursive scheduling for periodic tasks. The following code
            //   fragment shows how the Timer (and hence Interval) function used to be implemented:
            //
            //     var p = Normalize(period);
            //
            //     return new AnonymousObservable<long>(observer =>
            //     {
            //         var d = dueTime;
            //         long count = 0;
            //         return scheduler.Schedule(d, self =>
            //         {
            //             if (p > TimeSpan.Zero)
            //             {
            //                 var now = scheduler.Now;
            //                 d = d + p;
            //                 if (d <= now)
            //                     d = now + p;
            //             }
            //
            //             observer.OnNext(count);
            //             count = unchecked(count + 1);
            //             self(d);
            //         });
            //     });
            //
            //   Despite the purity of this approach, it suffered from a set of drawbacks:
            //
            //    1) Usage of IScheduler.Now to correct for time drift did have a positive effect for
            //       a limited number of scenarios, in particular when a short period was used. The
            //       major issues with this are:
            //
            //       a) Relying on absolute time at the LINQ layer in Rx's layer map, causing issues
            //          when the system clock changes. Various customers hit this issue, reported to
            //          us on the MSDN forums. Basically, when the clock goes forward, the recursive
            //          loop wants to catch up as quickly as it can; when it goes backwards, a long
            //          silence will occur. (See 2 for a discussion of WP7 related fixes.)
            //
            //       b) Even if a) would be addressed by using Rx v2.0's capabilities to monitor for
            //          system clock changes, the solution would violate the reasonable expectation
            //          of operators overloads using TimeSpan *not* relying on absolute time.
            //
            //       c) Drift correction doesn't work for large periods when the system encounters
            //          systematic drift. For example, in the lab we've seen cases of drift up to
            //          tens of seconds on a 24 hour timeframe. Correcting for this drift by making
            //          a recursive call with a due time of 24 * 3600 with 10 seconds of adjustment
            //          won't fix systematic drift.
            //
            //    2) This implementation has been plagued with issues around application container
            //       lifecycle models, in particular Windows Phone 7's model of tombstoning and in
            //       particular its "dormant state". This feature was introduced in Mango to enable
            //       fast application switching. Essentially, the phone's OS puts the application
            //       in a suspended state when the user navigates "forward" (or takes an incoming
            //       call for instance). When the application is woken up again, threads are resumed
            //       and we're faced with an illusion of missed events due to the use of absolute
            //       time, not relative to how the application observes it. This caused nightmare
            //       scenarios of fast battery drain due to the flood of catch-up work.
            //
            //       See http://msdn.microsoft.com/en-us/library/ff817008(v=vs.92).aspx for more
            //       information on this.
            //
            //    3) Recursive scheduling imposes a non-trivial cost due to the creation of many
            //       single-shot timers and closures. For high frequency timers, this can cause a
            //       lot of churn in the GC, which we like to avoid (operators shouldn't have hidden
            //       linear - or worse - allocation cost).
            //
            //   Notice these drawbacks weren't limited to the use of Timer and Interval directly,
            //   as many operators such as Sample, Buffer, and Window used such sequences for their
            //   periodic behavior (typically by delegating to a more general overload).
            //
            //   As a result, in Rx v2.0, we took the decision to improve periodic timing based on
            //   the following design decisions:
            //
            //    1) When the scheduler has the ability to run a periodic task, it should implement
            //       the ISchedulerPeriodic interface and expose it through the IServiceProvider
            //       interface. Passing the intent of the user through all layers of Rx, down to the
            //       underlying infrastructure provides delegation of responsibilities. This allows
            //       the target scheduler to optimize execution in various ways, e.g. by employing
            //       techniques such as timer coalescing.
            //
            //       See http://www.bing.com/search?q=windows+timer+coalescing for information on
            //       techniques like timer coalescing which may be applied more aggressively in
            //       future OS releases in order to reduce power consumption.
            //
            //    2) Emulation of periodic scheduling is used to avoid breaking existing code that
            //       uses schedulers without this capability. We expect those fallback paths to be
            //       exercised rarely, though the use of DisableOptimizations can trigger them as
            //       well. In such cases we rely on stopwatches or a carefully crafted recursive
            //       scheme to deal with (or maximally compensate for) slippage or time. Behavior
            //       of periodic tasks is expected to be as follows:
            //
            //         timer ticks   0-------1-------2-------3-------4-------5-------6----...
            //                       |       |       |       +====+  +==+    |       |
            //         user code     +~~~|   +~|     +~~~~~~~~~~~|+~~~~|+~~| +~~~|   +~~|
            //
            //       rather than the following scheme, where time slippage is introduced by user
            //       code running on the scheduler:
            //
            //         timer ticks   0####-------1##-------2############-------3#####-----...
            //                       |           |         |                   |
            //         user code     +~~~|       +~|       +~~~~~~~~~~~|       +~~~~|
            //
            //       (Side-note: Unfortunately, we didn't reserve the name Interval for the latter
            //                   behavior, but used it as an alias for "periodic scheduling" with
            //                   the former behavior, delegating to the Timer implementation. One
            //                   can simulate this behavior using Generate, which uses tail calls.)
            //
            //       This behavior is important for operations like Sample, Buffer, and Window, all
            //       of which expect proper spacing of events, even if the user code takes a long
            //       time to complete (considered a bad practice nonetheless, cf. ObserveOn).
            //
            //    3) To deal with the issue of suspensions induced by application lifecycle events
            //       in Windows Phone and WinRT applications, we decided to hook available system
            //       events through IHostLifecycleNotifications, discovered through the PEP in order
            //       to maintain portability of the core of Rx.
            //

            var periodic = scheduler.AsPeriodic();
#if WINDOWS
            // Workaround for WinRT not supporting <1ms resolution
            if(period < TimeSpan.FromMilliseconds(1))
            {
                periodic = null; // skip the periodic scheduler and use the stopwatch
            }
#endif
            if (periodic != null)
            {
                return periodic.SchedulePeriodic(state, period, action);
            }

            var swp = scheduler.AsStopwatchProvider();
            if (swp != null)
            {
                var spr = new SchedulePeriodicStopwatch<TState>(scheduler, state, period, action, swp);
                return spr.Start();
            }
            else
            {
                var spr = new SchedulePeriodicRecursive<TState>(scheduler, state, period, action);
                return spr.Start();
            }
        }

        private sealed class SchedulePeriodicStopwatch<TState> : IDisposable
        {
            private readonly IScheduler _scheduler;
            private readonly TimeSpan _period;
            private readonly Func<TState, TState> _action;
            private readonly IStopwatchProvider _stopwatchProvider;

            public SchedulePeriodicStopwatch(IScheduler scheduler, TState state, TimeSpan period, Func<TState, TState> action, IStopwatchProvider stopwatchProvider)
            {
                _scheduler = scheduler;
                _period = period;
                _action = action;
                _stopwatchProvider = stopwatchProvider;

                _state = state;
                _runState = Stopped;
            }

            private TState _state;

            private readonly object _gate = new();
            private readonly AutoResetEvent _resumeEvent = new(false);
            private volatile int _runState;
            private IStopwatch? _stopwatch;
            private TimeSpan _nextDue;
            private TimeSpan _suspendedAt;
            private TimeSpan _inactiveTime;

            //
            // State transition diagram:
            //                                        (c)
            //                             +-----------<-----------+
            //                            /                         \
            //                           /            (b)            \
            //                          |           +-->--SUSPENDED---+
            //                  (a)     v          /          |
            //    ^----STOPPED -->-- RUNNING -->--+           v (e)
            //                                     \          |
            //                                      +-->--DISPOSED----$
            //                                        (d)
            //
            //  (a) Start --> call to Schedule the Tick method
            //  (b) Suspending event handler --> Tick gets blocked waiting for _resumeEvent
            //  (c) Resuming event handler --> _resumeEvent is signaled, Tick continues
            //  (d) Dispose returned object from Start --> scheduled work is cancelled
            //  (e) Dispose returned object from Start --> unblocks _resumeEvent, Tick exits
            //
            private const int Stopped = 0;
            private const int Running = 1;
            private const int Suspended = 2;
            private const int Disposed = 3;

            private SingleAssignmentDisposableValue _task;

            public IDisposable Start()
            {
                RegisterHostLifecycleEventHandlers();

                _stopwatch = _stopwatchProvider.StartStopwatch();
                _nextDue = _period;
                _runState = Running;

                _task.Disposable = _scheduler.Schedule(this, _nextDue, static (@this, a) => @this.Tick(a));
                return this;
            }

            void IDisposable.Dispose()
            {
                _task.Dispose();
                Cancel();
            }

            private void Tick(Action<SchedulePeriodicStopwatch<TState>, TimeSpan> recurse)
            {
                _nextDue += _period;
                _state = _action(_state);

                var next = default(TimeSpan);

                while (true)
                {
                    lock (_gate)
                    {
                        if (_runState == Running)
                        {
                            //
                            // This is the fast path. We just let the stopwatch continue to
                            // run while we're suspended, but compensate for time that was
                            // recorded as inactive based on cumulative deltas computed in
                            // the suspend and resume event handlers.
                            //
                            next = Normalize(_nextDue - (_stopwatch!.Elapsed - _inactiveTime));
                            break;
                        }

                        if (_runState == Disposed)
                        {
                            //
                            // In case the periodic job gets disposed but we are currently
                            // waiting to come back out of suspension, we should make sure
                            // we don't remain blocked indefinitely. Hence, we set the event
                            // in the Cancel method and trap this case here to bail out from
                            // the scheduled work gracefully.
                            //
                            return;
                        }

                        //
                        // This is the least common case where we got suspended and need
                        // to block such that future reevaluations of the next due time
                        // will pick up the cumulative inactive time delta.
                        //
                        Debug.Assert(_runState == Suspended);
                    }

                    //
                    // Only happens in the SUSPENDED case; otherwise we will have broken from
                    // the loop or have quit the Tick method. After returning from the wait,
                    // we'll either be RUNNING again, quit due to a DISPOSED transition, or
                    // be extremely unlucky to find ourselves SUSPENDED again and be blocked
                    // once more.
                    //
                    _resumeEvent.WaitOne();
                }

                recurse(this, next);
            }

            private void Cancel()
            {
                UnregisterHostLifecycleEventHandlers();

                lock (_gate)
                {
                    _runState = Disposed;

                    if (!Environment.HasShutdownStarted)
                    {
                        _resumeEvent.Set();
                    }
                }
            }

            private void Suspending(object? sender, HostSuspendingEventArgs args)
            {
                //
                // The host is telling us we're about to be suspended. At this point, time
                // computations will still be in a valid range (next <= _period), but after
                // we're woken up again, Tick would start to go on a crusade to catch up.
                //
                // This has caused problems in the past, where the flood of events caused
                // batteries to drain etc (see design rationale discussion higher up).
                //
                // In order to mitigate this problem, we force Tick to suspend before its
                // next computation of the next due time. Notice we can't afford to block
                // during the Suspending event handler; the host expects us to respond to
                // this event quickly, such that we're not keeping the application from
                // suspending promptly.
                //
                lock (_gate)
                {
                    if (_runState == Running)
                    {
                        _suspendedAt = _stopwatch!.Elapsed; // NB: Non-null when >= Running.
                        _runState = Suspended;

                        if (!Environment.HasShutdownStarted)
                        {
                            _resumeEvent.Reset();
                        }
                    }
                }
            }

            private void Resuming(object? sender, HostResumingEventArgs args)
            {
                //
                // The host is telling us we're being resumed. At this point, code will
                // already be running in the process, so a past timer may still expire and
                // cause the code in Tick to run. Two interleavings are possible now:
                //
                //   1) We enter the gate first, and will adjust the cumulative inactive
                //      time delta used for correction. The code in Tick will have the
                //      illusion nothing happened and find itself RUNNING when entering
                //      the gate, resuming activities as before.
                //
                //   2) The code in Tick enters the gate first, and takes notice of the
                //      currently SUSPENDED state. It leaves the gate, entering the wait
                //      state for _resumeEvent. Next, we enter to adjust the cumulative
                //      inactive time delta, switch to the RUNNING state and signal the
                //      event for Tick to carry on and recompute its next due time based
                //      on the new cumulative delta.
                //
                lock (_gate)
                {
                    if (_runState == Suspended)
                    {
                        _inactiveTime += _stopwatch!.Elapsed - _suspendedAt; // NB: Non-null when >= Running.
                        _runState = Running;

                        if (!Environment.HasShutdownStarted)
                        {
                            _resumeEvent.Set();
                        }
                    }
                }
            }

            private void RegisterHostLifecycleEventHandlers()
            {
                HostLifecycleService.Suspending += Suspending;
                HostLifecycleService.Resuming += Resuming;
                HostLifecycleService.AddRef();
            }

            private void UnregisterHostLifecycleEventHandlers()
            {
                HostLifecycleService.Suspending -= Suspending;
                HostLifecycleService.Resuming -= Resuming;
                HostLifecycleService.Release();
            }
        }

        private sealed class SchedulePeriodicRecursive<TState>
        {
            private readonly IScheduler _scheduler;
            private readonly TimeSpan _period;
            private readonly Func<TState, TState> _action;

            public SchedulePeriodicRecursive(IScheduler scheduler, TState state, TimeSpan period, Func<TState, TState> action)
            {
                _scheduler = scheduler;
                _period = period;
                _action = action;

                _state = state;
            }

            private TState _state;
            private int _pendingTickCount;
            private IDisposable? _cancel;

            public IDisposable Start()
            {
                _pendingTickCount = 0;

                var d = new SingleAssignmentDisposable();
                _cancel = d;

                d.Disposable = _scheduler.Schedule(TICK, _period, Tick);

                return d;
            }

            //
            // The protocol using the three commands is explained in the Tick implementation below.
            //
            private const int TICK = 0;
            private const int DispatchStart = 1;
            private const int DispatchEnd = 2;

            private void Tick(int command, Action<int, TimeSpan> recurse)
            {
                switch (command)
                {
                    case TICK:
                        //
                        // Ticks keep going at the specified periodic rate. We do a head call such
                        // that no slippage is introduced because of DISPATCH_START work involving
                        // user code that may take arbitrarily long.
                        //
                        recurse(TICK, _period);

                        //
                        // If we're not transitioning from 0 to 1 pending tick, another processing
                        // request is in flight which will see a non-zero pending tick count after
                        // doing the final decrement, causing it to reschedule immediately. We can
                        // safely bail out, delegating work to the catch-up tail calls.
                        //
                        if (Interlocked.Increment(ref _pendingTickCount) == 1)
                        {
                            goto case DispatchStart;
                        }

                        break;

                    case DispatchStart:
                        try
                        {
                            _state = _action(_state);
                        }
                        catch (Exception e)
                        {
                            _cancel!.Dispose(); // NB: Non-null after Start is called.
                            e.Throw();
                        }

                        //
                        // This is very subtle. We can't do a goto case DISPATCH_END here because it
                        // wouldn't introduce interleaving of periodic ticks that are due. In order
                        // to have best effort behavior for schedulers that don't have concurrency,
                        // we yield by doing a recursive call here. Notice this doesn't heal all of
                        // the problem, because the TICK commands that may be dispatched before the
                        // scheduled DISPATCH_END will do a "recurse(TICK, period)", which is relative
                        // from the point of entrance. Really all we're doing here is damage control
                        // for the case there's no stopwatch provider which should be rare (notice
                        // the LocalScheduler base class always imposes a stopwatch, but it can get
                        // disabled using DisableOptimizations; legacy implementations of schedulers
                        // from the v1.x days will not have a stopwatch).
                        //
                        recurse(DispatchEnd, TimeSpan.Zero);

                        break;

                    case DispatchEnd:
                        //
                        // If work was due while we were still running user code, the count will have
                        // been incremented by the periodic tick handler above. In that case, we will
                        // reschedule ourselves for dispatching work immediately.
                        //
                        // Notice we don't run a loop here, in order to allow interleaving of work on
                        // the scheduler by making recursive calls. In case we would use AsyncLock to
                        // ensure serialized execution the owner could get stuck in such a loop, thus
                        // we make tail calls to play nice with the scheduler.
                        //
                        if (Interlocked.Decrement(ref _pendingTickCount) > 0)
                        {
                            recurse(DispatchStart, TimeSpan.Zero);
                        }

                        break;
                }
            }
        }

        private sealed class EmulatedStopwatch : IStopwatch
        {
            private readonly IScheduler _scheduler;
            private readonly DateTimeOffset _start;

            public EmulatedStopwatch(IScheduler scheduler)
            {
                _scheduler = scheduler;
                _start = _scheduler.Now;
            }

            public TimeSpan Elapsed => Normalize(_scheduler.Now - _start);
        }
    }
}
