// 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

#pragma warning disable CA1825 // Avoid zero-length array allocations.

using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Text;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.MetadataReader;
using Microsoft.VisualStudio.Debugger;
using Microsoft.VisualStudio.Debugger.Clr;
using Microsoft.VisualStudio.Debugger.ComponentInterfaces;
using Microsoft.VisualStudio.Debugger.Evaluation;
using Microsoft.VisualStudio.Debugger.Evaluation.ClrCompilation;
using Type = Microsoft.VisualStudio.Debugger.Metadata.Type;

namespace Microsoft.CodeAnalysis.ExpressionEvaluator
{
    /// <summary>
    /// Computes string representations of <see cref="DkmClrValue"/> instances.
    /// </summary>
    internal abstract partial class Formatter : IDkmClrFormatter, IDkmClrFormatter2, IDkmClrFullNameProvider, IDkmClrFullNameProvider2
    {
        private readonly string _defaultFormat;
        private readonly string _nullString;
        private readonly string _thisString;
        private string _hostValueNotFoundString => Resources.HostValueNotFound;

        internal Formatter(string defaultFormat, string nullString, string thisString)
        {
            _defaultFormat = defaultFormat;
            _nullString = nullString;
            _thisString = thisString;
        }

        string IDkmClrFormatter.GetValueString(DkmClrValue value, DkmInspectionContext inspectionContext, ReadOnlyCollection<string> formatSpecifiers)
        {
            var useQuotes = (inspectionContext.EvaluationFlags & DkmEvaluationFlags.NoQuotes) == 0;
            var options = useQuotes
                ? ObjectDisplayOptions.UseQuotes | ObjectDisplayOptions.EscapeNonPrintableCharacters
                : ObjectDisplayOptions.None;
            return GetValueString(value, inspectionContext, options, GetValueFlags.IncludeObjectId);
        }

        string IDkmClrFormatter.GetTypeName(DkmInspectionContext inspectionContext, DkmClrType type, DkmClrCustomTypeInfo typeInfo, ReadOnlyCollection<string> formatSpecifiers)
        {
            bool unused;
            return GetTypeName(new TypeAndCustomInfo(type, typeInfo), escapeKeywordIdentifiers: false, sawInvalidIdentifier: out unused);
        }

        bool IDkmClrFormatter.HasUnderlyingString(DkmClrValue value, DkmInspectionContext inspectionContext)
        {
            return HasUnderlyingString(value, inspectionContext);
        }

        string IDkmClrFormatter.GetUnderlyingString(DkmClrValue value, DkmInspectionContext inspectionContext)
        {
            return GetUnderlyingString(value, inspectionContext);
        }

        string IDkmClrFormatter2.GetValueString(DkmClrValue value, DkmClrCustomTypeInfo customTypeInfo, DkmInspectionContext inspectionContext, ReadOnlyCollection<string> formatSpecifiers)
        {
            return value.GetValueString(inspectionContext, formatSpecifiers);
        }

        string IDkmClrFormatter2.GetEditableValueString(DkmClrValue value, DkmInspectionContext inspectionContext, DkmClrCustomTypeInfo customTypeInfo)
        {
            return GetEditableValue(value, inspectionContext);
        }

        string IDkmClrFullNameProvider.GetClrTypeName(DkmInspectionContext inspectionContext, DkmClrType clrType, DkmClrCustomTypeInfo customTypeInfo)
        {
            Debug.Assert(inspectionContext != null);
            bool sawInvalidIdentifier;
            var name = GetTypeName(new TypeAndCustomInfo(clrType, customTypeInfo), escapeKeywordIdentifiers: true, sawInvalidIdentifier: out sawInvalidIdentifier);
            return sawInvalidIdentifier ? null : name;
        }

        string IDkmClrFullNameProvider.GetClrArrayIndexExpression(DkmInspectionContext inspectionContext, string[] indices)
        {
            return GetArrayIndexExpression(indices);
        }

        string IDkmClrFullNameProvider.GetClrCastExpression(DkmInspectionContext inspectionContext, string argument, DkmClrType type, DkmClrCustomTypeInfo customTypeInfo, DkmClrCastExpressionOptions castExpressionOptions)
        {
            bool sawInvalidIdentifier;
            var name = GetTypeName(new TypeAndCustomInfo(type, customTypeInfo), escapeKeywordIdentifiers: true, sawInvalidIdentifier: out sawInvalidIdentifier);
            if (sawInvalidIdentifier)
            {
                return null;
            }
            return GetCastExpression(argument, name, castExpressionOptions);
        }

        string IDkmClrFullNameProvider.GetClrObjectCreationExpression(DkmInspectionContext inspectionContext, DkmClrType type, DkmClrCustomTypeInfo customTypeInfo, string[] arguments)
        {
            bool sawInvalidIdentifier;
            var name = GetTypeName(new TypeAndCustomInfo(type, customTypeInfo), escapeKeywordIdentifiers: true, sawInvalidIdentifier: out sawInvalidIdentifier);
            if (sawInvalidIdentifier)
            {
                return null;
            }
            return GetObjectCreationExpression(name, arguments);
        }

        string IDkmClrFullNameProvider.GetClrValidIdentifier(DkmInspectionContext inspectionContext, string identifier)
        {
            var pooledBuilder = PooledStringBuilder.GetInstance();
            var builder = pooledBuilder.Builder;
            bool sawInvalidIdentifier;
            AppendIdentifierEscapingPotentialKeywords(builder, identifier, out sawInvalidIdentifier);
            var result = sawInvalidIdentifier ? null : builder.ToString();
            pooledBuilder.Free();
            return result;
        }

        string IDkmClrFullNameProvider.GetClrExpressionAndFormatSpecifiers(DkmInspectionContext inspectionContext, string expression, out ReadOnlyCollection<string> formatSpecifiers)
        {
            return TrimAndGetFormatSpecifiers(expression, out formatSpecifiers);
        }

        bool IDkmClrFullNameProvider.ClrExpressionMayRequireParentheses(DkmInspectionContext inspectionContext, string expression)
        {
            return NeedsParentheses(expression);
        }

        string IDkmClrFullNameProvider.GetClrMemberName(
            DkmInspectionContext inspectionContext,
            string parentFullName,
            DkmClrType declaringType,
            DkmClrCustomTypeInfo declaringTypeInfo,
            string memberName,
            bool memberAccessRequiresExplicitCast,
            bool memberIsStatic)
        {
            string qualifier;
            if (memberIsStatic)
            {
                bool sawInvalidIdentifier;
                qualifier = GetTypeName(new TypeAndCustomInfo(declaringType, declaringTypeInfo), escapeKeywordIdentifiers: true, sawInvalidIdentifier: out sawInvalidIdentifier);
                if (sawInvalidIdentifier)
                {
                    return null; // FullName wouldn't be parseable.
                }
            }
            else if (memberAccessRequiresExplicitCast)
            {
                bool sawInvalidIdentifier;
                var typeName = GetTypeName(new TypeAndCustomInfo(declaringType, declaringTypeInfo), escapeKeywordIdentifiers: true, sawInvalidIdentifier: out sawInvalidIdentifier);
                if (sawInvalidIdentifier)
                {
                    return null; // FullName wouldn't be parseable.
                }
                qualifier = GetCastExpression(parentFullName, typeName, DkmClrCastExpressionOptions.ParenthesizeEntireExpression);
            }
            else
            {
                qualifier = parentFullName;
            }
            return $"{qualifier}.{memberName}";
        }

        string IDkmClrFullNameProvider.GetClrExpressionForNull(DkmInspectionContext inspectionContext)
        {
            return _nullString;
        }

        string IDkmClrFullNameProvider.GetClrExpressionForThis(DkmInspectionContext inspectionContext)
        {
            return _thisString;
        }

        string IDkmClrFullNameProvider2.GetClrNameForLocalVariable(DkmInspectionContext inspectionContext, DkmClrModuleInstance moduleInstance, DkmClrMethodId methodId, DkmILRange iLRange, DkmClrLocalVariable localVariable)
        {
            return GetOriginalLocalVariableName(localVariable.Name);
        }

        string IDkmClrFullNameProvider2.GetClrNameForField(DkmInspectionContext inspectionContext, DkmClrModuleInstance moduleInstance, int fieldToken)
        {
            var import = (IMetadataImport)moduleInstance.GetMetaDataImport();

            // Just get some of information about properties. Get rest later only if needed.
            int hr = import.GetFieldProps(fieldToken, out _, null, 0, out var nameLength, out _, out _, out _, out _, out _, out _);
            const int S_OK = 0;
            if (hr != S_OK)
            {
                throw new ArgumentException("Invalid field token.", nameof(fieldToken));
            }

            var sb = new StringBuilder(nameLength);
            hr = import.GetFieldProps(fieldToken, out _, sb, sb.Capacity, out _, out _, out _, out _, out _, out _, out _);
            if (hr != S_OK)
            {
                throw new DkmException((DkmExceptionCode)hr);
            }

            string metadataName = sb.ToString();
            return GetOriginalFieldName(metadataName);
        }

        // CONSIDER: If the number or complexity of the "language-specific syntax helpers" grows (or if
        // we make this a public API, it would be good to consider abstracting them into a separate object
        // that can be passed to the ResultProvider on construction (a "LanguageSyntax" service of sorts).
        // It seems more natural to ask these questions of the ResultProvider, but adding such a component
        // for these few methods seemed a bit overly elaborate given the current internal usage.
        #region Language-specific syntax helpers

        internal abstract bool IsValidIdentifier(string name);

        internal abstract bool IsIdentifierPartCharacter(char c);

        internal abstract bool IsPredefinedType(Type type);

        internal abstract bool IsWhitespace(char c);

        internal abstract string GetOriginalLocalVariableName(string name);
        internal abstract string GetOriginalFieldName(string name);

        // Note: We could be less conservative (e.g. "new C()").
        private bool NeedsParentheses(string expr)
        {
            int parens = 0;
            for (int i = 0; i < expr.Length; i++)
            {
                var ch = expr[i];
                switch (ch)
                {
                    case '(':
                        // Cast, "(A)b", requires parentheses.
                        if ((parens == 0) && FollowsCloseParen(expr, i))
                        {
                            return true;
                        }
                        parens++;
                        break;
                    case '[':
                        parens++;
                        break;
                    case ')':
                    case ']':
                        parens--;
                        break;
                    case '.':
                        break;
                    default:
                        if (parens == 0)
                        {
                            if (this.IsIdentifierPartCharacter(ch))
                            {
                                // Cast, "(A)b", requires parentheses.
                                if (FollowsCloseParen(expr, i))
                                {
                                    return true;
                                }
                            }
                            else
                            {
                                return true;
                            }
                        }
                        break;
                }
            }
            return false;
        }

        private static bool FollowsCloseParen(string expr, int index)
        {
            return (index > 0) && (expr[index - 1] == ')');
        }

        internal abstract string TrimAndGetFormatSpecifiers(string expression, out ReadOnlyCollection<string> formatSpecifiers);

        internal static readonly ReadOnlyCollection<string> NoFormatSpecifiers = new ReadOnlyCollection<string>(new string[0]);

        internal static ReadOnlyCollection<string> AddFormatSpecifier(ReadOnlyCollection<string> formatSpecifiers, string formatSpecifier)
        {
            if (formatSpecifiers.Contains(formatSpecifier))
            {
                return formatSpecifiers;
            }
            var builder = ArrayBuilder<string>.GetInstance();
            builder.AddRange(formatSpecifiers);
            builder.Add(formatSpecifier);
            return builder.ToImmutableAndFree();
        }

        protected string RemoveLeadingAndTrailingContent(string expression, int start, int length, Predicate<char> leading, Predicate<char> trailing)
        {
            int oldLength = expression.Length;
            for (; start < oldLength && leading(expression[start]); start++)
            {
            }
            for (; length > start && trailing(expression[length - 1]); length--)
            {
            }
            if ((start > 0) || (length < oldLength))
            {
                return expression.Substring(start, length - start);
            }
            return expression;
        }

        protected string RemoveLeadingAndTrailingWhitespace(string expression)
        {
            return RemoveLeadingAndTrailingContent(expression, 0, expression.Length, IsWhitespace, IsWhitespace);
        }

        protected string RemoveFormatSpecifiers(string expression, out ReadOnlyCollection<string> formatSpecifiers)
        {
            var builder = ArrayBuilder<string>.GetInstance();
            int oldLength = expression.Length;
            int newLength = oldLength;
            for (var i = oldLength - 1; i >= 0; i--)
            {
                var ch = expression[i];
                if (ch == ',')
                {
                    builder.Add(RemoveLeadingAndTrailingContent(expression, i + 1, newLength, IsWhitespace, IsWhitespace));
                    newLength = i;
                }
                else if (!IsIdentifierPartCharacter(ch) && !IsWhitespace(ch))
                {
                    break;
                }
            }

            if (builder.Count == 0)
            {
                formatSpecifiers = NoFormatSpecifiers;
            }
            else
            {
                var specifiers = builder.ToArray();
                Array.Reverse(specifiers);
                formatSpecifiers = new ReadOnlyCollection<string>(specifiers);
            }
            builder.Free();

            Debug.Assert((formatSpecifiers.Count == 0) == (newLength == oldLength));
            if (newLength < oldLength)
            {
                return expression.Substring(0, newLength);
            }
            return expression;
        }

        #endregion
    }
}
