// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure;

internal static class SyntaxNodeExtensions
{
    /// <summary>
    /// Look inside a trivia list for a skipped token that contains the given position.
    /// </summary>
    private static readonly Func<SyntaxTriviaList, int, SyntaxToken> FindSkippedTokenBackwardFunc = FindSkippedTokenBackward;

    public static SyntaxNode GetRequiredParent(this SyntaxNode node)
        => node.Parent ?? throw new InvalidOperationException("Node's parent was null");

    public static SyntaxNode? GetParent(this SyntaxNode node, bool ascendOutOfTrivia)
    {
        var parent = node.Parent;
        if (parent == null && ascendOutOfTrivia)
        {
            if (node is IStructuredTriviaSyntax structuredTrivia)
            {
                parent = structuredTrivia.ParentTrivia.Token.Parent;
            }
        }

        return parent;
    }

    public static bool IsLiteralExpression([NotNullWhen(true)] this SyntaxNode? node)
        => node is LiteralExpressionSyntax;

    public static bool IsBinaryExpression([NotNullWhen(true)] this SyntaxNode? node)
        => node is BinaryExpressionSyntax;

    [return: NotNullIfNotNull("node")]
    public static SyntaxNode? WalkUpParentheses(this SyntaxNode? node)
    {
        while (node?.Parent?.RawKind == (int)SyntaxKind.ParenthesizedExpression)
        {
            node = node.Parent;
        }

        return node;
    }

    public static bool IsAnyInitializerExpression([NotNullWhen(true)] this SyntaxNode? node, [NotNullWhen(true)] out SyntaxNode? creationExpression)
    {
        if (node is InitializerExpressionSyntax
            {
                Parent: BaseObjectCreationExpressionSyntax or ArrayCreationExpressionSyntax or ImplicitArrayCreationExpressionSyntax
            })
        {
            creationExpression = node.Parent;
            return true;
        }

        creationExpression = null;
        return false;
    }

    public static bool IsSimpleAssignmentStatement([NotNullWhen(true)] this SyntaxNode? statement)
        => statement is ExpressionStatementSyntax exprStatement &&
           exprStatement.Expression.IsKind(SyntaxKind.SimpleAssignmentExpression);

    /// <summary>
    /// If the position is inside of token, return that token; otherwise, return the token to the left.
    /// </summary>
    public static SyntaxToken FindTokenOnLeftOfPosition(
        this SyntaxNode root,
        int position,
        bool includeSkipped = false,
        bool includeDirectives = false,
        bool includeDocumentationComments = false)
    {
        var findSkippedToken = includeSkipped ? FindSkippedTokenBackwardFunc : ((l, p) => default);

        var token = GetInitialToken(root, position, includeSkipped, includeDirectives, includeDocumentationComments);

        if (position <= token.SpanStart)
        {
            do
            {
                var skippedToken = findSkippedToken(token.LeadingTrivia, position);
                token = skippedToken.RawKind != 0
                    ? skippedToken
                    : token.GetPreviousToken(includeZeroWidth: false, includeSkipped: includeSkipped, includeDirectives: includeDirectives, includeDocumentationComments: includeDocumentationComments);
            }
            while (position <= token.SpanStart && root.FullSpan.Start < token.SpanStart);
        }
        else if (token.Span.End < position)
        {
            var skippedToken = findSkippedToken(token.TrailingTrivia, position);
            token = skippedToken.RawKind != 0 ? skippedToken : token;
        }

        if (token.Span.Length == 0)
        {
            token = token.GetPreviousToken();
        }

        return token;
    }

    private static SyntaxToken GetInitialToken(
        SyntaxNode root,
        int position,
        bool includeSkipped = false,
        bool includeDirectives = false,
        bool includeDocumentationComments = false)
    {
        return position < root.FullSpan.End || !(root is ICompilationUnitSyntax)
            ? root.FindToken(position, includeSkipped || includeDirectives || includeDocumentationComments)
            : root.GetLastToken(includeZeroWidth: true, includeSkipped: true, includeDirectives: true, includeDocumentationComments: true)
                  .GetPreviousToken(includeZeroWidth: false, includeSkipped: includeSkipped, includeDirectives: includeDirectives, includeDocumentationComments: includeDocumentationComments);
    }

    /// <summary>
    /// Look inside a trivia list for a skipped token that contains the given position.
    /// </summary>
    private static SyntaxToken FindSkippedTokenBackward(SyntaxTriviaList triviaList, int position)
    {
        foreach (var trivia in triviaList.Reverse())
        {
            if (trivia.HasStructure)
            {
                if (trivia.GetStructure() is ISkippedTokensTriviaSyntax skippedTokensTrivia)
                {
                    foreach (var token in skippedTokensTrivia.Tokens)
                    {
                        if (token.Span.Length > 0 && token.SpanStart <= position)
                        {
                            return token;
                        }
                    }
                }
            }
        }

        return default;
    }
}
