// 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.

#nullable disable

using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.CodeFixes.Suppression;

internal abstract partial class AbstractSuppressionCodeFixProvider
{
    /// <summary>
    /// Helper methods for pragma suppression add/remove batch fixers.
    /// </summary>
    private static class PragmaBatchFixHelpers
    {
        public static CodeAction CreateBatchPragmaFix(
            AbstractSuppressionCodeFixProvider suppressionFixProvider,
            Document document,
            ImmutableArray<IPragmaBasedCodeAction> pragmaActions,
            ImmutableArray<Diagnostic> pragmaDiagnostics,
            FixAllState fixAllState,
            CancellationToken cancellationToken)
        {
            return CodeAction.Create(
                ((CodeAction)pragmaActions[0]).Title,
                createChangedDocument: ct =>
                    BatchPragmaFixesAsync(suppressionFixProvider, document, pragmaActions, pragmaDiagnostics, cancellationToken),
                equivalenceKey: fixAllState.CodeActionEquivalenceKey);
        }

        private static async Task<Document> BatchPragmaFixesAsync(
            AbstractSuppressionCodeFixProvider suppressionFixProvider,
            Document document,
            ImmutableArray<IPragmaBasedCodeAction> pragmaActions,
            ImmutableArray<Diagnostic> diagnostics,
            CancellationToken cancellationToken)
        {
            // We apply all the pragma suppression fixes sequentially.
            // At every application, we track the updated locations for remaining diagnostics in the document.
            var currentDiagnosticSpans = new Dictionary<Diagnostic, TextSpan>();
            foreach (var diagnostic in diagnostics)
            {
                currentDiagnosticSpans.Add(diagnostic, diagnostic.Location.SourceSpan);
            }

            var currentDocument = document;
            for (var i = 0; i < pragmaActions.Length; i++)
            {
                var originalpragmaAction = pragmaActions[i];
                var diagnostic = diagnostics[i];
                // Get the diagnostic span for the diagnostic in latest document snapshot.
                if (!currentDiagnosticSpans.TryGetValue(diagnostic, out var currentDiagnosticSpan))
                {
                    // Diagnostic whose location conflicts with a prior fix.
                    continue;
                }

                // Compute and apply pragma suppression fix.
                var currentTree = await currentDocument.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
                var currentLocation = Location.Create(currentTree, currentDiagnosticSpan);
                diagnostic = Diagnostic.Create(
                    id: diagnostic.Id,
                    category: diagnostic.Descriptor.Category,
                    message: diagnostic.GetMessage(),
                    severity: diagnostic.Severity,
                    defaultSeverity: diagnostic.DefaultSeverity,
                    isEnabledByDefault: diagnostic.Descriptor.IsEnabledByDefault,
                    warningLevel: diagnostic.WarningLevel,
                    title: diagnostic.Descriptor.Title,
                    description: diagnostic.Descriptor.Description,
                    helpLink: diagnostic.Descriptor.HelpLinkUri,
                    location: currentLocation,
                    additionalLocations: diagnostic.AdditionalLocations,
                    customTags: diagnostic.Descriptor.CustomTags,
                    properties: diagnostic.Properties,
                    isSuppressed: diagnostic.IsSuppressed);

                var newSuppressionFixes = await suppressionFixProvider.GetFixesAsync(currentDocument, currentDiagnosticSpan, [diagnostic], cancellationToken).ConfigureAwait(false);
                var newSuppressionFix = newSuppressionFixes.SingleOrDefault();
                if (newSuppressionFix != null)
                {
                    var newPragmaAction = newSuppressionFix.Action as IPragmaBasedCodeAction ??
                        newSuppressionFix.Action.NestedActions.OfType<IPragmaBasedCodeAction>().SingleOrDefault();
                    if (newPragmaAction != null)
                    {
                        // Get the text changes with pragma suppression add/removals.
                        // Note: We do it one token at a time to ensure we get single text change in the new document, otherwise UpdateDiagnosticSpans won't function as expected.
                        // Update the diagnostics spans based on the text changes.
                        var startTokenChanges = await GetTextChangesAsync(newPragmaAction, currentDocument,
                            includeStartTokenChange: true, includeEndTokenChange: false, cancellationToken: cancellationToken).ConfigureAwait(false);

                        var endTokenChanges = await GetTextChangesAsync(newPragmaAction, currentDocument,
                            includeStartTokenChange: false, includeEndTokenChange: true, cancellationToken: cancellationToken).ConfigureAwait(false);

                        var currentText = await currentDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
                        var orderedChanges = startTokenChanges.Concat(endTokenChanges).OrderBy(change => change.Span).Distinct();
                        var newText = currentText.WithChanges(orderedChanges);
                        currentDocument = currentDocument.WithText(newText);

                        // Update the diagnostics spans based on the text changes.
                        UpdateDiagnosticSpans(diagnostics, currentDiagnosticSpans, orderedChanges);
                    }
                }
            }

            return currentDocument;
        }

        private static async Task<IEnumerable<TextChange>> GetTextChangesAsync(
            IPragmaBasedCodeAction pragmaAction,
            Document currentDocument,
            bool includeStartTokenChange,
            bool includeEndTokenChange,
            CancellationToken cancellationToken)
        {
            var newDocument = await pragmaAction.GetChangedDocumentAsync(includeStartTokenChange, includeEndTokenChange, cancellationToken).ConfigureAwait(false);
            return await newDocument.GetTextChangesAsync(currentDocument, cancellationToken).ConfigureAwait(false);
        }

        private static void UpdateDiagnosticSpans(ImmutableArray<Diagnostic> diagnostics, Dictionary<Diagnostic, TextSpan> currentDiagnosticSpans, IEnumerable<TextChange> textChanges)
        {
            static bool IsPriorSpan(TextSpan span, TextChange textChange) => span.End <= textChange.Span.Start;
            static bool IsFollowingSpan(TextSpan span, TextChange textChange) => span.Start >= textChange.Span.End;
            static bool IsEnclosingSpan(TextSpan span, TextChange textChange) => span.Contains(textChange.Span);

            foreach (var diagnostic in diagnostics)
            {
                // We use 'originalSpan' to identify if the diagnostic is prior/following/enclosing with respect to each text change.
                // We use 'currentSpan' to track updates made to the originalSpan by each text change.
                if (!currentDiagnosticSpans.TryGetValue(diagnostic, out var originalSpan))
                {
                    continue;
                }

                var currentSpan = originalSpan;
                foreach (var textChange in textChanges)
                {
                    if (IsPriorSpan(originalSpan, textChange))
                    {
                        // Prior span, needs no update.
                        continue;
                    }

                    var delta = textChange.NewText.Length - textChange.Span.Length;
                    if (delta != 0)
                    {
                        if (IsFollowingSpan(originalSpan, textChange))
                        {
                            // Following span.
                            var newStart = currentSpan.Start + delta;
                            currentSpan = new TextSpan(newStart, currentSpan.Length);
                            currentDiagnosticSpans[diagnostic] = currentSpan;
                        }
                        else if (IsEnclosingSpan(originalSpan, textChange))
                        {
                            // Enclosing span.
                            var newLength = currentSpan.Length + delta;
                            currentSpan = new TextSpan(currentSpan.Start, newLength);
                            currentDiagnosticSpans[diagnostic] = currentSpan;
                        }
                        else
                        {
                            // Overlapping span.
                            // Drop conflicting diagnostics.
                            currentDiagnosticSpans.Remove(diagnostic);
                            break;
                        }
                    }
                }
            }
        }
    }
}
