﻿// 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.Composition;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.NavigationBar;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
using static Microsoft.CodeAnalysis.NavigationBar.RoslynNavigationBarItem;

namespace Microsoft.CodeAnalysis.CSharp.NavigationBar
{
    [ExportLanguageService(typeof(INavigationBarItemService), LanguageNames.CSharp), Shared]
    internal class CSharpNavigationBarItemService : AbstractNavigationBarItemService
    {
        private static readonly SymbolDisplayFormat s_typeFormat =
            SymbolDisplayFormat.CSharpErrorMessageFormat.AddGenericsOptions(SymbolDisplayGenericsOptions.IncludeVariance);

        private static readonly SymbolDisplayFormat s_memberFormat =
            new(genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
                memberOptions: SymbolDisplayMemberOptions.IncludeParameters |
                               SymbolDisplayMemberOptions.IncludeExplicitInterface,
                typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameOnly,
                parameterOptions: SymbolDisplayParameterOptions.IncludeType |
                                  SymbolDisplayParameterOptions.IncludeName |
                                  SymbolDisplayParameterOptions.IncludeDefaultValue |
                                  SymbolDisplayParameterOptions.IncludeParamsRefOut,
                miscellaneousOptions: SymbolDisplayMiscellaneousOptions.UseSpecialTypes |
                                      SymbolDisplayMiscellaneousOptions.AllowDefaultLiteral |
                                      SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier);

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

        protected override async Task<ImmutableArray<RoslynNavigationBarItem>> GetItemsInCurrentProcessAsync(
            Document document, bool supportsCodeGeneration, CancellationToken cancellationToken)
        {
            var typesInFile = await GetTypesInFileAsync(document, cancellationToken).ConfigureAwait(false);
            var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
            return GetMembersInTypes(document.Project.Solution, tree, typesInFile, cancellationToken);
        }

        private static ImmutableArray<RoslynNavigationBarItem> GetMembersInTypes(
            Solution solution, SyntaxTree tree, IEnumerable<INamedTypeSymbol> types, CancellationToken cancellationToken)
        {
            using (Logger.LogBlock(FunctionId.NavigationBar_ItemService_GetMembersInTypes_CSharp, cancellationToken))
            {
                using var _1 = ArrayBuilder<RoslynNavigationBarItem>.GetInstance(out var items);

                foreach (var type in types)
                {
                    using var _2 = ArrayBuilder<RoslynNavigationBarItem>.GetInstance(out var memberItems);

                    foreach (var member in type.GetMembers())
                    {
                        if (member.IsImplicitlyDeclared ||
                            member.Kind == SymbolKind.NamedType ||
                            IsAccessor(member))
                        {
                            continue;
                        }

                        var method = member as IMethodSymbol;
                        if (method != null && method.PartialImplementationPart != null)
                        {
                            memberItems.AddIfNotNull(CreateItemForMember(solution, method, tree, cancellationToken));
                            memberItems.AddIfNotNull(CreateItemForMember(solution, method.PartialImplementationPart, tree, cancellationToken));
                        }
                        else
                        {
                            Debug.Assert(method == null || method.PartialDefinitionPart == null, "NavBar expected GetMembers to return partial method definition parts but the implementation part was returned.");

                            memberItems.AddIfNotNull(CreateItemForMember(solution, member, tree, cancellationToken));
                        }
                    }

                    memberItems.Sort((x, y) =>
                    {
                        var textComparison = x.Text.CompareTo(y.Text);
                        return textComparison != 0 ? textComparison : x.Grayed.CompareTo(y.Grayed);
                    });

                    var spans = GetSymbolLocation(solution, type, tree, cancellationToken);
                    if (spans == null)
                        continue;

                    items.Add(new SymbolItem(
                        type.Name,
                        text: type.ToDisplayString(s_typeFormat),
                        glyph: type.GetGlyph(),
                        isObsolete: type.IsObsolete(),
                        spans.Value,
                        childItems: memberItems.ToImmutable()));
                }

                items.Sort((x1, x2) => x1.Text.CompareTo(x2.Text));
                return items.ToImmutable();
            }
        }

        private static async Task<IEnumerable<INamedTypeSymbol>> GetTypesInFileAsync(Document document, CancellationToken cancellationToken)
        {
            var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);

            return GetTypesInFile(semanticModel, cancellationToken);
        }

        private static IEnumerable<INamedTypeSymbol> GetTypesInFile(SemanticModel semanticModel, CancellationToken cancellationToken)
        {
            using (Logger.LogBlock(FunctionId.NavigationBar_ItemService_GetTypesInFile_CSharp, cancellationToken))
            {
                var types = new HashSet<INamedTypeSymbol>();
                var nodesToVisit = new Stack<SyntaxNode>();

                nodesToVisit.Push(semanticModel.SyntaxTree.GetRoot(cancellationToken));

                while (!nodesToVisit.IsEmpty())
                {
                    if (cancellationToken.IsCancellationRequested)
                    {
                        return SpecializedCollections.EmptyEnumerable<INamedTypeSymbol>();
                    }

                    var node = nodesToVisit.Pop();
                    var type = GetType(semanticModel, node, cancellationToken);

                    if (type != null)
                    {
                        types.Add((INamedTypeSymbol)type);
                    }

                    if (node is BaseMethodDeclarationSyntax or
                        BasePropertyDeclarationSyntax or
                        BaseFieldDeclarationSyntax or
                        StatementSyntax or
                        ExpressionSyntax)
                    {
                        // quick bail out to prevent us from creating every nodes exist in current file
                        continue;
                    }

                    foreach (var child in node.ChildNodes())
                    {
                        nodesToVisit.Push(child);
                    }
                }

                return types;
            }
        }

        private static ISymbol? GetType(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken)
            => node switch
            {
                BaseTypeDeclarationSyntax t => semanticModel.GetDeclaredSymbol(t, cancellationToken),
                DelegateDeclarationSyntax d => semanticModel.GetDeclaredSymbol(d, cancellationToken),
                _ => null,
            };

        private static bool IsAccessor(ISymbol member)
        {
            if (member.Kind == SymbolKind.Method)
            {
                var method = (IMethodSymbol)member;

                return method.MethodKind is MethodKind.PropertyGet or MethodKind.PropertySet;
            }

            return false;
        }

        private static RoslynNavigationBarItem? CreateItemForMember(
            Solution solution, ISymbol member, SyntaxTree tree, CancellationToken cancellationToken)
        {
            var location = GetSymbolLocation(solution, member, tree, cancellationToken);
            if (location == null)
                return null;

            return new SymbolItem(
                member.Name,
                member.ToDisplayString(s_memberFormat),
                member.GetGlyph(),
                member.IsObsolete(),
                location.Value);
        }

        private static SymbolItemLocation? GetSymbolLocation(
            Solution solution, ISymbol symbol, SyntaxTree tree, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();

            if (symbol.Kind == SymbolKind.Field)
            {
                return symbol.ContainingType.TypeKind == TypeKind.Enum
                    ? GetSymbolLocation(solution, symbol, tree, static reference => GetEnumMemberSpan(reference))
                    : GetSymbolLocation(solution, symbol, tree, static reference => GetFieldReferenceSpan(reference));
            }
            else
            {
                return GetSymbolLocation(solution, symbol, tree, static reference => reference.Span);
            }
        }

        private static TextSpan GetFieldReferenceSpan(SyntaxReference reference)
        {
            var declaringNode = reference.GetSyntax();

            var spanStart = declaringNode.SpanStart;
            var spanEnd = declaringNode.Span.End;

            var fieldDeclaration = declaringNode.GetAncestor<FieldDeclarationSyntax>();
            if (fieldDeclaration != null)
            {
                var variables = fieldDeclaration.Declaration.Variables;

                if (variables.FirstOrDefault() == declaringNode)
                    spanStart = fieldDeclaration.SpanStart;

                if (variables.LastOrDefault() == declaringNode)
                    spanEnd = fieldDeclaration.Span.End;
            }

            return TextSpan.FromBounds(spanStart, spanEnd);
        }

        private static TextSpan GetEnumMemberSpan(SyntaxReference reference)
        {
            var declaringNode = reference.GetSyntax();
            if (declaringNode is EnumMemberDeclarationSyntax enumMember)
            {
                var enumDeclaration = enumMember.GetAncestor<EnumDeclarationSyntax>();

                if (enumDeclaration != null)
                {
                    var index = enumDeclaration.Members.IndexOf(enumMember);
                    if (index != -1 && index < enumDeclaration.Members.SeparatorCount)
                    {
                        // Cool, we have a comma, so do it
                        var start = enumMember.SpanStart;
                        var end = enumDeclaration.Members.GetSeparator(index).Span.End;

                        return TextSpan.FromBounds(start, end);
                    }
                }
            }

            return declaringNode.Span;
        }
    }
}
