﻿// 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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Navigation;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.VisualStudio.GraphModel;
using Microsoft.VisualStudio.GraphModel.CodeSchema;
using Microsoft.VisualStudio.GraphModel.Schemas;
using Microsoft.CodeAnalysis.FindUsages;
using Microsoft.CodeAnalysis.GoToDefinition;
using Microsoft.CodeAnalysis.Editor.Host;

namespace Microsoft.VisualStudio.LanguageServices.Implementation.Progression
{
    using Workspace = Microsoft.CodeAnalysis.Workspace;

    internal sealed class GraphNavigatorExtension : ForegroundThreadAffinitizedObject, IGraphNavigateToItem
    {
        private readonly Workspace _workspace;
        private readonly Lazy<IStreamingFindUsagesPresenter> _streamingPresenter;

        public GraphNavigatorExtension(
            IThreadingContext threadingContext,
            Workspace workspace,
            Lazy<IStreamingFindUsagesPresenter> streamingPresenter)
            : base(threadingContext)
        {
            _workspace = workspace;
            _streamingPresenter = streamingPresenter;
        }

        public void NavigateTo(GraphObject graphObject)
        {

            if (graphObject is GraphNode graphNode)
            {
                var sourceLocation = graphNode.GetValue<SourceLocation>(CodeNodeProperties.SourceLocation);
                if (sourceLocation.FileName == null)
                {
                    return;
                }

                var projectId = graphNode.GetValue<ProjectId>(RoslynGraphProperties.ContextProjectId);
                var symbolId = graphNode.GetValue<SymbolKey?>(RoslynGraphProperties.SymbolId);

                if (projectId != null)
                {
                    var solution = _workspace.CurrentSolution;
                    var project = solution.GetProject(projectId);
                    if (project == null)
                        return;

                    var document = project.Documents.FirstOrDefault(
                        d => string.Equals(
                            d.FilePath,
                            sourceLocation.FileName.LocalPath,
                            StringComparison.OrdinalIgnoreCase));

                    if (document == null)
                        return;

                    this.ThreadingContext.JoinableTaskFactory.Run(() =>
                        NavigateToAsync(sourceLocation, symbolId, project, document, CancellationToken.None));
                }
            }
        }

        private async Task NavigateToAsync(
            SourceLocation sourceLocation, SymbolKey? symbolId, Project project, Document document, CancellationToken cancellationToken)
        {
            // Notify of navigation so third parties can intercept the navigation
            if (symbolId != null)
            {
                var symbol = symbolId.Value.Resolve(await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false), cancellationToken: cancellationToken).Symbol;
                await GoToDefinitionHelpers.TryNavigateToLocationAsync(
                    symbol, project.Solution, this.ThreadingContext, _streamingPresenter.Value, cancellationToken).ConfigureAwait(false);
                return;
            }

            if (sourceLocation.IsValid)
            {
                // We must find the right document in this project. This may not be the
                // ContextDocumentId if you have a partial member that is shown under one
                // document, but only exists in the other

                if (document != null)
                {
                    var editorWorkspace = document.Project.Solution.Workspace;
                    var navigationService = editorWorkspace.Services.GetService<IDocumentNavigationService>();

                    // TODO: Get the platform to use and pass us an operation context, or create one ourselves.
                    await navigationService.TryNavigateToLineAndOffsetAsync(
                        this.ThreadingContext,
                        editorWorkspace,
                        document.Id,
                        sourceLocation.StartPosition.Line,
                        sourceLocation.StartPosition.Character,
                        NavigationOptions.Default,
                        cancellationToken).ConfigureAwait(false);
                }
            }
        }

        public int GetRank(GraphObject graphObject)
        {
            if (graphObject is GraphNode graphNode)
            {
                var sourceLocation = graphNode.GetValue<SourceLocation>(CodeNodeProperties.SourceLocation);
                var projectId = graphNode.GetValue<ProjectId>(RoslynGraphProperties.ContextProjectId);

                if (sourceLocation.IsValid && projectId != null)
                {
                    return GraphNavigateToItemRanks.OwnItem;
                }
            }

            return GraphNavigateToItemRanks.CanNavigateToItem;
        }
    }
}
