﻿// 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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Remote;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;

namespace Microsoft.CodeAnalysis.NavigateTo
{
    /// <summary>
    /// Host interface abstracting over all the external functionality the <see cref="NavigateToSearcher"/> needs. This
    /// provide an easy entry point for swapping out functionality of the host, including for testing purposes.
    /// </summary>
    internal interface INavigateToSearcherHost
    {
        INavigateToSearchService? GetNavigateToSearchService(Project project);

        /// <summary>
        /// Returns the fully loaded state for both the project system and the remote host.
        /// </summary>
        ValueTask<bool> IsFullyLoadedAsync(CancellationToken cancellationToken);
    }

    internal class DefaultNavigateToSearchHost(
        Solution solution,
        IAsynchronousOperationListener asyncListener,
        CancellationToken disposalToken) : INavigateToSearcherHost
    {
        private readonly Solution _solution = solution;
        private readonly IAsynchronousOperationListener _asyncListener = asyncListener;
        private readonly CancellationToken _disposalToken = disposalToken;

        /// <summary>
        /// Single task used to both hydrate the remote host with the initial workspace solution,
        /// and track if that work completed.  Prior to it completing, we will try to get all
        /// navigate-to requests from our caches.  Once it is populated though, we can attempt to
        /// use the latest data instead.
        /// </summary>
        private static readonly object s_gate = new();
        private static Task? s_remoteHostHydrateTask = null;

        public INavigateToSearchService? GetNavigateToSearchService(Project project)
            => project.GetLanguageService<INavigateToSearchService>();

        public async ValueTask<bool> IsFullyLoadedAsync(CancellationToken cancellationToken)
        {
            var service = _solution.Services.GetRequiredService<IWorkspaceStatusService>();

            // We consider ourselves fully loaded when both the project system has completed loaded
            // us, and we've totally hydrated the oop side.  Until that happens, we'll attempt to
            // return cached data from languages that support that.
            var isProjectSystemFullyLoaded = await service.IsFullyLoadedAsync(cancellationToken).ConfigureAwait(false);
            if (!isProjectSystemFullyLoaded)
                return false;

            var isRemoteHostFullyLoaded = GetRemoteHostHydrateTask().IsCompleted;
            return isRemoteHostFullyLoaded;
        }

        /// <summary>
        /// If we're in a solution that is using OOP, this kicks off a task to get the oop side in
        /// sync with us.  Until that happens, we'll continue to use the cached results from prior
        /// sessions so that we can get results very quickly right after launch without forcing the
        /// user to wait for OOP to hydrate the entire solution over.  This strikes a good balance
        /// of speed and accuracy as most of the time cached results will be fast and good enough,
        /// and eventually (usually within dozens of seconds, even for large projects) we will
        /// switch over to full and accurate results which can also come back quickly.
        /// </summary>
        /// <remarks>
        /// If we do report cached data, we inform the user of this so they know the results may be
        /// incomplete or inaccurate and that they can try again later if necessary.
        /// </remarks>
#pragma warning disable VSTHRD200 // Use "Async" suffix for async methods
        private Task GetRemoteHostHydrateTask()
#pragma warning restore VSTHRD200 // Use "Async" suffix for async methods
        {
            lock (s_gate)
            {
                // Only need to do this once.
                if (s_remoteHostHydrateTask == null)
                {
                    // If there are no projects in this solution that use OOP, then there's nothing we need to do.
                    if (_solution.Projects.All(p => !RemoteSupportedLanguages.IsSupported(p.Language)))
                    {
                        s_remoteHostHydrateTask = Task.CompletedTask;
                    }
                    else
                    {
                        var asyncToken = _asyncListener.BeginAsyncOperation(nameof(GetRemoteHostHydrateTask));

                        s_remoteHostHydrateTask = Task.Run(async () =>
                        {
                            var client = await RemoteHostClient.TryGetClientAsync(_solution.Services, _disposalToken).ConfigureAwait(false);
                            if (client != null)
                            {
                                await client.TryInvokeAsync<IRemoteNavigateToSearchService>(
                                    _solution,
                                    (service, solutionInfo, cancellationToken) =>
                                    service.HydrateAsync(solutionInfo, cancellationToken),
                                    _disposalToken).ConfigureAwait(false);
                            }
                        }, _disposalToken);
                        s_remoteHostHydrateTask.CompletesAsyncOperation(asyncToken);
                    }
                }

                return s_remoteHostHydrateTask;
            }
        }
    }
}
