// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;

namespace MSTest.TestFramework.SourceGeneration;

/// <summary>
/// Emits the repetitive <c>AppendLiteral</c>/<c>AppendFormatted</c> overload set that every
/// <c>Assert.*InterpolatedStringHandler</c> struct must declare for the C# interpolated string handler
/// pattern. The compiler performs ordinary member lookup for these methods on each named handler struct
/// (structs cannot inherit them), so without generation each handler has to repeat ~10 identical
/// forwarders. Handlers opt in via
/// <c>[GenerateAssertInterpolatedStringAppendMethods]</c>; the actual append logic lives once in
/// <c>AssertInterpolatedStringHandlerAppender</c>.
/// </summary>
[Generator(LanguageNames.CSharp)]
public sealed class AssertInterpolatedStringAppendMethodsGenerator : IIncrementalGenerator
{
    private const string MarkerAttributeMetadataName = "Microsoft.VisualStudio.TestTools.UnitTesting.GenerateAssertInterpolatedStringAppendMethodsAttribute";

    /// <inheritdoc />
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        IncrementalValuesProvider<HandlerInfo> handlers = context.SyntaxProvider.ForAttributeWithMetadataName(
            MarkerAttributeMetadataName,
            predicate: static (node, _) => node is StructDeclarationSyntax { Parent: ClassDeclarationSyntax or StructDeclarationSyntax },
            transform: static (ctx, _) => GetHandlerInfo(ctx));

        context.RegisterSourceOutput(handlers, static (spc, handler) => Emit(spc, handler));
    }

    private static HandlerInfo GetHandlerInfo(GeneratorAttributeSyntaxContext context)
    {
        var structSymbol = (INamedTypeSymbol)context.TargetSymbol;

        bool nullableLiteralParameter = context.Attributes.Any(attribute => attribute.NamedArguments.Any(namedArgument =>
            namedArgument.Key == "NullableLiteralParameter" && namedArgument.Value.Value is true));

        INamedTypeSymbol containingType = structSymbol.ContainingType;
        string typeParameters = string.Empty;
        if (structSymbol.TypeParameters.Length > 0)
        {
            var typeParameterBuilder = new StringBuilder("<");
            for (int i = 0; i < structSymbol.TypeParameters.Length; i++)
            {
                if (i > 0)
                {
                    typeParameterBuilder.Append(", ");
                }

                typeParameterBuilder.Append(structSymbol.TypeParameters[i].Name);
            }

            typeParameterBuilder.Append('>');
            typeParameters = typeParameterBuilder.ToString();
        }

        return new HandlerInfo(
            structSymbol.ContainingNamespace.ToDisplayString(),
            GetContainingTypeDeclaration(containingType),
            structSymbol.Name,
            typeParameters,
            nullableLiteralParameter);
    }

    private static string GetContainingTypeDeclaration(INamedTypeSymbol containingType)
    {
        // Emit the same accessibility and modifiers as the user-authored partial so the generated partial
        // never conflicts with it. Assert is declared as 'public sealed partial class Assert'.
        var builder = new StringBuilder();
        builder.Append(containingType.DeclaredAccessibility switch
        {
            Accessibility.Public => "public ",
            Accessibility.Internal => "internal ",
            Accessibility.Protected => "protected ",
            Accessibility.ProtectedOrInternal => "protected internal ",
            Accessibility.ProtectedAndInternal => "private protected ",
            Accessibility.Private => "private ",
            _ => string.Empty,
        });

        if (containingType.IsStatic)
        {
            builder.Append("static ");
        }
        else if (containingType.IsSealed)
        {
            builder.Append("sealed ");
        }
        else if (containingType.IsAbstract)
        {
            builder.Append("abstract ");
        }

        builder.Append("partial ");
        builder.Append(containingType.IsReferenceType ? "class " : "struct ");
        builder.Append(containingType.Name);
        return builder.ToString();
    }

    private static void Emit(SourceProductionContext context, HandlerInfo handler)
    {
        string literalParameterType = handler.NullableLiteralParameter ? "string?" : "string";

        var builder = new StringBuilder();
        builder.AppendLine("// <auto-generated/>");
        builder.AppendLine("// Copyright (c) Microsoft Corporation. All rights reserved.");
        builder.AppendLine("// Licensed under the MIT license. See LICENSE file in the project root for full license information.");
        builder.AppendLine("#nullable enable");
        builder.AppendLine();
        builder.AppendLine("using System;");
        builder.AppendLine();
        builder.AppendLine("#pragma warning disable CS1591");
        builder.AppendLine("#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters");
        builder.AppendLine("#pragma warning disable RS0027 // API with optional parameter(s) should have the most parameters amongst its public overloads");
        builder.AppendLine();
        builder.AppendLine($"namespace {handler.Namespace};");
        builder.AppendLine();
        builder.AppendLine(handler.ContainingTypeDeclaration);
        builder.AppendLine("{");
        builder.AppendLine($"    public readonly partial struct {handler.StructName}{handler.TypeParameters}");
        builder.AppendLine("    {");
        builder.AppendLine($"        public void AppendLiteral({literalParameterType} value) => AssertInterpolatedStringHandlerAppender.AppendLiteral(_builder!, value);");
        builder.AppendLine();
        builder.AppendLine("        public void AppendFormatted<T>(T value) => AppendFormatted(value, format: null);");
        builder.AppendLine();
        builder.AppendLine("#if NETCOREAPP3_1_OR_GREATER");
        builder.AppendLine("        public void AppendFormatted(ReadOnlySpan<char> value) => AssertInterpolatedStringHandlerAppender.AppendFormatted(_builder!, value);");
        builder.AppendLine();
        builder.AppendLine("        public void AppendFormatted(ReadOnlySpan<char> value, int alignment = 0, string? format = null) => AppendFormatted(value.ToString(), alignment, format);");
        builder.AppendLine("#endif");
        builder.AppendLine();
        builder.AppendLine("        public void AppendFormatted<T>(T value, string? format) => AssertInterpolatedStringHandlerAppender.AppendFormatted(_builder!, value, format);");
        builder.AppendLine();
        builder.AppendLine("        public void AppendFormatted<T>(T value, int alignment) => AssertInterpolatedStringHandlerAppender.AppendFormatted(_builder!, value, alignment);");
        builder.AppendLine();
        builder.AppendLine("        public void AppendFormatted<T>(T value, int alignment, string? format) => AssertInterpolatedStringHandlerAppender.AppendFormatted(_builder!, value, alignment, format);");
        builder.AppendLine();
        builder.AppendLine("        public void AppendFormatted(string? value) => AssertInterpolatedStringHandlerAppender.AppendFormatted(_builder!, value);");
        builder.AppendLine();
        builder.AppendLine("        public void AppendFormatted(string? value, int alignment = 0, string? format = null) => AssertInterpolatedStringHandlerAppender.AppendFormatted(_builder!, value, alignment, format);");
        builder.AppendLine();
        builder.AppendLine("        public void AppendFormatted(object? value, int alignment = 0, string? format = null) => AssertInterpolatedStringHandlerAppender.AppendFormatted(_builder!, value, alignment, format);");
        builder.AppendLine("    }");
        builder.AppendLine("}");

        context.AddSource($"{handler.StructName}.AppendMethods.g.cs", SourceText.From(builder.ToString(), Encoding.UTF8));
    }

    private readonly struct HandlerInfo
    {
        public HandlerInfo(string @namespace, string containingTypeDeclaration, string structName, string typeParameters, bool nullableLiteralParameter)
        {
            Namespace = @namespace;
            ContainingTypeDeclaration = containingTypeDeclaration;
            StructName = structName;
            TypeParameters = typeParameters;
            NullableLiteralParameter = nullableLiteralParameter;
        }

        public string Namespace { get; }

        public string ContainingTypeDeclaration { get; }

        public string StructName { get; }

        public string TypeParameters { get; }

        public bool NullableLiteralParameter { get; }
    }
}
