// 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.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;

namespace Microsoft.CodeAnalysis.SplitOrMergeIfStatements;

internal abstract class AbstractSplitIntoConsecutiveIfStatementsCodeRefactoringProvider
    : AbstractSplitIfStatementCodeRefactoringProvider
{
    // Converts:
    //    if (a || b)
    //        Console.WriteLine();
    //
    // To:
    //    if (a)
    //        Console.WriteLine();
    //    else if (b)
    //        Console.WriteLine();

    // Converts:
    //    if (a || b)
    //        return;
    //
    // To:
    //    if (a)
    //        return;
    //    if (b)
    //        return;

    // The second case is applied if control flow quits from inside the body.

    protected sealed override int GetLogicalExpressionKind(ISyntaxKindsService syntaxKinds)
        => syntaxKinds.LogicalOrExpression;

    protected sealed override CodeAction CreateCodeAction(Func<CancellationToken, Task<Document>> createChangedDocument, string ifKeywordText)
        => CodeAction.Create(
            string.Format(FeaturesResources.Split_into_consecutive_0_statements, ifKeywordText),
            createChangedDocument,
            nameof(FeaturesResources.Split_into_consecutive_0_statements) + "_" + ifKeywordText);

    protected sealed override async Task<SyntaxNode> GetChangedRootAsync(
        Document document,
        SyntaxNode root,
        SyntaxNode ifOrElseIf,
        SyntaxNode leftCondition,
        SyntaxNode rightCondition,
        CancellationToken cancellationToken)
    {
        var syntaxFacts = document.GetLanguageService<ISyntaxFactsService>();
        var blockFacts = document.GetLanguageService<IBlockFactsService>();
        var ifGenerator = document.GetLanguageService<IIfLikeStatementGenerator>();
        var generator = document.GetLanguageService<SyntaxGenerator>();

        leftCondition = leftCondition.WithAdditionalAnnotations(Formatter.Annotation);
        rightCondition = rightCondition.WithAdditionalAnnotations(Formatter.Annotation);

        var editor = new SyntaxEditor(root, generator);

        editor.ReplaceNode(ifOrElseIf, (currentNode, _) => ifGenerator.WithCondition(currentNode, leftCondition));

        if (await CanBeSeparateStatementsAsync(document, blockFacts, ifGenerator, ifOrElseIf, cancellationToken).ConfigureAwait(false))
        {
            // Generate:
            // if (a)
            //     return;
            // if (b)
            //     return;

            // At this point, ifLikeStatement must be a standalone if statement with no else clause.
            Debug.Assert(syntaxFacts.IsExecutableStatement(ifOrElseIf));
            Debug.Assert(ifGenerator.GetElseIfAndElseClauses(ifOrElseIf).Length == 0);

            var secondIfStatement = ifGenerator.WithCondition(ifOrElseIf, rightCondition)
                .WithPrependedLeadingTrivia(generator.ElasticCarriageReturnLineFeed);

            if (!blockFacts.IsExecutableBlock(ifOrElseIf.Parent))
            {
                // In order to insert a new statement, we have to be inside a block.
                editor.ReplaceNode(ifOrElseIf, (currentNode, _) => generator.ScopeBlock([currentNode]));
            }

            editor.InsertAfter(ifOrElseIf, secondIfStatement);
        }
        else
        {
            // Generate:
            // if (a)
            //     Console.WriteLine();
            // else if (b)
            //     Console.WriteLine();

            // If the if statement is not an else-if clause, we convert it to an else-if clause first (for VB).
            // Then we insert it right after our current if statement or else-if clause.

            var elseIfClause = ifGenerator.WithCondition(ifGenerator.ToElseIfClause(ifOrElseIf), rightCondition);

            ifGenerator.InsertElseIfClause(editor, ifOrElseIf, elseIfClause);
        }

        return editor.GetChangedRoot();
    }

    private static async Task<bool> CanBeSeparateStatementsAsync(
        Document document,
        IBlockFactsService blockFacts,
        IIfLikeStatementGenerator ifGenerator,
        SyntaxNode ifOrElseIf,
        CancellationToken cancellationToken)
    {
        // In order to make separate statements, ifOrElseIf must be an if statement, not an else-if clause.
        if (ifGenerator.IsElseIfClause(ifOrElseIf, out _))
        {
            return false;
        }

        // If there is an else clause, we *could* in theory separate these and move the current else clause to the second
        // statement, but we won't. It would break the else-if chain in an odd way. We'll insert an else-if instead.
        if (ifGenerator.GetElseIfAndElseClauses(ifOrElseIf).Length > 0)
        {
            return false;
        }

        var insideStatements = blockFacts.GetStatementContainerStatements(ifOrElseIf);
        if (insideStatements.Count == 0)
        {
            // Even though there are no statements inside, we still can't split this into separate statements
            // because it would change the semantics from short-circuiting to always evaluating the second condition,
            // breaking code like 'if (a == null || a.InstanceMethod())'.
            return false;
        }
        else
        {
            // There are statements inside. We can split this into separate statements and leave out the 'else' if
            // control flow can't reach the end of these statements (otherwise, it would continue to the second 'if'
            // and in the case that both conditions are true, run the same statements twice).
            // This will typically look like a single return, break, continue or a throw statement.

            var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
            var controlFlow = semanticModel.AnalyzeControlFlow(insideStatements[0], insideStatements[insideStatements.Count - 1]);

            return !controlFlow.EndPointIsReachable;
        }
    }
}
