﻿// 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.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Editor.TaskList;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.SolutionCrawler;
using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem;
using Microsoft.VisualStudio.LanguageServices.Implementation.TableDataSource;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Shell.TableManager;
using Roslyn.Utilities;
using TaskListItem = Microsoft.CodeAnalysis.TaskList.TaskListItem;

namespace Microsoft.VisualStudio.LanguageServices.TaskList
{
    [Export(typeof(VisualStudioTaskListService)), Shared]
    internal class VisualStudioTaskListService : ITaskListProvider
    {
        private readonly IThreadingContext _threadingContext;
        private readonly VisualStudioWorkspaceImpl _workspace;
        private readonly IGlobalOptionService _globalOptions;
        private readonly ITableManagerProvider _tableManagerProvider;
        private readonly IAsyncServiceProvider _asyncServiceProvider;

        public event EventHandler<TaskListUpdatedArgs>? TaskListUpdated;

        private readonly ConcurrentDictionary<DocumentId, ImmutableArray<TaskListItem>> _documentToTaskListItems = new();

        /// <summary>
        /// Queue where we enqueue the information we get from OOP to process in batch in the future.
        /// </summary>
        private readonly AsyncBatchingWorkQueue<(DocumentId documentId, ImmutableArray<TaskListItem> items)> _workQueue;

        [ImportingConstructor]
        [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
        public VisualStudioTaskListService(
            IThreadingContext threadingContext,
            VisualStudioWorkspaceImpl workspace,
            IGlobalOptionService globalOptions,
            ITableManagerProvider tableManagerProvider,
            SVsServiceProvider asyncServiceProvider,
            IAsynchronousOperationListenerProvider asynchronousOperationListenerProvider,
            [ImportMany] IEnumerable<Lazy<IEventListener, EventListenerMetadata>> eventListeners)
        {
            _threadingContext = threadingContext;
            _workspace = workspace;
            _globalOptions = globalOptions;
            _tableManagerProvider = tableManagerProvider;
            _asyncServiceProvider = (IAsyncServiceProvider)asyncServiceProvider;

            _workQueue = new AsyncBatchingWorkQueue<(DocumentId documentId, ImmutableArray<TaskListItem> items)>(
                TimeSpan.FromSeconds(1),
                ProcessTaskListItemsAsync,
                asynchronousOperationListenerProvider.GetListener(FeatureAttribute.TaskList),
                threadingContext.DisposalToken);
        }

        public void Start(VisualStudioWorkspace workspace)
        {
            // Fire and forget.
            _ = StartAsync(workspace);
        }

        private async Task StartAsync(VisualStudioWorkspace workspace)
        {
            // Have to catch all exceptions coming through here as this is called from a
            // fire-and-forget method and we want to make sure nothing leaks out.
            try
            {
                // Don't bother doing anything until the workspace has actually loaded.  We don't want to add to any
                // startup costs by doing work too early.
                var workspaceStatus = workspace.Services.GetRequiredService<IWorkspaceStatusService>();
                await workspaceStatus.WaitUntilFullyLoadedAsync(_threadingContext.DisposalToken).ConfigureAwait(false);

                // Wait until the task list is actually visible so that we don't perform pointless work analyzing files
                // when the user would not even see the results.  When we actually do register the analyzer (in
                // _listener.Start below), solution-crawler will reanalyze everything with this analyzer, so it will
                // still find and present all the relevant items to the user.
                await WaitUntilTaskListActivatedAsync().ConfigureAwait(false);

                // Now that we've started, create the actual VS todo list and have them hookup to us.
                _ = new VisualStudioTaskListTable(workspace, _threadingContext, _tableManagerProvider, this);

                // Now that we've hooked everything up, kick off the work to actually start computing and reporting items.
                RegisterIncrementalAnalyzerAndStartComputingTaskListItems();
            }
            catch (OperationCanceledException)
            {
                // Cancellation is normal (during VS closing).  Just ignore.
            }
            catch (Exception e) when (FatalError.ReportAndCatch(e))
            {
                // Otherwise report a watson for any other exception.  Don't bring down VS.  This is
                // a BG service we don't want impacting the user experience.
            }
        }

        private async Task WaitUntilTaskListActivatedAsync()
        {
            var cancellationToken = _threadingContext.DisposalToken;
            await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
            var taskList = await _asyncServiceProvider.GetServiceAsync<SVsTaskList, ITaskList>(_threadingContext.JoinableTaskFactory).ConfigureAwait(true);

            var control = taskList.TableControl.Control;

            // if control is already visible, we can proceed to collect task list items.
            if (control.IsVisible)
                return;

            // otherwise, wait for it to become visible.
            var taskSource = new TaskCompletionSource<bool>();
            control.IsVisibleChanged += Control_IsVisibleChanged;

            await taskSource.Task.ConfigureAwait(false);

            return;

            void Control_IsVisibleChanged(object sender, System.Windows.DependencyPropertyChangedEventArgs e)
            {
                if (control.IsVisible)
                {
                    control.IsVisibleChanged -= Control_IsVisibleChanged;
                    taskSource.TrySetResult(true);
                }
            }
        }

        public ImmutableArray<TaskListItem> GetTaskListItems(Workspace workspace, DocumentId documentId, CancellationToken cancellationToken)
            => _documentToTaskListItems.TryGetValue(documentId, out var values)
                ? values
                : ImmutableArray<TaskListItem>.Empty;

        private void RegisterIncrementalAnalyzerAndStartComputingTaskListItems()
        {
            // If we're in pull-diagnostics mode, then todo-comments will be handled by LSP.
            var diagnosticMode = _globalOptions.GetDiagnosticMode();
            if (diagnosticMode == DiagnosticMode.LspPull)
                return;

            // Do not register if solution crawler is explicitly off.
            if (!_globalOptions.GetOption(SolutionCrawlerRegistrationService.EnableSolutionCrawler))
                return;

            var services = _workspace.Services.SolutionServices;
            var registrationService = services.GetRequiredService<ISolutionCrawlerRegistrationService>();
            var analyzerProvider = new TaskListIncrementalAnalyzerProvider(_globalOptions, this);

            registrationService.AddAnalyzerProvider(
                analyzerProvider,
                new IncrementalAnalyzerProviderMetadata(
                    nameof(TaskListIncrementalAnalyzerProvider),
                    highPriorityForActiveFile: false,
                    workspaceKinds: WorkspaceKind.Host));
        }

        /// <summary>
        /// Callback from the OOP service back into us.
        /// </summary>
        public ValueTask ReportTaskListItemsAsync(DocumentId documentId, ImmutableArray<TaskListItem> items, CancellationToken cancellationToken)
        {
            try
            {
                _workQueue.AddWork((documentId, items));
                return ValueTaskFactory.CompletedTask;
            }
            catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken))
            {
                // report NFW before returning back to the remote process
                throw ExceptionUtilities.Unreachable();
            }
        }

        private ValueTask ProcessTaskListItemsAsync(
            ImmutableSegmentedList<(DocumentId documentId, ImmutableArray<TaskListItem> items)> docAndCommentsArray, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();

            using var _1 = ArrayBuilder<(DocumentId documentId, ImmutableArray<TaskListItem> items)>.GetInstance(out var filteredArray);
            AddFilteredItems(docAndCommentsArray, filteredArray);

            foreach (var (documentId, newItems) in filteredArray)
            {
                var oldComments = _documentToTaskListItems.TryGetValue(documentId, out var oldBoxedInfos)
                    ? oldBoxedInfos
                    : ImmutableArray<TaskListItem>.Empty;

                // only one thread can be executing ProcessTodoCommentInfosAsync at a time,
                // so it's safe to remove/add here.
                if (newItems.IsEmpty)
                {
                    _documentToTaskListItems.TryRemove(documentId, out _);
                }
                else
                {
                    _documentToTaskListItems[documentId] = newItems;
                }

                // If we have someone listening for updates, and our new items are different from
                // our old ones, then notify them of the change.
                var taskListUpdated = this.TaskListUpdated;
                if (TaskListUpdated != null && !oldComments.SequenceEqual(newItems))
                    TaskListUpdated?.Invoke(this, new TaskListUpdatedArgs(documentId, _workspace.CurrentSolution, documentId, newItems));
            }

            return ValueTaskFactory.CompletedTask;
        }

        private static void AddFilteredItems(
            ImmutableSegmentedList<(DocumentId documentId, ImmutableArray<TaskListItem> items)> array,
            ArrayBuilder<(DocumentId documentId, ImmutableArray<TaskListItem> items)> filteredArray)
        {
            using var _ = PooledHashSet<DocumentId>.GetInstance(out var seenDocumentIds);

            // Walk the list of todo comments in reverse, and skip any items for a document once
            // we've already seen it once.  That way, we're only reporting the most up to date
            // information for a document, and we're skipping the stale information.
            for (var i = array.Count - 1; i >= 0; i--)
            {
                var info = array[i];
                if (seenDocumentIds.Add(info.documentId))
                    filteredArray.Add(info);
            }
        }
    }
}
