' 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.

Imports System.Runtime.CompilerServices
Imports System.Threading
Imports Microsoft.CodeAnalysis
Imports Microsoft.CodeAnalysis.VisualBasic.Syntax

Namespace Microsoft.CodeAnalysis.VisualBasic.Extensions
    Partial Friend Module ExpressionSyntaxExtensions
        <Extension()>
        Public Function WalkUpParentheses(expression As ExpressionSyntax) As ExpressionSyntax
            While expression.IsParentKind(SyntaxKind.ParenthesizedExpression)
                expression = DirectCast(expression.Parent, ExpressionSyntax)
            End While

            Return expression
        End Function

        <Extension()>
        Public Function WalkDownParentheses(expression As ExpressionSyntax) As ExpressionSyntax
            While expression.IsKind(SyntaxKind.ParenthesizedExpression)
                expression = DirectCast(expression, ParenthesizedExpressionSyntax).Expression
            End While

            Return expression
        End Function

        <Extension()>
        Public Function IsLeftSideOfDot(expression As ExpressionSyntax) As Boolean
            If expression Is Nothing Then
                Return False
            End If

            Return _
                (expression.IsParentKind(SyntaxKind.QualifiedName) AndAlso DirectCast(expression.Parent, QualifiedNameSyntax).Left Is expression) OrElse
                (expression.IsParentKind(SyntaxKind.SimpleMemberAccessExpression) AndAlso DirectCast(expression.Parent, MemberAccessExpressionSyntax).Expression Is expression)
        End Function

        <Extension()>
        Public Function IsNewOnRightSideOfDotOrBang(expression As ExpressionSyntax) As Boolean
            Dim identifierName = TryCast(expression, IdentifierNameSyntax)
            If identifierName Is Nothing Then
                Return False
            End If

            If String.Compare(identifierName.Identifier.ToString(), "New", StringComparison.OrdinalIgnoreCase) <> 0 Then
                Return False
            End If

            Return identifierName.IsRightSideOfDotOrBang()
        End Function

        <Extension()>
        Public Function IsSimpleMemberAccessExpressionName(expression As ExpressionSyntax) As Boolean
            Return expression.IsParentKind(SyntaxKind.SimpleMemberAccessExpression) AndAlso
                   DirectCast(expression.Parent, MemberAccessExpressionSyntax).Name Is expression
        End Function

        <Extension()>
        Public Function IsAnyMemberAccessExpressionName(expression As ExpressionSyntax) As Boolean
            Return TypeOf expression?.Parent Is MemberAccessExpressionSyntax AndAlso
                   DirectCast(expression.Parent, MemberAccessExpressionSyntax).Name Is expression
        End Function

        <Extension()>
        Public Function IsRightSideOfDotOrBang(expression As ExpressionSyntax) As Boolean
            Return expression.IsAnyMemberAccessExpressionName() OrElse expression.IsRightSideOfQualifiedName()
        End Function

        <Extension()>
        Public Function IsRightSideOfDot(expression As ExpressionSyntax) As Boolean
            Return expression.IsSimpleMemberAccessExpressionName() OrElse expression.IsRightSideOfQualifiedName()
        End Function

        <Extension()>
        Public Function IsRightSideOfQualifiedName(expression As ExpressionSyntax) As Boolean
            Return expression.IsParentKind(SyntaxKind.QualifiedName) AndAlso
                   DirectCast(expression.Parent, QualifiedNameSyntax).Right Is expression
        End Function

        <Extension()>
        Public Function IsLeftSideOfQualifiedName(expression As ExpressionSyntax) As Boolean
            Return expression.IsParentKind(SyntaxKind.QualifiedName) AndAlso
                   DirectCast(expression.Parent, QualifiedNameSyntax).Left Is expression
        End Function

        <Extension()>
        Public Function IsAnyLiteralExpression(expression As ExpressionSyntax) As Boolean
            Return expression.IsKind(SyntaxKind.CharacterLiteralExpression) OrElse
                expression.IsKind(SyntaxKind.DateLiteralExpression) OrElse
                expression.IsKind(SyntaxKind.FalseLiteralExpression) OrElse
                expression.IsKind(SyntaxKind.NothingLiteralExpression) OrElse
                expression.IsKind(SyntaxKind.NumericLiteralExpression) OrElse
                expression.IsKind(SyntaxKind.StringLiteralExpression) OrElse
                expression.IsKind(SyntaxKind.TrueLiteralExpression)
        End Function

        <Extension()>
        Public Function DetermineType(expression As ExpressionSyntax,
                                      semanticModel As SemanticModel,
                                      cancellationToken As CancellationToken) As ITypeSymbol
            ' If a parameter appears to have a void return type, then just use 'object' instead.
            If expression IsNot Nothing Then
                Dim typeInfo = semanticModel.GetTypeInfo(expression, cancellationToken)
                Dim symbolInfo = semanticModel.GetSymbolInfo(expression, cancellationToken)
                If typeInfo.Type IsNot Nothing AndAlso typeInfo.Type.SpecialType = SpecialType.System_Void Then
                    Return semanticModel.Compilation.ObjectType
                End If

                Dim symbol = If(typeInfo.Type, symbolInfo.GetAnySymbol())
                If symbol IsNot Nothing Then
                    Return symbol.ConvertToType(semanticModel.Compilation)
                End If

                If TypeOf expression Is CollectionInitializerSyntax Then
                    Dim collectionInitializer = DirectCast(expression, CollectionInitializerSyntax)
                    Return DetermineType(collectionInitializer, semanticModel, cancellationToken)
                End If
            End If

            Return semanticModel.Compilation.ObjectType
        End Function

        <Extension()>
        Private Function DetermineType(collectionInitializer As CollectionInitializerSyntax,
                                      semanticModel As SemanticModel,
                                      cancellationToken As CancellationToken) As ITypeSymbol
            Dim rank = 1
            While collectionInitializer.Initializers.Count > 0 AndAlso
                  collectionInitializer.Initializers(0).Kind = SyntaxKind.CollectionInitializer
                rank += 1
                collectionInitializer = DirectCast(collectionInitializer.Initializers(0), CollectionInitializerSyntax)
            End While

            Dim type = collectionInitializer.Initializers.FirstOrDefault().DetermineType(semanticModel, cancellationToken)
            Return semanticModel.Compilation.CreateArrayTypeSymbol(type, rank)
        End Function

        Private Function IsUnnecessaryCast(
            castNode As ExpressionSyntax,
            castExpressionNode As ExpressionSyntax,
            semanticModel As SemanticModel,
            assumeCallKeyword As Boolean,
            cancellationToken As CancellationToken
        ) As Boolean

            Return CastAnalyzer.IsUnnecessary(castNode, castExpressionNode, semanticModel, assumeCallKeyword, cancellationToken)
        End Function

        <Extension>
        Public Function IsUnnecessaryCast(
            node As CastExpressionSyntax,
            semanticModel As SemanticModel,
            cancellationToken As CancellationToken,
            Optional assumeCallKeyword As Boolean = False
        ) As Boolean

            Return IsUnnecessaryCast(node, node.Expression, semanticModel, assumeCallKeyword, cancellationToken)
        End Function

        <Extension>
        Public Function IsUnnecessaryCast(
            node As PredefinedCastExpressionSyntax,
            semanticModel As SemanticModel,
            cancellationToken As CancellationToken,
            Optional assumeCallKeyword As Boolean = False
        ) As Boolean

            Return IsUnnecessaryCast(node, node.Expression, semanticModel, assumeCallKeyword, cancellationToken)
        End Function

        <Extension>
        Public Function GetOperatorPrecedence(expression As ExpressionSyntax) As OperatorPrecedence
            Select Case expression.Kind
                Case SyntaxKind.ExponentiateExpression
                    Return OperatorPrecedence.PrecedenceExponentiate
                Case SyntaxKind.UnaryMinusExpression,
                     SyntaxKind.UnaryPlusExpression
                    Return OperatorPrecedence.PrecedenceNegate
                Case SyntaxKind.MultiplyExpression,
                     SyntaxKind.DivideExpression
                    Return OperatorPrecedence.PrecedenceMultiply
                Case SyntaxKind.IntegerDivideExpression
                    Return OperatorPrecedence.PrecedenceIntegerDivide
                Case SyntaxKind.ModuloExpression
                    Return OperatorPrecedence.PrecedenceModulus
                Case SyntaxKind.AddExpression,
                     SyntaxKind.SubtractExpression
                    Return OperatorPrecedence.PrecedenceAdd
                Case SyntaxKind.ConcatenateExpression
                    Return OperatorPrecedence.PrecedenceConcatenate
                Case SyntaxKind.LeftShiftExpression,
                     SyntaxKind.RightShiftExpression
                    Return OperatorPrecedence.PrecedenceShift
                Case SyntaxKind.EqualsExpression,
                     SyntaxKind.NotEqualsExpression,
                     SyntaxKind.LessThanExpression,
                     SyntaxKind.GreaterThanExpression,
                     SyntaxKind.LessThanOrEqualExpression,
                     SyntaxKind.GreaterThanOrEqualExpression,
                     SyntaxKind.LikeExpression,
                     SyntaxKind.IsExpression,
                     SyntaxKind.IsNotExpression
                    Return OperatorPrecedence.PrecedenceRelational
                Case SyntaxKind.NotExpression
                    Return OperatorPrecedence.PrecedenceNot
                Case SyntaxKind.AndExpression,
                     SyntaxKind.AndAlsoExpression
                    Return OperatorPrecedence.PrecedenceAnd
                Case SyntaxKind.OrExpression,
                     SyntaxKind.OrElseExpression
                    Return OperatorPrecedence.PrecedenceOr
                Case SyntaxKind.ExclusiveOrExpression
                    Return OperatorPrecedence.PrecedenceXor
                Case Else
                    Return OperatorPrecedence.PrecedenceNone
            End Select
        End Function

#Disable Warning IDE0060 ' Remove unused parameter
        <Extension()>
        Public Function IsInOutContext(expression As ExpressionSyntax) As Boolean
#Enable Warning IDE0060 ' Remove unused parameter
            ' NOTE(cyrusn): VB has no concept of an out context.  Even when a parameter has an
            ' '<Out>' attribute on it, it's still treated as ref by VB.  So we always return false
            ' here.
            Return False
        End Function

        <Extension()>
        Public Function IsInRefContext(expression As ExpressionSyntax, semanticModel As SemanticModel, cancellationToken As CancellationToken) As Boolean
            Dim simpleArgument = TryCast(expression?.Parent, SimpleArgumentSyntax)

            If simpleArgument Is Nothing Then
                Return False
            ElseIf simpleArgument.IsNamed Then
                Dim info = semanticModel.GetSymbolInfo(simpleArgument.NameColonEquals.Name, cancellationToken)

                Dim parameter = TryCast(info.GetAnySymbol(), IParameterSymbol)
                Return parameter IsNot Nothing AndAlso parameter.RefKind <> RefKind.None

            Else
                Dim argumentList = TryCast(simpleArgument.Parent, ArgumentListSyntax)

                If argumentList IsNot Nothing Then
                    Dim parent = argumentList.Parent
                    Dim index = argumentList.Arguments.IndexOf(simpleArgument)

                    Dim info = semanticModel.GetSymbolInfo(parent, cancellationToken)
                    Dim symbol = info.GetAnySymbol()

                    If TypeOf symbol Is IMethodSymbol Then
                        Dim method = DirectCast(symbol, IMethodSymbol)
                        If index < method.Parameters.Length Then
                            Return method.Parameters(index).RefKind <> RefKind.None
                        End If
                    ElseIf TypeOf symbol Is IPropertySymbol Then
                        Dim prop = DirectCast(symbol, IPropertySymbol)
                        If index < prop.Parameters.Length Then
                            Return prop.Parameters(index).RefKind <> RefKind.None
                        End If
                    End If
                End If

            End If

            Return False
        End Function

#Disable Warning IDE0060 ' Remove unused parameter
        <Extension()>
        Public Function IsInInContext(expression As ExpressionSyntax) As Boolean
#Enable Warning IDE0060 ' Remove unused parameter
            ' NOTE: VB does not support in parameters. Always return False here.
            Return False
        End Function

        <Extension()>
        Public Function IsOnlyWrittenTo(expression As ExpressionSyntax) As Boolean
            If expression.IsRightSideOfDot() Then
                expression = TryCast(expression.Parent, ExpressionSyntax)
            End If

            If expression IsNot Nothing Then
                If expression.IsInOutContext() Then
                    Return True
                End If

                If expression.IsParentKind(SyntaxKind.SimpleAssignmentStatement) Then
                    Dim assignmentStatement = DirectCast(expression.Parent, AssignmentStatementSyntax)
                    If expression Is assignmentStatement.Left Then
                        Return True
                    End If
                End If

                If expression.IsParentKind(SyntaxKind.NameColonEquals) AndAlso
                   expression.Parent.IsParentKind(SyntaxKind.SimpleArgument) Then

                    ' <C(Prop:=1)>
                    ' this is only a write to Prop
                    Return True
                End If

                If expression.IsChildNode(Of NamedFieldInitializerSyntax)(Function(n) n.Name) Then
                    Return True
                End If

                Return False
            End If

            Return False
        End Function

        <Extension()>
        Public Function IsWrittenTo(expression As ExpressionSyntax, semanticModel As SemanticModel, cancellationToken As CancellationToken) As Boolean
            If IsOnlyWrittenTo(expression) Then
                Return True
            End If

            If expression.IsRightSideOfDot() Then
                expression = TryCast(expression.Parent, ExpressionSyntax)
            End If

            If expression IsNot Nothing Then
                If expression.IsInRefContext(semanticModel, cancellationToken) Then
                    Return True
                End If

                If TypeOf expression.Parent Is AssignmentStatementSyntax Then
                    Dim assignmentStatement = DirectCast(expression.Parent, AssignmentStatementSyntax)
                    If expression Is assignmentStatement.Left Then
                        Return True
                    End If
                End If

                If expression.IsChildNode(Of NamedFieldInitializerSyntax)(Function(n) n.Name) Then
                    Return True
                End If

                Return False
            End If

            Return False
        End Function

        ''' <summary>
        ''' Decompose a name or member access expression into its component parts.
        ''' </summary>
        ''' <param name="expression">The name or member access expression.</param>
        ''' <param name="qualifier">The qualifier (or left-hand-side) of the name expression. This may be null if there is no qualifier.</param>
        ''' <param name="name">The name of the expression.</param>
        ''' <param name="arity">The number of generic type parameters.</param>
        <Extension()>
        Public Sub DecomposeName(expression As ExpressionSyntax, ByRef qualifier As ExpressionSyntax, ByRef name As String, ByRef arity As Integer)
            Select Case expression.Kind
                Case SyntaxKind.SimpleMemberAccessExpression
                    Dim memberAccess = DirectCast(expression, MemberAccessExpressionSyntax)
                    qualifier = memberAccess.Expression
                    name = memberAccess.Name.Identifier.ValueText
                    arity = memberAccess.Name.Arity
                Case SyntaxKind.QualifiedName
                    Dim qualifiedName = DirectCast(expression, QualifiedNameSyntax)
                    qualifier = qualifiedName.Left
                    name = qualifiedName.Right.Identifier.ValueText
                    arity = qualifiedName.Arity
                Case SyntaxKind.GenericName
                    Dim genericName = DirectCast(expression, GenericNameSyntax)
                    qualifier = Nothing
                    name = genericName.Identifier.ValueText
                    arity = genericName.Arity
                Case SyntaxKind.IdentifierName
                    Dim identifierName = DirectCast(expression, IdentifierNameSyntax)
                    qualifier = Nothing
                    name = identifierName.Identifier.ValueText
                    arity = 0
                Case Else
                    qualifier = Nothing
                    name = Nothing
                    arity = 0
            End Select
        End Sub

        Private Function CanReplace(symbol As ISymbol) As Boolean
            Select Case symbol.Kind
                Case SymbolKind.Field,
                     SymbolKind.Local,
                     SymbolKind.Method,
                     SymbolKind.Parameter,
                     SymbolKind.Property,
                     SymbolKind.RangeVariable
                    Return True
            End Select

            Return False
        End Function

        <Extension>
        Public Function CanReplaceWithRValue(expression As ExpressionSyntax, semanticModel As SemanticModel, cancellationToken As CancellationToken) As Boolean
            Return expression IsNot Nothing AndAlso
                Not expression.IsWrittenTo(semanticModel, cancellationToken) AndAlso
                expression.CanReplaceWithLValue(semanticModel, cancellationToken)
        End Function

        <Extension>
        Public Function CanReplaceWithLValue(expression As ExpressionSyntax, semanticModel As SemanticModel, cancellationToken As CancellationToken) As Boolean
#If False Then
            ' Things that are definitely illegal to replace
            If ContainsImplicitMemberAccess(expression) Then
                Return False
            End If
#End If

            If expression.IsKind(SyntaxKind.MyBaseExpression) OrElse
               expression.IsKind(SyntaxKind.MyClassExpression) Then
                Return False
            End If

            If Not (TypeOf expression Is ObjectCreationExpressionSyntax) AndAlso
                   Not (TypeOf expression Is AnonymousObjectCreationExpressionSyntax) Then
                Dim symbolInfo = semanticModel.GetSymbolInfo(expression, cancellationToken)
                If Not symbolInfo.GetBestOrAllSymbols().All(AddressOf CanReplace) Then
                    ' If the expression is actually a reference to a type, then it can't be replaced
                    ' with an arbitrary expression.
                    Return False
                End If
            End If

            ' Technically, you could introduce an LValue for "Goo" in "Goo()" even if "Goo" binds
            ' to a method.  (i.e. by assigning to a Func<...> type).  However, this is so contrived
            ' and none of the features that use this extension consider this replaceable.
            If TypeOf expression.Parent Is InvocationExpressionSyntax Then

                ' If something is being invoked, then it's either something like Goo(), Goo.Bar(), or
                ' SomeExpr() (i.e. Blah[1]()).  In the first and second case, we only allow
                ' replacement if Goo and Goo.Bar didn't bind to a method.  If we can't bind it, we'll
                ' assume it's a method and we don't allow it to be replaced either.  However, if it's
                ' an arbitrary expression, we do allow replacement.
                If expression.IsKind(SyntaxKind.IdentifierName) OrElse expression.IsKind(SyntaxKind.SimpleMemberAccessExpression) Then
                    Dim symbolInfo = semanticModel.GetSymbolInfo(expression, cancellationToken)
                    If Not symbolInfo.GetBestOrAllSymbols().Any() Then
                        Return False
                    End If

                    ' don't allow it to be replaced if it is bound to an indexed property
                    Return Not symbolInfo.GetBestOrAllSymbols().OfType(Of IMethodSymbol)().Any() AndAlso
                           Not symbolInfo.GetBestOrAllSymbols().OfType(Of IPropertySymbol)().Any()
                Else
                    Return True
                End If
            End If

            ' expression in next statement's control variables should match one in the head
            Dim nextStatement = expression.FirstAncestorOrSelf(Of NextStatementSyntax)()
            If nextStatement IsNot Nothing Then
                Return False
            End If

            ' Direct parent kind checks.
            If expression.IsParentKind(SyntaxKind.EqualsValue) OrElse
               expression.IsParentKind(SyntaxKind.ParenthesizedExpression) OrElse
               expression.IsParentKind(SyntaxKind.SelectStatement) OrElse
               expression.IsParentKind(SyntaxKind.SyncLockStatement) OrElse
               expression.IsParentKind(SyntaxKind.CollectionInitializer) OrElse
               expression.IsParentKind(SyntaxKind.InferredFieldInitializer) OrElse
               expression.IsParentKind(SyntaxKind.BinaryConditionalExpression) OrElse
               expression.IsParentKind(SyntaxKind.TernaryConditionalExpression) OrElse
               expression.IsParentKind(SyntaxKind.ReturnStatement) OrElse
               expression.IsParentKind(SyntaxKind.YieldStatement) OrElse
               expression.IsParentKind(SyntaxKind.XmlEmbeddedExpression) OrElse
               expression.IsParentKind(SyntaxKind.ThrowStatement) OrElse
               expression.IsParentKind(SyntaxKind.IfStatement) OrElse
               expression.IsParentKind(SyntaxKind.WhileStatement) OrElse
               expression.IsParentKind(SyntaxKind.ElseIfStatement) OrElse
               expression.IsParentKind(SyntaxKind.ForEachStatement) OrElse
               expression.IsParentKind(SyntaxKind.ForStatement) OrElse
               expression.IsParentKind(SyntaxKind.ConditionalAccessExpression) OrElse
               expression.IsParentKind(SyntaxKind.TypeOfIsExpression) OrElse
               expression.IsParentKind(SyntaxKind.TypeOfIsNotExpression) Then

                Return True
            End If

            ' Parent type checks
            If TypeOf expression.Parent Is BinaryExpressionSyntax OrElse
               TypeOf expression.Parent Is AssignmentStatementSyntax OrElse
               TypeOf expression.Parent Is WhileOrUntilClauseSyntax OrElse
               TypeOf expression.Parent Is SingleLineLambdaExpressionSyntax OrElse
               TypeOf expression.Parent Is AwaitExpressionSyntax Then
                Return True
            End If

            ' Specific child checks.
            If expression.CheckParent(Of NamedFieldInitializerSyntax)(Function(n) n.Expression Is expression) OrElse
               expression.CheckParent(Of MemberAccessExpressionSyntax)(Function(m) m.Expression Is expression) OrElse
               expression.CheckParent(Of TryCastExpressionSyntax)(Function(t) t.Expression Is expression) OrElse
               expression.CheckParent(Of CatchFilterClauseSyntax)(Function(c) c.Filter Is expression) OrElse
               expression.CheckParent(Of SimpleArgumentSyntax)(Function(n) n.Expression Is expression) OrElse
               expression.CheckParent(Of DirectCastExpressionSyntax)(Function(d) d.Expression Is expression) OrElse
               expression.CheckParent(Of FunctionAggregationSyntax)(Function(f) f.Argument Is expression) OrElse
               expression.CheckParent(Of RangeArgumentSyntax)(Function(r) r.UpperBound Is expression) Then
                Return True
            End If

            ' Misc checks
            If TypeOf expression.Parent Is ExpressionRangeVariableSyntax AndAlso
               TypeOf expression.Parent.Parent Is QueryClauseSyntax Then
                Dim rangeVariable = DirectCast(expression.Parent, ExpressionRangeVariableSyntax)
                Dim selectClause = TryCast(rangeVariable.Parent, SelectClauseSyntax)

                ' Can't replace the expression in a select unless its the last select clause *or*
                ' it's a select of the form "select a = <expr>"
                If selectClause IsNot Nothing Then
                    If rangeVariable.NameEquals IsNot Nothing Then
                        Return True
                    End If

                    Dim queryExpression = TryCast(selectClause.Parent, QueryExpressionSyntax)
                    If queryExpression IsNot Nothing Then
                        Return queryExpression.Clauses.Last() Is selectClause
                    End If

                    Dim aggregateClause = TryCast(selectClause.Parent, AggregateClauseSyntax)
                    If aggregateClause IsNot Nothing Then
                        Return aggregateClause.AdditionalQueryOperators().Last() Is selectClause
                    End If

                    Return False
                End If

                ' Any other query type is ok.  Note(cyrusn): This may be too broad.
                Return True
            End If

            Return False
        End Function
    End Module
End Namespace
