// 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;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.PooledObjects;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.CSharp
{
    public static class SyntaxExtensions
    {
        /// <summary>
        /// Gets the expression-body syntax from an expression-bodied member. The
        /// given syntax must be for a member which could contain an expression-body.
        /// </summary>
        internal static ArrowExpressionClauseSyntax? GetExpressionBodySyntax(this CSharpSyntaxNode node)
        {
            ArrowExpressionClauseSyntax? arrowExpr = null;
            switch (node.Kind())
            {
                // The ArrowExpressionClause is the declaring syntax for the
                // 'get' SourcePropertyAccessorSymbol of properties and indexers.
                case SyntaxKind.ArrowExpressionClause:
                    arrowExpr = (ArrowExpressionClauseSyntax)node;
                    break;
                case SyntaxKind.MethodDeclaration:
                case SyntaxKind.OperatorDeclaration:
                case SyntaxKind.ConversionOperatorDeclaration:
                case SyntaxKind.ConstructorDeclaration:
                case SyntaxKind.DestructorDeclaration:
                    arrowExpr = ((BaseMethodDeclarationSyntax)node).ExpressionBody;
                    break;
                case SyntaxKind.GetAccessorDeclaration:
                case SyntaxKind.SetAccessorDeclaration:
                case SyntaxKind.InitAccessorDeclaration:
                case SyntaxKind.AddAccessorDeclaration:
                case SyntaxKind.RemoveAccessorDeclaration:
                case SyntaxKind.UnknownAccessorDeclaration:
                    arrowExpr = ((AccessorDeclarationSyntax)node).ExpressionBody;
                    break;
                case SyntaxKind.PropertyDeclaration:
                    arrowExpr = ((PropertyDeclarationSyntax)node).ExpressionBody;
                    break;
                case SyntaxKind.IndexerDeclaration:
                    arrowExpr = ((IndexerDeclarationSyntax)node).ExpressionBody;
                    break;
                default:
                    // Don't throw, just use for the assert in case this is used in the semantic model
                    ExceptionUtilities.UnexpectedValue(node.Kind());
                    break;
            }
            return arrowExpr;
        }

        /// <summary>
        /// Creates a new syntax token with all whitespace and end of line trivia replaced with
        /// regularly formatted trivia.
        /// </summary>
        /// <param name="token">The token to normalize.</param>
        /// <param name="indentation">A sequence of whitespace characters that defines a single level of indentation.</param>
        /// <param name="elasticTrivia">If true the replaced trivia is elastic trivia.</param>
        public static SyntaxToken NormalizeWhitespace(this SyntaxToken token, string indentation, bool elasticTrivia)
        {
            return SyntaxNormalizer.Normalize(token, indentation, CodeAnalysis.SyntaxNodeExtensions.DefaultEOL, elasticTrivia);
        }

        /// <summary>
        /// Return the identifier of an out declaration argument expression.
        /// </summary>
        internal static SyntaxToken Identifier(this DeclarationExpressionSyntax self)
        {
            return ((SingleVariableDesignationSyntax)self.Designation).Identifier;
        }

        /// <summary>
        /// Creates a new syntax token with all whitespace and end of line trivia replaced with
        /// regularly formatted trivia.
        /// </summary>
        /// <param name="token">The token to normalize.</param>
        /// <param name="indentation">An optional sequence of whitespace characters that defines a
        /// single level of indentation.</param>
        /// <param name="eol">An optional sequence of whitespace characters used for end of line.</param>
        /// <param name="elasticTrivia">If true the replaced trivia is elastic trivia.</param>
        public static SyntaxToken NormalizeWhitespace(this SyntaxToken token,
            string indentation = CodeAnalysis.SyntaxNodeExtensions.DefaultIndentation,
            string eol = CodeAnalysis.SyntaxNodeExtensions.DefaultEOL,
            bool elasticTrivia = false)
        {
            return SyntaxNormalizer.Normalize(token, indentation, eol, elasticTrivia);
        }

        /// <summary>
        /// Creates a new syntax trivia list with all whitespace and end of line trivia replaced with
        /// regularly formatted trivia.
        /// </summary>
        /// <param name="list">The trivia list to normalize.</param>
        /// <param name="indentation">A sequence of whitespace characters that defines a single level of indentation.</param>
        /// <param name="elasticTrivia">If true the replaced trivia is elastic trivia.</param>
        public static SyntaxTriviaList NormalizeWhitespace(this SyntaxTriviaList list, string indentation, bool elasticTrivia)
        {
            return SyntaxNormalizer.Normalize(list, indentation, CodeAnalysis.SyntaxNodeExtensions.DefaultEOL, elasticTrivia);
        }

        /// <summary>
        /// Creates a new syntax trivia list with all whitespace and end of line trivia replaced with
        /// regularly formatted trivia.
        /// </summary>
        /// <param name="list">The trivia list to normalize.</param>
        /// <param name="indentation">An optional sequence of whitespace characters that defines a
        /// single level of indentation.</param>
        /// <param name="eol">An optional sequence of whitespace characters used for end of line.</param>
        /// <param name="elasticTrivia">If true the replaced trivia is elastic trivia.</param>
        public static SyntaxTriviaList NormalizeWhitespace(this SyntaxTriviaList list,
            string indentation = CodeAnalysis.SyntaxNodeExtensions.DefaultIndentation,
            string eol = CodeAnalysis.SyntaxNodeExtensions.DefaultEOL,
            bool elasticTrivia = false)
        {
            return SyntaxNormalizer.Normalize(list, indentation, eol, elasticTrivia);
        }

        public static SyntaxTriviaList ToSyntaxTriviaList(this IEnumerable<SyntaxTrivia> sequence)
        {
            return SyntaxFactory.TriviaList(sequence);
        }

        internal static XmlNameAttributeElementKind GetElementKind(this XmlNameAttributeSyntax attributeSyntax)
        {
            Debug.Assert(attributeSyntax.Parent is object);
            CSharpSyntaxNode parentSyntax = attributeSyntax.Parent;
            SyntaxKind parentKind = parentSyntax.Kind();

            string parentName;
            if (parentKind == SyntaxKind.XmlEmptyElement)
            {
                var parent = (XmlEmptyElementSyntax)parentSyntax;
                parentName = parent.Name.LocalName.ValueText;
                Debug.Assert(parent.Name.Prefix is null);
            }
            else if (parentKind == SyntaxKind.XmlElementStartTag)
            {
                var parent = (XmlElementStartTagSyntax)parentSyntax;
                parentName = parent.Name.LocalName.ValueText;
                Debug.Assert(parent.Name.Prefix is null);
            }
            else
            {
                throw ExceptionUtilities.UnexpectedValue(parentKind);
            }

            if (DocumentationCommentXmlNames.ElementEquals(parentName, DocumentationCommentXmlNames.ParameterElementName))
            {
                return XmlNameAttributeElementKind.Parameter;
            }
            else if (DocumentationCommentXmlNames.ElementEquals(parentName, DocumentationCommentXmlNames.ParameterReferenceElementName))
            {
                return XmlNameAttributeElementKind.ParameterReference;
            }
            else if (DocumentationCommentXmlNames.ElementEquals(parentName, DocumentationCommentXmlNames.TypeParameterElementName))
            {
                return XmlNameAttributeElementKind.TypeParameter;
            }
            else if (DocumentationCommentXmlNames.ElementEquals(parentName, DocumentationCommentXmlNames.TypeParameterReferenceElementName))
            {
                return XmlNameAttributeElementKind.TypeParameterReference;
            }
            else
            {
                throw ExceptionUtilities.UnexpectedValue(parentName);
            }
        }

        internal static bool ReportDocumentationCommentDiagnostics(this SyntaxTree tree)
        {
            return tree.Options.DocumentationMode >= DocumentationMode.Diagnose;
        }

        /// <summary>
        /// Updates the given SimpleNameSyntax node with the given identifier token.
        /// This function is a wrapper that calls WithIdentifier on derived syntax nodes.
        /// </summary>
        /// <param name="simpleName"></param>
        /// <param name="identifier"></param>
        /// <returns>The given simple name updated with the given identifier.</returns>
        public static SimpleNameSyntax WithIdentifier(this SimpleNameSyntax simpleName, SyntaxToken identifier)
        {
            return simpleName.Kind() == SyntaxKind.IdentifierName
                ? (SimpleNameSyntax)((IdentifierNameSyntax)simpleName).WithIdentifier(identifier)
                : (SimpleNameSyntax)((GenericNameSyntax)simpleName).WithIdentifier(identifier);
        }

        internal static bool IsTypeInContextWhichNeedsDynamicAttribute(this IdentifierNameSyntax typeNode)
        {
            Debug.Assert(typeNode != null);
            return SyntaxFacts.IsInTypeOnlyContext(typeNode) && IsInContextWhichNeedsDynamicAttribute(typeNode);
        }

        internal static ExpressionSyntax SkipParens(this ExpressionSyntax expression)
        {
            while (expression.Kind() == SyntaxKind.ParenthesizedExpression)
            {
                expression = ((ParenthesizedExpressionSyntax)expression).Expression;
            }

            return expression;
        }

        /// <summary>
        /// Returns true if the expression on the left-hand-side of an assignment causes the assignment to be a deconstruction.
        /// </summary>
        internal static bool IsDeconstructionLeft(this ExpressionSyntax node)
        {
            switch (node.Kind())
            {
                case SyntaxKind.TupleExpression:
                    return true;
                case SyntaxKind.DeclarationExpression:
                    return ((DeclarationExpressionSyntax)node).Designation.Kind() == SyntaxKind.ParenthesizedVariableDesignation;
                default:
                    return false;
            }
        }

        internal static bool IsDeconstruction(this AssignmentExpressionSyntax self)
        {
            return self.Left.IsDeconstructionLeft();
        }

        private static bool IsInContextWhichNeedsDynamicAttribute(CSharpSyntaxNode node)
        {
            Debug.Assert(node != null);

            switch (node.Kind())
            {
                case SyntaxKind.Parameter:
                case SyntaxKind.FieldDeclaration:
                case SyntaxKind.MethodDeclaration:
                case SyntaxKind.IndexerDeclaration:
                case SyntaxKind.OperatorDeclaration:
                case SyntaxKind.ConversionOperatorDeclaration:
                case SyntaxKind.PropertyDeclaration:
                case SyntaxKind.DelegateDeclaration:
                case SyntaxKind.EventDeclaration:
                case SyntaxKind.EventFieldDeclaration:
                case SyntaxKind.BaseList:
                case SyntaxKind.SimpleBaseType:
                case SyntaxKind.PrimaryConstructorBaseType:
                    return true;

                case SyntaxKind.Block:
                case SyntaxKind.VariableDeclarator:
                case SyntaxKind.TypeParameterConstraintClause:
                case SyntaxKind.Attribute:
                case SyntaxKind.EqualsValueClause:
                    return false;

                default:
                    return node.Parent != null && IsInContextWhichNeedsDynamicAttribute(node.Parent);
            }
        }

        public static IndexerDeclarationSyntax Update(
            this IndexerDeclarationSyntax syntax,
            SyntaxList<AttributeListSyntax> attributeLists,
            SyntaxTokenList modifiers,
            TypeSyntax type,
            ExplicitInterfaceSpecifierSyntax explicitInterfaceSpecifier,
            SyntaxToken thisKeyword,
            BracketedParameterListSyntax parameterList,
            AccessorListSyntax accessorList)
        {
            return syntax.Update(
                attributeLists,
                modifiers,
                type,
                explicitInterfaceSpecifier,
                thisKeyword,
                parameterList,
                accessorList,
                expressionBody: null,
                semicolonToken: default);
        }

        public static OperatorDeclarationSyntax Update(
            this OperatorDeclarationSyntax syntax,
            SyntaxList<AttributeListSyntax> attributeLists,
            SyntaxTokenList modifiers,
            TypeSyntax returnType,
            SyntaxToken operatorKeyword,
            SyntaxToken operatorToken,
            ParameterListSyntax parameterList,
            BlockSyntax block,
            SyntaxToken semicolonToken)
        {
            return syntax.Update(
                attributeLists,
                modifiers,
                returnType,
                operatorKeyword,
                operatorToken,
                parameterList,
                block,
                expressionBody: null,
                semicolonToken);
        }

        public static MethodDeclarationSyntax Update(
            this MethodDeclarationSyntax syntax,
            SyntaxList<AttributeListSyntax> attributeLists,
            SyntaxTokenList modifiers,
            TypeSyntax returnType,
            ExplicitInterfaceSpecifierSyntax explicitInterfaceSpecifier,
            SyntaxToken identifier,
            TypeParameterListSyntax typeParameterList,
            ParameterListSyntax parameterList,
            SyntaxList<TypeParameterConstraintClauseSyntax> constraintClauses,
            BlockSyntax block,
            SyntaxToken semicolonToken)
        {
            return syntax.Update(
                attributeLists,
                modifiers,
                returnType,
                explicitInterfaceSpecifier,
                identifier,
                typeParameterList,
                parameterList,
                constraintClauses,
                block,
                expressionBody: null,
                semicolonToken);
        }

        /// <summary>
        /// If this declaration or identifier is part of a deconstruction, find the deconstruction.
        /// If found, returns either an assignment expression or a foreach variable statement.
        /// Returns null otherwise.
        /// </summary>
        internal static CSharpSyntaxNode? GetContainingDeconstruction(this ExpressionSyntax expr)
        {
            var kind = expr.Kind();
            if (kind != SyntaxKind.TupleExpression && kind != SyntaxKind.DeclarationExpression && kind != SyntaxKind.IdentifierName)
            {
                return null;
            }

            while (true)
            {
                Debug.Assert(expr.Kind() == SyntaxKind.TupleExpression || expr.Kind() == SyntaxKind.DeclarationExpression || expr.Kind() == SyntaxKind.IdentifierName);
                var parent = expr.Parent;
                if (parent == null) { return null; }

                switch (parent.Kind())
                {
                    case SyntaxKind.Argument:
                        if (parent.Parent?.Kind() == SyntaxKind.TupleExpression)
                        {
                            expr = (TupleExpressionSyntax)parent.Parent;
                            continue;
                        }
                        return null;
                    case SyntaxKind.SimpleAssignmentExpression:
                        if ((object)((AssignmentExpressionSyntax)parent).Left == expr)
                        {
                            return parent;
                        }
                        return null;
                    case SyntaxKind.ForEachVariableStatement:
                        if ((object)((ForEachVariableStatementSyntax)parent).Variable == expr)
                        {
                            return parent;
                        }
                        return null;
                    default:
                        return null;
                }
            }
        }

        internal static bool IsOutDeclaration(this DeclarationExpressionSyntax p)
        {
            return p.Parent?.Kind() == SyntaxKind.Argument
                && ((ArgumentSyntax)p.Parent).RefOrOutKeyword.Kind() == SyntaxKind.OutKeyword;
        }

        internal static bool IsOutVarDeclaration(this DeclarationExpressionSyntax p)
        {
            return p.Designation.Kind() == SyntaxKind.SingleVariableDesignation && p.IsOutDeclaration();
        }

#nullable enable
        /// <summary>
        /// Visits all the ArrayRankSpecifiers of a typeSyntax, invoking an action on each one in turn.
        /// </summary>
        /// <param name="type"></param>
        /// <param name="action"></param>
        /// <param name="argument">The argument that is passed to the action whenever it is invoked</param>
        internal static void VisitRankSpecifiers<TArg>(this TypeSyntax type, Action<ArrayRankSpecifierSyntax, TArg> action, in TArg argument)
        {
            // Use a manual stack here to avoid deeply nested recursion which can blow the real stack
            var stack = ArrayBuilder<SyntaxNode>.GetInstance();
            stack.Push(type);

            while (stack.Count > 0)
            {
                var current = stack.Pop();
                if (current is ArrayRankSpecifierSyntax rankSpecifier)
                {
                    action(rankSpecifier, argument);
                    continue;
                }
                else
                {
                    type = (TypeSyntax)current;
                }

                switch (type.Kind())
                {
                    case SyntaxKind.ArrayType:
                        var arrayTypeSyntax = (ArrayTypeSyntax)type;
                        for (int i = arrayTypeSyntax.RankSpecifiers.Count - 1; i >= 0; i--)
                        {
                            stack.Push(arrayTypeSyntax.RankSpecifiers[i]);
                        }
                        stack.Push(arrayTypeSyntax.ElementType);
                        break;
                    case SyntaxKind.NullableType:
                        var nullableTypeSyntax = (NullableTypeSyntax)type;
                        stack.Push(nullableTypeSyntax.ElementType);
                        break;
                    case SyntaxKind.PointerType:
                        var pointerTypeSyntax = (PointerTypeSyntax)type;
                        stack.Push(pointerTypeSyntax.ElementType);
                        break;
                    case SyntaxKind.FunctionPointerType:
                        var functionPointerTypeSyntax = (FunctionPointerTypeSyntax)type;
                        for (int i = functionPointerTypeSyntax.ParameterList.Parameters.Count - 1; i >= 0; i--)
                        {
                            TypeSyntax? paramType = functionPointerTypeSyntax.ParameterList.Parameters[i].Type;
                            Debug.Assert(paramType is object);
                            stack.Push(paramType);
                        }
                        break;
                    case SyntaxKind.TupleType:
                        var tupleTypeSyntax = (TupleTypeSyntax)type;
                        for (int i = tupleTypeSyntax.Elements.Count - 1; i >= 0; i--)
                        {
                            stack.Push(tupleTypeSyntax.Elements[i].Type);
                        }
                        break;
                    case SyntaxKind.RefType:
                        var refTypeSyntax = (RefTypeSyntax)type;
                        stack.Push(refTypeSyntax.Type);
                        break;
                    case SyntaxKind.GenericName:
                        var genericNameSyntax = (GenericNameSyntax)type;
                        for (int i = genericNameSyntax.TypeArgumentList.Arguments.Count - 1; i >= 0; i--)
                        {
                            stack.Push(genericNameSyntax.TypeArgumentList.Arguments[i]);
                        }
                        break;
                    case SyntaxKind.QualifiedName:
                        var qualifiedNameSyntax = (QualifiedNameSyntax)type;
                        stack.Push(qualifiedNameSyntax.Right);
                        stack.Push(qualifiedNameSyntax.Left);
                        break;
                    case SyntaxKind.AliasQualifiedName:
                        var aliasQualifiedNameSyntax = (AliasQualifiedNameSyntax)type;
                        stack.Push(aliasQualifiedNameSyntax.Name);
                        break;
                    case SyntaxKind.IdentifierName:
                    case SyntaxKind.OmittedTypeArgument:
                    case SyntaxKind.PredefinedType:
                        break;
                    default:
                        throw ExceptionUtilities.UnexpectedValue(type.Kind());
                }
            }

            stack.Free();
        }
    }
}
