// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable disable

using System;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using Microsoft.VisualStudio.LanguageServices;
using StreamJsonRpc;

namespace Microsoft.CodeAnalysis.ErrorReporting;

internal sealed partial class VisualStudioErrorReportingService
{
    private static string GetFormattedExceptionStack(Exception exception)
    {
        if (exception is AggregateException aggregate)
        {
            return GetStackForAggregateException(exception, aggregate);
        }

        if (exception is RemoteInvocationException)
        {
            return exception.ToString();
        }

        return GetStackForException(exception, includeMessageOnly: false);
    }

    private static string GetStackForAggregateException(Exception exception, AggregateException aggregate)
    {
        var text = GetStackForException(exception, includeMessageOnly: true);
        for (var i = 0; i < aggregate.InnerExceptions.Count; i++)
        {
            text = $"{text}{Environment.NewLine}---> (Inner Exception #{i}) {GetFormattedExceptionStack(aggregate.InnerExceptions[i])} <--- {Environment.NewLine}";
        }

        return text;
    }

    private static string GetStackForException(Exception exception, bool includeMessageOnly)
    {
        var message = exception.Message;
        var className = exception.GetType().ToString();
        var stackText = message.Length <= 0
            ? className
            : className + " : " + message;
        var innerException = exception.InnerException;
        if (innerException != null)
        {
            if (includeMessageOnly)
            {
                do
                {
                    stackText += " ---> " + innerException.Message;
                    innerException = innerException.InnerException;
                } while (innerException != null);
            }
            else
            {
                stackText += " ---> " + GetFormattedExceptionStack(innerException) + Environment.NewLine +
                             "   " + ServicesVSResources.End_of_inner_exception_stack;
            }
        }

        return stackText + Environment.NewLine + GetAsyncStackTrace(exception);
    }

    private static string GetAsyncStackTrace(Exception exception)
    {
        var stackTrace = new StackTrace(exception);
        var stackFrames = stackTrace.GetFrames();
        if (stackFrames == null)
        {
            return string.Empty;
        }

        var stackFrameLines = from frame in stackFrames
                              let method = frame.GetMethod()
                              let declaringType = method?.DeclaringType
                              where ShouldShowFrame(declaringType)
                              select FormatFrame(method, declaringType);
        var stringBuilder = new StringBuilder();
        return string.Join(Environment.NewLine, stackFrameLines);
    }

    private static bool ShouldShowFrame(Type declaringType)
        => !(declaringType != null && typeof(INotifyCompletion).IsAssignableFrom(declaringType));

    private static string FormatFrame(MethodBase method, Type declaringType)
    {
        var stringBuilder = new StringBuilder();
        stringBuilder.Append("   at ");
        var isAsync = FormatMethodName(stringBuilder, declaringType);
        if (!isAsync)
        {
            stringBuilder.Append(method?.Name);
            var methodInfo = method as MethodInfo;
            if (methodInfo?.IsGenericMethod == true)
            {
                FormatGenericArguments(stringBuilder, methodInfo.GetGenericArguments());
            }
        }
        else if (declaringType?.IsGenericType == true)
        {
            FormatGenericArguments(stringBuilder, declaringType.GetGenericArguments());
        }

        stringBuilder.Append('(');
        if (isAsync)
        {
            stringBuilder.Append(ServicesVSResources.Unknown_parameters);
        }
        else
        {
            FormatParameters(stringBuilder, method);
        }

        stringBuilder.Append(')');

        return stringBuilder.ToString();
    }

    private static bool FormatMethodName(StringBuilder stringBuilder, Type declaringType)
    {
        if (declaringType == null)
        {
            return false;
        }

        var fullName = declaringType.FullName.Replace('+', '.');
        if (typeof(IAsyncStateMachine).GetTypeInfo().IsAssignableFrom(declaringType))
        {
            stringBuilder.Append("async ");
            var start = fullName.LastIndexOf('<');
            var end = fullName.LastIndexOf('>');
            if (start >= 0 && end >= 0)
            {
                stringBuilder.Append(fullName.Remove(start, 1)[..(end - 1)]);
            }
            else
            {
                stringBuilder.Append(fullName);
            }

            return true;
        }
        else
        {
            stringBuilder.Append(fullName);
            stringBuilder.Append('.');
            return false;
        }
    }

    private static void FormatGenericArguments(StringBuilder stringBuilder, Type[] genericTypeArguments)
    {
        if (genericTypeArguments.Length <= 0)
        {
            return;
        }

        stringBuilder.Append("[" + string.Join(",", genericTypeArguments.Select(args => args.Name)) + "]");
    }

    private static void FormatParameters(StringBuilder stringBuilder, MethodBase method)
        => stringBuilder.Append(string.Join(",", method?.GetParameters().Select(t => (t.ParameterType?.Name ?? "<UnknownType>") + " " + t.Name) ?? []));
}
