// 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;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Shared.TestHooks;

namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities;

internal sealed class ResettableDelay
{
    public static readonly ResettableDelay CompletedDelay = new();

    private readonly int _delayInMilliseconds;
    private readonly TaskCompletionSource<object?> _taskCompletionSource = new();

    private int _lastSetTime;

    /// <summary>
    /// Create a ResettableDelay that will complete a task after a certain duration.  The delay
    /// can be reset at any point before it elapses in which case completion is postponed.  The
    /// delay can be reset multiple times.
    /// </summary>
    /// <param name="delayInMilliseconds">The time to delay before completing the task</param>
    public ResettableDelay(int delayInMilliseconds, IExpeditableDelaySource expeditableDelaySource, CancellationToken cancellationToken = default)
    {
        Contract.ThrowIfFalse(delayInMilliseconds >= 50, "Perf, only use delays >= 50ms");
        _delayInMilliseconds = delayInMilliseconds;

        Reset();

        _ = StartTimerAsync(expeditableDelaySource, cancellationToken);
    }

    private ResettableDelay()
    {
        // create resettableDelay with completed state
        _delayInMilliseconds = 0;
        _taskCompletionSource = new TaskCompletionSource<object?>();
        _taskCompletionSource.SetResult(null);

        Reset();
    }

    public Task Task => _taskCompletionSource.Task;

    public void Reset()
    {
        // Note: Environment.TickCount - this.lastSetTime is safe in the presence of overflow, but most
        // other operations are not.
        _lastSetTime = Environment.TickCount;
    }

    private async Task StartTimerAsync(IExpeditableDelaySource expeditableDelaySource, CancellationToken cancellationToken)
    {
        try
        {
            do
            {
                // Keep delaying until at least delayInMilliseconds has elapsed since lastSetTime
                if (!await expeditableDelaySource.Delay(TimeSpan.FromMilliseconds(_delayInMilliseconds), cancellationToken).ConfigureAwait(false))
                {
                    // The operation is being expedited.
                    break;
                }
            }
            while (Environment.TickCount - _lastSetTime < _delayInMilliseconds);

            _taskCompletionSource.SetResult(null);
        }
        catch (OperationCanceledException)
        {
            // Calling the "Try" variant because that's the only one that accepts the token to associate with the task
            _taskCompletionSource.TrySetCanceled(cancellationToken);
        }
    }
}
