// 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;
using System.Diagnostics;
using System.IO.Pipelines;
using System.Net.Http.HPack;
using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using Http2HeadersEnumerator = Microsoft.AspNetCore.Server.Kestrel.Core.Tests.Http2HeadersEnumerator;

namespace Microsoft.AspNetCore.Server.Kestrel.Microbenchmarks;

public abstract class Http2ConnectionBenchmarkBase
{
    private MemoryPool<byte> _memoryPool;
    private IHeaderDictionary _httpRequestHeaders;
    private Http2Connection _connection;
    private DynamicHPackEncoder _hpackEncoder;
    private Http2HeadersEnumerator _requestHeadersEnumerator;
    private int _currentStreamId;
    private byte[] _headersBuffer;
    private DuplexPipe.DuplexPipePair _connectionPair;
    private int _dataWritten;
    private Task _requestProcessingTask;

    private readonly Http2Frame _receiveHttpFrame = new();
    private readonly Http2Frame _sendHttpFrame = new();

    protected abstract Task ProcessRequest(HttpContext httpContext);

    [Params(0, 1, 3)]
    public int NumCookies { get; set; }

    public virtual void GlobalSetup()
    {
        _memoryPool = PinnedBlockMemoryPoolFactory.Create();

        var options = new PipeOptions(_memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false);

        _connectionPair = DuplexPipe.CreateConnectionPair(options, options);

        _httpRequestHeaders = new HttpRequestHeaders();
        _httpRequestHeaders[InternalHeaderNames.Method] = new StringValues("GET");
        _httpRequestHeaders[InternalHeaderNames.Path] = new StringValues("/");
        _httpRequestHeaders[InternalHeaderNames.Scheme] = new StringValues("http");
        _httpRequestHeaders[InternalHeaderNames.Authority] = new StringValues("localhost:80");

        if (NumCookies > 0)
        {
            var cookies = new string[NumCookies];
            for (var index = 0; index < NumCookies; index++)
            {
                cookies[index] = $"{index}={index + 1}";
            }
            _httpRequestHeaders[HeaderNames.Cookie] = cookies;
        }

        _headersBuffer = new byte[1024 * 16];
        _hpackEncoder = new DynamicHPackEncoder();

        var serviceContext = TestContextFactory.CreateServiceContext(
            serverOptions: new KestrelServerOptions(),
            dateHeaderValueManager: new DateHeaderValueManager(TimeProvider.System),
            timeProvider: TimeProvider.System);
        serviceContext.DateHeaderValueManager.OnHeartbeat();

        var featureCollection = new FeatureCollection();
        featureCollection.Set<IConnectionMetricsContextFeature>(new TestConnectionMetricsContextFeature());
        var connectionContext = TestContextFactory.CreateHttpConnectionContext(
            serviceContext: serviceContext,
            connectionContext: null,
            transport: _connectionPair.Transport,
            timeoutControl: new MockTimeoutControl(),
            memoryPool: _memoryPool,
            connectionFeatures: featureCollection);

        _connection = new Http2Connection(connectionContext);

        _requestHeadersEnumerator = new Http2HeadersEnumerator();

        _currentStreamId = 1;

        _requestProcessingTask = _connection.ProcessRequestsAsync(new DummyApplication(ProcessRequest, new MockHttpContextFactory()));

        _connectionPair.Application.Output.Write(Http2Connection.ClientPreface);
        _connectionPair.Application.Output.WriteSettings(new Http2PeerSettings
        {
            InitialWindowSize = 2147483647
        });
        _connectionPair.Application.Output.FlushAsync().GetAwaiter().GetResult();

        // Read past connection setup frames
        ReceiveFrameAsync(_connectionPair.Application.Input).GetAwaiter().GetResult();
        Debug.Assert(_receiveHttpFrame.Type == Http2FrameType.SETTINGS);
        ReceiveFrameAsync(_connectionPair.Application.Input).GetAwaiter().GetResult();
        Debug.Assert(_receiveHttpFrame.Type == Http2FrameType.WINDOW_UPDATE);
        ReceiveFrameAsync(_connectionPair.Application.Input).GetAwaiter().GetResult();
        Debug.Assert(_receiveHttpFrame.Type == Http2FrameType.SETTINGS);
    }

    [Benchmark]
    public async Task MakeRequest()
    {
        _requestHeadersEnumerator.Initialize(_httpRequestHeaders);
        _requestHeadersEnumerator.MoveNext();
        _connectionPair.Application.Output.WriteStartStream(streamId: _currentStreamId, _hpackEncoder, _requestHeadersEnumerator, _headersBuffer, endStream: true, frame: _sendHttpFrame);
        await _connectionPair.Application.Output.FlushAsync();

        while (true)
        {
            await ReceiveFrameAsync(_connectionPair.Application.Input);

            if (_receiveHttpFrame.StreamId != _currentStreamId && _receiveHttpFrame.StreamId != 0)
            {
                throw new Exception($"Unexpected stream ID: {_receiveHttpFrame.StreamId}");
            }

            if (_receiveHttpFrame.Type == Http2FrameType.DATA)
            {
                _dataWritten += _receiveHttpFrame.DataPayloadLength;
            }

            if (_dataWritten > 1024 * 32)
            {
                _connectionPair.Application.Output.WriteWindowUpdateAsync(streamId: 0, _dataWritten, _sendHttpFrame);
                await _connectionPair.Application.Output.FlushAsync();

                _dataWritten = 0;
            }

            if ((_receiveHttpFrame.HeadersFlags & Http2HeadersFrameFlags.END_STREAM) == Http2HeadersFrameFlags.END_STREAM)
            {
                break;
            }
        }

        _currentStreamId += 2;
    }

    internal async ValueTask ReceiveFrameAsync(PipeReader pipeReader, uint maxFrameSize = Http2PeerSettings.DefaultMaxFrameSize)
    {
        while (true)
        {
            var result = await pipeReader.ReadAsync();
            var buffer = result.Buffer;
            var consumed = buffer.Start;
            var examined = buffer.Start;

            try
            {
                if (Http2FrameReader.TryReadFrame(ref buffer, _receiveHttpFrame, maxFrameSize, out var framePayload))
                {
                    consumed = examined = framePayload.End;
                    return;
                }
                else
                {
                    examined = buffer.End;
                }

                if (result.IsCompleted)
                {
                    throw new IOException("The reader completed without returning a frame.");
                }
            }
            finally
            {
                pipeReader.AdvanceTo(consumed, examined);
            }
        }
    }

    [GlobalCleanup]
    public async ValueTask DisposeAsync()
    {
        _connectionPair.Application.Output.Complete();
        await _requestProcessingTask;
        _memoryPool?.Dispose();
    }

    private sealed class TestConnectionMetricsContextFeature : IConnectionMetricsContextFeature
    {
        public ConnectionMetricsContext MetricsContext { get; }
    }
}
