<|start_filename|>src/Ben.Http/ResponseExtensions.cs<|end_filename|>
﻿using System;
using System.Text.Json;
using System.Threading.Tasks;

using Microsoft.Net.Http.Headers;

namespace Ben.Http
{
    public static class ResponseExtensions
    {
        public static Task Text(this Response response, ReadOnlySpan<byte> utf8String)
        {
            var headers = response.Headers;

            headers.ContentLength = utf8String.Length;
            headers[HeaderNames.ContentType] = "text/plain";

            var writer = response.Writer;
            var output = writer.GetSpan(utf8String.Length);

            utf8String.CopyTo(output);
            writer.Advance(utf8String.Length);

            return Task.CompletedTask;
        }

        public static Task Json<TValue>(this Response response, TValue value)
        {
            response.Headers[HeaderNames.ContentType] = "application/json";

            JsonSerializer.Serialize(GetJsonWriter(response), value, SerializerOptions);

            return Task.CompletedTask;
        }

        public static Task NotFound(this Response response)
        {
            response.StatusCode = 404;

            return Task.CompletedTask;
        }

        [ThreadStatic]
        private static Utf8JsonWriter t_writer = null!;
        private static readonly JsonSerializerOptions SerializerOptions = new JsonSerializerOptions(new JsonSerializerOptions { });

        private static Utf8JsonWriter GetJsonWriter(Response response)
        {
            Utf8JsonWriter utf8JsonWriter = t_writer ??= new Utf8JsonWriter(response.Writer, new JsonWriterOptions { SkipValidation = true });
            utf8JsonWriter.Reset(response.Writer);
            return utf8JsonWriter;
        }
    }
}


<|start_filename|>samples/TechEmpowerGenerators/Program.cs<|end_filename|>
using System;
using Ben.Http;
using Sql = Npgsql.NpgsqlConnection;

var connection = Environment.GetEnvironmentVariable("connection");
var (server, app) = (new HttpServer("http://+:8080"), new HttpApp());

app.Get("/plaintext", () => "Hello, World!");

app.Get("/json", (req, res) => {
    res.Headers.ContentLength = 27;
    return res.Json(new Note { message = "Hello, World!" });
});

app.Get("/fortunes", async (req, res) => {
    using Sql db = new(connection);
    var model = await db.QueryAsync<(int id, string message)>("SELECT id, message FROM fortune");
    model.Add((0, "Additional fortune added at request time."));
    model.Sort((x, y) => string.CompareOrdinal(x.message, y.message));
    MustacheTemplates.RenderFortunes(model, res.Writer);
});

app.Get("/db", async (req, res) => {
    using Sql db = new(connection);
    await res.Json(await db.QueryRowAsync<World, int>(
        "SELECT id, randomnumber FROM world WHERE id = @id", 
        (name: "@id", value: ConcurrentRandom.Next(10000) + 1)));
});

await server.RunAsync(app);

struct Note { public string message { get; set; } }
struct World { public int id { get; set; } public int randomnumber { get; set; } }


<|start_filename|>src/Ben.Http/DatabaseExtensions.cs<|end_filename|>
﻿
using System;
using System.Collections.Generic;
using System.Data;
using System.Threading.Tasks;

namespace Ben.Http
{
    public static class DatabaseExtensions
    {
        public static Task<List<TResult>> QueryAsync<TResult>(this IDbConnection conn, string sql, bool autoClose = true)
        {
            throw new NotImplementedException("You need to add the Ben.Http.Generator to use this method.");
        }

        public static Task<TResult> QueryRowAsync<TResult, TValue>(this IDbConnection conn, string sql, (string name, TValue value) parameter, bool autoClose = true)
        {
            throw new NotImplementedException("You need to add the Ben.Http.Generator to use this method.");
        }
    }
}

<|start_filename|>src/Ben.Http/HttpServer.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Console;
using Microsoft.Extensions.Options;

namespace Ben.Http
{
    public class HttpServer : IDisposable
    {
        private IServerAddressesFeature _addresses = null!;
        private TaskCompletionSource _completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        private ILoggerFactory _loggerFactory;
        private IServer _server;

        public IFeatureCollection Features => _server.Features;

        public HttpServer(string listenAddress) : this(DefaultLoggerFactories.Empty)
        {
            _addresses = Features.Get<IServerAddressesFeature>();
            _addresses.Addresses.Add(listenAddress);
        }

        public HttpServer(string listenAddress, ILoggerFactory loggerFactory) : this(loggerFactory)
        {
            _addresses = Features.Get<IServerAddressesFeature>();
            _addresses.Addresses.Add(listenAddress);
        }

        public HttpServer(IEnumerable<string> listenAddresses, ILoggerFactory loggerFactory) : this(loggerFactory)
        {
            _addresses = Features.Get<IServerAddressesFeature>();
            foreach (var uri in listenAddresses)
            {
                _addresses.Addresses.Add(uri);
            };
        }

        private HttpServer(ILoggerFactory loggerFactory)
        {
            _loggerFactory = loggerFactory;
            _server = new KestrelServer(
                KestrelOptions.Defaults,
                new SocketTransportFactory(SocketOptions.Defaults, _loggerFactory),
                _loggerFactory);
        }

        public override string ToString()
        {
            var sb = new StringBuilder();
            sb.AppendLine($"Listening on:");
            foreach (var address in _addresses.Addresses)
            {
                sb.AppendLine($"=> {address}");
            }

            return sb.ToString();
        }

        public async Task RunAsync(HttpApp application, CancellationToken cancellationToken = default)
        {
            await _server.StartAsync(application, cancellationToken);

            cancellationToken.UnsafeRegister(static (o) => ((HttpServer)o!)._completion.TrySetResult(), this);
            
            await _completion.Task;

            await _server.StopAsync(default);
        }

        void IDisposable.Dispose() => _server.Dispose();

        private class DefaultLoggerFactories
        {
            public static ILoggerFactory Empty => new LoggerFactory();
        }

        private class KestrelOptions : IOptions<KestrelServerOptions>
        {
            private KestrelOptions()
            {
                Value = new KestrelServerOptions();
            }

            public static KestrelOptions Defaults { get; } = new KestrelOptions();

            public KestrelServerOptions Value { get; init; }
        }

        private class SocketOptions : IOptions<SocketTransportOptions>
        {
            public static SocketOptions Defaults { get; } = new SocketOptions 
            {
                Value = new SocketTransportOptions()
                {
                    WaitForDataBeforeAllocatingBuffer = false,
                    UnsafePreferInlineScheduling = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? Environment.GetEnvironmentVariable("DOTNET_SYSTEM_NET_SOCKETS_INLINE_COMPLETIONS") == "1" : false,
                } 
            };

            public SocketTransportOptions Value { get; init; } = new SocketTransportOptions();
        }

        private class LoggerOptions : IOptionsMonitor<ConsoleLoggerOptions>
        {
            public static LoggerOptions Default { get; } = new LoggerOptions();

            public ConsoleLoggerOptions CurrentValue { get; } = new ConsoleLoggerOptions();

            public ConsoleLoggerOptions Get(string name) => CurrentValue;

            public IDisposable OnChange(Action<ConsoleLoggerOptions, string> listener)
                => NullDisposable.Shared;

            private class NullDisposable : IDisposable
            {
                public static NullDisposable Shared { get; } = new NullDisposable();

                public void Dispose() { }
            }
        }
    }
}


<|start_filename|>src/Ben.Http/Context.cs<|end_filename|>
﻿using System.IO;
using System.IO.Pipelines;

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;

namespace Ben.Http
{
    public class Context
    {
        public Request Request { get; } = new Request();
        public Response Response { get; } = new Response();

        internal void Initialize(IFeatureCollection features)
        {
            Request.Initialize(features);
            Response.Initialize(features);
        }

        public void Reset()
        {
            Request.Reset();
            Response.Reset();
        }
    }

    public class Request
    {
        private IFeatureCollection _features = null!;
        private IHttpRequestFeature? _request;
        private IHttpRequestFeature RequestFeature => _request ??= _features.Get<IHttpRequestFeature>();

        internal void Initialize(IFeatureCollection features) => _features = features;

        /// <summary>
        /// The HTTP-version as defined in RFC 7230. E.g. "HTTP/1.1"
        /// </summary>
        public string Protocol => RequestFeature.Protocol;

        /// <summary>
        /// The request uri scheme. E.g. "http" or "https". Note this value is not included
        /// in the original request, it is inferred by checking if the transport used a TLS
        /// connection or not.
        /// </summary>
        public string Scheme => RequestFeature.Scheme;

        /// <summary>
        /// The request method as defined in RFC 7230. E.g. "GET", "HEAD", "POST", etc..
        /// </summary>
        public string Method => RequestFeature.Method;

        /// <summary>
        /// The first portion of the request path associated with application root. The value
        /// is un-escaped. The value may be string.Empty.
        /// </summary>
        public string PathBase => RequestFeature.PathBase;

        /// <summary>
        /// The portion of the request path that identifies the requested resource. The value
        /// is un-escaped. The value may be string.Empty if PathBase
        /// contains the full path.
        /// </summary>
        public string Path => RequestFeature.Path;

        /// <summary>
        /// The query portion of the request-target as defined in RFC 7230. The value may
        /// be string.Empty. If not empty then the leading '?' will be included. The value
        /// is in its original form, without un-escaping.
        /// </summary>
        public string QueryString => RequestFeature.QueryString;

        /// <summary>
        /// Headers included in the request, aggregated by header name. The values are not
        /// split or merged across header lines. E.g. The following headers: HeaderA: value1,
        /// value2 HeaderA: value3 Result in Headers["HeaderA"] = { "value1, value2", "value3" }
        /// </summary>
        public IHeaderDictionary Headers => RequestFeature.Headers;
        /// <summary>
        /// A System.IO.Stream representing the request body, if any. Stream.Null may be
        /// used to represent an empty request body.
        /// </summary>
        public Stream Body => RequestFeature.Body;

        internal void Reset() => _request = null;
    }

    public class Response
    {
        private IFeatureCollection _features = null!;
        private IHttpResponseFeature? _response;
        private IHttpResponseBodyFeature? _responseBody;

        private IHttpResponseFeature ResponseFeature => _response ??= _features.Get<IHttpResponseFeature>();
        private IHttpResponseBodyFeature ResponseBody => _responseBody ??= _features.Get<IHttpResponseBodyFeature>();

        internal void Initialize(IFeatureCollection features) => _features = features;

        /// <summary>
        ///  The status-code as defined in RFC 7230. The default value is 200.
        /// </summary>
        public int StatusCode 
        { 
            get => ResponseFeature.StatusCode; 
            set => ResponseFeature.StatusCode = value; 
        }

        /// <summary>
        /// The response headers to send. Headers with multiple values will be emitted as multiple headers.
        /// </summary>
        public IHeaderDictionary Headers => ResponseFeature.Headers;

        /// <summary>
        /// The System.IO.Stream for writing the response body.
        /// </summary>
        public Stream Stream => ResponseBody.Stream;

        /// <summary>
        /// A System.IO.Pipelines.PipeWriter representing the response body, if any.
        /// </summary>
        public PipeWriter Writer => ResponseBody.Writer;

        internal void Reset()
        {
            _response = null;
            _responseBody = null;
        }
    }
}


<|start_filename|>src/Ben.Http/Random.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Diagnostics;

namespace System
{
    public static class ConcurrentRandom
    {
        [ThreadStatic]
        private static Random? t_random;

        private static Random Random => t_random ??= CreateRandom();

        private static Random CreateRandom() => new Random((int)Stopwatch.GetTimestamp());

        public static int Next(int maxValue) => Random.Next(maxValue);
    }
}


<|start_filename|>src/Ben.Http/HttpApp.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;

using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Abstractions;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Net.Http.Headers;

namespace Ben.Http
{
    public delegate Task RequestHandler(Request req, Response res);

    public class HttpApp : IHttpApplication<Context>
    {
        private readonly Dictionary<string, RequestHandler> _routes = new();

        public void Get(string path, RequestHandler handler)
        {
            _routes[path] = handler;
        }

        public void Get(string path, Func<string> handler)
        {
            var utf8String = Encoding.UTF8.GetBytes(handler());

            _routes[path] = (req, res) =>
            {
                var headers = res.Headers;
                ReadOnlySpan<byte> data = utf8String;

                headers.ContentLength = data.Length;
                headers[HeaderNames.ContentType] = "text/plain";

                var writer = res.Writer;
                var output = writer.GetSpan(data.Length);

                data.CopyTo(output);
                writer.Advance(data.Length);

                return Task.CompletedTask;
            };
        }

        public override string ToString()
        {
            var sb = new StringBuilder();
            sb.AppendLine($"Paths:");
            foreach (var path in _routes.Keys)
            {
                sb.AppendLine($"=> {path}");
            }

            return sb.ToString();
        }

        public IEnumerable<string> Paths
            => PathsEnumerable();

        private IEnumerable<string> PathsEnumerable()
        {
            foreach (var path in _routes.Keys)
            {
                yield return path;
            }
        }

        Task IHttpApplication<Context>.ProcessRequestAsync(Context context)
        {
            var request = context.Request;
            var response = context.Response;
            response.Headers[HeaderNames.Server] = "Ben";
            if (_routes.TryGetValue(request.Path, out var handler))
            {
                return handler(request, context.Response);
            }

            response.StatusCode = 404;
            return Task.CompletedTask;
        }

        Context IHttpApplication<Context>.CreateContext(IFeatureCollection features)
        {
            Context context;
            if (features is IHostContextContainer<Context> container)
            {
                // The server allows us to store the HttpContext on the connection
                // between requests so we don't have to reallocate it each time.
                context = container.HostContext;
                if (context is null)
                {
                    context = new Context();
                    container.HostContext = context;
                }
            }
            else
            {
                // Server doesn't support pooling, so create a new Context
                context = new Context();
            }

            context.Initialize(features);

            return context;
        }

        void IHttpApplication<Context>.DisposeContext(Context context, Exception exception)
        {
            // As we may be pooling the HttpContext above; Reset its settings.
            context.Reset();
        }
    }
}


<|start_filename|>samples/PlaintextJsonViaExtensions/Program.cs<|end_filename|>
using System;
using Ben.Http;

var (server, app) = (new HttpServer($"http://+:8080"), new HttpApp());

// Assign routes
app.Get("/plaintext", () => "Hello, World!");

app.Get("/json", (req, res) => {
    res.Headers.ContentLength = 27;
    return res.Json(new Note { message = "Hello, World!" });
});

Console.Write($"{server} {app}"); // Display listening info

// Start the server
await server.RunAsync(app);

// Datastructures
struct Note { public string message { get; set; } }

<|start_filename|>samples/PlaintextJsonRaw/Program.cs<|end_filename|>
using System;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Net.Http.Headers;
using static System.Console;

using Ben.Http;

var port = 8080;

var server = new HttpServer($"http://+:{port}");
var app = new HttpApp();

// Assign routes
app.Get("/plaintext", Plaintext);
app.Get("/json", Json);

Write($"{server} {app}"); // Display listening info

await server.RunAsync(app);

// Route methods
async Task Plaintext(Request request, Response response)
{
    var payload = Settings.HelloWorld;

    var headers = response.Headers;

    headers.ContentLength = payload.Length;
    headers[HeaderNames.ContentType] = "text/plain";

    await response.Writer.WriteAsync(payload);
}

static Task Json(Request request, Response response)
{
    var headers = response.Headers;

    headers.ContentLength = 27;
    headers[HeaderNames.ContentType] = "application/json";

    return JsonSerializer.SerializeAsync(
        response.Stream, 
        new JsonMessage { message = "Hello, World!" }, 
        Settings.SerializerOptions);
}

// Settings and datastructures
struct JsonMessage { public string message { get; set; } }

static class Settings
{
    public static readonly byte[] HelloWorld = Encoding.UTF8.GetBytes("Hello, World!");
    public static readonly JsonSerializerOptions SerializerOptions = new JsonSerializerOptions(new JsonSerializerOptions { });
}

<|start_filename|>src/Ben.Http.Generator/MustacheGenerator.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

[Generator]
public class MustacheGenerator : ISourceGenerator
{
    public void Execute(GeneratorExecutionContext context)
    {
        var receiver = context.SyntaxReceiver as MustacheRenderReceiver;
        if (receiver is null) return;

        StringBuilder sb = new StringBuilder();
        sb.AppendLine($@"// Source Generated at {DateTimeOffset.Now:R}
using System;
using System.Buffers;
using System.IO.Pipelines;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Ben.Http.Templates;

namespace Ben.Http
{{
    public class MustacheTemplates
    {{
        // When passing a `new byte[] {{ ... }}` to a `ReadOnlySpan<byte>` receiver,
        // the C# compiler will emit a load of the data section rather than constructing a new array.
        // We make use of that here.");

        foreach (var file in context.AdditionalFiles)
        {
            var isMustacheTemplate = string.Equals(
                context.AnalyzerConfigOptions.GetOptions(file).TryGetAdditionalFileMetadataValue("IsMustacheTemplate"),
                "true",
                StringComparison.OrdinalIgnoreCase
            );

            if (!isMustacheTemplate)
                continue;

            var content = file.GetText(context.CancellationToken)?.ToString();

            if (string.IsNullOrWhiteSpace(content))
                continue;

            ProcessFile(context, file.Path, content!, receiver, sb);

        }

        sb.AppendLine(@"
    }
}");
        context.AddSource("MustacheTemplates", sb.ToString());
    }

    const int SpacesPerIndent = 4;
    private void ProcessFile(in GeneratorExecutionContext context, string filePath, string content, MustacheRenderReceiver? receiver, StringBuilder builder)
    {
        // Generate class name from file name
        var templateName = SanitizeIdentifier(Path.GetFileNameWithoutExtension(filePath));

        // Always output non-specific writer
        builder.AppendLine(@$"
        public static void Render{templateName}<T>(T model, IBufferWriter<byte> writer)
        {{
            // Emitted as an initial call site for the template,
            // when actually called a specific call site for the exact model will be additionally be emitted.
            throw new NotImplementedException();        
        }}
");

        List<InvocationExpressionSyntax>? invocations = null;
        if (receiver?.Invocations?.TryGetValue(templateName, out invocations) ?? false)
        {
            Debug.Assert(invocations != null);

            foreach (var invocation in invocations!)
            {
                var arguments = invocation.ArgumentList.Arguments;
                if (arguments.Count != 2) continue;

                var semanticModel = context.Compilation.GetSemanticModel(invocation.SyntaxTree);
                var modelType = semanticModel.GetTypeInfo(arguments[0].Expression).Type;

                var indentSpaces = new string(' ', SpacesPerIndent * 2);

                builder.AppendLine(@$"{indentSpaces}public static void Render{templateName}({modelType} model, PipeWriter writer)");
                builder.AppendLine(@$"{indentSpaces}{{");

                indentSpaces = IncreasedIndent(indentSpaces);

                if (modelType?.ToString().StartsWith("System.Collections.Generic.List<") ?? false)
                {
                    builder.AppendLine(@$"{indentSpaces}var input = CollectionsMarshal.AsSpan(model);");
                }
                else
                {
                    builder.AppendLine(@$"{indentSpaces}var input = model;");
                }
                builder.AppendLine(@$"{indentSpaces}var output = new BufferWriter<PipeWriter>(writer, sizeHint: 1600);");

                var remaining = RenderSubSection(content.AsSpan(), "input", modelType, default, indentSpaces, builder);

                builder.Append(@$"{indentSpaces}/*{Environment.NewLine}{indentSpaces}{remaining.ToString().Replace("\n", $"\n{indentSpaces}")}{Environment.NewLine}{indentSpaces}*/{Environment.NewLine}");
                builder.Append(@$"{indentSpaces}output.Write(");
                AddUtf8ByteArray(remaining, IncreasedIndent(indentSpaces), builder);
                builder.AppendLine(");");

                builder.AppendLine(@$"{indentSpaces}output.Commit();
        }}");
            }
        }
    }

    private static ReadOnlySpan<char> RenderSubSection(ReadOnlySpan<char> input, string name, ITypeSymbol type, ReadOnlySpan<char> sectionTag, string indentSpaces, StringBuilder builder)
    {
        for (var index = input.IndexOf("{{".AsSpan(), StringComparison.Ordinal); index >= 0; index = input.IndexOf("{{".AsSpan(), StringComparison.Ordinal))
        {
            var tag = input.Slice(index + 2);
            var end = tag.IndexOf("}}".AsSpan());
            if (end <= 0)
            {
                break;
            }
            tag = tag.Slice(0, end);

            if (index > 0)
            {
                var html = input.Slice(0, index);
                builder.Append(@$"{indentSpaces}/*{Environment.NewLine}{indentSpaces}{html.ToString().Replace("\n", $"\n{indentSpaces}")}{Environment.NewLine}{indentSpaces}*/{Environment.NewLine}");

                builder.Append(@$"{indentSpaces}output.Write(");
                AddUtf8ByteArray(html, IncreasedIndent(indentSpaces), builder);
                builder.AppendLine(");");
            }

            input = input.Slice(index + end + 4);
            if (tag[0] == '#')
            {
                tag = tag.Slice(1);
                ITypeSymbol? subType = type;
                if (tag.Length == 1 && tag[0] == '.')
                {
                    tag = name.AsSpan();
                }
                else
                {
                    var tagName = tag.ToString();
                    var symbol = type.GetMembers().OfType<IFieldSymbol>().Where(f => f.Name == tagName).FirstOrDefault()?.Type;
                    if (symbol is null)
                    {
                        symbol = type.GetMembers().OfType<IPropertySymbol>().Where(p => (p.GetMethod?.Name) == "get_" + tagName).FirstOrDefault()?.Type;
                    }

                    subType = symbol;

                    if (subType is null)
                    {
                        // Skip section
                        var sectionEndTag = "{{/" + tag.ToString() + "}}";
                        var sectionEnd = input.IndexOf(sectionEndTag.AsSpan(), StringComparison.Ordinal);
                        if (end >= 0)
                        {
                            input = input.Slice(sectionEnd + sectionEndTag.Length);
                        }
                        continue;
                    }
                }

                var isEnumerable = false;
                if (subType.SpecialType == SpecialType.None)
                {
                    foreach (var iface in subType.AllInterfaces)
                    {
                        if (iface.ConstructedFrom.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T)
                        {
                            isEnumerable = true;
                            subType = iface.TypeArguments[0];
                            break;
                        }
                    }
                }

                // section start
                builder.AppendLine($"{indentSpaces}// Start Section: {tag.ToString()}");

                if (isEnumerable)
                {
                    builder.AppendLine($"{indentSpaces}foreach (var item in {tag.ToString()})");
                    builder.AppendLine($"{indentSpaces}{{");

                    input = RemoveTrailingSpace(input);

                    input = RenderSubSection(input, "item", subType, tag, IncreasedIndent(indentSpaces), builder);

                    builder.AppendLine($"{indentSpaces}}}");
                }
                else if (subType.SpecialType == SpecialType.System_Boolean)
                {
                    builder.AppendLine($"{indentSpaces}if ({name}.{tag.ToString()})");
                    builder.AppendLine($"{indentSpaces}{{");

                    input = RemoveTrailingSpace(input);

                    input = RenderSubSection(input, name, type, tag, IncreasedIndent(indentSpaces), builder);

                    builder.AppendLine($"{indentSpaces}}}");
                }

                builder.AppendLine($"{indentSpaces}// End Section: {tag.ToString()}");
            }
            else if (tag[0] == '/')
            {
                tag = tag.Slice(1);
                // section end
                if (tag.SequenceEqual(sectionTag))
                {
                    input = RemoveTrailingSpace(input);
                    break;
                }
            }
            else
            {
                var tagName = tag.ToString();

                var symbol = type.GetMembers().OfType<IFieldSymbol>().Where(f => f.Name == tagName).FirstOrDefault()?.Type;
                if (symbol is null)
                {
                    symbol = type.GetMembers().OfType<IPropertySymbol>().Where(p => (p.GetMethod?.Name) == "get_" + tagName).FirstOrDefault()?.Type;
                }

                builder.AppendLine($"{indentSpaces}// Variable: {symbol} {name}.{tag.ToString()}");
                if (symbol is null)
                {
                    // Skip output
                }
                else if (symbol.SpecialType == SpecialType.System_Int32)
                {
                    builder.AppendLine($"{indentSpaces}output.WriteNumeric((uint){name}.{tag.ToString()});");
                }
                else if (symbol.SpecialType == SpecialType.System_String)
                {
                    builder.AppendLine($"{indentSpaces}output.WriteUtf8HtmlString({name}.{tag.ToString()});");
                }
                else
                {
                    builder.AppendLine($"{indentSpaces}output.WriteUtf8HtmlString({name}.{tag.ToString()}.ToString());");
                }
            }
        }

        return input;
    }

    private static ReadOnlySpan<char> RemoveTrailingSpace(ReadOnlySpan<char> input)
    {
        int offset;
        for (offset = 0; offset < input.Length; offset++)
        {
            var ch = input[offset];
            if (ch != '\n' && ch != '\r' && ch != ' ')
            {
                break;
            }
        }
        if (offset > 0)
        {
            input = input.Slice(offset);
        }

        return input;
    }

    private static string IncreasedIndent(string indentSpaces)
    {
        return indentSpaces + new string(' ', SpacesPerIndent);
    }

    private static void AddUtf8ByteArray(ReadOnlySpan<char> rawText, string indentSpaces, StringBuilder builder)
    {
        const int SpaceAt = 4;
        const int AdditionalSpaceAt = 8;
        const int WrapLineAt = 16;

        builder.Append(@"new byte[] {");

        if (rawText.Length > 0)
        {
            var output = 0;
            // Keep inline if only one line
            var isInline = rawText.Length <= WrapLineAt;
            foreach (var b in Encoding.UTF8.GetBytes(rawText.ToString()))
            {
                if (!isInline)
                {
                    if (output % WrapLineAt == 0)
                    {
                        builder.AppendLine();
                        builder.Append(indentSpaces);
                    }
                }

                if (output % WrapLineAt != 0)
                {
                    if (output % SpaceAt == 0)
                    {
                        builder.Append(' ');
                    }
                    if (output % AdditionalSpaceAt == 0)
                    {
                        builder.Append(' ');
                    }
                }

                builder.Append($"0x{b:x2},");
                output++;
            }
            // Replace the trailing comma; it is still valid C# without removing it
            // so its just about being tidy.
            if (isInline)
            {
                builder.Replace(',', '}', builder.Length - 1, 1);
            }
            else
            {
                builder.Replace(",", Environment.NewLine + indentSpaces.Substring(0, indentSpaces.Length - SpacesPerIndent) + "}", builder.Length - 1, 1);
            }
        }
        else
        {
            builder.Append("}");
        }
    }

    private static string SanitizeIdentifier(string symbolName)
    {
        if (string.IsNullOrWhiteSpace(symbolName)) return string.Empty;

        var sb = new StringBuilder(symbolName.Length);
        if (!char.IsLetter(symbolName[0]))
        {
            // Must start with a letter or an underscore
            sb.Append('_');
        }

        var capitalize = true;
        foreach (var ch in symbolName)
        {
            if (!char.IsLetterOrDigit(ch))
            {
                capitalize = true;
                continue;
            }

            sb.Append(capitalize ? char.ToUpper(ch) : ch);
            capitalize = false;
        }

        return sb.ToString();
    }

    public void Initialize(GeneratorInitializationContext context)
        => context.RegisterForSyntaxNotifications(() => new MustacheRenderReceiver());

    class MustacheRenderReceiver : ISyntaxReceiver
    {
        public Dictionary<string, List<InvocationExpressionSyntax>>? Invocations { get; private set; }

        public void OnVisitSyntaxNode(SyntaxNode node)
        {
            if (node.IsKind(SyntaxKind.InvocationExpression) &&
                node is InvocationExpressionSyntax invocation)
            {
                var expression = invocation.Expression;
                if (expression is MemberAccessExpressionSyntax member)
                {
                    var isMustache = false;
                    string? template = null;
                    if (member.IsKind(SyntaxKind.SimpleMemberAccessExpression))
                    {
                        foreach (SyntaxNode child in expression.ChildNodes())
                        {
                            if (!isMustache)
                            {
                                if (child is IdentifierNameSyntax classIdent)
                                {
                                    var valueText = classIdent.Identifier.ValueText;
                                    Console.Error.WriteLine(valueText);
                                    if (classIdent.Identifier.ValueText == "MustacheTemplates")
                                    {
                                        isMustache = true;
                                        continue;
                                    }
                                    else
                                    {
                                        break;
                                    }
                                }
                                else
                                {
                                    break;
                                }
                            }

                            if (child is IdentifierNameSyntax methodIdent)
                            {
                                var valueText = methodIdent.Identifier.ValueText;
                                if (valueText.IndexOf("Render", StringComparison.Ordinal) == 0)
                                {
                                    template = valueText.Substring("Render".Length);
                                }
                                break;
                            }
                        }

                        if (isMustache && template is not null)
                        {
                            if ((Invocations ??= new()).TryGetValue(template, out var list))
                            {
                                list.Add(invocation);
                            }
                            else
                            {
                                Invocations.Add(template, new() { invocation });
                            }
                        }
                    }
                }
            }
        }
    }
}

internal static class SourceGeneratorExtensions
{
    public static string? TryGetValue(this AnalyzerConfigOptions options, string key) =>
        options.TryGetValue(key, out var value) ? value : null;

    public static string? TryGetAdditionalFileMetadataValue(this AnalyzerConfigOptions options, string propertyName) =>
        options.TryGetValue($"build_metadata.AdditionalFiles.{propertyName}");
}

<|start_filename|>src/Ben.Http.Generator/DatabaseGenerator.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

[Generator]
public class DatabaseGenerator : ISourceGenerator
{
    public void Execute(GeneratorExecutionContext context)
    {
        if (context.SyntaxReceiver is not DatabaseReceiver receiver || 
            (receiver.QueryAsyncInvocations is null && 
             receiver.QueryRowAsyncInvocations is null)) return;


        StringBuilder sb = new StringBuilder();
        sb.AppendLine($@"// Source Generated at {DateTimeOffset.Now:R}
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;

namespace Ben.Http
{{
    public static class DatabaseExtensionsGenerated
    {{");

        if (receiver.QueryAsyncInvocations is not null)
        {
            OutputQueryAsync(context.Compilation, receiver.QueryAsyncInvocations, sb);
        }
        if (receiver.QueryRowAsyncInvocations is not null)
        {
            OutputQueryRowAsync(context.Compilation, receiver.QueryRowAsyncInvocations, sb);
        }

        sb.AppendLine(@"
    }
}");
        context.AddSource("Database", sb.ToString());
    }

    private void OutputQueryRowAsync(Compilation compilation, List<InvocationExpressionSyntax> queryAsyncInvocations, StringBuilder sb)
    {
        HashSet<(string dbType, string arg0, string arg1, string methodName)> invocations = new();
        foreach (var invocation in queryAsyncInvocations)
        {
            var arguments = invocation.ArgumentList.Arguments;
            if (arguments.Count != 2) continue;
            var argument = arguments[0].Expression;
            if (argument.Kind() != SyntaxKind.StringLiteralExpression)
            {
                continue;
            }
            argument = arguments[1].Expression;

            var expression = invocation.Expression;
            if (expression is MemberAccessExpressionSyntax member)
            {
                if (member.IsKind(SyntaxKind.SimpleMemberAccessExpression))
                {
                    var semanticModel = compilation.GetSemanticModel(invocation.SyntaxTree);
                    ITypeSymbol? type = null;
                    foreach (SyntaxNode child in expression.ChildNodes())
                    {
                        if (child is IdentifierNameSyntax identifier)
                        {
                            type = semanticModel.GetTypeInfo(identifier).Type;
                        }
                        else if (child is GenericNameSyntax methodIdent)
                        {
                            var typeArgs = methodIdent.TypeArgumentList;
                            if (type is not null && typeArgs.Arguments.Count == 2)
                            {
                                var dbType = type.ToString();
                                var arg0 = typeArgs.Arguments[0];
                                var arg1 = typeArgs.Arguments[1];
                                var argStr0 = arg0.ToString();
                                var argStr1 = arg1.ToString();
                                var methodName = SanitizeIdentifier(argStr0 + argStr1);

                                if (!invocations.Contains((dbType, argStr0, argStr1, methodName)))
                                {
                                    GenerateQueryRowMethod(semanticModel, dbType, methodName, arg0, arg1, sb);
                                    invocations.Add((dbType, argStr0, argStr1, methodName));
                                }
                            }
                            break;
                        }
                    }
                }
            }
        }

        foreach (var db in invocations.GroupBy(type => type.dbType))
        {
            sb.AppendLine(@$"
        public static Task<TResult> QueryRowAsync<TResult, TValue>(this {db.Key} conn, string sql, (string name, TValue value) parameter, bool autoClose = true)
        {{");
            foreach (var item in db)
            {
                sb.AppendLine(@$"
            if (typeof(TResult) == typeof({item.arg0}) && typeof(TValue) == typeof({item.arg1}))
            {{
                return (Task<TResult>)(object)QueryRowAsync_{item.methodName}(conn, sql, (parameter.name, ({item.arg1})(object)parameter.value!), autoClose);
            }}");

            }
            sb.AppendLine(@$"
            throw new NotImplementedException();
        }}");
        }
    }

    private void GenerateQueryRowMethod(SemanticModel semanticModel, string dbType, string methodName, TypeSyntax argResult, TypeSyntax argParam, StringBuilder sb)
    {
        var dbPrefix = dbType.Replace("Connection", "");
        sb.AppendLine(@$"
        private static async Task<{argResult}> QueryRowAsync_{methodName}({dbType} conn, string sql, (string name, {argParam} value) param, bool autoClose)
        {{
            using var cmd = new {dbPrefix}Command(sql, conn);
            var parameter = new {dbPrefix}Parameter<{argParam}>(parameterName: param.name, value: param.value);
            cmd.Parameters.Add(parameter);
            await conn.OpenAsync();

            using var reader = await cmd.ExecuteReaderAsync();
            await reader.ReadAsync();");

        var type = semanticModel.GetTypeInfo(argResult).Type;

        var fields = type.GetMembers().OfType<IFieldSymbol>().Where(f => !f.IsImplicitlyDeclared).ToArray();
        var properties = type.GetMembers().OfType<IPropertySymbol>().Where(f => !f.IsReadOnly).ToArray();

        sb.Append(@$"
            if (!autoClose)
            {{
                return new ()
                {{");
        foreach (var field in fields)
        {
            sb.Append(@$"
                    {field.Name} = reader.Get{GetType(field.Type.SpecialType)}(""{field.Name}""),");
        }
        foreach (var property in properties)
        {
            sb.Append(@$"
                    {property.Name} = reader.Get{GetType(property.Type.SpecialType)}(""{property.Name}""),");
        }

        sb.Remove(sb.Length - 1, 1);

        sb.Append(@$"
                }};
            }}
            else
            {{
                var retVal = new {argResult}()
                {{");
        foreach (var field in fields)
        {
            sb.Append(@$"
                    {field.Name} = reader.Get{GetType(field.Type.SpecialType)}(""{field.Name}""),");
        }
        foreach (var property in properties)
        {
            sb.Append(@$"
                    {property.Name} = reader.Get{GetType(property.Type.SpecialType)}(""{property.Name}""),");
        }

        sb.Remove(sb.Length - 1, 1);

        sb.AppendLine(@$"
                }};
            
                conn.Close();

                return retVal;
            }}
    
        }}");
    }


    private void OutputQueryAsync(Compilation compilation, List<InvocationExpressionSyntax> queryAsyncInvocations, StringBuilder sb)
    {
        HashSet<(string dbType, string arg, string methodName)> invocations = new();
        foreach (var invocation in queryAsyncInvocations)
        {
            var arguments = invocation.ArgumentList.Arguments;
            if (arguments.Count != 1) continue;
            var argument = arguments[0].Expression;
            if (argument.Kind() != SyntaxKind.StringLiteralExpression)
            {
                continue;
            }

            var expression = invocation.Expression;
            if (expression is MemberAccessExpressionSyntax member)
            {
                if (member.IsKind(SyntaxKind.SimpleMemberAccessExpression))
                {
                    var semanticModel = compilation.GetSemanticModel(invocation.SyntaxTree);
                    ITypeSymbol? type = null;
                    foreach (SyntaxNode child in expression.ChildNodes())
                    {
                        if (child is IdentifierNameSyntax identifier)
                        {
                            type = semanticModel.GetTypeInfo(identifier).Type;
                        }
                        else if (child is GenericNameSyntax methodIdent)
                        {
                            var typeArgs = methodIdent.TypeArgumentList;
                            if (type is not null && typeArgs.Arguments.Count == 1)
                            {
                                var dbType = type.ToString();
                                var arg = typeArgs.Arguments[0];
                                var argStr = arg.ToString();
                                var methodName = SanitizeIdentifier(argStr);

                                if (!invocations.Contains((dbType, argStr, methodName)))
                                {
                                    GenerateQueryMethod(semanticModel, dbType, methodName, arg, sb);
                                    invocations.Add((dbType, argStr, methodName));
                                }
                            }
                            break;
                        }
                    }
                }
            }
        }

        foreach (var db in invocations.GroupBy(type => type.dbType))
        {
            sb.AppendLine(@$"
        public static Task<List<TResult>> QueryAsync<TResult>(this {db.Key} conn, string sql, bool autoClose = true)
        {{");
            foreach (var item in db)
            {
                sb.AppendLine(@$"
            if (typeof(TResult) == typeof({item.arg}))
            {{
                return (Task<List<TResult>>)(object)QueryAsync_{item.methodName}(conn, sql, autoClose);
            }}");

            }
            sb.AppendLine(@$"
            throw new NotImplementedException();
        }}");
        }
    }

    private void GenerateQueryMethod(SemanticModel semanticModel, string dbType, string methodName, TypeSyntax arg, StringBuilder sb)
    {
        sb.AppendLine(@$"
        private static async Task<List<{arg}>> QueryAsync_{methodName}({dbType} conn, string sql, bool autoClose)
        {{
            using var cmd = conn.CreateCommand();
            cmd.CommandText = sql;
            var list = new List<{arg}>(16);
            await conn.OpenAsync();

            using var reader = await cmd.ExecuteReaderAsync();");

        var type = semanticModel.GetTypeInfo(arg).Type;

        var count = 0;
        var fields = type.GetMembers().OfType<IFieldSymbol>().Where(f => !f.IsImplicitlyDeclared).ToArray();
        var properties = type.GetMembers().OfType<IPropertySymbol>().Where(f => !f.IsReadOnly).ToArray();

        foreach (var field in fields)
        {
            sb.Append(@$"
            var f{count} = reader.GetOrdinal(""{field.Name}"");");

            count++;
        }
        foreach (var propery in properties)
        {
            sb.Append(@$"
            var f{count} = reader.GetOrdinal(""{propery.Name}"");");

            count++;
        }
        count = 0;

        sb.Append(@$"
            while (await reader.ReadAsync())
            {{
                list.Add(new ()
                {{");
        foreach (var field in fields)
        {
            sb.Append(@$"
                    {field.Name} = reader.Get{GetType(field.Type.SpecialType)}(f{count}),");

            count++;
        }
        foreach (var property in properties)
        {
            sb.Append(@$"
                    {property.Name} = reader.Get{GetType(property.Type.SpecialType)}(f{count}),");

            count++;
        }

        sb.Remove(sb.Length - 1, 1);

        sb.AppendLine(@$"
                }});
            }}

            if (autoClose) conn.Close();

            return list;      
        }}");
    }

    private string GetType(SpecialType specialType)
    {
        var type = specialType.ToString();
        return type.Replace("System_", "");
    }

    private static string SanitizeIdentifier(string symbolName)
    {
        if (string.IsNullOrWhiteSpace(symbolName)) return string.Empty;

        var sb = new StringBuilder(symbolName.Length);
        if (!char.IsLetter(symbolName[0]))
        {
            // Must start with a letter or an underscore
            sb.Append('_');
        }

        var capitalize = true;
        foreach (var ch in symbolName)
        {
            if (!char.IsLetterOrDigit(ch))
            {
                capitalize = true;
                continue;
            }

            sb.Append(capitalize ? char.ToUpper(ch) : ch);
            capitalize = false;
        }

        return sb.ToString();
    }

    public void Initialize(GeneratorInitializationContext context)
        => context.RegisterForSyntaxNotifications(() => new DatabaseReceiver());

    class DatabaseReceiver : ISyntaxReceiver
    {
        public List<InvocationExpressionSyntax>? QueryAsyncInvocations { get; private set; }
        public List<InvocationExpressionSyntax>? QueryRowAsyncInvocations { get; private set; }

        public void OnVisitSyntaxNode(SyntaxNode node)
        {
            if (node.IsKind(SyntaxKind.InvocationExpression) &&
                node is InvocationExpressionSyntax invocation)
            {
                var expression = invocation.Expression;
                if (expression is MemberAccessExpressionSyntax member)
                {
                    if (member.IsKind(SyntaxKind.SimpleMemberAccessExpression))
                    {
                        foreach (SyntaxNode child in expression.ChildNodes())
                        {
                            if (child is GenericNameSyntax methodIdent)
                            {
                                var valueText = methodIdent.Identifier.ValueText;
                                switch (valueText)
                                {
                                    case "QueryAsync":
                                        (QueryAsyncInvocations ??= new()).Add(invocation);
                                        break;
                                    case "QueryRowAsync":
                                        (QueryRowAsyncInvocations ??= new()).Add(invocation);
                                        break;
                                }
                                break;
                            }
                        }
                    }
                }
            }
        }
    }
}
