// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using Microsoft.Extensions.Internal;
#nullable enable

namespace Microsoft.Extensions.StackTrace.Sources
{
    internal class StackTraceHelper
    {
        public static IList<StackFrameInfo> GetFrames(Exception exception, out AggregateException? error)
        {
            if (exception == null)
            {
                error = default;
                return Array.Empty<StackFrameInfo>();
            }

            var needFileInfo = true;
            var stackTrace = new System.Diagnostics.StackTrace(exception, needFileInfo);
            var stackFrames = stackTrace.GetFrames();

            if (stackFrames == null)
            {
                error = default;
                return Array.Empty<StackFrameInfo>();
            }

            var frames = new List<StackFrameInfo>(stackFrames.Length);

            List<Exception>? exceptions = null;

            for (var i = 0; i < stackFrames.Length; i++)
            {
                var frame = stackFrames[i];
                var method = frame.GetMethod();

                // Always show last stackFrame
                if (!ShowInStackTrace(method) && i < stackFrames.Length - 1)
                {
                    continue;
                }

                var stackFrame = new StackFrameInfo(frame.GetFileLineNumber(), frame.GetFileName(), frame, GetMethodDisplayString(frame.GetMethod()));
                frames.Add(stackFrame);
            }

            if (exceptions != null)
            {
                error = new AggregateException(exceptions);
                return frames;
            }

            error = default;
            return frames;
        }

        internal static MethodDisplayInfo? GetMethodDisplayString(MethodBase? method)
        {
            // Special case: no method available
            if (method == null)
            {
                return null;
            }

            // Type name
            var type = method.DeclaringType;

            var methodName = method.Name;

            string? subMethod = null;
            if (type != null && type.IsDefined(typeof(CompilerGeneratedAttribute)) &&
                (typeof(IAsyncStateMachine).IsAssignableFrom(type) || typeof(IEnumerator).IsAssignableFrom(type)))
            {
                // Convert StateMachine methods to correct overload +MoveNext()
                if (TryResolveStateMachineMethod(ref method, out type))
                {
                    subMethod = methodName;
                }
            }

            string? declaringTypeName = null;
            // ResolveStateMachineMethod may have set declaringType to null
            if (type != null)
            {
                declaringTypeName = TypeNameHelper.GetTypeDisplayName(type, includeGenericParameterNames: true);
            }

            string? genericArguments = null;
            if (method.IsGenericMethod)
            {
                genericArguments = "<" + string.Join(", ", method.GetGenericArguments()
                    .Select(arg => TypeNameHelper.GetTypeDisplayName(arg, fullName: false, includeGenericParameterNames: true))) + ">";
            }

            // Method parameters
            var parameters = method.GetParameters().Select(parameter =>
            {
                var parameterType = parameter.ParameterType;

                var prefix = string.Empty;
                if (parameter.IsOut)
                {
                    prefix = "out";
                }
                else if (parameterType != null && parameterType.IsByRef)
                {
                    prefix = "ref";
                }

                var parameterTypeString = "?";
                if (parameterType != null)
                {
                    if (parameterType.IsByRef)
                    {
                        parameterType = parameterType.GetElementType();
                    }

                    parameterTypeString = TypeNameHelper.GetTypeDisplayName(parameterType!, fullName: false, includeGenericParameterNames: true);
                }

                return new ParameterDisplayInfo
                {
                    Prefix = prefix,
                    Name = parameter.Name,
                    Type = parameterTypeString,
                };
            });

            var methodDisplayInfo = new MethodDisplayInfo(declaringTypeName, method.Name, genericArguments, subMethod, parameters);

            return methodDisplayInfo;
        }

        private static bool ShowInStackTrace(MethodBase? method)
        {
            Debug.Assert(method != null);

            // Don't show any methods marked with the StackTraceHiddenAttribute
            // https://github.com/dotnet/coreclr/pull/14652
            if (HasStackTraceHiddenAttribute(method))
            {
                return false;
            }


            var type = method.DeclaringType;
            if (type == null)
            {
                return true;
            }

            if (HasStackTraceHiddenAttribute(type))
            {
                return false;
            }

            // Fallbacks for runtime pre-StackTraceHiddenAttribute
            if (type == typeof(ExceptionDispatchInfo) && method.Name == "Throw")
            {
                return false;
            }
            else if (type == typeof(TaskAwaiter) ||
                type == typeof(TaskAwaiter<>) ||
                type == typeof(ConfiguredTaskAwaitable.ConfiguredTaskAwaiter) ||
                type == typeof(ConfiguredTaskAwaitable<>.ConfiguredTaskAwaiter))
            {
                switch (method.Name)
                {
                    case "HandleNonSuccessAndDebuggerNotification":
                    case "ThrowForNonSuccess":
                    case "ValidateEnd":
                    case "GetResult":
                        return false;
                }
            }

            return true;
        }

        private static bool TryResolveStateMachineMethod(ref MethodBase method, out Type? declaringType)
        {
            Debug.Assert(method != null);
            Debug.Assert(method.DeclaringType != null);

            declaringType = method.DeclaringType;

            var parentType = declaringType.DeclaringType;
            if (parentType == null)
            {
                return false;
            }

            var methods = parentType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly);
            if (methods == null)
            {
                return false;
            }

            foreach (var candidateMethod in methods)
            {
                var attributes = candidateMethod.GetCustomAttributes<StateMachineAttribute>();
                if (attributes == null)
                {
                    continue;
                }

                foreach (var asma in attributes)
                {
                    if (asma.StateMachineType == declaringType)
                    {
                        method = candidateMethod;
                        declaringType = candidateMethod.DeclaringType;
                        // Mark the iterator as changed; so it gets the + annotation of the original method
                        // async statemachines resolve directly to their builder methods so aren't marked as changed
                        return asma is IteratorStateMachineAttribute;
                    }
                }
            }

            return false;
        }

        private static bool HasStackTraceHiddenAttribute(MemberInfo memberInfo)
        {
            IList<CustomAttributeData> attributes;
            try
            {
                // Accessing MembmerInfo.GetCustomAttributesData throws for some types (such as types in dynamically generated assemblies).
                // We'll skip looking up StackTraceHiddenAttributes on such types.
                attributes = memberInfo.GetCustomAttributesData();
            }
            catch
            {
                return false;
            }

            for (var i = 0; i < attributes.Count; i++)
            {
                if (attributes[i].AttributeType.Name == "StackTraceHiddenAttribute")
                {
                    return true;
                }
            }

            return false;
        }
    }
}
