// 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.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.CodeAnalysis.CSharp.RemoveUnnecessarySuppressions;

[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.RemoveUnnecessaryNullableWarningSuppressions), Shared]
[method: ImportingConstructor]
[method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")]
internal sealed class CSharpRemoveUnnecessaryNullableWarningSuppressionsCodeFixProvider() : CodeFixProvider
{
    public override ImmutableArray<string> FixableDiagnosticIds => [IDEDiagnosticIds.RemoveUnnecessaryNullableWarningSuppression];

    public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
    {
        context.RegisterCodeFix(CodeAction.Create(
            AnalyzersResources.Remove_unnecessary_suppression,
            cancellationToken => FixSingleDocumentAsync(context.Document, context.Diagnostics, cancellationToken),
            nameof(AnalyzersResources.Remove_unnecessary_suppression)),
            context.Diagnostics);
    }

    private static async Task<Document> FixSingleDocumentAsync(Document document, ImmutableArray<Diagnostic> diagnostics, CancellationToken cancellationToken)
    {
        var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);

        // Keep track of the unnecessary spans in all linked documents.  If we're trying to fix something that would
        // cause a problem in another linked document, then we'll still fix it (since the analyzer did report it), we'll
        // just report a warning on it letting the user know in the preview window.
        using var _1 = ArrayBuilder<HashSet<TextSpan>>.GetInstance(out var linkedSpansArray);
        foreach (var linkedDocument in document.GetLinkedDocuments())
        {
            using var _2 = ArrayBuilder<PostfixUnaryExpressionSyntax>.GetInstance(out var unnecessaryNodes);

            var linkedSemanticModel = await linkedDocument.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
            UnnecessaryNullableWarningSuppressionsUtilities.AddUnnecessaryNodes(
                linkedSemanticModel, unnecessaryNodes, cancellationToken);

            linkedSpansArray.Add([.. unnecessaryNodes.Select(n => n.Span)]);
        }

        var editor = new SyntaxEditor(root, document.Project.Solution.Services);

        FixAllInDocument(
            editor,
            diagnostics.Select(static d => d.AdditionalLocations[0].SourceSpan),
            expr =>
            {
                // If we don't see this span in any of the linked documents, then that means analyzing that document
                // didn't reveal this suppression as unnecessary.  Therefore, we need to add a warning annotation
                // message to inform the user that removing this suppression may cause warnings in other projects.
                foreach (var linkedSpans in linkedSpansArray)
                {
                    if (!linkedSpans.Contains(expr.Span))
                        return WarningAnnotation.Create(CodeFixesResources.May_be_neccessary_in_other_project_this_document_is_linked_in);
                }

                return null;
            });

        return document.WithSyntaxRoot(editor.GetChangedRoot());
    }

    private static void FixAllInDocument(
        SyntaxEditor editor,
        IEnumerable<TextSpan> spans,
        Func<PostfixUnaryExpressionSyntax, SyntaxAnnotation?>? getAnnotation)
    {
        var root = editor.OriginalRoot;

        foreach (var span in spans.OrderByDescending(d => d.Start))
        {
            if (root.FindNode(span, getInnermostNodeForTie: true) is PostfixUnaryExpressionSyntax unaryExpression)
            {
                var annotation = getAnnotation?.Invoke(unaryExpression);
                editor.ReplaceNode(
                    unaryExpression,
                    (current, _) =>
                    {
                        var result = ((PostfixUnaryExpressionSyntax)current).Operand.WithTriviaFrom(current);
                        return annotation != null
                            ? result.WithAdditionalAnnotations(annotation)
                            : result;
                    });
            }
        }
    }

    public override FixAllProvider? GetFixAllProvider()
        => new RemoveUnnecessaryNullableWarningSuppressionsFixAllProvider();

    /// <summary>
    /// Fix-all for removing unnecessary `!` operators works in a fairly specialized fashion.  The core problem is that
    /// it's normal to have situations where a `!` operator is unnecessary in one linked document in one project, but
    /// necessary in another.  Consider something as mundane as `string.IsNullOrEmpty(s)`.  In projects that reference a
    /// modern, annotated, BCL, the nullable attributes on this method will allow the compiler to determine that `s` is
    /// non-null after the call, allowing superfluous `!` operators to be removed.  However, in projects that reference
    /// an unannotated BCL, no such determination can be made, and the `!` on a following statement may be necessary.
    ///
    /// To deal with this, we consider all linked documents together.  If a `!` operator is unnecessary in *all* linked
    /// documents, then we can remove it.  Otherwise, we must keep it.
    /// </summary>
    private sealed class RemoveUnnecessaryNullableWarningSuppressionsFixAllProvider : MultiProjectSafeFixAllProvider
    {
#if !CODE_STYLE
        internal override CodeActionCleanup Cleanup => CodeActionCleanup.SyntaxOnly;
#endif

        protected override void FixAll(SyntaxEditor editor, IEnumerable<TextSpan> commonSpans)
            => FixAllInDocument(editor, commonSpans, getAnnotation: null);
    }
}
