// 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.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.PooledObjects;

namespace Microsoft.CodeAnalysis.CSharp.UseLabeledJumpStatements;

internal static partial class CSharpUseLabeledJumpStatementsHelpers
{
    /// <summary>
    /// A <c>goto</c> emulating a multi-level <c>break</c> out of an enclosing loop or <c>switch</c>:
    /// <code>
    /// while (...)                  // breakTarget
    /// {
    ///     while (...)
    ///         goto found;          // gotos
    /// }
    /// found: ...                   // labelDeclaration
    /// </code>
    /// </summary>
    public static bool TryGetGotoBreakPattern(
        GotoStatementSyntax gotoStatement,
        SemanticModel semanticModel,
        CancellationToken cancellationToken,
        [NotNullWhen(true)] out StatementSyntax? breakTarget,
        [NotNullWhen(true)] out LabeledStatementSyntax? labelDeclaration,
        out ImmutableArray<GotoStatementSyntax> gotos)
    {
        breakTarget = null;
        labelDeclaration = null;
        gotos = default;

        if (!TryResolveLabel(gotoStatement, semanticModel, cancellationToken, out var label, out var declaration))
            return false;

        // GetPreviousStatement handles the block, switch-section, and top-level (global statement) cases.
        var precedingConstruct = declaration.GetPreviousStatement();

        // A prior fix may have already labeled this loop ('outer: while (...)').  Unwrap to the loop/switch itself so
        // the construct is still recognized (the existing label is reused later, in the fix).
        if (precedingConstruct is LabeledStatementSyntax labeledConstruct)
            precedingConstruct = labeledConstruct.Statement;

        if (!precedingConstruct.IsBreakableConstruct())
            return false;

        // The triggering goto must itself be one of the collected jumps (an unrelated 'goto' to the same label from
        // outside the loop is not part of this pattern).
        if (!TryCollectMultiLevelJumps(precedingConstruct, label, semanticModel, includeSwitch: true, cancellationToken, out gotos) ||
            !gotos.Contains(gotoStatement))
        {
            return false;
        }

        breakTarget = precedingConstruct;
        labelDeclaration = declaration;
        return true;
    }

    /// <summary>
    /// A <c>goto</c> emulating a multi-level <c>continue</c> of an enclosing loop:
    /// <code>
    /// while (...)                  // loop
    /// {
    ///     while (...)
    ///         goto next;           // gotos
    ///     next: ;                  // labelDeclaration
    /// }
    /// </code>
    /// </summary>
    public static bool TryGetGotoContinuePattern(
        GotoStatementSyntax gotoStatement,
        SemanticModel semanticModel,
        CancellationToken cancellationToken,
        [NotNullWhen(true)] out StatementSyntax? loop,
        [NotNullWhen(true)] out LabeledStatementSyntax? labelDeclaration,
        out ImmutableArray<GotoStatementSyntax> gotos)
    {
        loop = null;
        labelDeclaration = null;
        gotos = default;

        // Requiring an empty label as the very last statement of a loop body keeps the rewrite equivalent: jumping
        // there would otherwise run trailing work that a 'continue' would skip.
        if (TryResolveLabel(gotoStatement, semanticModel, cancellationToken, out var label, out var declaration) &&
            declaration is
            {
                Parent: BlockSyntax { Parent: StatementSyntax candidateLoop, Statements: [.., var lastStatement] },
                Statement: EmptyStatementSyntax
            } &&
            lastStatement == declaration &&
            candidateLoop.IsContinuableConstruct() &&
            TryCollectMultiLevelJumps(candidateLoop, label, semanticModel, includeSwitch: false, cancellationToken, out gotos) &&
            gotos.Contains(gotoStatement))
        {
            loop = candidateLoop;
            labelDeclaration = declaration;
            return true;
        }

        return false;
    }

    private static bool TryResolveLabel(
        GotoStatementSyntax gotoStatement,
        SemanticModel semanticModel,
        CancellationToken cancellationToken,
        [NotNullWhen(true)] out ILabelSymbol? label,
        [NotNullWhen(true)] out LabeledStatementSyntax? labeledStatement)
    {
        label = null;
        labeledStatement = null;

        if (!IsPlainGoto(gotoStatement, out var labelReference))
            return false;

        label = semanticModel.GetSymbolInfo(labelReference, cancellationToken).Symbol as ILabelSymbol;
        labeledStatement = label?.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax(cancellationToken) as LabeledStatementSyntax;

        return label != null && labeledStatement != null;
    }

    private static bool TryCollectMultiLevelJumps(
        StatementSyntax loop,
        ILabelSymbol label,
        SemanticModel semanticModel,
        bool includeSwitch,
        CancellationToken cancellationToken,
        out ImmutableArray<GotoStatementSyntax> gotos)
    {
        gotos = default;

        // Every reference to the label must be a 'goto' nested in the loop (so the label can be moved onto the loop
        // and each jump converted), reaching the loop must not cross a 'finally', and at least one jump must be nested
        // deeply enough that a plain (unlabeled) 'break'/'continue' would not reach this loop.
        using var _ = ArrayBuilder<GotoStatementSyntax>.GetInstance(out var builder);
        var anyRequiresLabel = false;
        foreach (var candidate in loop.DescendantNodes().OfType<GotoStatementSyntax>())
        {
            if (!IsPlainGoto(candidate, out var candidateLabel) ||
                !Equals(semanticModel.GetSymbolInfo(candidateLabel, cancellationToken).Symbol, label))
            {
                continue;
            }

            if (CrossesFinally(candidate, loop))
                return false;

            builder.Add(candidate);
            anyRequiresLabel |= CrossesInnerLoopOrSwitch(candidate, loop, includeSwitch);
        }

        if (builder.Count == 0 || !anyRequiresLabel)
            return false;

        gotos = builder.ToImmutable();
        return true;
    }

    private static bool IsPlainGoto(GotoStatementSyntax gotoStatement, [NotNullWhen(true)] out IdentifierNameSyntax? label)
    {
        if (gotoStatement is GotoStatementSyntax(SyntaxKind.GotoStatement) { Expression: IdentifierNameSyntax identifier })
        {
            label = identifier;
            return true;
        }

        label = null;
        return false;
    }

    private static bool CrossesFinally(SyntaxNode jump, SyntaxNode target)
    {
        for (var current = jump; current != null && current != target; current = current.Parent)
        {
            if (current is FinallyClauseSyntax)
                return true;
        }

        return false;
    }

    private static bool CrossesInnerLoopOrSwitch(SyntaxNode jump, SyntaxNode target, bool includeSwitch)
    {
        for (var current = jump.Parent; current != null && current != target; current = current.Parent)
        {
            if (includeSwitch ? current.IsBreakableConstruct() : current.IsContinuableConstruct())
                return true;
        }

        return false;
    }
}
