// 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.Composition;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Formatting;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.RemoveUnusedParametersAndValues;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.CSharp.RemoveUnusedParametersAndValues
{
    [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.RemoveUnusedValues), Shared]
    [ExtensionOrder(After = PredefinedCodeFixProviderNames.AddImport)]
    internal class CSharpRemoveUnusedValuesCodeFixProvider :
        AbstractRemoveUnusedValuesCodeFixProvider<ExpressionSyntax, StatementSyntax, BlockSyntax,
            ExpressionStatementSyntax, LocalDeclarationStatementSyntax, VariableDeclaratorSyntax,
            ForEachStatementSyntax, SwitchSectionSyntax, SwitchLabelSyntax, CatchClauseSyntax, CatchClauseSyntax>
    {
        [ImportingConstructor]
        [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")]
        public CSharpRemoveUnusedValuesCodeFixProvider()
        {
        }

        protected override ISyntaxFormatting GetSyntaxFormatting()
            => CSharpSyntaxFormatting.Instance;

        protected override BlockSyntax WrapWithBlockIfNecessary(IEnumerable<StatementSyntax> statements)
            => SyntaxFactory.Block(statements);

        protected override SyntaxToken GetForEachStatementIdentifier(ForEachStatementSyntax node)
            => node.Identifier;

        protected override LocalDeclarationStatementSyntax GetCandidateLocalDeclarationForRemoval(VariableDeclaratorSyntax declarator)
            => declarator.Parent?.Parent as LocalDeclarationStatementSyntax;

        protected override SyntaxNode TryUpdateNameForFlaggedNode(SyntaxNode node, SyntaxToken newName)
        {
            switch (node.Kind())
            {
                case SyntaxKind.IdentifierName:
                    var identifierName = (IdentifierNameSyntax)node;
                    return identifierName.WithIdentifier(newName.WithTriviaFrom(identifierName.Identifier));

                case SyntaxKind.VariableDeclarator:
                    var variableDeclarator = (VariableDeclaratorSyntax)node;
                    if (newName.ValueText == AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.DiscardVariableName &&
                        variableDeclarator.Initializer?.Value is ImplicitObjectCreationExpressionSyntax implicitObjectCreation &&
                        variableDeclarator.Parent is VariableDeclarationSyntax parent)
                    {
                        // If we are generating a discard on the left of an initialization with an implicit object creation on the right,
                        // then we need to replace the implicit object creation with an explicit one.
                        // For example: 'TypeName v = new();' must be changed to '_ = new TypeName();'
                        var objectCreationNode = SyntaxFactory.ObjectCreationExpression(
                            newKeyword: implicitObjectCreation.NewKeyword,
                            type: parent.Type,
                            argumentList: implicitObjectCreation.ArgumentList,
                            initializer: implicitObjectCreation.Initializer);
                        variableDeclarator = variableDeclarator.WithInitializer(variableDeclarator.Initializer.WithValue(objectCreationNode));
                    }

                    return variableDeclarator.WithIdentifier(newName.WithTriviaFrom(variableDeclarator.Identifier));

                case SyntaxKind.SingleVariableDesignation:
                    return newName.ValueText == AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.DiscardVariableName
                        ? SyntaxFactory.DiscardDesignation().WithTriviaFrom(node)
                        : SyntaxFactory.SingleVariableDesignation(newName).WithTriviaFrom(node);

                case SyntaxKind.CatchDeclaration:
                    var catchDeclaration = (CatchDeclarationSyntax)node;
                    return catchDeclaration.WithIdentifier(newName.WithTriviaFrom(catchDeclaration.Identifier));

                case SyntaxKind.VarPattern:
                    return node.IsParentKind(SyntaxKind.Subpattern)
                        ? SyntaxFactory.DiscardPattern().WithTriviaFrom(node)
                        : SyntaxFactory.DiscardDesignation();

                default:
                    Debug.Fail($"Unexpected node kind for local/parameter declaration or reference: '{node.Kind()}'");
                    return null;
            }
        }

        protected override SyntaxNode TryUpdateParentOfUpdatedNode(SyntaxNode parent, SyntaxNode newNameNode, SyntaxEditor editor, ISyntaxFacts syntaxFacts, SemanticModel semanticModel)
        {
            if (newNameNode.IsKind(SyntaxKind.DiscardDesignation))
            {
                var triviaToAppend = newNameNode.GetLeadingTrivia().AddRange(newNameNode.GetTrailingTrivia());

                // 1) `... is MyType variable` -> `... is MyType`
                // 2) `... is MyType /*1*/ variable /*2*/` -> `... is MyType /*1*/  /*2*/`
                if (parent is DeclarationPatternSyntax declarationPattern &&
                    parent.SyntaxTree.Options.LanguageVersion() >= LanguageVersion.CSharp9)
                {
                    var trailingTrivia = declarationPattern.Type.GetTrailingTrivia().AddRange(triviaToAppend);
                    return SyntaxFactory.TypePattern(declarationPattern.Type).WithTrailingTrivia(trailingTrivia);
                }

                // 1) `... is { } variable` -> `... is { }`
                // 2) `... is { } /*1*/ variable /*2*/` -> `... is { } /*1*/  /*2*/`
                if (parent is RecursivePatternSyntax recursivePattern)
                {
                    var withoutDesignation = recursivePattern.WithDesignation(null);
                    return withoutDesignation.WithAppendedTrailingTrivia(triviaToAppend);
                }

                // 1) `... is [] variable` -> `... is []`
                // 2) `... is [] /*1*/ variable /*2*/` -> `... is [] /*1*/  /*2*/`
                if (parent is ListPatternSyntax listPattern)
                {
                    var withoutDesignation = listPattern.WithDesignation(null);
                    return withoutDesignation.WithAppendedTrailingTrivia(triviaToAppend);
                }
            }
            else if (parent is AssignmentExpressionSyntax assignment &&
                assignment.Right is ImplicitObjectCreationExpressionSyntax implicitObjectCreation &&
                newNameNode is IdentifierNameSyntax { Identifier.ValueText: AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.DiscardVariableName } &&
                semanticModel.GetTypeInfo(implicitObjectCreation).Type is { } type)
            {
                // If we are generating a discard on the left of an assignment with an implicit object creation on the right,
                // then we need to replace the implicit object creation with an explicit one.
                // For example: 'v = new();' must be changed to '_ = new TypeOfV();'
                var objectCreationNode = SyntaxFactory.ObjectCreationExpression(
                    newKeyword: implicitObjectCreation.NewKeyword,
                    type: type.GenerateTypeSyntax(allowVar: false),
                    argumentList: implicitObjectCreation.ArgumentList,
                    initializer: implicitObjectCreation.Initializer);
                return assignment.Update((ExpressionSyntax)newNameNode, assignment.OperatorToken, objectCreationNode);
            }

            return null;
        }

        protected override SyntaxNode ComputeReplacementNode(SyntaxNode originalOldNode, SyntaxNode changedOldNode, SyntaxNode proposedReplacementNode)
        {
            // Check for the following change: `{ ... } variable` -> `{ ... }`
            // Since the internals of this patterns might be changed during previous iterations
            // we apply the same change (remove `variable` declaration) to the `changedOldNode`
            if (originalOldNode is RecursivePatternSyntax originalOldRecursivePattern &&
                proposedReplacementNode is RecursivePatternSyntax proposedReplacementRecursivePattern &&
                proposedReplacementRecursivePattern.IsEquivalentTo(originalOldRecursivePattern.WithDesignation(null)))
            {
                proposedReplacementNode = ((RecursivePatternSyntax)changedOldNode).WithDesignation(null)
                                                                                  .WithTrailingTrivia(proposedReplacementNode.GetTrailingTrivia());
            }

            // Check for the following change: `[...] variable` -> `[...]`
            // Since the internals of this patterns might be changed during previous iterations
            // we apply the same change (remove `variable` declaration) to the `changedOldNode`
            if (originalOldNode is ListPatternSyntax originalOldListPattern &&
                proposedReplacementNode is ListPatternSyntax proposedReplacementListPattern &&
                proposedReplacementListPattern.IsEquivalentTo(originalOldListPattern.WithDesignation(null)))
            {
                proposedReplacementNode = ((ListPatternSyntax)changedOldNode).WithDesignation(null)
                                                                             .WithTrailingTrivia(proposedReplacementNode.GetTrailingTrivia());
            }

            return proposedReplacementNode.WithAdditionalAnnotations(Formatter.Annotation);
        }

        protected override void InsertAtStartOfSwitchCaseBlockForDeclarationInCaseLabelOrClause(SwitchSectionSyntax switchCaseBlock, SyntaxEditor editor, LocalDeclarationStatementSyntax declarationStatement)
        {
            var firstStatement = switchCaseBlock.Statements.FirstOrDefault();
            if (firstStatement != null)
            {
                editor.InsertBefore(firstStatement, declarationStatement);
            }
            else
            {
                // Switch section without any statements is an error case.
                // Insert before containing switch statement.
                editor.InsertBefore(switchCaseBlock.Parent, declarationStatement);
            }
        }

        protected override SyntaxNode GetReplacementNodeForCompoundAssignment(
            SyntaxNode originalCompoundAssignment,
            SyntaxNode newAssignmentTarget,
            SyntaxEditor editor,
            ISyntaxFactsService syntaxFacts)
        {
            // 1. Compound assignment is changed to simple assignment.
            // For example, "x += MethodCall();", where assignment to 'x' is redundant
            // is replaced with "_ = MethodCall();" or "var unused = MethodCall();
            //
            // 2. Null coalesce assignment is changed to assignment with null coalesce
            // expression on the right.
            // For example, "x ??= MethodCall();", where assignment to 'x' is redundant
            // is replaced with "_ = x ?? MethodCall();" or "var unused = x ?? MethodCall();
            //
            // 3. However, if the node is not parented by an expression statement then we
            // don't generate an assignment, but just the expression.
            // For example, "return x += MethodCall();" is replaced with "return x + MethodCall();"
            // and "return x ??= MethodCall();" is replaced with "return x ?? MethodCall();"

            if (originalCompoundAssignment is not AssignmentExpressionSyntax assignmentExpression)
            {
                Debug.Fail($"Unexpected kind for originalCompoundAssignment: {originalCompoundAssignment.Kind()}");
                return originalCompoundAssignment;
            }

            var leftOfAssignment = assignmentExpression.Left;
            var rightOfAssignment = assignmentExpression.Right;

            if (originalCompoundAssignment.Parent.IsKind(SyntaxKind.ExpressionStatement))
            {
                if (!originalCompoundAssignment.IsKind(SyntaxKind.CoalesceAssignmentExpression))
                {
                    // Case 1. Simple compound assignment parented by an expression statement.
                    return editor.Generator.AssignmentStatement(newAssignmentTarget, rightOfAssignment);
                }
                else
                {
                    // Case 2. Null coalescing compound assignment parented by an expression statement.
                    // Remove leading trivia from 'leftOfAssignment' as it should have been moved to 'newAssignmentTarget'.
                    leftOfAssignment = leftOfAssignment.WithoutLeadingTrivia();
                    return editor.Generator.AssignmentStatement(newAssignmentTarget,
                        SyntaxFactory.BinaryExpression(SyntaxKind.CoalesceExpression, leftOfAssignment, rightOfAssignment));
                }
            }
            else
            {
                // Case 3. Compound assignment not parented by an expression statement.
                var mappedBinaryExpressionKind = originalCompoundAssignment.Kind().MapCompoundAssignmentKindToBinaryExpressionKind();
                if (mappedBinaryExpressionKind == SyntaxKind.None)
                {
                    return originalCompoundAssignment;
                }

                return SyntaxFactory.BinaryExpression(mappedBinaryExpressionKind, leftOfAssignment, rightOfAssignment);
            }
        }

        protected override SyntaxNode GetReplacementNodeForVarPattern(SyntaxNode originalVarPattern, SyntaxNode newNameNode)
        {
            if (originalVarPattern is not VarPatternSyntax pattern)
                throw ExceptionUtilities.Unreachable();

            // If the replacement node is DiscardDesignationSyntax
            // then we need to just change the incoming var's pattern designation
            if (newNameNode is DiscardDesignationSyntax discardDesignation)
            {
                return pattern.WithDesignation(discardDesignation.WithTriviaFrom(pattern.Designation));
            }

            // Otherwise just return new node as a replacement.
            // This would be the default behavior if there was no special case described above
            return newNameNode;
        }
    }
}
