﻿// 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.Immutable;
using System.Composition;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.Features.RQName;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.FindSymbols.Finders;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Shared.Collections;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.FindUsages
{
    using static FindUsagesHelpers;

    internal interface IDefinitionsAndReferencesFactory : IWorkspaceService
    {
        Task<DefinitionItem?> GetThirdPartyDefinitionItemAsync(
            Solution solution, DefinitionItem definitionItem, CancellationToken cancellationToken);
    }

    [ExportWorkspaceService(typeof(IDefinitionsAndReferencesFactory)), Shared]
    internal class DefaultDefinitionsAndReferencesFactory : IDefinitionsAndReferencesFactory
    {
        [ImportingConstructor]
        [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
        public DefaultDefinitionsAndReferencesFactory()
        {
        }

        /// <summary>
        /// Provides an extension point that allows for other workspace layers to add additional
        /// results to the results found by the FindReferences engine.
        /// </summary>
        public virtual Task<DefinitionItem?> GetThirdPartyDefinitionItemAsync(
            Solution solution, DefinitionItem definitionItem, CancellationToken cancellationToken)
        {
            return SpecializedTasks.Null<DefinitionItem>();
        }
    }

    internal static class DefinitionItemExtensions
    {
        private static readonly SymbolDisplayFormat s_namePartsFormat = new(
            memberOptions: SymbolDisplayMemberOptions.IncludeContainingType);

        public static DefinitionItem ToNonClassifiedDefinitionItem(
            this ISymbol definition,
            Solution solution,
            bool includeHiddenLocations)
            => ToNonClassifiedDefinitionItem(definition, solution, FindReferencesSearchOptions.Default, includeHiddenLocations);

        public static DefinitionItem ToNonClassifiedDefinitionItem(
            this ISymbol definition,
            Solution solution,
            FindReferencesSearchOptions options,
            bool includeHiddenLocations)
            => ToNonClassifiedDefinitionItem(definition, definition.Locations, solution, options, isPrimary: false, includeHiddenLocations);

        private static DefinitionItem ToNonClassifiedDefinitionItem(
            ISymbol definition,
            ImmutableArray<Location> locations,
            Solution solution,
            FindReferencesSearchOptions options,
            bool isPrimary,
            bool includeHiddenLocations)
            => ToDefinitionItem(definition, TryGetSourceLocations(definition, solution, locations, includeHiddenLocations), solution, options, isPrimary);

        public static async ValueTask<DefinitionItem> ToClassifiedDefinitionItemAsync(
            this ISymbol definition,
            IFindUsagesContext context,
            Solution solution,
            FindReferencesSearchOptions options,
            bool isPrimary,
            bool includeHiddenLocations,
            CancellationToken cancellationToken)
        {
            var unclassifiedSpans = TryGetSourceLocations(definition, solution, definition.Locations, includeHiddenLocations);
            var classifiedSpans = unclassifiedSpans.IsDefault ? default : await ClassifyDocumentSpansAsync(context, unclassifiedSpans, cancellationToken).ConfigureAwait(false);

            return ToDefinitionItem(definition, classifiedSpans, solution, options, isPrimary);
        }

        public static async ValueTask<DefinitionItem> ToClassifiedDefinitionItemAsync(
            this SymbolGroup group, IFindUsagesContext context, Solution solution, FindReferencesSearchOptions options, bool isPrimary, bool includeHiddenLocations, CancellationToken cancellationToken)
        {
            // Make a single definition item that knows about all the locations of all the symbols in the group.
            var definition = group.Symbols.First();

            var allLocations = group.Symbols.SelectMany(s => s.Locations).ToImmutableArray();
            var unclassifiedSpans = TryGetSourceLocations(definition, solution, allLocations, includeHiddenLocations);
            var classifiedSpans = unclassifiedSpans.IsDefault ? default : await ClassifyDocumentSpansAsync(context, unclassifiedSpans, cancellationToken).ConfigureAwait(false);

            return ToDefinitionItem(definition, classifiedSpans, solution, options, isPrimary);
        }

        private static DefinitionItem ToDefinitionItem(
            ISymbol definition,
            ImmutableArray<DocumentSpan> sourceLocations,
            Solution solution,
            FindReferencesSearchOptions options,
            bool isPrimary)
        {
            // Ensure we're working with the original definition for the symbol. I.e. When we're 
            // creating definition items, we want to create them for types like Dictionary<TKey,TValue>
            // not some random instantiation of that type.  
            //
            // This ensures that the type will both display properly to the user, as well as ensuring
            // that we can accurately resolve the type later on when we try to navigate to it.
            if (!definition.IsTupleField())
            {
                // In an earlier implementation of the compiler APIs, tuples and tuple fields symbols were definitions
                // We pretend this is still the case
                definition = definition.OriginalDefinition;
            }

            var displayParts = GetDisplayParts(definition);
            var nameDisplayParts = definition.ToDisplayParts(s_namePartsFormat).ToTaggedText();

            var tags = GlyphTags.GetTags(definition.GetGlyph());
            var displayIfNoReferences = definition.ShouldShowWithNoReferenceLocations(
                options, showMetadataSymbolsWithoutReferences: false);

            var properties = GetProperties(definition, isPrimary);

            if (sourceLocations.IsDefault)
            {
                return DefinitionItem.CreateMetadataDefinition(
                    tags, displayParts, nameDisplayParts, solution,
                    definition, properties, displayIfNoReferences);
            }

            if (sourceLocations.IsEmpty)
            {
                // If we got no definition locations, then create a sentinel one
                // that we can display but which will not allow navigation.
                return DefinitionItem.CreateNonNavigableItem(
                    tags, displayParts,
                    DefinitionItem.GetOriginationParts(definition),
                    properties, displayIfNoReferences);
            }

            var displayableProperties = AbstractReferenceFinder.GetAdditionalFindUsagesProperties(definition);

            return DefinitionItem.Create(
                tags, displayParts, sourceLocations,
                nameDisplayParts, properties, displayableProperties, displayIfNoReferences);
        }

        private static ImmutableArray<DocumentSpan> TryGetSourceLocations(ISymbol definition, Solution solution, ImmutableArray<Location> locations, bool includeHiddenLocations)
        {
            // If it's a namespace, don't create any normal location.  Namespaces
            // come from many different sources, but we'll only show a single 
            // root definition node for it.  That node won't be navigable.
            if (definition.Kind == SymbolKind.Namespace)
            {
                return ImmutableArray<DocumentSpan>.Empty;
            }

            // If it's a namespace, don't create any normal location.  Namespaces
            // come from many different sources, but we'll only show a single 
            // root definition node for it.  That node won't be navigable.
            using var sourceLocations = TemporaryArray<DocumentSpan>.Empty;

            foreach (var location in locations)
            {
                if (location.IsInMetadata)
                {
                    return default;
                }

                if (location.IsInSource)
                {
                    if (!location.IsVisibleSourceLocation() &&
                        !includeHiddenLocations)
                    {
                        continue;
                    }

                    var document = solution.GetDocument(location.SourceTree);
                    if (document != null)
                    {
                        sourceLocations.Add(new DocumentSpan(document, location.SourceSpan));
                    }
                }
            }

            return sourceLocations.ToImmutableAndClear();
        }

        private static ValueTask<ImmutableArray<DocumentSpan>> ClassifyDocumentSpansAsync(IFindUsagesContext context, ImmutableArray<DocumentSpan> unclassifiedSpans, CancellationToken cancellationToken)
            => unclassifiedSpans.SelectAsArrayAsync(async (documentSpan, context, cancellationToken) =>
            {
                var options = await context.GetOptionsAsync(documentSpan.Document.Project.Language, cancellationToken).ConfigureAwait(false);
                return await ClassifiedSpansAndHighlightSpanFactory.GetClassifiedDocumentSpanAsync(documentSpan.Document, documentSpan.SourceSpan, options.ClassificationOptions, cancellationToken).ConfigureAwait(false);
            }, context, cancellationToken);

        private static ImmutableDictionary<string, string> GetProperties(ISymbol definition, bool isPrimary)
        {
            var properties = ImmutableDictionary<string, string>.Empty;

            if (isPrimary)
            {
                properties = properties.Add(DefinitionItem.Primary, "");
            }

            var rqName = RQNameInternal.From(definition);
            if (rqName != null)
            {
                properties = properties.Add(DefinitionItem.RQNameKey1, rqName);
            }

            if (definition?.IsConstructor() == true)
            {
                // If the symbol being considered is a constructor include the containing type in case
                // a third party wants to navigate to that.
                rqName = RQNameInternal.From(definition.ContainingType);
                if (rqName != null)
                {
                    properties = properties.Add(DefinitionItem.RQNameKey2, rqName);
                }
            }

            return properties;
        }

        public static async Task<SourceReferenceItem?> TryCreateSourceReferenceItemAsync(
            this ReferenceLocation referenceLocation,
            IFindUsagesContext context,
            DefinitionItem definitionItem,
            bool includeHiddenLocations,
            CancellationToken cancellationToken)
        {
            var location = referenceLocation.Location;

            Debug.Assert(location.IsInSource);
            if (!location.IsVisibleSourceLocation() &&
                !includeHiddenLocations)
            {
                return null;
            }

            var document = referenceLocation.Document;
            var sourceSpan = location.SourceSpan;

            var options = await context.GetOptionsAsync(document.Project.Language, cancellationToken).ConfigureAwait(false);

            var documentSpan = await ClassifiedSpansAndHighlightSpanFactory.GetClassifiedDocumentSpanAsync(
                document, sourceSpan, options.ClassificationOptions, cancellationToken).ConfigureAwait(false);

            return new SourceReferenceItem(definitionItem, documentSpan, referenceLocation.SymbolUsageInfo, referenceLocation.AdditionalProperties);
        }
    }
}
