#nullable enable

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Godot.NativeInterop;

namespace Godot
{
    internal static class DelegateUtils
    {
        [UnmanagedCallersOnly]
        internal static godot_bool DelegateEquals(IntPtr delegateGCHandleA, IntPtr delegateGCHandleB)
        {
            try
            {
                var @delegateA = (Delegate?)GCHandle.FromIntPtr(delegateGCHandleA).Target;
                var @delegateB = (Delegate?)GCHandle.FromIntPtr(delegateGCHandleB).Target;
                return (@delegateA! == @delegateB!).ToGodotBool();
            }
            catch (Exception e)
            {
                ExceptionUtils.LogException(e);
                return godot_bool.False;
            }
        }

        [UnmanagedCallersOnly]
        internal static int DelegateHash(IntPtr delegateGCHandle)
        {
            try
            {
                var @delegate = (Delegate?)GCHandle.FromIntPtr(delegateGCHandle).Target;
                return @delegate?.GetHashCode() ?? 0;
            }
            catch (Exception e)
            {
                ExceptionUtils.LogException(e);
                return 0;
            }
        }

        [UnmanagedCallersOnly]
        internal static unsafe int GetArgumentCount(IntPtr delegateGCHandle, godot_bool* outIsValid)
        {
            try
            {
                var @delegate = (Delegate?)GCHandle.FromIntPtr(delegateGCHandle).Target;
                int? argCount = @delegate?.Method?.GetParameters().Length;
                if (argCount is null)
                {
                    *outIsValid = godot_bool.False;
                    return 0;
                }
                *outIsValid = godot_bool.True;
                return argCount.Value;
            }
            catch (Exception e)
            {
                ExceptionUtils.LogException(e);
                *outIsValid = godot_bool.False;
                return 0;
            }
        }

        [UnmanagedCallersOnly]
        internal static unsafe void InvokeWithVariantArgs(IntPtr delegateGCHandle, void* trampoline,
            godot_variant** args, int argc, godot_variant* outRet)
        {
            try
            {
                if (trampoline == null)
                {
                    throw new ArgumentNullException(nameof(trampoline),
                        "Cannot dynamically invoke delegate because the trampoline is null.");
                }

                var @delegate = (Delegate)GCHandle.FromIntPtr(delegateGCHandle).Target!;
                var trampolineFn = (delegate* managed<object, NativeVariantPtrArgs, out godot_variant, void>)trampoline;

                trampolineFn(@delegate, new NativeVariantPtrArgs(args, argc), out godot_variant ret);

                *outRet = ret;
            }
            catch (Exception e)
            {
                ExceptionUtils.LogException(e);
                *outRet = default;
            }
        }

        // TODO: Check if we should be using BindingFlags.DeclaredOnly (would give better reflection performance).

        private enum TargetKind : uint
        {
            Static,
            GodotObject,
            CompilerGenerated
        }

        internal static bool TrySerializeDelegate(Delegate @delegate, Collections.Array serializedData)
        {
            if (@delegate is null)
            {
                return false;
            }

            if (@delegate is MulticastDelegate multicastDelegate)
            {
                bool someDelegatesSerialized = false;

                Delegate[] invocationList = multicastDelegate.GetInvocationList();

                if (invocationList.Length > 1)
                {
                    var multiCastData = new Collections.Array();

                    foreach (Delegate oneDelegate in invocationList)
                        someDelegatesSerialized |= TrySerializeDelegate(oneDelegate, multiCastData);

                    if (!someDelegatesSerialized)
                        return false;

                    serializedData.Add(multiCastData);
                    return true;
                }
            }

            if (TrySerializeSingleDelegate(@delegate, out byte[]? buffer))
            {
                serializedData.Add((Span<byte>)buffer);
                return true;
            }

            return false;
        }

        private static bool TrySerializeSingleDelegate(Delegate @delegate, [MaybeNullWhen(false)] out byte[] buffer)
        {
            buffer = null;

            object? target = @delegate.Target;

            switch (target)
            {
                case null:
                {
                    using (var stream = new MemoryStream())
                    using (var writer = new BinaryWriter(stream))
                    {
                        writer.Write((ulong)TargetKind.Static);

                        SerializeType(writer, @delegate.GetType());

                        if (!TrySerializeMethodInfo(writer, @delegate.Method))
                            return false;

                        buffer = stream.ToArray();
                        return true;
                    }
                }
                case GodotObject godotObject:
                {
                    using (var stream = new MemoryStream())
                    using (var writer = new BinaryWriter(stream))
                    {
                        writer.Write((ulong)TargetKind.GodotObject);
                        // ReSharper disable once RedundantCast
                        writer.Write((ulong)godotObject.GetInstanceId());

                        SerializeType(writer, @delegate.GetType());

                        if (!TrySerializeMethodInfo(writer, @delegate.Method))
                            return false;

                        buffer = stream.ToArray();
                        return true;
                    }
                }
                default:
                {
                    Type targetType = target.GetType();

                    if (targetType.IsDefined(typeof(CompilerGeneratedAttribute), true))
                    {
                        // Compiler generated. Probably a closure. Try to serialize it.

                        using (var stream = new MemoryStream())
                        using (var writer = new BinaryWriter(stream))
                        {
                            writer.Write((ulong)TargetKind.CompilerGenerated);
                            SerializeType(writer, targetType);

                            SerializeType(writer, @delegate.GetType());

                            if (!TrySerializeMethodInfo(writer, @delegate.Method))
                                return false;

                            FieldInfo[] fields = targetType.GetFields(BindingFlags.Instance | BindingFlags.Public);

                            writer.Write(fields.Length);

                            foreach (FieldInfo field in fields)
                            {
                                Type fieldType = field.FieldType;

                                Variant.Type variantType = GD.TypeToVariantType(fieldType);

                                if (variantType == Variant.Type.Nil)
                                    return false;

                                static byte[] VarToBytes(in godot_variant var)
                                {
                                    NativeFuncs.godotsharp_var_to_bytes(var, godot_bool.True, out var varBytes);
                                    using (varBytes)
                                        return Marshaling.ConvertNativePackedByteArrayToSystemArray(varBytes);
                                }

                                writer.Write(field.Name);

                                var fieldValue = field.GetValue(target);
                                using var fieldValueVariant = RuntimeTypeConversionHelper.ConvertToVariant(fieldValue);
                                byte[] valueBuffer = VarToBytes(fieldValueVariant);
                                writer.Write(valueBuffer.Length);
                                writer.Write(valueBuffer);
                            }

                            buffer = stream.ToArray();
                            return true;
                        }
                    }

                    return false;
                }
            }
        }

        private static bool TrySerializeMethodInfo(BinaryWriter writer, MethodInfo methodInfo)
        {
            SerializeType(writer, methodInfo.DeclaringType);

            writer.Write(methodInfo.Name);

            int flags = 0;

            if (methodInfo.IsPublic)
                flags |= (int)BindingFlags.Public;
            else
                flags |= (int)BindingFlags.NonPublic;

            if (methodInfo.IsStatic)
                flags |= (int)BindingFlags.Static;
            else
                flags |= (int)BindingFlags.Instance;

            writer.Write(flags);

            Type returnType = methodInfo.ReturnType;
            bool hasReturn = methodInfo.ReturnType != typeof(void);

            writer.Write(hasReturn);
            if (hasReturn)
                SerializeType(writer, returnType);

            ParameterInfo[] parameters = methodInfo.GetParameters();

            writer.Write(parameters.Length);

            if (parameters.Length > 0)
            {
                for (int i = 0; i < parameters.Length; i++)
                    SerializeType(writer, parameters[i].ParameterType);
            }

            return true;
        }

        private static void SerializeType(BinaryWriter writer, Type? type)
        {
            if (type == null)
            {
                int genericArgumentsCount = -1;
                writer.Write(genericArgumentsCount);
            }
            else if (type.IsGenericType)
            {
                Type genericTypeDef = type.GetGenericTypeDefinition();
                Type[] genericArgs = type.GetGenericArguments();

                int genericArgumentsCount = genericArgs.Length;
                writer.Write(genericArgumentsCount);

                writer.Write(genericTypeDef.Assembly.GetName().Name ?? "");
                writer.Write(genericTypeDef.FullName ?? genericTypeDef.ToString());

                for (int i = 0; i < genericArgs.Length; i++)
                    SerializeType(writer, genericArgs[i]);
            }
            else
            {
                int genericArgumentsCount = 0;
                writer.Write(genericArgumentsCount);

                writer.Write(type.Assembly.GetName().Name ?? "");
                writer.Write(type.FullName ?? type.ToString());
            }
        }

        [UnmanagedCallersOnly]
        internal static unsafe godot_bool TrySerializeDelegateWithGCHandle(IntPtr delegateGCHandle,
            godot_array* nSerializedData)
        {
            try
            {
                var serializedData = Collections.Array.CreateTakingOwnershipOfDisposableValue(
                    NativeFuncs.godotsharp_array_new_copy(*nSerializedData));

                var @delegate = (Delegate)GCHandle.FromIntPtr(delegateGCHandle).Target!;

                return TrySerializeDelegate(@delegate, serializedData)
                    .ToGodotBool();
            }
            catch (Exception e)
            {
                ExceptionUtils.LogException(e);
                return godot_bool.False;
            }
        }

        [UnmanagedCallersOnly]
        internal static unsafe godot_bool TryDeserializeDelegateWithGCHandle(godot_array* nSerializedData,
            IntPtr* delegateGCHandle)
        {
            try
            {
                var serializedData = Collections.Array.CreateTakingOwnershipOfDisposableValue(
                    NativeFuncs.godotsharp_array_new_copy(*nSerializedData));

                if (TryDeserializeDelegate(serializedData, out Delegate? @delegate))
                {
                    *delegateGCHandle = GCHandle.ToIntPtr(CustomGCHandle.AllocStrong(@delegate));
                    return godot_bool.True;
                }
                else
                {
                    *delegateGCHandle = IntPtr.Zero;
                    return godot_bool.False;
                }
            }
            catch (Exception e)
            {
                ExceptionUtils.LogException(e);
                *delegateGCHandle = default;
                return godot_bool.False;
            }
        }

        internal static bool TryDeserializeDelegate(Collections.Array serializedData,
            [MaybeNullWhen(false)] out Delegate @delegate)
        {
            @delegate = null;

            if (serializedData.Count == 1)
            {
                var elem = serializedData[0].Obj;

                if (elem == null)
                    return false;

                if (elem is Collections.Array multiCastData)
                    return TryDeserializeDelegate(multiCastData, out @delegate);

                return TryDeserializeSingleDelegate((byte[])elem, out @delegate);
            }

            var delegates = new List<Delegate>(serializedData.Count);

            foreach (Variant variantElem in serializedData)
            {
                var elem = variantElem.Obj;

                if (elem == null)
                    continue;

                if (elem is Collections.Array multiCastData)
                {
                    if (TryDeserializeDelegate(multiCastData, out Delegate? oneDelegate))
                        delegates.Add(oneDelegate);
                }
                else
                {
                    if (TryDeserializeSingleDelegate((byte[])elem, out Delegate? oneDelegate))
                        delegates.Add(oneDelegate);
                }
            }

            if (delegates.Count <= 0)
                return false;

            @delegate = delegates.Count == 1 ? delegates[0] : Delegate.Combine(delegates.ToArray())!;
            return true;
        }

        private static bool TryDeserializeSingleDelegate(byte[] buffer, [MaybeNullWhen(false)] out Delegate @delegate)
        {
            @delegate = null;

            using (var stream = new MemoryStream(buffer, writable: false))
            using (var reader = new BinaryReader(stream))
            {
                var targetKind = (TargetKind)reader.ReadUInt64();

                switch (targetKind)
                {
                    case TargetKind.Static:
                    {
                        Type? delegateType = DeserializeType(reader);
                        if (delegateType == null)
                            return false;

                        if (!TryDeserializeMethodInfo(reader, out MethodInfo? methodInfo))
                            return false;

                        @delegate = Delegate.CreateDelegate(delegateType, null, methodInfo, throwOnBindFailure: false);

                        if (@delegate == null)
                            return false;

                        return true;
                    }
                    case TargetKind.GodotObject:
                    {
                        ulong objectId = reader.ReadUInt64();
                        GodotObject? godotObject = GodotObject.InstanceFromId(objectId);
                        if (godotObject == null)
                            return false;

                        Type? delegateType = DeserializeType(reader);
                        if (delegateType == null)
                            return false;

                        if (!TryDeserializeMethodInfo(reader, out MethodInfo? methodInfo))
                            return false;

                        @delegate = Delegate.CreateDelegate(delegateType, godotObject, methodInfo,
                            throwOnBindFailure: false);

                        if (@delegate == null)
                            return false;

                        return true;
                    }
                    case TargetKind.CompilerGenerated:
                    {
                        Type? targetType = DeserializeType(reader);
                        if (targetType == null)
                            return false;

                        Type? delegateType = DeserializeType(reader);
                        if (delegateType == null)
                            return false;

                        if (!TryDeserializeMethodInfo(reader, out MethodInfo? methodInfo))
                            return false;

                        int fieldCount = reader.ReadInt32();

                        object recreatedTarget = Activator.CreateInstance(targetType)!;

                        for (int i = 0; i < fieldCount; i++)
                        {
                            string name = reader.ReadString();
                            int valueBufferLength = reader.ReadInt32();
                            byte[] valueBuffer = reader.ReadBytes(valueBufferLength);

                            FieldInfo? fieldInfo = targetType.GetField(name,
                                BindingFlags.Instance | BindingFlags.Public);

                            if (fieldInfo != null)
                            {
                                var variantValue = GD.BytesToVarWithObjects(valueBuffer);
                                object? managedValue = RuntimeTypeConversionHelper.ConvertToObjectOfType(
                                    (godot_variant)variantValue.NativeVar, fieldInfo.FieldType);
                                fieldInfo.SetValue(recreatedTarget, managedValue);
                            }
                        }

                        @delegate = Delegate.CreateDelegate(delegateType, recreatedTarget, methodInfo,
                            throwOnBindFailure: false);

                        if (@delegate == null)
                            return false;

                        return true;
                    }
                    default:
                        return false;
                }
            }
        }

        private static bool TryDeserializeMethodInfo(BinaryReader reader,
            [MaybeNullWhen(false)] out MethodInfo methodInfo)
        {
            methodInfo = null;

            Type? declaringType = DeserializeType(reader);

            if (declaringType == null)
                return false;

            string methodName = reader.ReadString();

            BindingFlags flags = (BindingFlags)reader.ReadInt32();

            bool hasReturn = reader.ReadBoolean();
            Type? returnType = hasReturn ? DeserializeType(reader) : typeof(void);

            int parametersCount = reader.ReadInt32();
            var parameterTypes = parametersCount == 0 ? Type.EmptyTypes : new Type[parametersCount];

            for (int i = 0; i < parametersCount; i++)
            {
                Type? parameterType = DeserializeType(reader);
                if (parameterType == null)
                    return false;
                parameterTypes[i] = parameterType;
            }

#pragma warning disable REFL045 // These flags are insufficient to match any members
            // TODO: Suppressing invalid warning, remove when issue is fixed
            // https://github.com/DotNetAnalyzers/ReflectionAnalyzers/issues/209
            methodInfo = declaringType.GetMethod(methodName, flags, null, parameterTypes, null);
#pragma warning restore REFL045
            return methodInfo != null && methodInfo.ReturnType == returnType;
        }

        private static Type? DeserializeType(BinaryReader reader)
        {
            int genericArgumentsCount = reader.ReadInt32();

            if (genericArgumentsCount == -1)
                return null;

            string assemblyName = reader.ReadString();

            if (assemblyName.Length == 0)
            {
                GD.PushError($"Missing assembly name of type when attempting to deserialize delegate");
                return null;
            }

            string typeFullName = reader.ReadString();
            var type = ReflectionUtils.FindTypeInLoadedAssemblies(assemblyName, typeFullName);

            if (type == null)
                return null; // Type not found

            if (genericArgumentsCount != 0)
            {
                var genericArgumentTypes = new Type[genericArgumentsCount];

                for (int i = 0; i < genericArgumentsCount; i++)
                {
                    Type? genericArgumentType = DeserializeType(reader);
                    if (genericArgumentType == null)
                        return null;
                    genericArgumentTypes[i] = genericArgumentType;
                }

                type = type.MakeGenericType(genericArgumentTypes);
            }

            return type;
        }

        // Returns true, if unloading the delegate is necessary for assembly unloading to succeed.
        // This check is not perfect and only intended to prevent things in GodotTools from being reloaded.
        internal static bool IsDelegateCollectible(Delegate @delegate)
        {
            if (@delegate.GetType().IsCollectible)
                return true;

            if (@delegate is MulticastDelegate multicastDelegate)
            {
                Delegate[] invocationList = multicastDelegate.GetInvocationList();

                if (invocationList.Length > 1)
                {
                    foreach (Delegate oneDelegate in invocationList)
                        if (IsDelegateCollectible(oneDelegate))
                            return true;

                    return false;
                }
            }

            if (@delegate.Method.IsCollectible)
                return true;

            object? target = @delegate.Target;

            if (target is not null && target.GetType().IsCollectible)
                return true;

            return false;
        }

        internal static class RuntimeTypeConversionHelper
        {
            public static godot_variant ConvertToVariant(object? obj)
            {
                if (obj == null)
                    return default;

                switch (obj)
                {
                    case bool @bool:
                        return VariantUtils.CreateFrom(@bool);
                    case char @char:
                        return VariantUtils.CreateFrom(@char);
                    case sbyte int8:
                        return VariantUtils.CreateFrom(int8);
                    case short int16:
                        return VariantUtils.CreateFrom(int16);
                    case int int32:
                        return VariantUtils.CreateFrom(int32);
                    case long int64:
                        return VariantUtils.CreateFrom(int64);
                    case byte uint8:
                        return VariantUtils.CreateFrom(uint8);
                    case ushort uint16:
                        return VariantUtils.CreateFrom(uint16);
                    case uint uint32:
                        return VariantUtils.CreateFrom(uint32);
                    case ulong uint64:
                        return VariantUtils.CreateFrom(uint64);
                    case float @float:
                        return VariantUtils.CreateFrom(@float);
                    case double @double:
                        return VariantUtils.CreateFrom(@double);
                    case Vector2 vector2:
                        return VariantUtils.CreateFrom(vector2);
                    case Vector2I vector2I:
                        return VariantUtils.CreateFrom(vector2I);
                    case Rect2 rect2:
                        return VariantUtils.CreateFrom(rect2);
                    case Rect2I rect2I:
                        return VariantUtils.CreateFrom(rect2I);
                    case Transform2D transform2D:
                        return VariantUtils.CreateFrom(transform2D);
                    case Vector3 vector3:
                        return VariantUtils.CreateFrom(vector3);
                    case Vector3I vector3I:
                        return VariantUtils.CreateFrom(vector3I);
                    case Vector4 vector4:
                        return VariantUtils.CreateFrom(vector4);
                    case Vector4I vector4I:
                        return VariantUtils.CreateFrom(vector4I);
                    case Basis basis:
                        return VariantUtils.CreateFrom(basis);
                    case Quaternion quaternion:
                        return VariantUtils.CreateFrom(quaternion);
                    case Transform3D transform3D:
                        return VariantUtils.CreateFrom(transform3D);
                    case Projection projection:
                        return VariantUtils.CreateFrom(projection);
                    case Aabb aabb:
                        return VariantUtils.CreateFrom(aabb);
                    case Color color:
                        return VariantUtils.CreateFrom(color);
                    case Plane plane:
                        return VariantUtils.CreateFrom(plane);
                    case Callable callable:
                        return VariantUtils.CreateFrom(callable);
                    case Signal signal:
                        return VariantUtils.CreateFrom(signal);
                    case string @string:
                        return VariantUtils.CreateFrom(@string);
                    case byte[] byteArray:
                        return VariantUtils.CreateFrom(byteArray);
                    case int[] int32Array:
                        return VariantUtils.CreateFrom(int32Array);
                    case long[] int64Array:
                        return VariantUtils.CreateFrom(int64Array);
                    case float[] floatArray:
                        return VariantUtils.CreateFrom(floatArray);
                    case double[] doubleArray:
                        return VariantUtils.CreateFrom(doubleArray);
                    case string[] stringArray:
                        return VariantUtils.CreateFrom(stringArray);
                    case Vector2[] vector2Array:
                        return VariantUtils.CreateFrom(vector2Array);
                    case Vector3[] vector3Array:
                        return VariantUtils.CreateFrom(vector3Array);
                    case Color[] colorArray:
                        return VariantUtils.CreateFrom(colorArray);
                    case StringName[] stringNameArray:
                        return VariantUtils.CreateFrom(stringNameArray);
                    case NodePath[] nodePathArray:
                        return VariantUtils.CreateFrom(nodePathArray);
                    case Rid[] ridArray:
                        return VariantUtils.CreateFrom(ridArray);
                    case GodotObject[] godotObjectArray:
                        return VariantUtils.CreateFrom(godotObjectArray);
                    case StringName stringName:
                        return VariantUtils.CreateFrom(stringName);
                    case NodePath nodePath:
                        return VariantUtils.CreateFrom(nodePath);
                    case Rid rid:
                        return VariantUtils.CreateFrom(rid);
                    case Collections.Dictionary godotDictionary:
                        return VariantUtils.CreateFrom(godotDictionary);
                    case Collections.Array godotArray:
                        return VariantUtils.CreateFrom(godotArray);
                    case Variant variant:
                        return VariantUtils.CreateFrom(variant);
                    case GodotObject godotObject:
                        return VariantUtils.CreateFrom(godotObject);
                    case Enum @enum:
                        return VariantUtils.CreateFrom(Convert.ToInt64(@enum, CultureInfo.InvariantCulture));
                    case Collections.IGenericGodotDictionary godotDictionary:
                        return VariantUtils.CreateFrom(godotDictionary.UnderlyingDictionary);
                    case Collections.IGenericGodotArray godotArray:
                        return VariantUtils.CreateFrom(godotArray.UnderlyingArray);
                }

                GD.PushError("Attempted to convert an unmarshallable managed type to Variant. Name: '" +
                             obj.GetType().FullName + ".");
                return new godot_variant();
            }

            private delegate object? ConvertToSystemObjectFunc(in godot_variant managed);

            private static readonly System.Collections.Generic.Dictionary<Type, ConvertToSystemObjectFunc>
                _toSystemObjectFuncByType = new()
                {
                    [typeof(bool)] = (in godot_variant variant) => VariantUtils.ConvertTo<bool>(variant),
                    [typeof(char)] = (in godot_variant variant) => VariantUtils.ConvertTo<char>(variant),
                    [typeof(sbyte)] = (in godot_variant variant) => VariantUtils.ConvertTo<sbyte>(variant),
                    [typeof(short)] = (in godot_variant variant) => VariantUtils.ConvertTo<short>(variant),
                    [typeof(int)] = (in godot_variant variant) => VariantUtils.ConvertTo<int>(variant),
                    [typeof(long)] = (in godot_variant variant) => VariantUtils.ConvertTo<long>(variant),
                    [typeof(byte)] = (in godot_variant variant) => VariantUtils.ConvertTo<byte>(variant),
                    [typeof(ushort)] = (in godot_variant variant) => VariantUtils.ConvertTo<ushort>(variant),
                    [typeof(uint)] = (in godot_variant variant) => VariantUtils.ConvertTo<uint>(variant),
                    [typeof(ulong)] = (in godot_variant variant) => VariantUtils.ConvertTo<ulong>(variant),
                    [typeof(float)] = (in godot_variant variant) => VariantUtils.ConvertTo<float>(variant),
                    [typeof(double)] = (in godot_variant variant) => VariantUtils.ConvertTo<double>(variant),
                    [typeof(Vector2)] = (in godot_variant variant) => VariantUtils.ConvertTo<Vector2>(variant),
                    [typeof(Vector2I)] = (in godot_variant variant) => VariantUtils.ConvertTo<Vector2I>(variant),
                    [typeof(Rect2)] = (in godot_variant variant) => VariantUtils.ConvertTo<Rect2>(variant),
                    [typeof(Rect2I)] = (in godot_variant variant) => VariantUtils.ConvertTo<Rect2I>(variant),
                    [typeof(Transform2D)] = (in godot_variant variant) => VariantUtils.ConvertTo<Transform2D>(variant),
                    [typeof(Vector3)] = (in godot_variant variant) => VariantUtils.ConvertTo<Vector3>(variant),
                    [typeof(Vector3I)] = (in godot_variant variant) => VariantUtils.ConvertTo<Vector3I>(variant),
                    [typeof(Basis)] = (in godot_variant variant) => VariantUtils.ConvertTo<Basis>(variant),
                    [typeof(Quaternion)] = (in godot_variant variant) => VariantUtils.ConvertTo<Quaternion>(variant),
                    [typeof(Transform3D)] = (in godot_variant variant) => VariantUtils.ConvertTo<Transform3D>(variant),
                    [typeof(Vector4)] = (in godot_variant variant) => VariantUtils.ConvertTo<Vector4>(variant),
                    [typeof(Vector4I)] = (in godot_variant variant) => VariantUtils.ConvertTo<Vector4I>(variant),
                    [typeof(Aabb)] = (in godot_variant variant) => VariantUtils.ConvertTo<Aabb>(variant),
                    [typeof(Color)] = (in godot_variant variant) => VariantUtils.ConvertTo<Color>(variant),
                    [typeof(Plane)] = (in godot_variant variant) => VariantUtils.ConvertTo<Plane>(variant),
                    [typeof(Callable)] = (in godot_variant variant) => VariantUtils.ConvertTo<Callable>(variant),
                    [typeof(Signal)] = (in godot_variant variant) => VariantUtils.ConvertTo<Signal>(variant),
                    [typeof(string)] = (in godot_variant variant) => VariantUtils.ConvertTo<string>(variant),
                    [typeof(byte[])] = (in godot_variant variant) => VariantUtils.ConvertTo<byte[]>(variant),
                    [typeof(int[])] = (in godot_variant variant) => VariantUtils.ConvertTo<int[]>(variant),
                    [typeof(long[])] = (in godot_variant variant) => VariantUtils.ConvertTo<long[]>(variant),
                    [typeof(float[])] = (in godot_variant variant) => VariantUtils.ConvertTo<float[]>(variant),
                    [typeof(double[])] = (in godot_variant variant) => VariantUtils.ConvertTo<double[]>(variant),
                    [typeof(string[])] = (in godot_variant variant) => VariantUtils.ConvertTo<string[]>(variant),
                    [typeof(Vector2[])] = (in godot_variant variant) => VariantUtils.ConvertTo<Vector2[]>(variant),
                    [typeof(Vector3[])] = (in godot_variant variant) => VariantUtils.ConvertTo<Vector3[]>(variant),
                    [typeof(Color[])] = (in godot_variant variant) => VariantUtils.ConvertTo<Color[]>(variant),
                    [typeof(StringName[])] =
                        (in godot_variant variant) => VariantUtils.ConvertTo<StringName[]>(variant),
                    [typeof(NodePath[])] = (in godot_variant variant) => VariantUtils.ConvertTo<NodePath[]>(variant),
                    [typeof(Rid[])] = (in godot_variant variant) => VariantUtils.ConvertTo<Rid[]>(variant),
                    [typeof(StringName)] = (in godot_variant variant) => VariantUtils.ConvertTo<StringName>(variant),
                    [typeof(NodePath)] = (in godot_variant variant) => VariantUtils.ConvertTo<NodePath>(variant),
                    [typeof(Rid)] = (in godot_variant variant) => VariantUtils.ConvertTo<Rid>(variant),
                    [typeof(Godot.Collections.Dictionary)] = (in godot_variant variant) =>
                        VariantUtils.ConvertTo<Godot.Collections.Dictionary>(variant),
                    [typeof(Godot.Collections.Array)] =
                        (in godot_variant variant) => VariantUtils.ConvertTo<Godot.Collections.Array>(variant),
                    [typeof(Variant)] = (in godot_variant variant) => VariantUtils.ConvertTo<Variant>(variant),
                };

            public static object? ConvertToObjectOfType(in godot_variant variant, Type type)
            {
                if (_toSystemObjectFuncByType.TryGetValue(type, out var func))
                    return func(variant);

                if (typeof(GodotObject).IsAssignableFrom(type))
                    return VariantUtils.ConvertTo<GodotObject>(variant);

                if (typeof(GodotObject[]).IsAssignableFrom(type))
                {
                    static GodotObject[] ConvertToSystemArrayOfGodotObject(in godot_array nativeArray, Type type)
                    {
                        var array = Collections.Array.CreateTakingOwnershipOfDisposableValue(
                            NativeFuncs.godotsharp_array_new_copy(nativeArray));

                        int length = array.Count;
                        var ret = (GodotObject[])Activator.CreateInstance(type, length)!;

                        for (int i = 0; i < length; i++)
                            ret[i] = array[i].AsGodotObject();

                        return ret;
                    }

                    using var godotArray = NativeFuncs.godotsharp_variant_as_array(variant);
                    return ConvertToSystemArrayOfGodotObject(godotArray, type);
                }

                if (type.IsEnum)
                {
                    var enumUnderlyingType = type.GetEnumUnderlyingType();

                    switch (Type.GetTypeCode(enumUnderlyingType))
                    {
                        case TypeCode.SByte:
                            return Enum.ToObject(type, VariantUtils.ConvertToInt8(variant));
                        case TypeCode.Int16:
                            return Enum.ToObject(type, VariantUtils.ConvertToInt16(variant));
                        case TypeCode.Int32:
                            return Enum.ToObject(type, VariantUtils.ConvertToInt32(variant));
                        case TypeCode.Int64:
                            return Enum.ToObject(type, VariantUtils.ConvertToInt64(variant));
                        case TypeCode.Byte:
                            return Enum.ToObject(type, VariantUtils.ConvertToUInt8(variant));
                        case TypeCode.UInt16:
                            return Enum.ToObject(type, VariantUtils.ConvertToUInt16(variant));
                        case TypeCode.UInt32:
                            return Enum.ToObject(type, VariantUtils.ConvertToUInt32(variant));
                        case TypeCode.UInt64:
                            return Enum.ToObject(type, VariantUtils.ConvertToUInt64(variant));
                        default:
                        {
                            GD.PushError(
                                "Attempted to convert Variant to enum value of unsupported underlying type. Name: " +
                                type.FullName + " : " + enumUnderlyingType.FullName + ".");
                            return null;
                        }
                    }
                }

                if (type.IsGenericType)
                {
                    var genericTypeDef = type.GetGenericTypeDefinition();

                    if (genericTypeDef == typeof(Godot.Collections.Dictionary<,>))
                    {
                        var ctor = type.GetConstructor(new[] { typeof(Godot.Collections.Dictionary) });

                        if (ctor == null)
                            throw new InvalidOperationException("Dictionary constructor not found");

                        return ctor.Invoke(new object?[]
                        {
                            VariantUtils.ConvertTo<Godot.Collections.Dictionary>(variant)
                        });
                    }

                    if (genericTypeDef == typeof(Godot.Collections.Array<>))
                    {
                        var ctor = type.GetConstructor(new[] { typeof(Godot.Collections.Array) });

                        if (ctor == null)
                            throw new InvalidOperationException("Array constructor not found");

                        return ctor.Invoke(new object?[]
                        {
                            VariantUtils.ConvertTo<Godot.Collections.Array>(variant)
                        });
                    }
                }

                GD.PushError($"Attempted to convert Variant to unsupported type. Name: {type.FullName}.");
                return null;
            }
        }
    }
}
