﻿// 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.Immutable;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.ExternalAccess.UnitTesting.SolutionCrawler
{
    internal sealed partial class UnitTestingSolutionCrawlerRegistrationService
    {
        internal sealed partial class UnitTestingWorkCoordinator
        {
            private sealed partial class UnitTestingIncrementalAnalyzerProcessor
            {
#if false // Not used in unit testing crawling
                private sealed class UnitTestingHighPriorityProcessor : UnitTestingIdleProcessor
                {
                    private readonly UnitTestingIncrementalAnalyzerProcessor _processor;
                    private readonly UnitTestingAsyncDocumentWorkItemQueue _workItemQueue;
                    private readonly object _gate = new();

                    private Lazy<ImmutableArray<IUnitTestingIncrementalAnalyzer>> _lazyAnalyzers;

                    // whether this processor is running or not
                    private Task _running;

                    public UnitTestingHighPriorityProcessor(
                        IAsynchronousOperationListener listener,
                        UnitTestingIncrementalAnalyzerProcessor processor,
                        Lazy<ImmutableArray<IUnitTestingIncrementalAnalyzer>> lazyAnalyzers,
                        TimeSpan backOffTimeSpan,
                        CancellationToken shutdownToken)
                        : base(listener, backOffTimeSpan, shutdownToken)
                    {
                        _processor = processor;
                        _lazyAnalyzers = lazyAnalyzers;

                        _running = Task.CompletedTask;
                        _workItemQueue = new UnitTestingAsyncDocumentWorkItemQueue(processor._registration.ProgressReporter);

                        Start();
                    }

                    protected override void OnPaused()
                    {
                    }

                    public ImmutableArray<IUnitTestingIncrementalAnalyzer> Analyzers
                    {
                        get
                        {
                            lock (_gate)
                            {
                                return _lazyAnalyzers.Value;
                            }
                        }
                    }

                    public Task Running => _running;

                    public int WorkItemCount => _workItemQueue.WorkItemCount;
                    public bool HasAnyWork => _workItemQueue.HasAnyWork;

                    public void AddAnalyzer(IUnitTestingIncrementalAnalyzer analyzer)
                    {
                        lock (_gate)
                        {
                            var analyzers = _lazyAnalyzers.Value;
                            _lazyAnalyzers = new Lazy<ImmutableArray<IUnitTestingIncrementalAnalyzer>>(() => analyzers.Add(analyzer));
                        }
                    }

                    public void Enqueue(UnitTestingWorkItem item)
                    {
                        Contract.ThrowIfFalse(item.DocumentId != null, "can only enqueue a document work item");

                        // Don't enqueue item if we don't have any high priority analyzers
                        if (Analyzers.IsEmpty)
                        {
                            return;
                        }

                        // we only put workitem in high priority queue if there is a text change.
                        // this is to prevent things like opening a file, changing in other files keep enqueuing
                        // expensive high priority work.
                        if (!item.InvocationReasons.Contains(UnitTestingPredefinedInvocationReasons.SyntaxChanged))
                        {
                            return;
                        }

                        if (!_processor._documentTracker.SupportsDocumentTracking
                            && _processor._registration.WorkspaceKind is WorkspaceKind.RemoteWorkspace)
                        {
                            Debug.Fail($"Unexpected use of '{nameof(ExportUnitTestingIncrementalAnalyzerProviderAttribute.HighPriorityForActiveFile)}' in workspace kind '{_processor._registration.WorkspaceKind}' that cannot support active file tracking.");
                        }

                        // check whether given item is for active document, otherwise, nothing to do here
                        if (_processor._documentTracker.TryGetActiveDocument() != item.DocumentId)
                        {
                            return;
                        }

                        // we need to clone due to waiter
                        EnqueueActiveFileItem(item.WithAsyncToken(Listener.BeginAsyncOperation("ActiveFile")));
                    }

                    private void EnqueueActiveFileItem(UnitTestingWorkItem item)
                    {
                        Contract.ThrowIfNull(item.DocumentId);

                        UpdateLastAccessTime();
                        var added = _workItemQueue.AddOrReplace(item);

                        Logger.Log(FunctionId.WorkCoordinator_ActiveFileEnqueue, s_enqueueLogger, Environment.TickCount, item.DocumentId, !added);
                        UnitTestingSolutionCrawlerLogger.LogActiveFileEnqueue(_processor._logAggregator);
                    }

                    protected override Task WaitAsync(CancellationToken cancellationToken)
                        => _workItemQueue.WaitAsync(cancellationToken);

                    protected override async Task ExecuteAsync()
                    {
                        Debug.Assert(!Analyzers.IsEmpty);

                        if (CancellationToken.IsCancellationRequested)
                        {
                            return;
                        }

                        var source = new TaskCompletionSource<object?>();
                        try
                        {
                            // mark it as running
                            _running = source.Task;
                            // okay, there must be at least one item in the map
                            // see whether we have work item for the document
                            Contract.ThrowIfFalse(GetNextWorkItem(out var workItem, out var documentCancellation));

                            var solution = _processor._registration.GetSolutionToAnalyze();

                            // okay now we have work to do
                            await ProcessDocumentAsync(solution, Analyzers, workItem, documentCancellation).ConfigureAwait(false);
                        }
                        catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e))
                        {
                            throw ExceptionUtilities.Unreachable;
                        }
                        finally
                        {
                            // mark it as done running
                            source.SetResult(null);
                        }
                    }

                    private bool GetNextWorkItem(out UnitTestingWorkItem workItem, out CancellationToken cancellationToken)
                    {
                        // GetNextWorkItem since it can't fail. we still return bool to confirm that this never fail.
                        var documentId = _processor._documentTracker.TryGetActiveDocument();
                        if (documentId != null)
                        {
                            if (_workItemQueue.TryTake(documentId, out workItem, out cancellationToken))
                            {
                                return true;
                            }
                        }

                        return _workItemQueue.TryTakeAnyWork(
                            preferableProjectId: null,
#if false // Not used in unit testing crawling
                            dependencyGraph: _processor.DependencyGraph,
                            analyzerService: _processor.DiagnosticAnalyzerService,
#endif
                            workItem: out workItem,
                            cancellationToken: out cancellationToken);
                    }

                    private async Task ProcessDocumentAsync(Solution solution, ImmutableArray<IUnitTestingIncrementalAnalyzer> analyzers, UnitTestingWorkItem workItem, CancellationToken cancellationToken)
                    {
                        Contract.ThrowIfNull(workItem.DocumentId);

                        if (CancellationToken.IsCancellationRequested)
                        {
                            return;
                        }

                        var processedEverything = false;
                        var documentId = workItem.DocumentId;

                        try
                        {
                            using (Logger.LogBlock(FunctionId.WorkCoordinator_ProcessDocumentAsync, w => w.ToString(), workItem, cancellationToken))
                            {
                                var document = solution.GetDocument(documentId);
                                if (document != null)
                                {
                                    await _processor.ProcessDocumentAnalyzersAsync(document, analyzers, workItem, cancellationToken).ConfigureAwait(false);
                                }

                                if (!cancellationToken.IsCancellationRequested)
                                {
                                    processedEverything = true;
                                }
                            }
                        }
                        catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken))
                        {
                            throw ExceptionUtilities.Unreachable;
                        }
                        finally
                        {
                            // we got cancelled in the middle of processing the document.
                            // let's make sure newly enqueued work item has all the flag needed.
                            // Avoid retry attempts after cancellation is requested, since work will not be processed
                            // after that point.
                            if (!processedEverything && !CancellationToken.IsCancellationRequested)
                            {
                                _workItemQueue.AddOrReplace(workItem.Retry(Listener.BeginAsyncOperation("ReenqueueWorkItem")));
                            }

                            UnitTestingSolutionCrawlerLogger.LogProcessActiveFileDocument(_processor._logAggregator, documentId.Id, processedEverything);

                            // remove one that is finished running
                            _workItemQueue.MarkWorkItemDoneFor(workItem.DocumentId);
                        }
                    }

                    public void Shutdown()
                        => _workItemQueue.Dispose();
                }
#endif
            }
        }
    }
}
