using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using Godot.NativeInterop;

#nullable enable

namespace Godot
{
    internal static class DebuggingUtils
    {
        private static void AppendTypeName(this StringBuilder sb, Type type)
        {
            // Use the C# type keyword for built-in types.
            // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/built-in-types
            if (type == typeof(void))
                sb.Append("void");
            else if (type == typeof(bool))
                sb.Append("bool");
            else if (type == typeof(byte))
                sb.Append("byte");
            else if (type == typeof(sbyte))
                sb.Append("sbyte");
            else if (type == typeof(char))
                sb.Append("char");
            else if (type == typeof(decimal))
                sb.Append("decimal");
            else if (type == typeof(double))
                sb.Append("double");
            else if (type == typeof(float))
                sb.Append("float");
            else if (type == typeof(int))
                sb.Append("int");
            else if (type == typeof(uint))
                sb.Append("uint");
            else if (type == typeof(nint))
                sb.Append("nint");
            else if (type == typeof(nuint))
                sb.Append("nuint");
            else if (type == typeof(long))
                sb.Append("long");
            else if (type == typeof(ulong))
                sb.Append("ulong");
            else if (type == typeof(short))
                sb.Append("short");
            else if (type == typeof(ushort))
                sb.Append("ushort");
            else if (type == typeof(object))
                sb.Append("object");
            else if (type == typeof(string))
                sb.Append("string");
            else
                sb.Append(type);
        }

        internal static void InstallTraceListener()
        {
            Trace.Listeners.Clear();
            Trace.Listeners.Add(new GodotTraceListener());
        }

        [StructLayout(LayoutKind.Sequential)]
        // ReSharper disable once InconsistentNaming
        internal ref struct godot_stack_info
        {
            public godot_string File;
            public godot_string Func;
            public int Line;
        }

        [StructLayout(LayoutKind.Sequential)]
        // ReSharper disable once InconsistentNaming
        internal ref struct godot_stack_info_vector
        {
            private IntPtr _writeProxy;
            private unsafe godot_stack_info* _ptr;

            public readonly unsafe godot_stack_info* Elements
            {
                [MethodImpl(MethodImplOptions.AggressiveInlining)]
                get => _ptr;
            }

            public void Resize(int size)
            {
                if (size < 0)
                    throw new ArgumentOutOfRangeException(nameof(size));
                var err = NativeFuncs.godotsharp_stack_info_vector_resize(ref this, size);
                if (err != Error.Ok)
                    throw new InvalidOperationException("Failed to resize vector. Error code is: " + err.ToString());
            }

            public unsafe void Dispose()
            {
                if (_ptr == null)
                    return;
                NativeFuncs.godotsharp_stack_info_vector_destroy(ref this);
                _ptr = null;
            }
        }

        internal static unsafe StackFrame? GetCurrentStackFrame(int skipFrames = 0)
        {
            // We skip 2 frames:
            // The first skipped frame is the current method.
            // The second skipped frame is a method in NativeInterop.NativeFuncs.
            var stackTrace = new StackTrace(skipFrames: 2 + skipFrames, fNeedFileInfo: true);
            return stackTrace.GetFrame(0);
        }

        [UnmanagedCallersOnly]
        internal static unsafe void GetCurrentStackInfo(void* destVector)
        {
            try
            {
                var vector = (godot_stack_info_vector*)destVector;

                // We skip 2 frames:
                // The first skipped frame is the current method.
                // The second skipped frame is a method in NativeInterop.NativeFuncs.
                var stackTrace = new StackTrace(skipFrames: 2, fNeedFileInfo: true);
                int frameCount = stackTrace.FrameCount;

                if (frameCount == 0)
                    return;

                vector->Resize(frameCount);

                int i = 0;
                foreach (StackFrame frame in stackTrace.GetFrames())
                {
                    var method = frame.GetMethod();

                    if (method is MethodInfo methodInfo && methodInfo.IsDefined(typeof(StackTraceHiddenAttribute)))
                    {
                        // Skip methods marked hidden from the stack trace.
                        continue;
                    }

                    string? fileName = frame.GetFileName();
                    int fileLineNumber = frame.GetFileLineNumber();

                    GetStackFrameMethodDecl(frame, out string methodDecl);

                    godot_stack_info* stackInfo = &vector->Elements[i];

                    // Assign directly to element in Vector. This way we don't need to worry
                    // about disposal if an exception is thrown. The Vector takes care of it.
                    stackInfo->File = Marshaling.ConvertStringToNative(fileName);
                    stackInfo->Func = Marshaling.ConvertStringToNative(methodDecl);
                    stackInfo->Line = fileLineNumber;

                    i++;
                }

                // Resize the vector again in case we skipped some frames.
                vector->Resize(i);
            }
            catch (Exception e)
            {
                ExceptionUtils.LogException(e);
            }
        }

        internal static void GetStackFrameMethodDecl(StackFrame frame, out string methodDecl)
        {
            MethodBase? methodBase = frame.GetMethod();

            if (methodBase == null)
            {
                methodDecl = string.Empty;
                return;
            }

            var sb = new StringBuilder();

            if (methodBase is MethodInfo methodInfo)
            {
                sb.AppendTypeName(methodInfo.ReturnType);
                sb.Append(' ');
            }

            sb.Append(methodBase.DeclaringType?.FullName ?? "<unknown>");
            sb.Append('.');
            sb.Append(methodBase.Name);

            if (methodBase.IsGenericMethod)
            {
                Type[] genericParams = methodBase.GetGenericArguments();

                sb.Append('<');

                for (int j = 0; j < genericParams.Length; j++)
                {
                    if (j > 0)
                        sb.Append(", ");

                    sb.AppendTypeName(genericParams[j]);
                }

                sb.Append('>');
            }

            sb.Append('(');

            bool varArgs = (methodBase.CallingConvention & CallingConventions.VarArgs) != 0;

            ParameterInfo[] parameter = methodBase.GetParameters();

            for (int i = 0; i < parameter.Length; i++)
            {
                if (i > 0)
                    sb.Append(", ");

                if (i == parameter.Length - 1 && varArgs)
                    sb.Append("params ");

                sb.AppendTypeName(parameter[i].ParameterType);
            }

            sb.Append(')');

            methodDecl = sb.ToString();
        }
    }
}
