// 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.IO.Pipelines;
using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Testing;

namespace Microsoft.AspNetCore.Server.Kestrel.Microbenchmarks;

public class RequestParsingBenchmark
{
    private MemoryPool<byte> _memoryPool;

    public Pipe Pipe { get; set; }

    internal Http1Connection Http1Connection { get; set; }

    [IterationSetup]
    public void Setup()
    {
        _memoryPool = PinnedBlockMemoryPoolFactory.Create();
        var options = new PipeOptions(_memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false);
        var pair = DuplexPipe.CreateConnectionPair(options, options);

        var serviceContext = TestContextFactory.CreateServiceContext(
            serverOptions: new KestrelServerOptions(),
            httpParser: new HttpParser<Http1ParsingHandler>(),
            dateHeaderValueManager: new DateHeaderValueManager(TimeProvider.System));

        var connectionContext = TestContextFactory.CreateHttpConnectionContext(
            serviceContext: serviceContext,
            connectionContext: null,
            transport: pair.Transport,
            memoryPool: _memoryPool,
            connectionFeatures: new FeatureCollection(),
            timeoutControl: new TimeoutControl(timeoutHandler: null, TimeProvider.System));

        var http1Connection = new Http1Connection(connectionContext);

        http1Connection.Reset();

        Http1Connection = http1Connection;
        Pipe = new Pipe(new PipeOptions(_memoryPool));
    }

    [Benchmark(Baseline = true, OperationsPerInvoke = RequestParsingData.InnerLoopCount)]
    public void PlaintextTechEmpower()
    {
        for (var i = 0; i < RequestParsingData.InnerLoopCount; i++)
        {
            InsertData(RequestParsingData.PlaintextTechEmpowerRequest);
            ParseData();
        }
    }

    [Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount)]
    public void PlaintextAbsoluteUri()
    {
        for (var i = 0; i < RequestParsingData.InnerLoopCount; i++)
        {
            InsertData(RequestParsingData.PlaintextAbsoluteUriRequest);
            ParseData();
        }
    }

    [Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount * RequestParsingData.Pipelining)]
    public void PipelinedPlaintextTechEmpower()
    {
        for (var i = 0; i < RequestParsingData.InnerLoopCount; i++)
        {
            InsertData(RequestParsingData.PlaintextTechEmpowerPipelinedRequests);
            ParseData();
        }
    }

    [Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount * RequestParsingData.Pipelining)]
    public void PipelinedPlaintextTechEmpowerDrainBuffer()
    {
        for (var i = 0; i < RequestParsingData.InnerLoopCount; i++)
        {
            InsertData(RequestParsingData.PlaintextTechEmpowerPipelinedRequests);
            ParseDataDrainBuffer();
        }
    }

    [Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount)]
    public void LiveAspNet()
    {
        for (var i = 0; i < RequestParsingData.InnerLoopCount; i++)
        {
            InsertData(RequestParsingData.LiveaspnetRequest);
            ParseData();
        }
    }

    [Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount * RequestParsingData.Pipelining)]
    public void PipelinedLiveAspNet()
    {
        for (var i = 0; i < RequestParsingData.InnerLoopCount; i++)
        {
            InsertData(RequestParsingData.LiveaspnetPipelinedRequests);
            ParseData();
        }
    }

    [Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount)]
    public void Unicode()
    {
        for (var i = 0; i < RequestParsingData.InnerLoopCount; i++)
        {
            InsertData(RequestParsingData.UnicodeRequest);
            ParseData();
        }
    }

    [Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount * RequestParsingData.Pipelining)]
    public void UnicodePipelined()
    {
        for (var i = 0; i < RequestParsingData.InnerLoopCount; i++)
        {
            InsertData(RequestParsingData.UnicodePipelinedRequests);
            ParseData();
        }
    }

    private void InsertData(byte[] bytes)
    {
        Pipe.Writer.Write(bytes);
        // There should not be any backpressure and task completes immediately
        Pipe.Writer.FlushAsync().GetAwaiter().GetResult();
    }

    private void ParseDataDrainBuffer()
    {
        var awaitable = Pipe.Reader.ReadAsync();
        if (!awaitable.IsCompleted)
        {
            // No more data
            return;
        }

        var readableBuffer = awaitable.GetAwaiter().GetResult().Buffer;
        var reader = new SequenceReader<byte>(readableBuffer);
        do
        {
            Http1Connection.Reset();

            if (!Http1Connection.TakeStartLine(ref reader))
            {
                ErrorUtilities.ThrowInvalidRequestLine();
            }

            if (!Http1Connection.TakeMessageHeaders(ref reader, trailers: false))
            {
                ErrorUtilities.ThrowInvalidRequestHeaders();
            }
        }
        while (!reader.End);

        Pipe.Reader.AdvanceTo(readableBuffer.End);
    }

    private void ParseData()
    {
        do
        {
            var awaitable = Pipe.Reader.ReadAsync();
            if (!awaitable.IsCompleted)
            {
                // No more data
                return;
            }

            var result = awaitable.GetAwaiter().GetResult();
            var readableBuffer = result.Buffer;
            var reader = new SequenceReader<byte>(readableBuffer);

            Http1Connection.Reset();

            if (!Http1Connection.TakeStartLine(ref reader))
            {
                ErrorUtilities.ThrowInvalidRequestLine();
            }
            Pipe.Reader.AdvanceTo(reader.Position, reader.Position);

            result = Pipe.Reader.ReadAsync().GetAwaiter().GetResult();
            readableBuffer = result.Buffer;
            reader = new SequenceReader<byte>(readableBuffer);

            if (!Http1Connection.TakeMessageHeaders(ref reader, trailers: false))
            {
                ErrorUtilities.ThrowInvalidRequestHeaders();
            }
            Pipe.Reader.AdvanceTo(reader.Position, reader.Position);
        }
        while (true);
    }

    [IterationCleanup]
    public void Cleanup()
    {
        _memoryPool.Dispose();
    }
}
