﻿// 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.Threading.Tasks;
using Microsoft.CodeAnalysis.AddImportOnPaste;
using Microsoft.CodeAnalysis.AddMissingImports;
using Microsoft.CodeAnalysis.CodeCleanup;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.Editor.BackgroundWorkIndicator;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Commanding;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Editor.Commanding.Commands;
using Microsoft.VisualStudio.Threading;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.AddImport
{
    internal abstract class AbstractAddImportsPasteCommandHandler(
        IThreadingContext threadingContext,
        IGlobalOptionService globalOptions,
        IAsynchronousOperationListenerProvider listenerProvider) : IChainedCommandHandler<PasteCommandArgs>
    {
        /// <summary>
        /// The command handler display name
        /// </summary>
        public abstract string DisplayName { get; }

        /// <summary>
        /// The thread await dialog text shown to the user if the operation takes a long time
        /// </summary>
        protected abstract string DialogText { get; }

        private readonly IThreadingContext _threadingContext = threadingContext;
        private readonly IGlobalOptionService _globalOptions = globalOptions;
        private readonly IAsynchronousOperationListener _listener = listenerProvider.GetListener(FeatureAttribute.AddImportsOnPaste);

        public CommandState GetCommandState(PasteCommandArgs args, Func<CommandState> nextCommandHandler)
            => nextCommandHandler();

        public void ExecuteCommand(PasteCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext)
        {
            var language = args.SubjectBuffer.GetLanguageName();

            // If the feature is not explicitly enabled we can exit early
            if (language is null || !_globalOptions.GetOption(AddImportOnPasteOptionsStorage.AddImportsOnPaste, language))
            {
                nextCommandHandler();
                return;
            }

            // Capture the pre-paste caret position
            var caretPosition = args.TextView.GetCaretPoint(args.SubjectBuffer);
            if (!caretPosition.HasValue)
            {
                nextCommandHandler();
                return;
            }

            // Create a tracking span from the pre-paste caret position that will grow as text is inserted.
            var trackingSpan = caretPosition.Value.Snapshot.CreateTrackingSpan(caretPosition.Value.Position, 0, SpanTrackingMode.EdgeInclusive);

            // Perform the paste command before adding imports
            nextCommandHandler();

            if (executionContext.OperationContext.UserCancellationToken.IsCancellationRequested)
            {
                return;
            }

            try
            {
                ExecuteCommandWorker(args, executionContext, trackingSpan);
            }
            catch (OperationCanceledException)
            {
                // According to Editor command handler API guidelines, it's best if we return early if cancellation
                // is requested instead of throwing. Otherwise, we could end up in an invalid state due to already
                // calling nextCommandHandler().
            }
        }

        private void ExecuteCommandWorker(
            PasteCommandArgs args,
            CommandExecutionContext executionContext,
            ITrackingSpan trackingSpan)
        {
            if (!args.SubjectBuffer.CanApplyChangeDocumentToWorkspace())
            {
                return;
            }

            // Don't perform work if we're inside the interactive window
            if (args.TextView.IsNotSurfaceBufferOfTextView(args.SubjectBuffer))
            {
                return;
            }

            // Applying the post-paste snapshot to the tracking span gives us the span of pasted text.
            var snapshotSpan = trackingSpan.GetSpan(args.SubjectBuffer.CurrentSnapshot);

            var sourceTextContainer = args.SubjectBuffer.AsTextContainer();
            if (!Workspace.TryGetWorkspace(sourceTextContainer, out var workspace))
            {
                return;
            }

            var document = sourceTextContainer.GetOpenDocumentInCurrentContext();
            if (document is null)
            {
                return;
            }

            // We're showing our own UI, ensure the editor doesn't show anything itself.
            executionContext.OperationContext.TakeOwnership();

            var token = _listener.BeginAsyncOperation(nameof(ExecuteAsync));

            ExecuteAsync(document, snapshotSpan, args.TextView)
                .ReportNonFatalErrorAsync()
                .CompletesAsyncOperation(token);
        }

        private async Task ExecuteAsync(Document document, SnapshotSpan snapshotSpan, ITextView textView)
        {
            _threadingContext.ThrowIfNotOnUIThread();

            var indicatorFactory = document.Project.Solution.Services.GetRequiredService<IBackgroundWorkIndicatorFactory>();
            using var backgroundWorkContext = indicatorFactory.Create(
                textView,
                snapshotSpan,
                DialogText,
                cancelOnEdit: true,
                cancelOnFocusLost: true);

            var cancellationToken = backgroundWorkContext.UserCancellationToken;

            // We're going to log the same thing on success or failure since this blocks the UI thread. This measurement is 
            // intended to tell us how long we're blocking the user from typing with this action. 
            using var blockLogger = Logger.LogBlock(FunctionId.CommandHandler_Paste_ImportsOnPaste, KeyValueLogMessage.Create(LogType.UserAction), cancellationToken);

            await TaskScheduler.Default;

            var addMissingImportsService = document.GetRequiredLanguageService<IAddMissingImportsFeatureService>();

            var cleanupOptions = await document.GetCodeCleanupOptionsAsync(_globalOptions, cancellationToken).ConfigureAwait(false);

            var options = new AddMissingImportsOptions(
                CleanupOptions: cleanupOptions,
                HideAdvancedMembers: _globalOptions.GetOption(CompletionOptionsStorage.HideAdvancedMembers, document.Project.Language));

            var textSpan = snapshotSpan.Span.ToTextSpan();
            var updatedDocument = await addMissingImportsService.AddMissingImportsAsync(document, textSpan, options, cancellationToken).ConfigureAwait(false);

            if (updatedDocument is null)
            {
                return;
            }

            // Required to switch back to the UI thread to call TryApplyChanges
            await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
            document.Project.Solution.Workspace.TryApplyChanges(updatedDocument.Project.Solution);
        }
    }
}
