﻿// 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.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.SolutionCrawler;
using Microsoft.CodeAnalysis.TaskList;

namespace Microsoft.VisualStudio.LanguageServices.TaskList
{
    internal sealed class TaskListIncrementalAnalyzer : IncrementalAnalyzerBase
    {
        private readonly object _gate = new();
        private ImmutableArray<string> _lastTokenList = ImmutableArray<string>.Empty;
        private ImmutableArray<TaskListItemDescriptor> _lastDescriptors = ImmutableArray<TaskListItemDescriptor>.Empty;

        /// <summary>
        /// Set of documents that we have reported an non-empty set of todo comments for.  Used so that we don't bother
        /// notifying the host about documents with empty-todo lists (the common case). Note: no locking is needed for
        /// this set as the incremental analyzer is guaranteed to make all calls sequentially to us.
        /// </summary>
        private readonly HashSet<DocumentId> _documentsWithTaskListItems = new();
        private readonly IGlobalOptionService _globalOptions;
        private readonly VisualStudioTaskListService _listener;

        public TaskListIncrementalAnalyzer(
            IGlobalOptionService globalOptions,
            VisualStudioTaskListService listener)
        {
            _globalOptions = globalOptions;
            _listener = listener;
        }

        public override Task RemoveDocumentAsync(DocumentId documentId, CancellationToken cancellationToken)
        {
            // Remove the doc id from what we're tracking to prevent unbounded growth in the set.

            // If the doc that is being removed is not in the set of docs we've told the host has todo comments,
            // then no need to notify the host at all about it.
            if (!_documentsWithTaskListItems.Remove(documentId))
                return Task.CompletedTask;

            // Otherwise, report that there should now be no todo comments for this doc.
            return _listener.ReportTaskListItemsAsync(documentId, ImmutableArray<TaskListItem>.Empty, cancellationToken).AsTask();
        }

        private ImmutableArray<TaskListItemDescriptor> GetDescriptors(ImmutableArray<string> tokenList)
        {
            lock (_gate)
            {
                if (!tokenList.SequenceEqual(_lastTokenList))
                {
                    _lastDescriptors = TaskListItemDescriptor.Parse(tokenList);
                    _lastTokenList = tokenList;
                }

                return _lastDescriptors;
            }
        }

        public override async Task AnalyzeSyntaxAsync(Document document, InvocationReasons reasons, CancellationToken cancellationToken)
        {
            var service = document.GetLanguageService<ITaskListService>();
            if (service == null)
                return;

            var options = _globalOptions.GetTaskListOptions();
            var descriptors = GetDescriptors(options.Descriptors);

            // We're out of date.  Recompute this info.
            var items = await service.GetTaskListItemsAsync(
                document, descriptors, cancellationToken).ConfigureAwait(false);

            if (items.IsEmpty)
            {
                // Remove this doc from the set of docs with todo comments in it. If this was a doc that previously
                // had todo comments in it, then fall through and notify the host so it can clear them out.
                // Otherwise, bail out as there's no need to inform the host of this.
                if (!_documentsWithTaskListItems.Remove(document.Id))
                    return;
            }
            else
            {
                // Doc has some todo comments, record that, and let the host know.
                _documentsWithTaskListItems.Add(document.Id);
            }

            // Now inform VS about this new information
            await _listener.ReportTaskListItemsAsync(document.Id, items, cancellationToken).ConfigureAwait(false);
        }
    }
}
