﻿// 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.Composition;
using Microsoft.CodeAnalysis.CodeLens;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.LanguageService;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Host.Mef;

namespace Microsoft.CodeAnalysis.CSharp.CodeLens
{
    [ExportLanguageService(typeof(ICodeLensDisplayInfoService), LanguageNames.CSharp), Shared]
    internal sealed class CSharpCodeLensDisplayInfoService : ICodeLensDisplayInfoService
    {
        private static readonly SymbolDisplayFormat Format =
            SymbolDisplayFormat.CSharpErrorMessageFormat.RemoveMemberOptions(
                SymbolDisplayMemberOptions.IncludeExplicitInterface);

        [ImportingConstructor]
        [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
        public CSharpCodeLensDisplayInfoService()
        {
        }

        /// <summary>
        /// Returns the node that should be displayed
        /// </summary>
        public SyntaxNode GetDisplayNode(SyntaxNode node)
        {
            while (true)
            {
                switch (node.Kind())
                {
                    // LocalDeclarations do not have symbols themselves, you need a variable declarator
                    case SyntaxKind.LocalDeclarationStatement:
                        var localDeclarationNode = (LocalDeclarationStatementSyntax)node;
                        node = localDeclarationNode.Declaration.Variables.FirstOrDefault();
                        continue;

                    // Field and event declarations do not have symbols themselves, you need a variable declarator
                    case SyntaxKind.FieldDeclaration:
                    case SyntaxKind.EventFieldDeclaration:
                        var fieldNode = (BaseFieldDeclarationSyntax)node;
                        node = fieldNode.Declaration.Variables.FirstOrDefault();
                        continue;

                    // Variable is a field without access modifier. Parent is FieldDeclaration
                    case SyntaxKind.VariableDeclaration:
                        node = node.Parent;
                        continue;

                    // Built in types   
                    case SyntaxKind.PredefinedType:
                        node = node.Parent;
                        continue;

                    case SyntaxKind.MultiLineDocumentationCommentTrivia:
                    case SyntaxKind.SingleLineDocumentationCommentTrivia:
                        // For DocumentationCommentTrivia node, node.Parent is null. Obtain parent through ParentTrivia.Token
                        if (node.IsStructuredTrivia)
                        {
                            var structuredTriviaSyntax = (StructuredTriviaSyntax)node;
                            node = structuredTriviaSyntax.ParentTrivia.Token.Parent;
                            continue;
                        }

                        return null;

                    default:
                        return node;
                }
            }
        }

        /// <summary>
        /// Gets the DisplayName for the given node.
        /// </summary>
        public string GetDisplayName(SemanticModel semanticModel, SyntaxNode node)
        {
            if (node == null)
            {
                return FeaturesResources.paren_Unknown_paren;
            }

            if (CSharpSyntaxFacts.Instance.IsGlobalAssemblyAttribute(node))
            {
                return "assembly: " + node.ConvertToSingleLine();
            }

            // Don't discriminate between getters and setters for indexers
            if (node.Parent.IsKind(SyntaxKind.AccessorList) &&
                node.Parent.Parent.IsKind(SyntaxKind.IndexerDeclaration))
            {
                return GetDisplayName(semanticModel, node.Parent.Parent);
            }

            switch (node.Kind())
            {
                case SyntaxKind.ConstructorDeclaration:
                    {
                        // The constructor's name will be the name of the class, not ctor like we want
                        var symbol = semanticModel.GetDeclaredSymbol(node);
                        var displayName = symbol.ToDisplayString(Format);
                        var openParenIndex = displayName.IndexOf('(');
                        var lastDotBeforeOpenParenIndex = displayName.LastIndexOf('.', openParenIndex, openParenIndex);

                        var constructorName = symbol.IsStatic ? "cctor" : "ctor";

                        return displayName[..(lastDotBeforeOpenParenIndex + 1)] +
                               constructorName +
                               displayName[openParenIndex..];
                    }

                case SyntaxKind.IndexerDeclaration:
                    {
                        // The name will be "namespace.class.this[type] - we want "namespace.class[type] Indexer"
                        var symbol = semanticModel.GetDeclaredSymbol(node);
                        var displayName = symbol.ToDisplayString(Format);
                        var openBracketIndex = displayName.IndexOf('[');
                        var lastDotBeforeOpenBracketIndex = displayName.LastIndexOf('.', openBracketIndex, openBracketIndex);

                        return displayName[..lastDotBeforeOpenBracketIndex] +
                               displayName[openBracketIndex..] +
                               " Indexer";
                    }

                case SyntaxKind.OperatorDeclaration:
                    {
                        // The name will be "namespace.class.operator +(type)" - we want namespace.class.+(type) Operator
                        var symbol = semanticModel.GetDeclaredSymbol(node);
                        var displayName = symbol.ToDisplayString(Format);
                        var spaceIndex = displayName.IndexOf(' ');
                        var lastDotBeforeSpaceIndex = displayName.LastIndexOf('.', spaceIndex, spaceIndex);

                        return displayName[..(lastDotBeforeSpaceIndex + 1)] +
                               displayName[(spaceIndex + 1)..] +
                               " Operator";
                    }

                case SyntaxKind.ConversionOperatorDeclaration:
                    {
                        // The name will be "namespace.class.operator +(type)" - we want namespace.class.+(type) Operator
                        var symbol = semanticModel.GetDeclaredSymbol(node);
                        var displayName = symbol.ToDisplayString(Format);
                        var firstSpaceIndex = displayName.IndexOf(' ');
                        var secondSpaceIndex = displayName.IndexOf(' ', firstSpaceIndex + 1);
                        var lastDotBeforeSpaceIndex = displayName.LastIndexOf('.', firstSpaceIndex, firstSpaceIndex);

                        return displayName[..(lastDotBeforeSpaceIndex + 1)] +
                               displayName[(secondSpaceIndex + 1)..] +
                               " Operator";
                    }

                case SyntaxKind.UsingDirective:
                    {
                        // We want to see usings formatted as simply "Using", prefaced by the namespace they are in
                        var enclosingScopeString = GetEnclosingScopeString(node, semanticModel, Format);
                        return string.IsNullOrEmpty(enclosingScopeString) ? "Using" : enclosingScopeString + " Using";
                    }

                case SyntaxKind.ExternAliasDirective:
                    {
                        // We want to see aliases formatted as "Alias", prefaced by their enclosing scope, if any
                        var enclosingScopeString = GetEnclosingScopeString(node, semanticModel, Format);
                        return string.IsNullOrEmpty(enclosingScopeString) ? "Alias" : enclosingScopeString + " Alias";
                    }

                default:
                    {
                        var symbol = semanticModel.GetDeclaredSymbol(node);
                        return symbol != null ? symbol.ToDisplayString(Format) : FeaturesResources.paren_Unknown_paren;
                    }
            }
        }

        private static string GetEnclosingScopeString(SyntaxNode node, SemanticModel semanticModel, SymbolDisplayFormat symbolDisplayFormat)
        {
            var scopeNode = node;
            while (scopeNode != null && !SyntaxFacts.IsNamespaceMemberDeclaration(scopeNode.Kind()))
            {
                scopeNode = scopeNode.Parent;
            }

            if (scopeNode == null)
            {
                return null;
            }

            var scopeSymbol = semanticModel.GetDeclaredSymbol(scopeNode);
            return scopeSymbol.ToDisplayString(symbolDisplayFormat);
        }
    }
}
