﻿// 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.ComponentModel.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.BackgroundWorkIndicator;
using Microsoft.CodeAnalysis.Editor.Commanding.Commands;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.OrganizeImports;
using Microsoft.CodeAnalysis.Organizing;
using Microsoft.CodeAnalysis.RemoveUnnecessaryImports;
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.Commanding;
using Microsoft.VisualStudio.Threading;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Editor.Implementation.Organizing
{
    [Export(typeof(ICommandHandler))]
    [ContentType(ContentTypeNames.CSharpContentType)]
    [ContentType(ContentTypeNames.VisualBasicContentType)]
    [ContentType(ContentTypeNames.XamlContentType)]
    [Name(PredefinedCommandHandlerNames.OrganizeDocument)]
    [method: ImportingConstructor]
    [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")]
    internal class OrganizeDocumentCommandHandler(
        IThreadingContext threadingContext,
        IGlobalOptionService globalOptions,
        IAsynchronousOperationListenerProvider listenerProvider) :
        ICommandHandler<OrganizeDocumentCommandArgs>,
        ICommandHandler<SortImportsCommandArgs>,
        ICommandHandler<SortAndRemoveUnnecessaryImportsCommandArgs>
    {
        private readonly IThreadingContext _threadingContext = threadingContext;
        private readonly IGlobalOptionService _globalOptions = globalOptions;
        private readonly IAsynchronousOperationListener _listener = listenerProvider.GetListener(FeatureAttribute.OrganizeDocument);

        public string DisplayName => EditorFeaturesResources.Organize_Document;

        public CommandState GetCommandState(OrganizeDocumentCommandArgs args)
            => GetCommandState(args, _ => EditorFeaturesResources.Organize_Document, needsSemantics: true);

        public CommandState GetCommandState(SortImportsCommandArgs args)
            => GetCommandState(args, o => o.SortImportsDisplayStringWithAccelerator, needsSemantics: false);

        public CommandState GetCommandState(SortAndRemoveUnnecessaryImportsCommandArgs args)
            => GetCommandState(args, o => o.SortAndRemoveUnusedImportsDisplayStringWithAccelerator, needsSemantics: true);

        private static CommandState GetCommandState(EditorCommandArgs args, Func<IOrganizeImportsService, string> descriptionString, bool needsSemantics)
        {
            if (IsCommandSupported(args, needsSemantics, out var workspace))
            {
                var organizeImportsService = workspace.Services.SolutionServices.GetProjectServices(args.SubjectBuffer)!.GetRequiredService<IOrganizeImportsService>();
                return new CommandState(isAvailable: true, displayText: descriptionString(organizeImportsService));
            }
            else
            {
                return CommandState.Unspecified;
            }
        }

        private static bool IsCommandSupported(EditorCommandArgs args, bool needsSemantics, [NotNullWhen(true)] out Workspace? workspace)
        {
            workspace = null;
            if (args.SubjectBuffer.TryGetWorkspace(out var retrievedWorkspace))
            {
                workspace = retrievedWorkspace;
                if (!workspace.CanApplyChange(ApplyChangesKind.ChangeDocument))
                {
                    return false;
                }

                if (workspace.Kind == WorkspaceKind.MiscellaneousFiles)
                {
                    return !needsSemantics;
                }

                return args.SubjectBuffer.SupportsRefactorings();
            }

            return false;
        }

        private bool ExecuteCommand(
            EditorCommandArgs commandArgs,
            CommandExecutionContext context,
            Func<ITextSnapshot, IUIThreadOperationContext, Task<Document?>> getCurrentDocumentAsync,
            Func<Document, CancellationToken, Task<Document>> getChangedDocumentAsync)
        {
            // We're showing our own UI, ensure the editor doesn't show anything itself.
            context.OperationContext.TakeOwnership();

            var token = _listener.BeginAsyncOperation(nameof(ExecuteCommand));
            _ = ExecuteAsync(commandArgs, getCurrentDocumentAsync, getChangedDocumentAsync)
                .ReportNonFatalErrorAsync()
                .CompletesAsyncOperation(token);

            return true;
        }

        private async Task ExecuteAsync(
            EditorCommandArgs commandArgs,
            Func<ITextSnapshot, IUIThreadOperationContext, Task<Document?>> getCurrentDocumentAsync,
            Func<Document, CancellationToken, Task<Document>> getChangedDocumentAsync)
        {
            await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync();

            var subjectBuffer = commandArgs.SubjectBuffer;
            var textView = commandArgs.TextView;

            var caretPoint = textView.GetCaretPoint(subjectBuffer);
            if (caretPoint is null)
                return;

            if (!subjectBuffer.TryGetWorkspace(out var workspace))
                return;

            var snapshotSpan = textView.GetTextElementSpan(caretPoint.Value);

            var indicatorFactory = workspace.Services.GetRequiredService<IBackgroundWorkIndicatorFactory>();
            using var backgroundWorkContext = indicatorFactory.Create(
                commandArgs.TextView,
                snapshotSpan,
                EditorFeaturesResources.Organizing_document,
                cancelOnEdit: true,
                cancelOnFocusLost: false);

            var cancellationToken = backgroundWorkContext.UserCancellationToken;

            await TaskScheduler.Default;

            var currentDocument = await getCurrentDocumentAsync(snapshotSpan.Snapshot, backgroundWorkContext).ConfigureAwait(false);
            if (currentDocument is null)
                return;

            var newDocument = await getChangedDocumentAsync(currentDocument, cancellationToken).ConfigureAwait(false);
            if (currentDocument == newDocument)
                return;

            var changes = await newDocument.GetTextChangesAsync(currentDocument, cancellationToken).ConfigureAwait(false);

            // Required to switch back to the UI thread to call TryApplyChanges
            await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            // We're about to make an edit ourselves.  so disable the cancellation that happens on editing.
            backgroundWorkContext.CancelOnEdit = false;

            commandArgs.SubjectBuffer.ApplyChanges(changes);
        }

        public bool ExecuteCommand(OrganizeDocumentCommandArgs args, CommandExecutionContext context)
            => ExecuteCommand(
                args, context,
                // Need full semantics for this operation.
                (snapshot, context) => snapshot.GetFullyLoadedOpenDocumentInCurrentContextWithChangesAsync(context),
                (document, cancellationToken) => OrganizingService.OrganizeAsync(document, cancellationToken: cancellationToken));

        public bool ExecuteCommand(SortImportsCommandArgs args, CommandExecutionContext context)
            => ExecuteCommand(
                args, context,
                // Only need syntax for this operation.
                (snapshot, context) => Task.FromResult(snapshot.GetOpenDocumentInCurrentContextWithChanges()),
                async (document, cancellationToken) =>
                {
                    var organizeImportsService = document.GetRequiredLanguageService<IOrganizeImportsService>();
                    var options = await document.GetOrganizeImportsOptionsAsync(_globalOptions, cancellationToken).ConfigureAwait(false);
                    return await organizeImportsService.OrganizeImportsAsync(document, options, cancellationToken).ConfigureAwait(false);
                });

        public bool ExecuteCommand(SortAndRemoveUnnecessaryImportsCommandArgs args, CommandExecutionContext context)
            => ExecuteCommand(
                args, context,
                // Need full semantics for this operation.
                (snapshot, context) => snapshot.GetFullyLoadedOpenDocumentInCurrentContextWithChangesAsync(context),
                async (document, cancellationToken) =>
                {
                    var formattingOptions = document.SupportsSyntaxTree
                        ? await document.GetSyntaxFormattingOptionsAsync(_globalOptions, cancellationToken).ConfigureAwait(false)
                        : null;

                    var removeImportsService = document.GetRequiredLanguageService<IRemoveUnnecessaryImportsService>();
                    var organizeImportsService = document.GetRequiredLanguageService<IOrganizeImportsService>();

                    var newDocument = await removeImportsService.RemoveUnnecessaryImportsAsync(document, formattingOptions, cancellationToken).ConfigureAwait(false);
                    var options = await document.GetOrganizeImportsOptionsAsync(_globalOptions, cancellationToken).ConfigureAwait(false);
                    return await organizeImportsService.OrganizeImportsAsync(newDocument, options, cancellationToken).ConfigureAwait(false);
                });
    }
}
