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

namespace Godot.SourceGenerators.Internal;

[Generator]
public class UnmanagedCallbacksGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
        context.RegisterForPostInitialization(ctx => { GenerateAttribute(ctx); });
    }

    public void Execute(GeneratorExecutionContext context)
    {
        INamedTypeSymbol[] unmanagedCallbacksClasses = context
            .Compilation.SyntaxTrees
            .SelectMany(tree =>
                tree.GetRoot().DescendantNodes()
                    .OfType<ClassDeclarationSyntax>()
                    .SelectUnmanagedCallbacksClasses(context.Compilation)
                    // Report and skip non-partial classes
                    .Where(x =>
                    {
                        if (x.cds.IsPartial())
                        {
                            if (x.cds.IsNested() && !x.cds.AreAllOuterTypesPartial(out var typeMissingPartial))
                            {
                                Common.ReportNonPartialUnmanagedCallbacksOuterClass(context, typeMissingPartial!);
                                return false;
                            }

                            return true;
                        }

                        Common.ReportNonPartialUnmanagedCallbacksClass(context, x.cds, x.symbol);
                        return false;
                    })
                    .Select(x => x.symbol)
            )
            .Distinct<INamedTypeSymbol>(SymbolEqualityComparer.Default)
            .ToArray();

        foreach (var symbol in unmanagedCallbacksClasses)
        {
            var attr = symbol.GetGenerateUnmanagedCallbacksAttribute();
            if (attr == null || attr.ConstructorArguments.Length != 1)
            {
                // TODO: Report error or throw exception, this is an invalid case and should never be reached
                System.Diagnostics.Debug.Fail("FAILED!");
                continue;
            }

            var funcStructType = (INamedTypeSymbol?)attr.ConstructorArguments[0].Value;
            if (funcStructType == null)
            {
                // TODO: Report error or throw exception, this is an invalid case and should never be reached
                System.Diagnostics.Debug.Fail("FAILED!");
                continue;
            }

            var data = new CallbacksData(symbol, funcStructType);
            GenerateInteropMethodImplementations(context, data);
            GenerateUnmanagedCallbacksStruct(context, data);
        }
    }

    private void GenerateAttribute(GeneratorPostInitializationContext context)
    {
        string source = @"using System;

namespace Godot.SourceGenerators.Internal
{
internal class GenerateUnmanagedCallbacksAttribute : Attribute
{
    public Type FuncStructType { get; }

    public GenerateUnmanagedCallbacksAttribute(Type funcStructType)
    {
        FuncStructType = funcStructType;
    }
}
}";

        context.AddSource("GenerateUnmanagedCallbacksAttribute.generated",
            SourceText.From(source, Encoding.UTF8));
    }

    private void GenerateInteropMethodImplementations(GeneratorExecutionContext context, CallbacksData data)
    {
        var symbol = data.NativeTypeSymbol;

        INamespaceSymbol namespaceSymbol = symbol.ContainingNamespace;
        string classNs = namespaceSymbol != null && !namespaceSymbol.IsGlobalNamespace ?
            namespaceSymbol.FullQualifiedName() :
            string.Empty;
        bool hasNamespace = classNs.Length != 0;
        bool isInnerClass = symbol.ContainingType != null;

        var source = new StringBuilder();
        var methodSource = new StringBuilder();
        var methodCallArguments = new StringBuilder();
        var methodSourceAfterCall = new StringBuilder();

        source.Append(
            @"using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Godot.Bridge;
using Godot.NativeInterop;

#pragma warning disable CA1707 // Disable warning: Identifiers should not contain underscores

");

        if (hasNamespace)
        {
            source.Append("namespace ");
            source.Append(classNs);
            source.Append("\n{\n");
        }

        if (isInnerClass)
        {
            var containingType = symbol.ContainingType;

            while (containingType != null)
            {
                source.Append("partial ");
                source.Append(containingType.GetDeclarationKeyword());
                source.Append(" ");
                source.Append(containingType.NameWithTypeParameters());
                source.Append("\n{\n");

                containingType = containingType.ContainingType;
            }
        }

        source.Append("[System.Runtime.CompilerServices.SkipLocalsInit]\n");
        source.Append($"unsafe partial class {symbol.Name}\n");
        source.Append("{\n");
        source.Append($"    private static {data.FuncStructSymbol.FullQualifiedName()} _unmanagedCallbacks;\n\n");

        foreach (var callback in data.Methods)
        {
            methodSource.Clear();
            methodCallArguments.Clear();
            methodSourceAfterCall.Clear();

            source.Append("    [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]\n");
            source.Append($"    {SyntaxFacts.GetText(callback.DeclaredAccessibility)} ");

            if (callback.IsStatic)
                source.Append("static ");

            source.Append("partial ");
            source.Append(callback.ReturnType.FullQualifiedName());
            source.Append(' ');
            source.Append(callback.Name);
            source.Append('(');

            for (int i = 0; i < callback.Parameters.Length; i++)
            {
                var parameter = callback.Parameters[i];

                source.Append(parameter.ToDisplayString());
                source.Append(' ');
                source.Append(parameter.Name);

                if (parameter.RefKind == RefKind.Out)
                {
                    // Only assign default if the parameter won't be passed by-ref or copied later.
                    if (IsGodotInteropStruct(parameter.Type))
                        methodSource.Append($"        {parameter.Name} = default;\n");
                }

                if (IsByRefParameter(parameter))
                {
                    if (IsGodotInteropStruct(parameter.Type))
                    {
                        methodSource.Append("        ");
                        AppendCustomUnsafeAsPointer(methodSource, parameter, out string varName);
                        methodCallArguments.Append(varName);
                    }
                    else if (parameter.Type.IsValueType)
                    {
                        methodSource.Append("        ");
                        AppendCopyToStackAndGetPointer(methodSource, parameter, out string varName);
                        methodCallArguments.Append($"&{varName}");

                        if (parameter.RefKind is RefKind.Out or RefKind.Ref)
                        {
                            methodSourceAfterCall.Append($"        {parameter.Name} = {varName};\n");
                        }
                    }
                    else
                    {
                        // If it's a by-ref param and we can't get the pointer
                        // just pass it by-ref and let it be pinned.
                        AppendRefKind(methodCallArguments, parameter.RefKind)
                            .Append(' ')
                            .Append(parameter.Name);
                    }
                }
                else
                {
                    methodCallArguments.Append(parameter.Name);
                }

                if (i < callback.Parameters.Length - 1)
                {
                    source.Append(", ");
                    methodCallArguments.Append(", ");
                }
            }

            source.Append(")\n");
            source.Append("    {\n");

            source.Append(methodSource);
            source.Append("        ");

            if (!callback.ReturnsVoid)
            {
                if (methodSourceAfterCall.Length != 0)
                    source.Append($"{callback.ReturnType.FullQualifiedName()} ret = ");
                else
                    source.Append("return ");
            }

            source.Append($"_unmanagedCallbacks.{callback.Name}(");
            source.Append(methodCallArguments);
            source.Append(");\n");

            if (methodSourceAfterCall.Length != 0)
            {
                source.Append(methodSourceAfterCall);

                if (!callback.ReturnsVoid)
                    source.Append("        return ret;\n");
            }

            source.Append("    }\n\n");
        }

        source.Append("}\n");

        if (isInnerClass)
        {
            var containingType = symbol.ContainingType;

            while (containingType != null)
            {
                source.Append("}\n"); // outer class

                containingType = containingType.ContainingType;
            }
        }

        if (hasNamespace)
            source.Append("\n}");

        source.Append("\n\n#pragma warning restore CA1707\n");

        context.AddSource($"{data.NativeTypeSymbol.FullQualifiedName().SanitizeQualifiedNameForUniqueHint()}.generated",
            SourceText.From(source.ToString(), Encoding.UTF8));
    }

    private void GenerateUnmanagedCallbacksStruct(GeneratorExecutionContext context, CallbacksData data)
    {
        var symbol = data.FuncStructSymbol;

        INamespaceSymbol namespaceSymbol = symbol.ContainingNamespace;
        string classNs = namespaceSymbol != null && !namespaceSymbol.IsGlobalNamespace ?
            namespaceSymbol.FullQualifiedName() :
            string.Empty;
        bool hasNamespace = classNs.Length != 0;
        bool isInnerClass = symbol.ContainingType != null;

        var source = new StringBuilder();

        source.Append(
            @"using System.Runtime.InteropServices;
using Godot.NativeInterop;

#pragma warning disable CA1707 // Disable warning: Identifiers should not contain underscores

");
        if (hasNamespace)
        {
            source.Append("namespace ");
            source.Append(classNs);
            source.Append("\n{\n");
        }

        if (isInnerClass)
        {
            var containingType = symbol.ContainingType;

            while (containingType != null)
            {
                source.Append("partial ");
                source.Append(containingType.GetDeclarationKeyword());
                source.Append(" ");
                source.Append(containingType.NameWithTypeParameters());
                source.Append("\n{\n");

                containingType = containingType.ContainingType;
            }
        }

        source.Append("[StructLayout(LayoutKind.Sequential)]\n");
        source.Append($"unsafe partial struct {symbol.Name}\n{{\n");

        foreach (var callback in data.Methods)
        {
            source.Append("    ");
            source.Append(callback.DeclaredAccessibility == Accessibility.Public ? "public " : "internal ");

            source.Append("delegate* unmanaged<");

            foreach (var parameter in callback.Parameters)
            {
                if (IsByRefParameter(parameter))
                {
                    if (IsGodotInteropStruct(parameter.Type) || parameter.Type.IsValueType)
                    {
                        AppendPointerType(source, parameter.Type);
                    }
                    else
                    {
                        // If it's a by-ref param and we can't get the pointer
                        // just pass it by-ref and let it be pinned.
                        AppendRefKind(source, parameter.RefKind)
                            .Append(' ')
                            .Append(parameter.Type.FullQualifiedName());
                    }
                }
                else
                {
                    source.Append(parameter.Type.FullQualifiedName());
                }

                source.Append(", ");
            }

            source.Append(callback.ReturnType.FullQualifiedName());
            source.Append($"> {callback.Name};\n");
        }

        source.Append("}\n");

        if (isInnerClass)
        {
            var containingType = symbol.ContainingType;

            while (containingType != null)
            {
                source.Append("}\n"); // outer class

                containingType = containingType.ContainingType;
            }
        }

        if (hasNamespace)
            source.Append("}\n");

        source.Append("\n#pragma warning restore CA1707\n");

        context.AddSource($"{symbol.FullQualifiedName().SanitizeQualifiedNameForUniqueHint()}.generated",
            SourceText.From(source.ToString(), Encoding.UTF8));
    }

    private static bool IsGodotInteropStruct(ITypeSymbol type) =>
        GodotInteropStructs.Contains(type.FullQualifiedName());

    private static bool IsByRefParameter(IParameterSymbol parameter) =>
        parameter.RefKind is RefKind.In or RefKind.Out or RefKind.Ref;

    private static StringBuilder AppendRefKind(StringBuilder source, RefKind refKind) =>
        refKind switch
        {
            RefKind.In => source.Append("in"),
            RefKind.Out => source.Append("out"),
            RefKind.Ref => source.Append("ref"),
            _ => source,
        };

    private static void AppendPointerType(StringBuilder source, ITypeSymbol type)
    {
        source.Append(type.FullQualifiedName());
        source.Append('*');
    }

    private static void AppendCustomUnsafeAsPointer(StringBuilder source, IParameterSymbol parameter,
        out string varName)
    {
        varName = $"{parameter.Name}_ptr";

        AppendPointerType(source, parameter.Type);
        source.Append(' ');
        source.Append(varName);
        source.Append(" = ");

        source.Append('(');
        AppendPointerType(source, parameter.Type);
        source.Append(')');

        if (parameter.RefKind == RefKind.In)
            source.Append("CustomUnsafe.ReadOnlyRefAsPointer(in ");
        else
            source.Append("CustomUnsafe.AsPointer(ref ");

        source.Append(parameter.Name);

        source.Append(");\n");
    }

    private static void AppendCopyToStackAndGetPointer(StringBuilder source, IParameterSymbol parameter,
        out string varName)
    {
        varName = $"{parameter.Name}_copy";

        source.Append(parameter.Type.FullQualifiedName());
        source.Append(' ');
        source.Append(varName);
        if (parameter.RefKind is RefKind.In or RefKind.Ref)
        {
            source.Append(" = ");
            source.Append(parameter.Name);
        }

        source.Append(";\n");
    }

    private static readonly string[] GodotInteropStructs =
    {
        "Godot.NativeInterop.godot_ref",
        "Godot.NativeInterop.godot_variant_call_error",
        "Godot.NativeInterop.godot_variant",
        "Godot.NativeInterop.godot_string",
        "Godot.NativeInterop.godot_string_name",
        "Godot.NativeInterop.godot_node_path",
        "Godot.NativeInterop.godot_signal",
        "Godot.NativeInterop.godot_callable",
        "Godot.NativeInterop.godot_array",
        "Godot.NativeInterop.godot_dictionary",
        "Godot.NativeInterop.godot_packed_byte_array",
        "Godot.NativeInterop.godot_packed_int32_array",
        "Godot.NativeInterop.godot_packed_int64_array",
        "Godot.NativeInterop.godot_packed_float32_array",
        "Godot.NativeInterop.godot_packed_float64_array",
        "Godot.NativeInterop.godot_packed_string_array",
        "Godot.NativeInterop.godot_packed_vector2_array",
        "Godot.NativeInterop.godot_packed_vector3_array",
        "Godot.NativeInterop.godot_packed_color_array",
    };
}