﻿// 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.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Snippets;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Roslyn.Utilities;

namespace Microsoft.VisualStudio.LanguageServices.Implementation.LanguageService
{
    /// <summary>
    /// Creates services on the first connection of an applicable subject buffer to an IWpfTextView. 
    /// This ensures the services are available by the time an open document or the interactive window needs them.
    /// </summary>
    internal abstract class AbstractCreateServicesOnTextViewConnection : IWpfTextViewConnectionListener
    {
        private readonly string _languageName;
        private readonly AsyncBatchingWorkQueue<ProjectId?> _workQueue;
        private bool _initialized = false;

        protected VisualStudioWorkspace Workspace { get; }
        protected IGlobalOptionService GlobalOptions { get; }

        protected virtual Task InitializeServiceForProjectWithOpenedDocumentAsync(Project project)
            => Task.CompletedTask;

        public AbstractCreateServicesOnTextViewConnection(
            VisualStudioWorkspace workspace,
            IGlobalOptionService globalOptions,
            IAsynchronousOperationListenerProvider listenerProvider,
            IThreadingContext threadingContext,
            string languageName)
        {
            Workspace = workspace;
            GlobalOptions = globalOptions;
            _languageName = languageName;

            _workQueue = new AsyncBatchingWorkQueue<ProjectId?>(
                    TimeSpan.FromSeconds(1),
                    BatchProcessProjectsWithOpenedDocumentAsync,
                    EqualityComparer<ProjectId?>.Default,
                    listenerProvider.GetListener(FeatureAttribute.CompletionSet),
                    threadingContext.DisposalToken);

            Workspace.DocumentOpened += QueueWorkOnDocumentOpened;
        }

        void IWpfTextViewConnectionListener.SubjectBuffersConnected(IWpfTextView textView, ConnectionReason reason, Collection<ITextBuffer> subjectBuffers)
        {
            if (!_initialized)
            {
                _initialized = true;
                // use `null` to trigger per VS session intialization task
                _workQueue.AddWork((ProjectId?)null);
            }
        }

        void IWpfTextViewConnectionListener.SubjectBuffersDisconnected(IWpfTextView textView, ConnectionReason reason, Collection<ITextBuffer> subjectBuffers)
        {
        }

        private async ValueTask BatchProcessProjectsWithOpenedDocumentAsync(ImmutableSegmentedList<ProjectId?> projectIds, CancellationToken cancellationToken)
        {
            foreach (var projectId in projectIds)
            {
                cancellationToken.ThrowIfCancellationRequested();

                if (projectId is null)
                {
                    InitializePerVSSessionServices();
                }
                else if (Workspace.CurrentSolution.GetProject(projectId) is Project project)
                {
                    // Preload project completion providers at document open also helps avoid redundant file reads
                    // from a race caused by multiple features (codefix, refactoring, etc.) attempting to get extensions
                    // from analyzer references at the same time when they are not cached.
                    if (project.GetLanguageService<CompletionService>() is CompletionService completionService)
                        completionService.TriggerLoadProjectProviders(project);

                    await InitializeServiceForProjectWithOpenedDocumentAsync(project).ConfigureAwait(false);
                }
            }
        }

        private void QueueWorkOnDocumentOpened(object sender, DocumentEventArgs e)
        {
            if (e.Document.Project.Language == _languageName)
                _workQueue.AddWork(e.Document.Project.Id);
        }

        private void InitializePerVSSessionServices()
        {
            var languageServices = Workspace.Services.GetExtendedLanguageServices(_languageName);

            _ = languageServices.GetService<ISnippetInfoService>();

            // Preload completion providers on a background thread since assembly loads can be slow
            // https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1242321
            languageServices.GetService<CompletionService>()?.LoadImportedProviders();
        }
    }
}
