﻿// 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.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.Peek;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.GoToDefinition;
using Microsoft.CodeAnalysis.Navigation;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.SymbolMapping;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Editor.Implementation.Peek
{
    internal sealed class PeekableItemSource : IPeekableItemSource
    {
        private readonly ITextBuffer _textBuffer;
        private readonly IPeekableItemFactory _peekableItemFactory;
        private readonly IPeekResultFactory _peekResultFactory;
        private readonly IThreadingContext _threadingContext;
        private readonly IUIThreadOperationExecutor _uiThreadOperationExecutor;

        public PeekableItemSource(
            ITextBuffer textBuffer,
            IPeekableItemFactory peekableItemFactory,
            IPeekResultFactory peekResultFactory,
            IThreadingContext threadingContext,
            IUIThreadOperationExecutor uiThreadOperationExecutor)
        {
            _textBuffer = textBuffer;
            _peekableItemFactory = peekableItemFactory;
            _peekResultFactory = peekResultFactory;
            _threadingContext = threadingContext;
            _uiThreadOperationExecutor = uiThreadOperationExecutor;
        }

        public void AugmentPeekSession(IPeekSession session, IList<IPeekableItem> peekableItems)
        {
            if (!string.Equals(session.RelationshipName, PredefinedPeekRelationships.Definitions.Name, StringComparison.OrdinalIgnoreCase))
            {
                return;
            }

            var triggerPoint = session.GetTriggerPoint(_textBuffer.CurrentSnapshot);
            if (!triggerPoint.HasValue)
            {
                return;
            }

            var document = triggerPoint.Value.Snapshot.GetOpenDocumentInCurrentContextWithChanges();
            if (document == null)
            {
                return;
            }

            _uiThreadOperationExecutor.Execute(EditorFeaturesResources.Peek, EditorFeaturesResources.Loading_Peek_information, allowCancellation: true, showProgress: false, action: context =>
            {
                _threadingContext.JoinableTaskFactory.Run(() => AugumentPeekSessionAsync(peekableItems, context, triggerPoint.Value, document));
            });
        }

        private async Task AugumentPeekSessionAsync(
            IList<IPeekableItem> peekableItems, IUIThreadOperationContext context, SnapshotPoint triggerPoint, Document document)
        {
            var cancellationToken = context.UserCancellationToken;
            var services = document.Project.Solution.Services;

            if (!document.SupportsSemanticModel)
            {
                // For documents without semantic models, just try to use the goto-def service
                // as a reasonable place to peek at.
                var goToDefinitionService = document.GetLanguageService<IGoToDefinitionService>();
                if (goToDefinitionService == null)
                {
                    return;
                }

                var navigableItems = await goToDefinitionService.FindDefinitionsAsync(document, triggerPoint.Position, cancellationToken).ConfigureAwait(false);
                await foreach (var item in GetPeekableItemsForNavigableItemsAsync(
                    navigableItems, document.Project, _peekResultFactory, cancellationToken).ConfigureAwait(false))
                {
                    peekableItems.Add(item);
                }
            }
            else
            {
                var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
                var semanticInfo = await SymbolFinder.GetSemanticInfoAtPositionAsync(
                    semanticModel,
                    triggerPoint.Position,
                    services,
                    cancellationToken).ConfigureAwait(false);
                var symbol = semanticInfo.GetAnySymbol(includeType: true);
                if (symbol == null)
                {
                    return;
                }

                symbol = symbol.GetOriginalUnreducedDefinition();

                // Get the symbol back from the originating workspace
                var symbolMappingService = services.GetRequiredService<ISymbolMappingService>();

                var mappingResult = await symbolMappingService.MapSymbolAsync(document, symbol, cancellationToken).ConfigureAwait(false);

                mappingResult ??= new SymbolMappingResult(document.Project, symbol);

                peekableItems.AddRange(await _peekableItemFactory.GetPeekableItemsAsync(
                    mappingResult.Symbol, mappingResult.Project, _peekResultFactory, cancellationToken).ConfigureAwait(false));
            }
        }

        private static async IAsyncEnumerable<IPeekableItem> GetPeekableItemsForNavigableItemsAsync(
            IEnumerable<INavigableItem>? navigableItems, Project project,
            IPeekResultFactory peekResultFactory,
            [EnumeratorCancellation] CancellationToken cancellationToken)
        {
            if (navigableItems != null)
            {
                var workspace = project.Solution.Workspace;
                var navigationService = workspace.Services.GetRequiredService<IDocumentNavigationService>();

                foreach (var item in navigableItems)
                {
                    var document = item.Document;
                    if (await navigationService.CanNavigateToPositionAsync(
                            workspace, document.Id, item.SourceSpan.Start, cancellationToken).ConfigureAwait(false))
                    {
                        var text = await document.GetTextAsync(project.Solution, cancellationToken).ConfigureAwait(false);
                        var linePositionSpan = text.Lines.GetLinePositionSpan(item.SourceSpan);
                        if (document.FilePath != null)
                        {
                            yield return new ExternalFilePeekableItem(
                                new FileLinePositionSpan(document.FilePath, linePositionSpan),
                                PredefinedPeekRelationships.Definitions, peekResultFactory);
                        }
                    }
                }
            }
        }

        public void Dispose()
        {
        }
    }
}
