// 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.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixesAndRefactorings;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.CodeFixes
{
    internal abstract partial class SyntaxEditorBasedCodeFixProvider : CodeFixProvider
    {
        private static readonly ImmutableArray<FixAllScope> s_defaultSupportedFixAllScopes =
            ImmutableArray.Create(FixAllScope.Document, FixAllScope.Project, FixAllScope.Solution,
                FixAllScope.ContainingMember, FixAllScope.ContainingType);

        private readonly bool _supportsFixAll;

        protected SyntaxEditorBasedCodeFixProvider(bool supportsFixAll = true)
            => _supportsFixAll = supportsFixAll;

        public sealed override FixAllProvider? GetFixAllProvider()
        {
            if (!_supportsFixAll)
                return null;

            return FixAllProvider.Create(
                async (fixAllContext, document, diagnostics) =>
                {
                    var model = await document.GetRequiredSemanticModelAsync(fixAllContext.CancellationToken).ConfigureAwait(false);

                    // Ensure that diagnostics for this document are always in document location order.  This provides a
                    // consistent and deterministic order for fixers that want to update a document.
                    //
                    // Also ensure that we do not pass in duplicates by invoking Distinct.  See
                    // https://github.com/dotnet/roslyn/issues/31381, that seems to be causing duplicate diagnostics.
                    var filteredDiagnostics = diagnostics.Distinct()
                                                         .WhereAsArray(d => this.IncludeDiagnosticDuringFixAll(d, document, model, fixAllContext.CodeActionEquivalenceKey, fixAllContext.CancellationToken))
                                                         .Sort((d1, d2) => d1.Location.SourceSpan.Start - d2.Location.SourceSpan.Start);

                    if (filteredDiagnostics.Length == 0)
                        return document;

                    return await FixAllAsync(document, filteredDiagnostics, fixAllContext.GetOptionsProvider(), fixAllContext.CancellationToken).ConfigureAwait(false);
                },
                s_defaultSupportedFixAllScopes);
        }

        protected void RegisterCodeFix(CodeFixContext context, string title, string equivalenceKey, Diagnostic? diagnostic = null)
            => context.RegisterCodeFix(CodeAction.Create(title, GetDocumentUpdater(context, diagnostic), equivalenceKey), context.Diagnostics);

        protected void RegisterCodeFix(CodeFixContext context, string title, string equivalenceKey, CodeActionPriority priority, Diagnostic? diagnostic = null)
            => context.RegisterCodeFix(CodeAction.Create(title, GetDocumentUpdater(context, diagnostic), equivalenceKey, priority), context.Diagnostics);

        protected Func<CancellationToken, Task<Document>> GetDocumentUpdater(CodeFixContext context, Diagnostic? diagnostic = null)
        {
            var diagnostics = ImmutableArray.Create(diagnostic ?? context.Diagnostics[0]);
            return cancellationToken => FixAllAsync(context.Document, diagnostics, context.GetOptionsProvider(), cancellationToken);
        }

        private Task<Document> FixAllAsync(
            Document document, ImmutableArray<Diagnostic> diagnostics, CodeActionOptionsProvider options, CancellationToken cancellationToken)
        {
            return FixAllWithEditorAsync(
                document,
                editor => FixAllAsync(document, diagnostics, editor, options, cancellationToken),
                cancellationToken);
        }

        internal static async Task<Document> FixAllWithEditorAsync(
            Document document,
            Func<SyntaxEditor, Task> editAsync,
            CancellationToken cancellationToken)
        {
            var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
            var editor = new SyntaxEditor(root, document.Project.Solution.Services);

            await editAsync(editor).ConfigureAwait(false);

            var newRoot = editor.GetChangedRoot();
            return document.WithSyntaxRoot(newRoot);
        }

        /// <summary>
        /// Fixes all <paramref name="diagnostics"/> in the specified <paramref name="editor"/>.
        /// The fixes are applied to the <paramref name="document"/>'s syntax tree via <paramref name="editor"/>.
        /// The implementation may query options of any document in the <paramref name="document"/>'s solution
        /// with <paramref name="fallbackOptions"/> providing default values for options not specified explicitly in the corresponding editorconfig.
        /// </summary>
        protected abstract Task FixAllAsync(
            Document document, ImmutableArray<Diagnostic> diagnostics, SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken);

        /// <summary>
        /// Whether or not this diagnostic should be included when performing a FixAll.  This is
        /// useful for providers that create multiple diagnostics for the same issue (For example,
        /// one main diagnostic and multiple 'faded out code' diagnostics).  FixAll can be invoked
        /// from any of those, but we'll only want perform an edit for only one diagnostic for each
        /// of those sets of diagnostics.
        ///
        /// This overload differs from <see cref="IncludeDiagnosticDuringFixAll(Diagnostic, Document, SemanticModel, string, CancellationToken)"/>
        /// in that it also passes along the <see cref="SemanticModel"/>.
        ///
        /// This overload differs from <see cref="IncludeDiagnosticDuringFixAll(Diagnostic)"/> in
        /// that it also passes along the <see cref="FixAllState"/> in case that would be useful
        /// (for example if the <see cref="IFixAllState.CodeActionEquivalenceKey"/> is used.
        ///
        /// Only one of these three overloads needs to be overridden if you want to customize
        /// behavior.
        /// </summary>
        protected virtual bool IncludeDiagnosticDuringFixAll(Diagnostic diagnostic, Document document, SemanticModel model, string? equivalenceKey, CancellationToken cancellationToken)
            => IncludeDiagnosticDuringFixAll(diagnostic, document, equivalenceKey, cancellationToken);

        protected virtual bool IncludeDiagnosticDuringFixAll(Diagnostic diagnostic, Document document, string? equivalenceKey, CancellationToken cancellationToken)
            => IncludeDiagnosticDuringFixAll(diagnostic);

        /// <summary>
        /// Whether or not this diagnostic should be included when performing a FixAll.  This is
        /// useful for providers that create multiple diagnostics for the same issue (For example,
        /// one main diagnostic and multiple 'faded out code' diagnostics).  FixAll can be invoked
        /// from any of those, but we'll only want perform an edit for only one diagnostic for each
        /// of those sets of diagnostics.
        ///
        /// By default, all diagnostics will be included in fix-all unless they are filtered out
        /// here. If only the diagnostic needs to be queried to make this determination, only this
        /// overload needs to be overridden.  However, if information from <see cref="FixAllState"/>
        /// is needed (for example <see cref="IFixAllState.CodeActionEquivalenceKey"/>), then <see
        /// cref="IncludeDiagnosticDuringFixAll(Diagnostic, Document, SemanticModel, string, CancellationToken)"/>
        /// should be overridden instead.
        ///
        /// Only one of these two overloads needs to be overridden if you want to customize
        /// behavior.
        /// </summary>
        protected virtual bool IncludeDiagnosticDuringFixAll(Diagnostic diagnostic)
            => true;
    }
}
