// 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;
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.CodeFixes;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.RemoveUnusedVariable;

internal abstract class AbstractRemoveUnusedVariableCodeFixProvider<TLocalDeclarationStatement, TVariableDeclarator, TVariableDeclaration> : SyntaxEditorBasedCodeFixProvider
    where TLocalDeclarationStatement : SyntaxNode
    where TVariableDeclarator : SyntaxNode
    where TVariableDeclaration : SyntaxNode
{
    protected abstract bool IsCatchDeclarationIdentifier(SyntaxToken token);

    protected abstract SyntaxNode GetNodeToRemoveOrReplace(SyntaxNode node);

    protected abstract void RemoveOrReplaceNode(SyntaxEditor editor, SyntaxNode node, IBlockFactsService blockFacts);

    protected abstract SeparatedSyntaxList<SyntaxNode> GetVariables(TLocalDeclarationStatement localDeclarationStatement);

    protected abstract bool ShouldOfferFixForLocalDeclaration(IBlockFactsService blockFacts, SyntaxNode node);

    public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
    {
        var diagnostic = context.Diagnostics.First();

        var document = context.Document;
        var root = await document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
        var node = root.FindNode(diagnostic.Location.SourceSpan);

        var blockFacts = document.GetRequiredLanguageService<IBlockFactsService>();

        if (ShouldOfferFixForLocalDeclaration(blockFacts, node))
        {
            RegisterCodeFix(context, FeaturesResources.Remove_unused_variable, nameof(FeaturesResources.Remove_unused_variable));
        }
    }

    protected override async Task FixAllAsync(Document document, ImmutableArray<Diagnostic> diagnostics, SyntaxEditor syntaxEditor, CancellationToken cancellationToken)
    {
        var nodesToRemove = new HashSet<SyntaxNode>();

        // Create actions and keep their SpanStart. 
        // Execute actions ordered descending by SpanStart to avoid conflicts.
        var actionsToPerform = new List<(int, Action)>();
        var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        var documentEditor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);
        var documentsToBeSearched = ImmutableHashSet.Create(document);

        foreach (var diagnostic in diagnostics)
        {
            var token = diagnostic.Location.FindToken(cancellationToken);
            var node = root.FindNode(diagnostic.Location.SourceSpan);
            if (IsCatchDeclarationIdentifier(token))
            {
                (int, Action) pair = (token.Parent.SpanStart,
                    () => syntaxEditor.ReplaceNode(
                        token.Parent,
                        token.Parent.ReplaceToken(token, default(SyntaxToken)).WithAdditionalAnnotations(Formatter.Annotation)));
                actionsToPerform.Add(pair);
            }
            else
            {
                nodesToRemove.Add(node);
            }

            var symbol = documentEditor.SemanticModel.GetDeclaredSymbol(node, cancellationToken);
            var referencedSymbols = await SymbolFinder.FindReferencesAsync(symbol, document.Project.Solution, documentsToBeSearched, cancellationToken).ConfigureAwait(false);

            foreach (var referencedSymbol in referencedSymbols)
            {
                if (referencedSymbol?.Locations != null)
                {
                    foreach (var location in referencedSymbol.Locations)
                    {
                        var referencedSymbolNode = root.FindNode(location.Location.SourceSpan);
                        if (referencedSymbolNode != null)
                        {
                            var nodeToRemoveOrReplace = GetNodeToRemoveOrReplace(referencedSymbolNode);
                            if (nodeToRemoveOrReplace != null)
                            {
                                nodesToRemove.Add(nodeToRemoveOrReplace);
                            }
                        }
                    }
                }
            }
        }

        MergeNodesToRemove(nodesToRemove);
        var blockFacts = document.GetLanguageService<IBlockFactsService>();
        foreach (var node in nodesToRemove)
            actionsToPerform.Add((node.SpanStart, () => RemoveOrReplaceNode(syntaxEditor, node, blockFacts)));

        // Process nodes in reverse order 
        // to complete with nested declarations before processing the outer ones.
        foreach (var node in actionsToPerform.OrderByDescending(n => n.Item1))
            node.Item2();
    }

    protected static void RemoveNode(SyntaxEditor editor, SyntaxNode node, IBlockFactsService blockFacts)
    {
        var localDeclaration = node.GetAncestorOrThis<TLocalDeclarationStatement>();
        var removeOptions = CreateSyntaxRemoveOptions(localDeclaration, blockFacts);
        editor.RemoveNode(node, removeOptions);
    }

    private static SyntaxRemoveOptions CreateSyntaxRemoveOptions(
        TLocalDeclarationStatement localDeclaration,
        IBlockFactsService blockFacts)
    {
        var removeOptions = SyntaxGenerator.DefaultRemoveOptions;

        if (localDeclaration != null)
        {
            if (localDeclaration.GetLeadingTrivia().Contains(t => t.IsDirective))
            {
                removeOptions |= SyntaxRemoveOptions.KeepLeadingTrivia;
            }
            else
            {
                var statementParent = blockFacts.GetImmediateParentExecutableBlockForStatement(localDeclaration);
                if (statementParent != null)
                {
                    var siblings = blockFacts.GetExecutableBlockStatements(statementParent);
                    var localDeclarationIndex = siblings.IndexOf(localDeclaration);
                    if (localDeclarationIndex != 0)
                    {
                        // if we're removing the first statement in a block, then we
                        // want to have the elastic marker on it so that the next statement
                        // properly formats with the space left behind.  But if it's
                        // not the first statement then just keep the trivia as is
                        // so that the statement before and after it stay appropriately
                        // spaced apart.
                        removeOptions &= ~SyntaxRemoveOptions.AddElasticMarker;
                    }
                }
            }
        }

        return removeOptions;
    }

    // Merges node like
    // var unused1 = 0, unused2 = 0;
    // to remove the whole line.
    private void MergeNodesToRemove(HashSet<SyntaxNode> nodesToRemove)
    {
        var candidateLocalDeclarationsToRemove = new HashSet<TLocalDeclarationStatement>();
        foreach (var variableDeclarator in nodesToRemove.OfType<TVariableDeclarator>())
        {
            // Parents of the variable declarator could be candaditaes for removal for example 
            // if all declarators in a declaration will be removed.

            if (variableDeclarator.Parent?.Parent is TLocalDeclarationStatement candidate)
            {
                candidateLocalDeclarationsToRemove.Add(candidate);
            }
        }

        foreach (var candidate in candidateLocalDeclarationsToRemove)
        {
            var hasUsedLocal = false;
            foreach (var variable in GetVariables(candidate))
            {
                if (!nodesToRemove.Contains(variable))
                {
                    hasUsedLocal = true;
                    break;
                }
            }

            if (!hasUsedLocal)
            {
                nodesToRemove.Add(candidate);
                foreach (var variable in GetVariables(candidate))
                {
                    nodesToRemove.Remove(variable);
                }
            }
        }
    }
}
