﻿// 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 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) ?? Array.Empty<string>()));
    }
}
