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

namespace Spangle.SourceGenerator.Rtmp;

[Generator(LanguageNames.CSharp)]
[SuppressMessage("ReSharper", "SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault")]
public class Amf0SerializableGenerator : IIncrementalGenerator
{
    private const string StructAttributeName       = "Amf0SerializableAttribute";
    private const string FieldAttributeName        = "Amf0FieldAttribute";
    private const string FieldAttrPositionPropName = "Position";
    private const string WriteMethodName           = "WriteAsAmf0Command";
    private const string WriteBytesMethodName      = "WriteBytes";

    private static readonly string s_namespace = typeof(Amf0SerializableGenerator).Namespace;

    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        context.RegisterPostInitializationOutput(ctx =>
        {
            ctx.AddSource($"{StructAttributeName}.g.cs", $$"""
namespace {{s_namespace}};

using System;

[AttributeUsage(AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
internal sealed class {{StructAttributeName}} : Attribute
{
}

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
internal sealed class {{FieldAttributeName}} : Attribute
{
    public int {{FieldAttrPositionPropName}} { get; private set; }
    public {{FieldAttributeName}}(int {{FieldAttrPositionPropName.ToLowerInvariant()}})
    {
        {{FieldAttrPositionPropName}} = {{FieldAttrPositionPropName.ToLowerInvariant()}};
    }
}
""");
        });

        IncrementalValuesProvider<GeneratorAttributeSyntaxContext> source =
            context.SyntaxProvider.ForAttributeWithMetadataName(
                $"{s_namespace}.{StructAttributeName}",
                static (node, _) => node is StructDeclarationSyntax,
                static (context, _) => context);

        context.RegisterSourceOutput(source, Emit);
    }

    private static void Emit(SourceProductionContext context, GeneratorAttributeSyntaxContext source)
    {
        var typeSymbol = (INamedTypeSymbol)source.TargetSymbol;
        var typeNode = (TypeDeclarationSyntax)source.TargetNode;

        if (typeSymbol.GetMembers(WriteMethodName).Length != 0)
        {
            context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.WriteAsAmf0CommandMethodExists,
                typeNode.Identifier.GetLocation(), typeSymbol.Name));
            return;
        }

        string ns = typeSymbol.ContainingNamespace.IsGlobalNamespace
            ? ""
            : $"namespace {typeSymbol.ContainingNamespace};";
        string accessibility = typeSymbol.DeclaredAccessibility.ToString().ToLowerInvariant();

        string fullType = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
            .Replace("global::", "")
            .Replace("<", "_")
            .Replace(">", "_");

        // Extract Field/Property
        var publicMembers = typeSymbol.GetMembers()
            .Where(x => x is (IFieldSymbol or IPropertySymbol)
                and
                {
                    IsStatic: false, DeclaredAccessibility: Accessibility.Public, IsImplicitlyDeclared: false,
                    CanBeReferencedByName: true
                } && x.GetAttributes().Any(attr => attr.AttributeClass?.Name is FieldAttributeName))
            .OrderBy(x =>
                (int)x.GetAttributes().First(attr => attr.AttributeClass?.Name is FieldAttributeName)
                    .ConstructorArguments
                    .First().Value!);

        IList<(string, string)> serializerMethods = new List<(string, string)>();
        foreach (ISymbol fieldOrProp in publicMembers)
        {
            (string name, string? serializerMethod) = GetFieldSerializerMethodString(fieldOrProp);
            if (serializerMethod is null)
            {
                context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.NotSupportedTypeField,
                    typeNode.Identifier.GetLocation(), GetFieldTypeName(fieldOrProp)));
                return;
            }

            serializerMethods.Add((name, serializerMethod));
        }

        StringBuilder codeBuilder = new StringBuilder().AppendLine($$"""
// <auto-generated/>
#nullable enable
#pragma warning disable CS8600
#pragma warning disable CS8601
#pragma warning disable CS8602
#pragma warning disable CS8603
#pragma warning disable CS8604

using System.Buffers;
using Spangle.Transport.Rtmp.Amf0;

{{ns}}

{{accessibility}} partial struct {{typeSymbol.Name}} : IAmf0Serializable
{
    /// <summary>
    /// Serialize this struct as an AMF0 byte sequence.
    /// </summary>
    public int {{WriteBytesMethodName}}(IBufferWriter<byte> writer)
    {
        var total = 0;
""");
        foreach ((_, string method) in serializerMethods)
        {
            codeBuilder.AppendLine($$"""
        total += {{method}};
""");
        }

        codeBuilder.AppendLine("""
        return total;
    }
}
""");

        context.AddSource($"{fullType}.g.cs", codeBuilder.ToString());
    }

    private static string GetFieldTypeName(ISymbol fieldOrProp)
    {
        return fieldOrProp.Kind switch
        {
            SymbolKind.Field    => ((IFieldSymbol)fieldOrProp).Type.Name,
            SymbolKind.Property => ((IPropertySymbol)fieldOrProp).Type.Name,
            _                   => throw new ArgumentException($"Not a suitable kind of symbol : {fieldOrProp.Kind}")
        };
    }

    private static (string, string?) GetFieldSerializerMethodString(ISymbol fieldOrProp)
    {
        string name = fieldOrProp.Name;
        string typeName = GetFieldTypeName(fieldOrProp);

        // Type names must be canonical. e.g. int => Int32
        string? method = typeName switch
        {
            // Numeric subset of IConvertible
            "Byte" or "SByte"
                or "UInt16" or "Int16"
                or "UInt32" or "Int32"
                or "Double" or "Single" // float
                or "Decimal"
                => $"Amf0Writer.WriteNumber(writer, Convert.ToDouble({fieldOrProp.Name}))",
            "Boolean"
                => $"Amf0Writer.WriteBool(writer, {fieldOrProp.Name})",
            "String"
                => $"Amf0Writer.WriteString(writer, {fieldOrProp.Name})",
            "IReadOnlyDictionary"
                => $"Amf0Writer.WriteObject(writer, {fieldOrProp.Name})",
            "Object"
                => $"Amf0Writer.Write(writer, {fieldOrProp.Name})",
            _
                => null
        };
        return (name, method);
    }
}
