// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;

namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;

internal enum KeepAliveState
{
    None,
    SendPing,
    PingSent,
    Timeout
}

/// <summary>
/// Used by a <see cref="Http2Connection"/> to determine whether the connection is still alive.
/// If no frames have been received within a given period of time, triggers a ping that should
/// draw a response. After a further timeout, signals that the connection should be dropped.
/// </summary>
internal sealed class Http2KeepAlive
{
    // An empty ping payload
    internal static readonly ReadOnlySequence<byte> PingPayload = new ReadOnlySequence<byte>(new byte[8]);

    private readonly long _keepAliveInterval;
    private readonly long _keepAliveTimeout;
    private readonly TimeProvider _timeProvider;
    private long _lastFrameReceivedTimestamp;
    private long _pingSentTimestamp;

    // Internal for testing
    internal KeepAliveState _state;

    public Http2KeepAlive(TimeSpan keepAliveInterval, TimeSpan keepAliveTimeout, TimeProvider timeProvider)
    {
        _keepAliveInterval = keepAliveInterval.ToTicks(timeProvider);
        _keepAliveTimeout = keepAliveTimeout == TimeSpan.MaxValue ? long.MaxValue
            : keepAliveTimeout.ToTicks(timeProvider);
        _timeProvider = timeProvider;
    }

    public KeepAliveState ProcessKeepAlive(bool frameReceived)
    {
        var timestamp = _timeProvider.GetTimestamp();

        if (frameReceived)
        {
            // To err on the side of caution, add a second to the time when calculating the ping sent time.
            _lastFrameReceivedTimestamp = timestamp + _timeProvider.TimestampFrequency;

            // Any frame received after the keep alive interval is exceeded resets the state back to none.
            if (_state == KeepAliveState.PingSent)
            {
                _pingSentTimestamp = 0;
                _state = KeepAliveState.None;
            }
        }
        else
        {
            switch (_state)
            {
                case KeepAliveState.None:
                    // Check whether keep alive interval has passed since last frame received
                    if (timestamp > (_lastFrameReceivedTimestamp + _keepAliveInterval))
                    {
                        // Ping will be sent immeditely after this method finishes.
                        // Set the status directly to ping sent and set the timestamp
                        _state = KeepAliveState.PingSent;
                        // To err on the side of caution, add a second to the time when calculating the ping sent time.
                        _pingSentTimestamp = timestamp + _timeProvider.TimestampFrequency;

                        // Indicate that the ping needs to be sent. This is only returned once
                        return KeepAliveState.SendPing;
                    }
                    break;
                case KeepAliveState.PingSent:
                    if (_keepAliveTimeout != long.MaxValue)
                    {
                        if (timestamp > (_pingSentTimestamp + _keepAliveTimeout))
                        {
                            _state = KeepAliveState.Timeout;
                        }
                    }
                    break;
            }
        }

        return _state;
    }
}
