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

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.DocumentationComments;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.LanguageService;

internal abstract partial class AbstractSymbolDisplayService
{
    protected abstract partial class AbstractSymbolDescriptionBuilder
    {
        private static readonly SymbolDisplayFormat s_typeParameterOwnerFormat =
            new(globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted,
                typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
                genericsOptions:
                    SymbolDisplayGenericsOptions.IncludeTypeParameters |
                    SymbolDisplayGenericsOptions.IncludeVariance |
                    SymbolDisplayGenericsOptions.IncludeTypeConstraints,
                memberOptions: SymbolDisplayMemberOptions.IncludeContainingType,
                parameterOptions: SymbolDisplayParameterOptions.None,
                miscellaneousOptions:
                    SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers |
                    SymbolDisplayMiscellaneousOptions.UseSpecialTypes |
                    SymbolDisplayMiscellaneousOptions.UseErrorTypeSymbolName);

        private static readonly SymbolDisplayFormat s_memberSignatureDisplayFormat =
            new(globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted,
                genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters | SymbolDisplayGenericsOptions.IncludeTypeConstraints,
                memberOptions:
                    SymbolDisplayMemberOptions.IncludeRef |
                    SymbolDisplayMemberOptions.IncludeType |
                    SymbolDisplayMemberOptions.IncludeParameters |
                    SymbolDisplayMemberOptions.IncludeContainingType,
                kindOptions:
                    SymbolDisplayKindOptions.IncludeMemberKeyword,
                propertyStyle:
                    SymbolDisplayPropertyStyle.ShowReadWriteDescriptor,
                parameterOptions:
                    SymbolDisplayParameterOptions.IncludeName |
                    SymbolDisplayParameterOptions.IncludeType |
                    SymbolDisplayParameterOptions.IncludeParamsRefOut |
                    SymbolDisplayParameterOptions.IncludeExtensionThis |
                    SymbolDisplayParameterOptions.IncludeDefaultValue |
                    SymbolDisplayParameterOptions.IncludeOptionalBrackets,
                localOptions:
                    SymbolDisplayLocalOptions.IncludeRef |
                    SymbolDisplayLocalOptions.IncludeType,
                miscellaneousOptions:
                    SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers |
                    SymbolDisplayMiscellaneousOptions.UseSpecialTypes |
                    SymbolDisplayMiscellaneousOptions.UseErrorTypeSymbolName |
                    SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier |
                    SymbolDisplayMiscellaneousOptions.AllowDefaultLiteral |
                    SymbolDisplayMiscellaneousOptions.CollapseTupleTypes);

        private static readonly SymbolDisplayFormat s_descriptionStyle =
            new(typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
                delegateStyle: SymbolDisplayDelegateStyle.NameAndSignature,
                genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters | SymbolDisplayGenericsOptions.IncludeVariance | SymbolDisplayGenericsOptions.IncludeTypeConstraints,
                parameterOptions: SymbolDisplayParameterOptions.IncludeType | SymbolDisplayParameterOptions.IncludeName | SymbolDisplayParameterOptions.IncludeParamsRefOut | SymbolDisplayParameterOptions.IncludeDefaultValue,
                miscellaneousOptions: SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | SymbolDisplayMiscellaneousOptions.CollapseTupleTypes,
                kindOptions: SymbolDisplayKindOptions.IncludeNamespaceKeyword | SymbolDisplayKindOptions.IncludeTypeKeyword);

        private static readonly SymbolDisplayFormat s_globalNamespaceStyle =
            new(globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Included);

        private readonly SemanticModel _semanticModel;
        private readonly int _position;
        private readonly Dictionary<SymbolDescriptionGroups, IList<SymbolDisplayPart>> _groupMap = [];
        private readonly Dictionary<SymbolDescriptionGroups, ImmutableArray<TaggedText>> _documentationMap = [];
        private readonly Func<ISymbol?, string?> _getNavigationHint;

        protected readonly LanguageServices LanguageServices;
        protected readonly SymbolDescriptionOptions Options;
        protected readonly CancellationToken CancellationToken;

        protected AbstractSymbolDescriptionBuilder(
            SemanticModel semanticModel,
            int position,
            LanguageServices languageServices,
            SymbolDescriptionOptions options,
            CancellationToken cancellationToken)
        {
            LanguageServices = languageServices;
            Options = options;
            CancellationToken = cancellationToken;
            _semanticModel = semanticModel;
            _position = position;
            _getNavigationHint = GetNavigationHint;
        }

        protected abstract void AddExtensionPrefix();
        protected abstract void AddAwaitablePrefix();
        protected abstract void AddAwaitableExtensionPrefix();
        protected abstract void AddDeprecatedPrefix();
        protected abstract void AddEnumUnderlyingTypeSeparator();
        protected abstract Task<ImmutableArray<SymbolDisplayPart>> GetInitializerSourcePartsAsync(ISymbol symbol);
        protected abstract ImmutableArray<SymbolDisplayPart> ToMinimalDisplayParts(ISymbol symbol, SemanticModel semanticModel, int position, SymbolDisplayFormat format);
        protected abstract string? GetNavigationHint(ISymbol? symbol);
        protected abstract string GetCommentText(SyntaxTrivia trivia);

        protected abstract SymbolDisplayFormat MinimallyQualifiedFormat { get; }
        protected abstract SymbolDisplayFormat MinimallyQualifiedFormatWithConstants { get; }
        protected abstract SymbolDisplayFormat MinimallyQualifiedFormatWithConstantsAndModifiers { get; }

        protected SemanticModel GetSemanticModel(SyntaxTree tree)
        {
            if (_semanticModel.SyntaxTree == tree)
            {
                return _semanticModel;
            }

            var model = _semanticModel.GetOriginalSemanticModel();
            if (model.Compilation.ContainsSyntaxTree(tree))
            {
                return model.Compilation.GetSemanticModel(tree);
            }

            // it is from one of its p2p references
            foreach (var referencedCompilation in model.Compilation.GetReferencedCompilations())
            {
                // find the reference that contains the given tree
                if (referencedCompilation.ContainsSyntaxTree(tree))
                {
                    return referencedCompilation.GetSemanticModel(tree);
                }
            }

            // the tree, a source symbol is defined in, doesn't exist in universe
            // how this can happen?
            Debug.Assert(false, "How?");
            return null;
        }

        protected Compilation Compilation
            => _semanticModel.Compilation;

        protected SemanticModel SemanticModel
            => _semanticModel;

        protected int Position
            => _position;

        protected virtual ImmutableArray<SymbolDisplayPart> WrapConstraints(ISymbol symbol, ImmutableArray<SymbolDisplayPart> displayParts)
            => displayParts;

        private async Task AddPartsAsync(ImmutableArray<ISymbol> symbols)
        {
            var firstSymbol = symbols[0];

            // Grab the doc comment once as computing it for each portion we're concatenating can be expensive for
            // LSIF (which does this for every symbol in an entire solution).
            var firstSymbolDocumentationComment = GetAppropriateDocumentationComment(firstSymbol);

            await AddDescriptionPartAsync(firstSymbol).ConfigureAwait(false);
            AddOverloadCountPart(symbols);

            var typeDisplayInfo = GetStructuralTypeDisplayInfo(firstSymbol);
            FixAllStructuralTypes(typeDisplayInfo);
            AddExceptions(firstSymbolDocumentationComment, typeDisplayInfo);
            AddCaptures(this._semanticModel, firstSymbol, typeDisplayInfo);
            AddDocumentationContent(firstSymbol, firstSymbolDocumentationComment, typeDisplayInfo);
        }

        private DocumentationComment GetAppropriateDocumentationComment(ISymbol firstSymbol)
        {
            // For locals, we synthesize the documentation comment from the leading trivia of the local declaration.
            return firstSymbol is ILocalSymbol localSymbol
                ? SynthesizeDocumentationCommentForLocal(localSymbol)
                : firstSymbol.GetAppropriateDocumentationComment(this.Compilation, this.CancellationToken);
        }

        private DocumentationComment SynthesizeDocumentationCommentForLocal(
            ILocalSymbol localSymbol)
        {
            if (localSymbol.DeclaringSyntaxReferences is not [var reference])
                return DocumentationComment.Empty;

            var cancellationToken = this.CancellationToken;
            var node = reference.GetSyntax(cancellationToken);

            var syntaxFacts = LanguageServices.GetRequiredService<ISyntaxFactsService>();
            var statement = node.AncestorsAndSelf().FirstOrDefault(syntaxFacts.IsStatement);
            if (statement is null)
                return DocumentationComment.Empty;

            var leadingTrivia = statement.GetLeadingTrivia();

            var startIndex = leadingTrivia.Count;

            // Consume any directly leading contiguous comments right before the statement.
            using var _1 = ArrayBuilder<SyntaxTrivia>.GetInstance(out var commentsAndNewLines);
            while (true)
            {
                // Skip indentation if present.
                if (syntaxFacts.IsWhitespaceTrivia(GetTrivia(startIndex - 1)))
                    startIndex--;

                if (!syntaxFacts.IsEndOfLineTrivia(GetTrivia(--startIndex)) ||
                    !syntaxFacts.IsSingleLineCommentTrivia(GetTrivia(--startIndex)))
                {
                    break;
                }

                commentsAndNewLines.Add(GetTrivia(startIndex));
                commentsAndNewLines.Add(GetTrivia(startIndex + 1));
            }

            if (commentsAndNewLines.Count == 0)
                return DocumentationComment.Empty;

            // Remove the last trivia, as it's always an end of line trivia.  Then reverse the array since we placed
            // the elements in reverse order as we walked backwards.
            commentsAndNewLines.Count--;
            commentsAndNewLines.ReverseContents();

            // Concatenate the comment text and new lines into a single string.
            // The even items are the comments, and the odd items are the new lines.
            var text = commentsAndNewLines.Select((t, i) => i % 2 == 0 ? GetCommentText(t) : t.ToFullString()).Join("");

            // Try seeing if the text is actually just an real xml doc comment written by the user.
            var docComment = DocumentationComment.FromXmlFragment(text);
            if (docComment.SummaryText != null)
                return docComment;

            // Otherwise, try wrapping with <summary> tags to make it a valid doc comment.
            docComment = DocumentationComment.FromXmlFragment($"<summary>{text}</summary>");
            if (docComment.SummaryText != null)
                return docComment;

            // Try one final time, this type escaping `<` and `>` characters so that they don't cause XML parse errors.
            docComment = DocumentationComment.FromXmlFragment($"<summary>{text.Replace("<", "&lt;").Replace(">", "&gt;")}</summary>");
            if (docComment.SummaryText != null)
                return docComment;

            return DocumentationComment.Empty;

            SyntaxTrivia GetTrivia(int index)
                => index < 0 || index >= leadingTrivia.Count ? default : leadingTrivia[index];
        }

        protected virtual void AddDocumentationContent(
            ISymbol symbol,
            DocumentationComment documentationComment,
            StructuralTypeDisplayInfo typeDisplayInfo)
        {
            var formatter = LanguageServices.GetRequiredService<IDocumentationCommentFormattingService>();
            var format = ISymbolExtensions2.CrefFormat;

            _documentationMap.Add(
                SymbolDescriptionGroups.Documentation,
                formatter.Format(documentationComment.SummaryText, symbol, _semanticModel, _position, format, typeDisplayInfo, CancellationToken));

            _documentationMap.Add(
                SymbolDescriptionGroups.RemarksDocumentation,
                formatter.Format(documentationComment.RemarksText, symbol, _semanticModel, _position, format, typeDisplayInfo, CancellationToken));

            AddDocumentationPartsWithPrefix(documentationComment.ReturnsText, SymbolDescriptionGroups.ReturnsDocumentation, FeaturesResources.Returns_colon);
            AddDocumentationPartsWithPrefix(documentationComment.ValueText, SymbolDescriptionGroups.ValueDocumentation, FeaturesResources.Value_colon);

            return;

            void AddDocumentationPartsWithPrefix(string? rawXmlText, SymbolDescriptionGroups group, string prefix)
            {
                if (string.IsNullOrEmpty(rawXmlText))
                    return;

                var parts = formatter.Format(rawXmlText, symbol, _semanticModel, _position, format, typeDisplayInfo, CancellationToken);
                if (!parts.IsDefaultOrEmpty)
                {
                    _documentationMap.Add(group,
                    [
                        new TaggedText(TextTags.Text, prefix),
                        .. LineBreak().ToTaggedText(),
                        new TaggedText(TextTags.ContainerStart, "  "),
                        .. parts,
                        new TaggedText(TextTags.ContainerEnd, string.Empty),
                    ]);
                }
            }
        }

        private void AddExceptions(
            DocumentationComment documentationComment,
            StructuralTypeDisplayInfo typeDisplayInfo)
        {
            if (documentationComment.ExceptionTypes.Any())
            {
                var parts = new List<SymbolDisplayPart>();
                parts.AddLineBreak();
                parts.AddText(WorkspacesResources.Exceptions_colon);
                foreach (var exceptionString in documentationComment.ExceptionTypes)
                {
                    parts.AddRange(LineBreak());
                    parts.AddRange(Space(count: 2));
                    parts.AddRange(AbstractDocumentationCommentFormattingService.CrefToSymbolDisplayParts(
                        exceptionString, _position, _semanticModel, typeDisplayInfo: typeDisplayInfo));
                }

                AddToGroup(SymbolDescriptionGroups.Exceptions, parts);
            }
        }

        /// <summary>
        /// If the symbol is a local or anonymous function (lambda or delegate), adds the variables captured
        /// by that local or anonymous function to the "Captures" group.
        /// </summary>
        /// <param name="symbol"></param>
        protected abstract void AddCaptures(SemanticModel semanticModel, ISymbol symbol, StructuralTypeDisplayInfo displayInfo);

        /// <summary>
        /// Given the body of a local or an anonymous function (lambda or delegate), add the variables captured
        /// by that local or anonymous function to the "Captures" group.
        /// </summary>
        protected void AddCaptures(SemanticModel semanticModel, SyntaxNode syntax, StructuralTypeDisplayInfo displayInfo)
        {
            if (semanticModel.IsSpeculativeSemanticModel)
            {
                // The region analysis APIs used below are not meaningful/applicable in the context of speculation (because they are designed
                // to ask questions about an expression if it were in a certain *scope* of code, not if it were inserted at a certain *position*).
                //
                // But in the context of symbol completion, we do prepare a description for the symbol while speculating. Only the "main description"
                // section of that description will be displayed. We still add a "captures" section, just in case.
                AddToGroup(SymbolDescriptionGroups.Captures, LineBreak());
                AddToGroup(SymbolDescriptionGroups.Captures, PlainText($"{WorkspacesResources.Variables_captured_colon} ?"));
                return;
            }

            var analysis = semanticModel.AnalyzeDataFlow(syntax);
            var captures = analysis.CapturedInside.Except(analysis.VariablesDeclared).ToImmutableArray();
            if (!captures.IsEmpty)
            {
                var parts = new List<SymbolDisplayPart>();
                parts.AddLineBreak();
                parts.AddText(WorkspacesResources.Variables_captured_colon);
                var first = true;
                foreach (var captured in captures)
                {
                    if (!first)
                    {
                        parts.AddRange(Punctuation(","));
                    }

                    parts.AddRange(Space(count: 1));
                    parts.AddRange(displayInfo.ReplaceStructuralTypes(
                        ToMinimalDisplayParts(captured, s_formatForCaptures), semanticModel, syntax.SpanStart));
                    first = false;
                }

                AddToGroup(SymbolDescriptionGroups.Captures, parts);
            }
        }

        private static readonly SymbolDisplayFormat s_formatForCaptures = SymbolDisplayFormat.MinimallyQualifiedFormat
            .RemoveLocalOptions(SymbolDisplayLocalOptions.IncludeType)
            .RemoveParameterOptions(SymbolDisplayParameterOptions.IncludeType);

        public async Task<ImmutableArray<SymbolDisplayPart>> BuildDescriptionAsync(
            ImmutableArray<ISymbol> symbolGroup, SymbolDescriptionGroups groups)
        {
            Contract.ThrowIfFalse(symbolGroup.Length > 0);

            await AddPartsAsync(symbolGroup).ConfigureAwait(false);

            return BuildDescription(groups);
        }

        public async Task<IDictionary<SymbolDescriptionGroups, ImmutableArray<TaggedText>>> BuildDescriptionSectionsAsync(ImmutableArray<ISymbol> symbolGroup)
        {
            Contract.ThrowIfFalse(symbolGroup.Length > 0);

            await AddPartsAsync(symbolGroup).ConfigureAwait(false);

            return BuildDescriptionSections();
        }

        private async Task AddDescriptionPartAsync(ISymbol symbol)
        {
            if (symbol.IsObsolete())
            {
                AddDeprecatedPrefix();
            }

            if (symbol is IDiscardSymbol discard)
            {
                AddDescriptionForDiscard(discard);
            }
            else if (symbol is IDynamicTypeSymbol)
            {
                AddDescriptionForDynamicType();
            }
            else if (symbol is IFieldSymbol field)
            {
                await AddDescriptionForFieldAsync(field).ConfigureAwait(false);
            }
            else if (symbol is ILocalSymbol local)
            {
                await AddDescriptionForLocalAsync(local).ConfigureAwait(false);
            }
            else if (symbol is IMethodSymbol method)
            {
                AddDescriptionForMethod(method);
            }
            else if (symbol is ILabelSymbol label)
            {
                AddDescriptionForLabel(label);
            }
            else if (symbol is INamedTypeSymbol namedType)
            {
                if (namedType.IsTupleType)
                {
                    AddToGroup(SymbolDescriptionGroups.MainDescription,
                        symbol.ToDisplayParts(s_descriptionStyle));
                }
                else
                {
                    AddDescriptionForNamedType(namedType);
                }
            }
            else if (symbol is INamespaceSymbol namespaceSymbol)
            {
                AddDescriptionForNamespace(namespaceSymbol);
            }
            else if (symbol is IParameterSymbol parameter)
            {
                await AddDescriptionForParameterAsync(parameter).ConfigureAwait(false);
            }
            else if (symbol is IPropertySymbol property)
            {
                AddDescriptionForProperty(property);
            }
            else if (symbol is IRangeVariableSymbol rangeVariable)
            {
                AddDescriptionForRangeVariable(rangeVariable);
            }
            else if (symbol is ITypeParameterSymbol typeParameter)
            {
                AddDescriptionForTypeParameter(typeParameter);
            }
            else if (symbol is IAliasSymbol alias)
            {
                await AddDescriptionPartAsync(alias.Target).ConfigureAwait(false);
            }
            else
            {
                AddDescriptionForArbitrarySymbol(symbol);
            }
        }

        private ImmutableArray<SymbolDisplayPart> BuildDescription(SymbolDescriptionGroups groups)
        {
            var finalParts = new List<SymbolDisplayPart>();
            var orderedGroups = _groupMap.Keys.OrderBy((g1, g2) => g1 - g2);

            foreach (var group in orderedGroups)
            {
                if ((groups & group) == 0)
                {
                    continue;
                }

                if (!finalParts.IsEmpty())
                {
                    var newLines = GetPrecedingNewLineCount(group);
                    finalParts.AddRange(LineBreak(newLines));
                }

                var parts = _groupMap[group];
                finalParts.AddRange(parts);
            }

            return finalParts.AsImmutable();
        }

        private static int GetPrecedingNewLineCount(SymbolDescriptionGroups group)
        {
            switch (group)
            {
                case SymbolDescriptionGroups.MainDescription:
                    // these parts are continuations of whatever text came before them
                    return 0;

                case SymbolDescriptionGroups.Documentation:
                case SymbolDescriptionGroups.RemarksDocumentation:
                case SymbolDescriptionGroups.ReturnsDocumentation:
                case SymbolDescriptionGroups.ValueDocumentation:
                    return 1;

                case SymbolDescriptionGroups.StructuralTypes:
                    return 0;

                case SymbolDescriptionGroups.Exceptions:
                case SymbolDescriptionGroups.TypeParameterMap:
                case SymbolDescriptionGroups.Captures:
                    // Everything else is in a group on its own
                    return 2;

                default:
                    throw ExceptionUtilities.UnexpectedValue(group);
            }
        }

        private Dictionary<SymbolDescriptionGroups, ImmutableArray<TaggedText>> BuildDescriptionSections()
        {
            var includeNavigationHints = Options.QuickInfoOptions.IncludeNavigationHintsInQuickInfo;

            // Merge the two maps into one final result.
            var result = new Dictionary<SymbolDescriptionGroups, ImmutableArray<TaggedText>>(_documentationMap);
            foreach (var (group, parts) in _groupMap)
            {
                // To support CodeGeneration symbols, which do not support ToDisplayString we need to pass custom implementation:
                var taggedText = parts.ToTaggedText(TaggedTextStyle.None, _getNavigationHint, includeNavigationHints);
                if (group == SymbolDescriptionGroups.MainDescription)
                {
                    // Mark the main description as a code block.
                    taggedText = taggedText
                        .Insert(0, new TaggedText(TextTags.CodeBlockStart, string.Empty))
                        .Add(new TaggedText(TextTags.CodeBlockEnd, string.Empty));
                }

                result[group] = taggedText;
            }

            return result;
        }

        private void AddDescriptionForDynamicType()
        {
            AddToGroup(SymbolDescriptionGroups.MainDescription,
                Keyword("dynamic"));
            AddToGroup(SymbolDescriptionGroups.Documentation,
                PlainText(FeaturesResources.Represents_an_object_whose_operations_will_be_resolved_at_runtime));
        }

        private void AddDescriptionForNamedType(INamedTypeSymbol symbol)
        {
            if (symbol.IsAwaitableNonDynamic(_semanticModel, _position))
            {
                AddAwaitablePrefix();
            }

            if (symbol.TypeKind == TypeKind.Delegate)
            {
                var style = s_descriptionStyle.WithMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.UseSpecialTypes | SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier);

                // Under the covers anonymous delegates are represented with generic types. However, we don't want
                // to see the unbound form of that generic. We want to see the fully instantiated signature.
                var isAnonymousDelegate = symbol.IsAnonymousDelegateType();
                var displayParts = isAnonymousDelegate
                    ? symbol.ToDisplayParts(style)
                    : symbol.OriginalDefinition.ToDisplayParts(style);

                AddToGroup(SymbolDescriptionGroups.MainDescription, WrapConstraints(symbol, displayParts));

                // For non-anonymous delegates, we display OriginalDefinition which won't carry nullable
                // annotation, so we need to add '?' separately.
                if (!isAnonymousDelegate && symbol.NullableAnnotation == NullableAnnotation.Annotated)
                    AddToGroup(SymbolDescriptionGroups.MainDescription, new SymbolDisplayPart(SymbolDisplayPartKind.Punctuation, null, "?"));
            }
            else
            {
                AddToGroup(SymbolDescriptionGroups.MainDescription,
                    WrapConstraints(symbol.OriginalDefinition, symbol.OriginalDefinition.ToDisplayParts(s_descriptionStyle)));

                if (symbol.NullableAnnotation == NullableAnnotation.Annotated)
                    AddToGroup(SymbolDescriptionGroups.MainDescription, new SymbolDisplayPart(SymbolDisplayPartKind.Punctuation, null, "?"));
            }

            if (!symbol.IsUnboundGenericType &&
                !TypeArgumentsAndParametersAreSame(symbol) &&
                !symbol.IsAnonymousDelegateType())
            {
                var allTypeParameters = symbol.GetAllTypeParameters();
                var allTypeArguments = symbol.GetAllTypeArguments();

                AddTypeParameterMapPart(allTypeParameters, allTypeArguments);
            }

            if (symbol.IsEnumType() && symbol.EnumUnderlyingType!.SpecialType != SpecialType.System_Int32)
            {
                AddEnumUnderlyingTypeSeparator();
                var underlyingTypeDisplayParts = symbol.EnumUnderlyingType.ToDisplayParts(s_descriptionStyle.WithMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.UseSpecialTypes));
                AddToGroup(SymbolDescriptionGroups.MainDescription, underlyingTypeDisplayParts);
            }
        }

        private static bool TypeArgumentsAndParametersAreSame(INamedTypeSymbol symbol)
        {
            var typeArguments = symbol.GetAllTypeArguments();
            var typeParameters = symbol.GetAllTypeParameters();

            for (var i = 0; i < typeArguments.Length; i++)
            {
                var typeArgument = typeArguments[i];
                var typeParameter = typeParameters[i];
                if (typeArgument is ITypeParameterSymbol && typeArgument.Name == typeParameter.Name)
                {
                    continue;
                }

                return false;
            }

            return true;
        }

        private void AddDescriptionForNamespace(INamespaceSymbol symbol)
        {
            if (symbol.IsGlobalNamespace)
            {
                AddToGroup(SymbolDescriptionGroups.MainDescription,
                    symbol.ToDisplayParts(s_globalNamespaceStyle));
            }
            else
            {
                AddToGroup(SymbolDescriptionGroups.MainDescription,
                    symbol.ToDisplayParts(s_descriptionStyle));
            }
        }

        private async Task AddDescriptionForFieldAsync(IFieldSymbol symbol)
        {
            var parts = await GetFieldPartsAsync(symbol).ConfigureAwait(false);

            // Don't bother showing disambiguating text for enum members. The icon displayed
            // on Quick Info should be enough.
            if (symbol.ContainingType != null && symbol.ContainingType.TypeKind == TypeKind.Enum)
            {
                AddToGroup(SymbolDescriptionGroups.MainDescription, parts);
            }
            else
            {
                AddToGroup(SymbolDescriptionGroups.MainDescription,
                    symbol.IsConst
                        ? Description(FeaturesResources.constant)
                        : Description(FeaturesResources.field),
                    parts);
            }
        }

        private async Task<ImmutableArray<SymbolDisplayPart>> GetFieldPartsAsync(IFieldSymbol symbol)
        {
            if (symbol.IsConst)
            {
                var initializerParts = await GetInitializerSourcePartsAsync(symbol).ConfigureAwait(false);
                if (!initializerParts.IsDefaultOrEmpty)
                {
                    return
                    [
                        .. ToMinimalDisplayParts(symbol, MinimallyQualifiedFormat),
                        .. Space(),
                        .. Punctuation("="),
                        .. Space(),
                        .. initializerParts,
                    ];
                }
            }

            return ToMinimalDisplayParts(symbol, MinimallyQualifiedFormatWithConstantsAndModifiers);
        }

        private async Task AddDescriptionForLocalAsync(ILocalSymbol symbol)
        {
            var parts = await GetLocalPartsAsync(symbol).ConfigureAwait(false);

            AddToGroup(SymbolDescriptionGroups.MainDescription,
                symbol.IsConst
                    ? Description(FeaturesResources.local_constant)
                    : Description(FeaturesResources.local_variable),
                parts);
        }

        private async Task<ImmutableArray<SymbolDisplayPart>> GetLocalPartsAsync(ILocalSymbol symbol)
        {
            if (symbol.IsConst)
            {
                var initializerParts = await GetInitializerSourcePartsAsync(symbol).ConfigureAwait(false);
                if (initializerParts != null)
                {
                    return
                    [
                        .. ToMinimalDisplayParts(symbol, MinimallyQualifiedFormat),
                        .. Space(),
                        .. Punctuation("="),
                        .. Space(),
                        .. initializerParts,
                    ];
                }
            }

            return ToMinimalDisplayParts(symbol, MinimallyQualifiedFormatWithConstants);
        }

        private void AddDescriptionForLabel(ILabelSymbol symbol)
        {
            AddToGroup(SymbolDescriptionGroups.MainDescription,
                Description(FeaturesResources.label),
                ToMinimalDisplayParts(symbol));
        }

        private void AddDescriptionForRangeVariable(IRangeVariableSymbol symbol)
        {
            AddToGroup(SymbolDescriptionGroups.MainDescription,
               Description(FeaturesResources.range_variable),
               ToMinimalDisplayParts(symbol));
        }

        private void AddDescriptionForMethod(IMethodSymbol method)
        {
            // TODO : show duplicated member case
            var awaitable = method.IsAwaitableNonDynamic(_semanticModel, _position);
            var extension = method.IsExtensionMethod || method.MethodKind == MethodKind.ReducedExtension;
            if (awaitable && extension)
            {
                AddAwaitableExtensionPrefix();
            }
            else if (awaitable)
            {
                AddAwaitablePrefix();
            }
            else if (extension)
            {
                AddExtensionPrefix();
            }

            AddToGroup(SymbolDescriptionGroups.MainDescription,
                ToMinimalDisplayParts(method, s_memberSignatureDisplayFormat));
        }

        private async Task AddDescriptionForParameterAsync(IParameterSymbol symbol)
        {
            if (symbol.IsOptional)
            {
                var initializerParts = await GetInitializerSourcePartsAsync(symbol).ConfigureAwait(false);
                if (!initializerParts.IsDefaultOrEmpty)
                {
                    var parts = ToMinimalDisplayParts(symbol, MinimallyQualifiedFormat).ToList();
                    parts.AddRange(Space());
                    parts.AddRange(Punctuation("="));
                    parts.AddRange(Space());
                    parts.AddRange(initializerParts);

                    AddToGroup(SymbolDescriptionGroups.MainDescription,
                        Description(FeaturesResources.parameter), parts);

                    return;
                }
            }

            AddToGroup(SymbolDescriptionGroups.MainDescription,
                Description(symbol.IsDiscard ? FeaturesResources.discard : FeaturesResources.parameter),
                ToMinimalDisplayParts(symbol, MinimallyQualifiedFormatWithConstants));
        }

        private void AddDescriptionForDiscard(IDiscardSymbol symbol)
        {
            AddToGroup(SymbolDescriptionGroups.MainDescription,
                Description(FeaturesResources.discard),
                ToMinimalDisplayParts(symbol, MinimallyQualifiedFormatWithConstants));
        }

        protected void AddDescriptionForProperty(IPropertySymbol symbol)
        {
            AddToGroup(SymbolDescriptionGroups.MainDescription,
                ToMinimalDisplayParts(symbol, s_memberSignatureDisplayFormat));
        }

        private void AddDescriptionForArbitrarySymbol(ISymbol symbol)
        {
            AddToGroup(SymbolDescriptionGroups.MainDescription,
                ToMinimalDisplayParts(symbol));
        }

        private void AddDescriptionForTypeParameter(ITypeParameterSymbol symbol)
        {
            Contract.ThrowIfTrue(symbol.TypeParameterKind == TypeParameterKind.Cref);
            AddToGroup(SymbolDescriptionGroups.MainDescription,
                ToMinimalDisplayParts(symbol),
                Space(),
                PlainText(FeaturesResources.in_),
                Space(),
                ToMinimalDisplayParts(symbol.ContainingSymbol, s_typeParameterOwnerFormat));
        }

        private void AddOverloadCountPart(
            ImmutableArray<ISymbol> symbolGroup)
        {
            var count = GetOverloadCount(symbolGroup);
            if (count >= 1)
            {
                AddToGroup(SymbolDescriptionGroups.MainDescription,
                    Space(),
                    Punctuation("("),
                    Punctuation("+"),
                    Space(),
                    PlainText(count.ToString()),
                    Space(),
                    count == 1 ? PlainText(FeaturesResources.overload) : PlainText(FeaturesResources.overloads_),
                    Punctuation(")"));
            }
        }

        private static int GetOverloadCount(ImmutableArray<ISymbol> symbolGroup)
        {
            return symbolGroup.Select(s => s.OriginalDefinition)
                              .Where(s => !s.Equals(symbolGroup.First().OriginalDefinition))
                              .Where(s => s is IMethodSymbol || s.IsIndexer())
                              .Count();
        }

        protected void AddTypeParameterMapPart(
            ImmutableArray<ITypeParameterSymbol> typeParameters,
            ImmutableArray<ITypeSymbol> typeArguments)
        {
            using var _ = ArrayBuilder<SymbolDisplayPart>.GetInstance(out var parts);

            var count = typeParameters.Length;
            for (var i = 0; i < count; i++)
            {
                parts.AddRange(TypeParameterName(typeParameters[i].Name));
                parts.AddRange(Space());

                parts.AddRange(PlainText(FeaturesResources.is_));
                parts.AddRange(Space());
                parts.AddRange(ToMinimalDisplayParts(typeArguments[i]));

                if (i < count - 1)
                {
                    parts.AddRange(LineBreak());
                }
            }

            AddToGroup(SymbolDescriptionGroups.TypeParameterMap, parts);
        }

        protected void AddToGroup(SymbolDescriptionGroups group, params SymbolDisplayPart[] partsArray)
            => AddToGroup(group, (IEnumerable<SymbolDisplayPart>)partsArray);

        protected void AddToGroup(SymbolDescriptionGroups group, params IEnumerable<SymbolDisplayPart>[] partsArray)
        {
            var partsList = partsArray.Flatten().ToList();
            if (partsList.Count > 0)
            {
                if (!_groupMap.TryGetValue(group, out var existingParts))
                {
                    existingParts = [];
                    _groupMap.Add(group, existingParts);
                }

                existingParts.AddRange(partsList);
            }
        }

        private static IEnumerable<SymbolDisplayPart> Description(string description)
        {
            return
            [
                .. Punctuation("("),
                .. PlainText(description),
                .. Punctuation(")"),
                .. Space(),
            ];
        }

        protected static IEnumerable<SymbolDisplayPart> Keyword(string text)
            => Part(SymbolDisplayPartKind.Keyword, text);

        protected static IEnumerable<SymbolDisplayPart> LineBreak(int count = 1)
        {
            for (var i = 0; i < count; i++)
            {
                yield return new SymbolDisplayPart(SymbolDisplayPartKind.LineBreak, null, "\r\n");
            }
        }

        protected static IEnumerable<SymbolDisplayPart> PlainText(string text)
            => Part(SymbolDisplayPartKind.Text, text);

        protected static IEnumerable<SymbolDisplayPart> Punctuation(string text)
            => Part(SymbolDisplayPartKind.Punctuation, text);

        protected static IEnumerable<SymbolDisplayPart> Space(int count = 1)
        {
            yield return new SymbolDisplayPart(SymbolDisplayPartKind.Space, null, new string(' ', count));
        }

        protected ImmutableArray<SymbolDisplayPart> ToMinimalDisplayParts(ISymbol symbol, SymbolDisplayFormat? format = null)
        {
            format ??= MinimallyQualifiedFormat;
            return ToMinimalDisplayParts(symbol, _semanticModel, _position, format);
        }

        private static IEnumerable<SymbolDisplayPart> Part(SymbolDisplayPartKind kind, ISymbol? symbol, string text)
        {
            yield return new SymbolDisplayPart(kind, symbol, text);
        }

        private static IEnumerable<SymbolDisplayPart> Part(SymbolDisplayPartKind kind, string text)
            => Part(kind, null, text);

        private static IEnumerable<SymbolDisplayPart> TypeParameterName(string text)
            => Part(SymbolDisplayPartKind.TypeParameterName, text);
    }
}
