using System;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
namespace AMWD.Common.Utilities
{
// originally inspired by a code from Yves Goergen (unclassified.software).
///
/// Implements an awaitable task that runs after a specified delay. The delay can be reset.
/// By resetting the delay, the task can be executed multiple times.
///
public class DelayedTask
{
#region Data
private Timer _timer;
private bool _nextRunPending;
///
/// The synchronisation object.
///
protected readonly object syncLock = new();
///
/// The exception handler.
///
protected Action exceptionHandler;
///
/// Provides the for the method.
///
protected TaskCompletionSourceWrapper tcs;
///
/// Gets or sets the action to execute.
///
protected Action Action { get; set; }
///
/// Gets a value indicating whether the timer is running and an execution is scheduled. This
/// is mutually exclusive to .
///
public bool IsWaitingToRun { get; private set; }
///
/// Gets a value indicating whether the action is currently running. This is mutually
/// exclusive to .
///
public bool IsRunning { get; private set; }
///
/// Gets or sets the delay to wait before executing the action.
///
public TimeSpan Delay { get; protected set; }
#endregion Data
#region Static methods
///
/// Creates a new task instance that executes the specified action after the delay, but does
/// not start it yet.
///
/// The action to execute.
/// The delay.
///
public static DelayedTask Create(Action action, TimeSpan delay)
=> new() { Action = action, Delay = delay };
///
/// Creates a new task instance that executes the specified action after the delay, but does
/// not start it yet.
///
/// The action to execute.
/// The delay.
///
public static DelayedTaskWithResult Create(Func action, TimeSpan delay)
=> DelayedTaskWithResult.Create(action, delay);
///
/// Executes the specified action after the delay.
///
/// The action to execute.
/// The delay.
///
public static DelayedTask Run(Action action, TimeSpan delay)
=> new DelayedTask { Action = action, Delay = delay }.Start();
///
/// Executes the specified action after the delay.
///
/// The action to execute.
/// The delay.
///
public static DelayedTaskWithResult Run(Func action, TimeSpan delay)
=> DelayedTaskWithResult.Run(action, delay);
#endregion Static methods
#region Constructors
///
/// Initializes a new instance of the class.
///
protected DelayedTask()
{
tcs = CreateTcs();
SetLastResult(tcs);
}
#endregion Constructors
#region Public instance methods
///
/// Resets the delay and restarts the timer. If an execution is currently pending, it is
/// postponed until the full delay has elapsed again. If no execution is pending, the action
/// will be executed again after the delay.
///
public void Reset()
{
lock (syncLock)
{
if (!IsWaitingToRun && !IsRunning)
{
// Let callers wait for the next execution
tcs = CreateTcs();
}
IsWaitingToRun = true;
if (_timer != null)
{
_timer.Change(Delay, Timeout.InfiniteTimeSpan);
}
else
{
_timer = new Timer(OnTimerCallback, null, Delay, Timeout.InfiniteTimeSpan);
}
}
}
///
/// Cancels the delay. Any pending execution is cleared. If the action was pending but not
/// yet executing, this task is cancelled. If the action was not pending or is already
/// executing, this task will be completed successfully after the action has completed.
///
public void Cancel()
{
TaskCompletionSourceWrapper localTcs = null;
lock (syncLock)
{
IsWaitingToRun = false;
_nextRunPending = false;
_timer?.Dispose();
_timer = null;
if (!IsRunning)
{
localTcs = tcs;
}
}
// Complete the task (as cancelled) so that nobody needs to wait for an execution that
// isn't currently scheduled
localTcs?.TrySetCanceled();
}
///
/// Starts a pending execution immediately, not waiting for the timer to elapse.
///
/// true, if an execution was started; otherwise, false.
public bool ExecutePending()
{
lock (syncLock)
{
if (!IsWaitingToRun && !IsRunning)
{
return false;
}
IsWaitingToRun = true;
if (_timer != null)
{
_timer.Change(TimeSpan.Zero, Timeout.InfiniteTimeSpan);
}
else
{
_timer = new Timer(OnTimerCallback, null, TimeSpan.Zero, Timeout.InfiniteTimeSpan);
}
return true;
}
}
///
/// Gets an awaiter used to await this .
///
/// An awaiter instance.
public TaskAwaiter GetAwaiter()
{
lock (syncLock)
{
return tcs.Task.GetAwaiter();
}
}
///
/// Gets a that represents the current awaitable
/// operation.
///
public Task Task
{
get
{
lock (syncLock)
{
return tcs.Task;
}
}
}
///
/// Performs an implicit conversion from to
/// .
///
/// The instance to cast.
/// A that represents the current
/// awaitable operation.
public static implicit operator Task(DelayedTask delayedTask) => delayedTask.Task;
///
/// Gets the exception of the last execution. If the action has not yet thrown any
/// exceptions, this will return null.
///
public Exception Exception => Task.Exception;
///
/// Adds an unhandled exception handler to this instance.
///
/// The action that handles an exception.
/// The current instance.
public DelayedTask WithExceptionHandler(Action exceptionHandler)
{
this.exceptionHandler = exceptionHandler;
return this;
}
#endregion Public instance methods
#region Non-public methods
///
/// Starts the current instance after creating it.
///
/// The current instance.
protected DelayedTask Start()
{
tcs = CreateTcs();
IsWaitingToRun = true;
_timer = new Timer(OnTimerCallback, null, Delay, Timeout.InfiniteTimeSpan);
return this;
}
///
/// Creates a instance.
///
///
protected virtual TaskCompletionSourceWrapper CreateTcs()
=> new TaskCompletionSourceWrapper