﻿// 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.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor;
using Microsoft.CodeAnalysis.Editor.Host;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.FindUsages;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Navigation;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.CodeAnalysis.GoToDefinition
{
    internal abstract class AbstractAsyncGoToDefinitionService : AbstractFindDefinitionService, IAsyncGoToDefinitionService
    {
        private readonly IThreadingContext _threadingContext;
        private readonly IStreamingFindUsagesPresenter _streamingPresenter;

        protected AbstractAsyncGoToDefinitionService(
            IThreadingContext threadingContext,
            IStreamingFindUsagesPresenter streamingPresenter)
        {
            _threadingContext = threadingContext;
            _streamingPresenter = streamingPresenter;
        }

        private static Task<INavigableLocation?> GetNavigableLocationAsync(
            Document document, int position, CancellationToken cancellationToken)
        {
            var solution = document.Project.Solution;
            var workspace = solution.Workspace;
            var service = workspace.Services.GetRequiredService<IDocumentNavigationService>();

            return service.GetLocationForPositionAsync(
                workspace, document.Id, position, virtualSpace: 0, cancellationToken);
        }

        public async Task<(INavigableLocation? location, TextSpan symbolSpan)> FindDefinitionLocationAsync(
            Document document,
            int position,
            bool includeType,
            CancellationToken cancellationToken)
        {
            var symbolService = document.GetRequiredLanguageService<IGoToDefinitionSymbolService>();
            var (controlFlowTarget, controlFlowSpan) = await symbolService.GetTargetIfControlFlowAsync(
                document, position, cancellationToken).ConfigureAwait(false);
            if (controlFlowTarget != null)
            {
                var location = await GetNavigableLocationAsync(
                    document, controlFlowTarget.Value, cancellationToken).ConfigureAwait(false);
                return (location, controlFlowSpan);
            }
            else
            {
                // Try to compute the referenced symbol and attempt to go to definition for the symbol.
                var (symbol, project, span) = await symbolService.GetSymbolProjectAndBoundSpanAsync(
                    document, position, includeType, cancellationToken).ConfigureAwait(false);
                if (symbol is null)
                    return default;

                // if the symbol only has a single source location, and we're already on it,
                // try to see if there's a better symbol we could navigate to.
                var remappedLocation = await GetAlternativeLocationIfAlreadyOnDefinitionAsync(
                    project, position, symbol, originalDocument: document, cancellationToken).ConfigureAwait(false);
                if (remappedLocation != null)
                    return (remappedLocation, span);

                var isThirdPartyNavigationAllowed = await IsThirdPartyNavigationAllowedAsync(
                    symbol, position, document, cancellationToken).ConfigureAwait(false);

                var location = await GoToDefinitionHelpers.GetDefinitionLocationAsync(
                    symbol,
                    project.Solution,
                    _threadingContext,
                    _streamingPresenter,
                    thirdPartyNavigationAllowed: isThirdPartyNavigationAllowed,
                    cancellationToken: cancellationToken).ConfigureAwait(false);
                return (location, span);
            }
        }

        /// <summary>
        /// Attempts to find a better definition for the symbol, if the user is already on the definition of it.
        /// </summary>
        /// <param name="project">The project context to use for finding symbols</param>
        /// <param name="originalDocument">The document the user is navigating from. This may not be part of the project supplied.</param>
        private async Task<INavigableLocation?> GetAlternativeLocationIfAlreadyOnDefinitionAsync(
            Project project, int position, ISymbol symbol, Document originalDocument, CancellationToken cancellationToken)
        {
            var solution = project.Solution;

            var sourceLocations = symbol.Locations.WhereAsArray(loc => loc.IsInSource);
            if (sourceLocations.Length != 1)
                return null;

            var definitionLocation = sourceLocations[0];
            if (!definitionLocation.SourceSpan.IntersectsWith(position))
                return null;

            var definitionTree = definitionLocation.SourceTree;
            var definitionDocument = solution.GetDocument(definitionTree);
            if (definitionDocument != originalDocument)
                return null;

            // Ok, we were already on the definition. Look for better symbols we could show results
            // for instead. For now, just see if we're on an interface member impl. If so, we can
            // instead navigate to the actual interface member.
            //
            // In the future we can expand this with other mappings if appropriate.
            var interfaceImpls = symbol.ExplicitOrImplicitInterfaceImplementations();
            if (interfaceImpls.Length == 0)
                return null;

            var title = string.Format(EditorFeaturesResources._0_implemented_members,
                FindUsagesHelpers.GetDisplayName(symbol));

            using var _ = ArrayBuilder<DefinitionItem>.GetInstance(out var builder);
            foreach (var impl in interfaceImpls)
            {
                builder.AddRange(await GoToDefinitionFeatureHelpers.GetDefinitionsAsync(
                    impl, solution, thirdPartyNavigationAllowed: false, cancellationToken).ConfigureAwait(false));
            }

            var definitions = builder.ToImmutable();

            return await _streamingPresenter.GetStreamingLocationAsync(
                _threadingContext, solution.Workspace, title, definitions, cancellationToken).ConfigureAwait(false);
        }

        private static async Task<bool> IsThirdPartyNavigationAllowedAsync(
            ISymbol symbolToNavigateTo, int caretPosition, Document document, CancellationToken cancellationToken)
        {
            var syntaxRoot = document.GetRequiredSyntaxRootSynchronously(cancellationToken);
            var syntaxFactsService = document.GetRequiredLanguageService<ISyntaxFactsService>();
            var containingTypeDeclaration = syntaxFactsService.GetContainingTypeDeclaration(syntaxRoot, caretPosition);

            if (containingTypeDeclaration != null)
            {
                var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
                Debug.Assert(semanticModel != null);

                // Allow third parties to navigate to all symbols except types/constructors
                // if we are navigating from the corresponding type.

                if (semanticModel.GetDeclaredSymbol(containingTypeDeclaration, cancellationToken) is ITypeSymbol containingTypeSymbol &&
                    (symbolToNavigateTo is ITypeSymbol || symbolToNavigateTo.IsConstructor()))
                {
                    var candidateTypeSymbol = symbolToNavigateTo is ITypeSymbol
                        ? symbolToNavigateTo
                        : symbolToNavigateTo.ContainingType;

                    if (Equals(containingTypeSymbol, candidateTypeSymbol))
                    {
                        // We are navigating from the same type, so don't allow third parties to perform the navigation.
                        // This ensures that if we navigate to a class from within that class, we'll stay in the same file
                        // rather than navigate to, say, XAML.
                        return false;
                    }
                }
            }

            return true;
        }
    }
}
