' 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.LanguageService
Imports Microsoft.CodeAnalysis.VisualBasic.Syntax

Namespace Microsoft.CodeAnalysis.VisualBasic.Extensions
    Friend Module ParenthesizedExpressionSyntaxExtensions

        Private Function EndsQuery(token As SyntaxToken, semanticModel As SemanticModel, cancellationToken As CancellationToken) As Boolean
            Dim query = token.Parent.FirstAncestorOrSelf(Of QueryExpressionSyntax)()
            If query IsNot Nothing Then
                Return query.GetLastToken() = token
            Else
                Dim invocationAtLast = token.Parent.FirstAncestorOrSelf(Of InvocationExpressionSyntax)()
                Return invocationAtLast IsNot Nothing AndAlso
                   invocationAtLast.GetLastToken() = token AndAlso
                   invocationAtLast.CanRemoveEmptyArgumentList(semanticModel) AndAlso
                   EndsQuery(invocationAtLast.Expression.GetLastToken(), semanticModel, cancellationToken)
            End If

            Return False
        End Function

        Private Function EndsVariableDeclarator(token As SyntaxToken) As Boolean
            Dim variableDeclarator = token.Parent.FirstAncestorOrSelf(Of VariableDeclaratorSyntax)()
            Return variableDeclarator IsNot Nothing AndAlso
                   variableDeclarator.GetLastToken() = token
        End Function

        Private Function EndsLambda(token As SyntaxToken) As Boolean
            Dim lambda = token.Parent.FirstAncestorOrSelf(Of SingleLineLambdaExpressionSyntax)()
            Return lambda IsNot Nothing AndAlso
                   lambda.GetLastToken() = token
        End Function

        <Extension>
        Public Function CanRemoveParentheses(
            node As ParenthesizedExpressionSyntax,
            semanticModel As SemanticModel,
            Optional cancellationToken As CancellationToken = Nothing
        ) As Boolean

            If node.OpenParenToken.IsMissing OrElse node.CloseParenToken.IsMissing Then
                ' Cases:
                '   (3
                Return False
            End If

            Dim expression = node.Expression

            ' Cases:
            '   ((Goo))
            If expression.IsKind(SyntaxKind.ParenthesizedExpression) Then
                Return True
            End If

            '   ((Goo, Bar))
            If expression.IsKind(SyntaxKind.TupleExpression) Then
                Return True
            End If

            ' Cases:
            '   ("x"c)
            '   (#1/1/2001#)
            '   (False)
            '   (Nothing)
            '   (1)
            '   ("")
            '   (True)
            If 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) Then

                Return True
            End If

            ' Case:
            '   ($"")
            If expression.IsKind(SyntaxKind.InterpolatedStringExpression) Then
                Return True
            End If

            ' Cases:
            '   (Me)
            '   (MyBase)
            '   (MyClass)
            If expression.IsKind(SyntaxKind.MeExpression) OrElse
               expression.IsKind(SyntaxKind.MyBaseExpression) OrElse
               expression.IsKind(SyntaxKind.MyClassExpression) Then

                Return True
            End If

            ' Cases:
            '   (DirectCast(Goo))
            '   (TryCast(Goo))
            '   (CType(Goo, Bar))
            '   (CInt(Goo))
            If expression.IsKind(SyntaxKind.DirectCastExpression) OrElse
               expression.IsKind(SyntaxKind.TryCastExpression) OrElse
               expression.IsKind(SyntaxKind.CTypeExpression) OrElse
               TypeOf expression Is PredefinedCastExpressionSyntax Then

                Return True
            End If

            ' Cases:
            '   (AddressOf Goo)
            '   (New With {.Goo = ""})
            '   (If(True, 1, 2))
            '   (If(Nothing, 1))
            '   (NameOf(Goo))
            If expression.IsKind(SyntaxKind.AddressOfExpression) OrElse
               expression.IsKind(SyntaxKind.AnonymousObjectCreationExpression) OrElse
               expression.IsKind(SyntaxKind.TernaryConditionalExpression) OrElse
               expression.IsKind(SyntaxKind.BinaryConditionalExpression) OrElse
               expression.IsKind(SyntaxKind.NameOfExpression) Then

                Return True
            End If

            ' Cases:
            '   List(Of Integer()) From {({1})} -to- {({1})}
            '   List(Of Integer()) From {{({1})}} -to- {{{1}}}
            '   $"{ ({1}) } - to- $"{ {1} }"
            '   {({1})} -to- {({1})}
            '   ({1}) -to- {1}
            If expression.IsKind(SyntaxKind.CollectionInitializer) Then

                If node.IsParentKind(SyntaxKind.Interpolation) Then
                    Dim interpolation = DirectCast(node.Parent, InterpolationSyntax)

                    If interpolation.OpenBraceToken.Span.End = node.OpenParenToken.Span.Start AndAlso
                       node.OpenParenToken.Span.End = expression.Span.Start Then

                        ' In an interpolation, we need to be careful not to remove a parenthesis if it touches a curly brace
                        ' on the left and the right. Otherwise, code will parse differently.

                        Return False
                    End If
                End If

                If Not node.IsParentKind(SyntaxKind.CollectionInitializer) Then
                    ' Standalone parenthesized array literal.
                    ' Parentheses are insignificant.
                    ' Ex. x = ({1})
                    Return True
                End If

                If node.Parent.IsParentKind(SyntaxKind.ObjectCollectionInitializer) AndAlso
                   DirectCast(node.Parent.Parent, ObjectCollectionInitializerSyntax).Initializer Is node.Parent Then
                    ' This is a parenthesized array literal as a collection item within ObjectCollectionInitializer.
                    ' Parentheses are significant in this case and should not be removed.
                    ' Ex. List(Of Integer()) From {({1})}
                    Return False
                End If

                If node.Parent.IsParentKind(SyntaxKind.CollectionInitializer) AndAlso
                   node.Parent.Parent.IsParentKind(SyntaxKind.ObjectCollectionInitializer) AndAlso
                   DirectCast(node.Parent.Parent.Parent, ObjectCollectionInitializerSyntax).Initializer Is node.Parent.Parent Then
                    ' This is a parenthesized array literal within first level sub-collection initializer within ObjectCollectionInitializer.
                    ' Parentheses are insignificant in this case.
                    ' Ex. List(Of Integer()) From {{({1})}}
                    Return True
                End If

                ' This is a parenthesized array literal as an item expression within another array literal.
                ' Parentheses are significant in this case and should not be removed.
                Return False
            End If

            Dim firstToken = expression.GetFirstToken()
            Dim previousToken = node.OpenParenToken.GetPreviousToken()

            ' Case:
            '   0 > <x/>.Value
            '   0 < <x/>.Value
            If firstToken.IsKind(SyntaxKind.LessThanToken) AndAlso
               previousToken.IsKind(SyntaxKind.LessThanToken, SyntaxKind.GreaterThanToken) Then

                Return False
            End If

            ' Cases:
            '   (<xml/>)
            '   (<xml></xml>)
            '   (<x/>.@a)
            '   (<x/>...<b>)
            '   (<x/>.<a>)
            If expression.IsKind(SyntaxKind.XmlEmptyElement) OrElse
               expression.IsKind(SyntaxKind.XmlElement) OrElse
               expression.IsKind(SyntaxKind.XmlAttributeAccessExpression) OrElse
               expression.IsKind(SyntaxKind.XmlDescendantAccessExpression) OrElse
               expression.IsKind(SyntaxKind.XmlElementAccessExpression) Then
                Return True
            End If

            Dim lastToken = expression.GetLastToken()
            Dim nextToken = node.CloseParenToken.GetNextToken()

            ' Cases:
            '   Dim x = (Goo)
            If node.IsParentKind(SyntaxKind.EqualsValue) AndAlso
               Not EndsQuery(lastToken, semanticModel, cancellationToken) AndAlso
               Not EndsLambda(lastToken) AndAlso
               Not nextToken.IsKindOrHasMatchingText(SyntaxKind.CommaToken) Then
                Return True
            End If

            ' Cases:
            '   (New Goo)
            '   (New Goo())
            If expression.IsKind(SyntaxKind.ObjectCreationExpression) Then
                Dim objectCreation = DirectCast(expression, ObjectCreationExpressionSyntax)

                If nextToken.IsKindOrHasMatchingText(SyntaxKind.DotToken) Then
                    If objectCreation.ArgumentList Is Nothing Then
                        ' Note we can remove the parentheses when the next token is dot only
                        ' if the type of the ObjectCreationExpression is a predefined type.
                        ' So, we can remove parentheses in this case...
                        '
                        '     Call (New Integer).ToString
                        '
                        ' But not this one...
                        '
                        '     Call (New Int32).ToString

                        Return TypeOf objectCreation.Type Is PredefinedTypeSyntax
                    End If
                End If

                If nextToken.IsKindOrHasMatchingText(SyntaxKind.OpenParenToken) Then
                    Return False
                End If

                Return True
            End If

            ' Cases:
            ' 1.   (Goo)
            ' 2.   (Goo())
            ' 3.   <x/>.GetHashCode()
            ' 4.   1 < (<x/>.GetHashCode()) Or 1 > (<x/>.GetHashCode())
            If expression.IsKind(SyntaxKind.InvocationExpression) Then
                Dim invocationExpression = DirectCast(expression, InvocationExpressionSyntax)

                If invocationExpression.Expression.IsKind(SyntaxKind.SimpleMemberAccessExpression) Then
                    Dim memberAccess = DirectCast(invocationExpression.Expression, MemberAccessExpressionSyntax)
                    If (TypeOf memberAccess.Expression Is XmlNodeSyntax AndAlso
                        (previousToken.IsKindOrHasMatchingText(SyntaxKind.LessThanToken) OrElse
                        previousToken.IsKindOrHasMatchingText(SyntaxKind.GreaterThanToken))) Then

                        Return False
                    End If
                End If

                If invocationExpression.ArgumentList Is Nothing Then
                    Return Not nextToken.IsKindOrHasMatchingText(SyntaxKind.OpenParenToken)
                End If

                Return True
            End If

            ' Cases:
            '   (Goo.Bar)
            '   (Goo)
            If expression.IsKind(SyntaxKind.IdentifierName) OrElse
               expression.IsKind(SyntaxKind.SimpleMemberAccessExpression) Then

                ' If this is a local, field or property is passed to a ByRef parameter, we should
                ' keep the parentheses to ensure that we don't change copy-back semantics.
                If TypeOf node.Parent Is ArgumentSyntax Then
                    Dim symbol = semanticModel.GetSymbolInfo(expression, cancellationToken).Symbol
                    If symbol IsNot Nothing Then
                        If symbol.MatchesKind(SymbolKind.Local, SymbolKind.Field, SymbolKind.Property) Then
                            Dim argument = DirectCast(node.Parent, ArgumentSyntax)
                            Dim parameter = argument.DetermineParameter(semanticModel, cancellationToken:=cancellationToken)

                            If parameter IsNot Nothing AndAlso
                               parameter.RefKind <> RefKind.None Then

                                Return False
                            End If
                        End If
                    End If
                End If

                ' If the next token is an open paren, we need to be careful to ensure
                ' that it is the opening of the argument list of a parenting invocation
                ' for which this is the expression.
                If nextToken.IsKindOrHasMatchingText(SyntaxKind.OpenParenToken) Then
                    If node.IsParentKind(SyntaxKind.InvocationExpression) Then
                        Dim parentInvocation = DirectCast(node.Parent, InvocationExpressionSyntax)
                        If parentInvocation.Expression Is node AndAlso
                           parentInvocation.ArgumentList IsNot Nothing AndAlso
                           parentInvocation.ArgumentList.OpenParenToken = nextToken Then

                            Return True
                        End If
                    End If

                    Return False
                End If

                Return True
            End If

            ' Case:
            '   (Goo(Of Bar))
            If expression.IsKind(SyntaxKind.GenericName) Then
                If Not nextToken.IsKindOrHasMatchingText(SyntaxKind.OpenParenToken) Then
                    Return True
                End If
            End If

            Dim isNodeCloseParenLastTokenOfStatement = node.CloseParenToken.IsLastTokenOfStatement(checkColonTrivia:=True)
            Dim nextNextToken = nextToken.GetNextToken()

            ' Dim z = Function() (From x In "") ' OK
            ' Select 1
            ' End Select
            ' Select is the only keyword in LINQ which has dual usage 1. Case selection 2. Query Select Clause
            If isNodeCloseParenLastTokenOfStatement AndAlso
                EndsQuery(lastToken, semanticModel, cancellationToken) AndAlso
                nextToken.Kind = SyntaxKind.SelectKeyword AndAlso
                nextNextToken.Kind <> SyntaxKind.CaseKeyword Then
                Return False
            End If

            ' (Await Task.Run(Function() i)) <EOL>
            ' (Await Task.Run(Function() i)),
            If node.Expression.IsKind(SyntaxKind.AwaitExpression) AndAlso
                (isNodeCloseParenLastTokenOfStatement OrElse
                nextToken.Kind = SyntaxKind.CommaToken) Then
                Return True
            End If

            ' Cases:
            '   (1 + 1) * 8
            '   (1 + 1).ToString
            '   (1 + 1)()
            If TypeOf expression Is BinaryExpressionSyntax OrElse
               TypeOf expression Is UnaryExpressionSyntax Then

                Dim parentExpression = TryCast(node.Parent, ExpressionSyntax)
                If parentExpression IsNot Nothing Then
                    If parentExpression.IsKind(SyntaxKind.SimpleMemberAccessExpression) OrElse
                       parentExpression.IsKind(SyntaxKind.InvocationExpression) Then
                        Return False
                    End If

                    Dim precedence = expression.GetOperatorPrecedence()
                    Dim parentPrecedence = parentExpression.GetOperatorPrecedence()

                    ' Only remove if the expression's precedence is higher than its parent.
                    If parentPrecedence <> OperatorPrecedence.PrecedenceNone AndAlso
                       precedence < parentPrecedence Then

                        Return False
                    End If

                    ' If the expression's precedence is the same as its parent, and both are binary expressions,
                    ' check for associativity and commutability.
                    If precedence <> OperatorPrecedence.PrecedenceNone AndAlso precedence = parentPrecedence Then
                        Dim binaryExpression = TryCast(expression, BinaryExpressionSyntax)
                        Dim parentBinaryExpression = TryCast(parentExpression, BinaryExpressionSyntax)

                        If binaryExpression IsNot Nothing AndAlso parentBinaryExpression IsNot Nothing Then
                            ' All binary expressions are left associative, so if the expression
                            ' is on the left side of a binary expression the parentheses can be removed.
                            If parentBinaryExpression.Left Is node Then
                                Return True
                            End If

                            ' If both the expression and its parent are binary expressions and their kinds
                            ' are the same, and the parenthesized expression is on the right and the 
                            ' operation is associative, it can sometimes be safe to remove these parens.
                            '
                            ' i.e. if you have "a AndAlso (b AndAlso c)" it can be converted to "a AndAlso b AndAlso c" 
                            ' as that New interpretation "(a AndAlso b) AndAlso c" operates the exact same way at 
                            ' runtime.
                            '
                            ' Specifically: 
                            '  1) the operands are still executed in the same order a, b, then c.
                            '     So even if they have side effects, it will Not matter.
                            '  2) the same shortcircuiting happens.
                            '  3) the result will always be the same (for logical operators, there are 
                            '     additional conditions that are checked for non-logical operators).
                            If IsAssociative(parentBinaryExpression.Kind) AndAlso
                               expression.Kind = parentExpression.Kind Then

                                Return VisualBasicSemanticFacts.Instance.IsSafeToChangeAssociativity(
                                    binaryExpression, parentBinaryExpression, semanticModel)
                            End If
                        End If

                        Return False
                    End If
                End If

                Return True
            End If

            ' Cases:
            '   (Sub() From x in y), Goo
            '   Dim a = (Sub() If True Then Dim x), b = a
            '   Dim y = (Function() Console.ReadLine)()
            '   Call (Sub() Exit Sub)
            '   Dim x = <x <%= (Sub() If True Then Else) %>/>
            If TypeOf expression Is SingleLineLambdaExpressionSyntax Then
                If node.CloseParenToken.IsLastTokenOfStatementWithEndOfLine() AndAlso
                    lastToken.Kind = SyntaxKind.ThenKeyword Then
                    Return False
                End If

                If nextToken.IsKindOrHasMatchingText(SyntaxKind.CommaToken) Then
                    Dim lastStatement = lastToken.Parent.GetFirstEnclosingStatement()
                    If EndsQuery(lastToken, semanticModel, cancellationToken) OrElse EndsVariableDeclarator(lastToken) OrElse
                            (EndsLambda(lastToken) AndAlso
                            Not previousToken.IsKindOrHasMatchingText(SyntaxKind.OpenParenToken) AndAlso
                            lastStatement IsNot Nothing AndAlso lastStatement.Kind = SyntaxKind.ReDimStatement) Then
                        Return False
                    End If

                    Return True
                End If

                ' case:
                ' (Sub() If True Then Dim y = Sub(z As Integer)
                '                             End Sub).Invoke()
                If nextToken.IsKindOrHasMatchingText(SyntaxKind.DotToken) AndAlso
                       nextToken.Parent.IsKind(SyntaxKind.SimpleMemberAccessExpression) Then
                    Return False
                End If

                ' case:
                ' 1. Call (Sub() If True Then Dim y = Sub(z As Integer)
                '                             End Sub)
                ' 2. (Sub() If True Then Dim y = Sub()
                '                                End Sub) Is Nothing
                ' 3. TypeOf (Sub() If True Then Dim y = Sub()
                '                                End Sub) Is Object
                ' 4. TypeOf (Sub() If True Then Dim y = Sub()
                '                                End Sub) IsNot Object
                If (node.Parent.Kind = SyntaxKind.InvocationExpression OrElse
                        node.Parent.Kind = SyntaxKind.IsExpression OrElse
                        node.Parent.Kind = SyntaxKind.TypeOfIsExpression OrElse
                        node.Parent.Kind = SyntaxKind.TypeOfIsNotExpression) Then
                    Return False
                End If

                ' case:
                ' 1. (Sub() If True Then) Implements I.A
                ' 2. If True Then : Dim x As Action = (Sub() If True Then) : Else : Return : End If
                If nextToken.IsKindOrHasMatchingText(SyntaxKind.CloseParenToken) OrElse
                       nextToken.IsKindOrHasMatchingText(SyntaxKind.CloseBraceToken) OrElse
                       lastToken.IsLastTokenOfStatement(checkColonTrivia:=True) OrElse
                       node.Parent.Kind = SyntaxKind.XmlEmbeddedExpression Then
                    Return True
                End If

                ' case:
                ' 1 .
                ' Dim a = Sub() If False Then Console.WriteLine() Else Dim q = From x In ""
                ' [Take]()
                If isNodeCloseParenLastTokenOfStatement AndAlso
                    EndsQuery(lastToken, semanticModel, cancellationToken) AndAlso
                  nextToken.IsKeyword Then
                    Return True
                End If

                If isNodeCloseParenLastTokenOfStatement AndAlso
                    Not EndsQuery(lastToken, semanticModel, cancellationToken) Then
                    Return True
                End If

                Return False
            End If

            If TypeOf expression Is MultiLineLambdaExpressionSyntax Then
                Return True
            End If

            ' Cases:
            '   {(From x in y), From x in y}
            '
            '   Dim q = (From x in "")
            '   Select 1
            '   End Select
            '
            '   With New StringBuilder
            '       Dim q = (From x in "")
            '       .Length = 0
            '   End With
            '
            ' Dim y = (From c In "" Distinct)
            '    !A = !B
            If EndsQuery(lastToken, semanticModel, cancellationToken) Then
                If nextToken.IsKindOrHasMatchingText(SyntaxKind.CloseParenToken) OrElse
                   nextToken.IsKindOrHasMatchingText(SyntaxKind.CloseBraceToken) OrElse
                   node.CloseParenToken.IsLastTokenOfStatement() Then

                    If Not (nextToken.IsKindOrHasMatchingText(SyntaxKind.DotToken) AndAlso
                            nextToken.Parent.IsKind(SyntaxKind.SimpleMemberAccessExpression)) AndAlso
                       Not (nextToken.IsKindOrHasMatchingText(SyntaxKind.SelectKeyword) AndAlso
                            nextToken.Parent.IsKind(SyntaxKind.SelectStatement)) AndAlso
                        Not (nextToken.IsKindOrHasMatchingText(SyntaxKind.ExclamationToken) AndAlso
                             lastToken.IsKeyword AndAlso
                             nextToken.Parent.IsKind(SyntaxKind.DictionaryAccessExpression)) Then

                        Return True
                    End If
                End If

                Return False
            End If

            ' case:
            ' (GetType(String)) => GetType(String)
            If expression.IsKind(SyntaxKind.GetTypeExpression) Then
                Return True
            End If

            ' case:
            ' 1. (!b) => !b
            If expression.Kind = SyntaxKind.DictionaryAccessExpression AndAlso
                node.CloseParenToken.IsLastTokenOfStatement() Then
                Return True
            End If

            Return False
        End Function

        Private Function IsAssociative(kind As SyntaxKind) As Boolean
            Select Case kind
                Case SyntaxKind.AddExpression,
                     SyntaxKind.MultiplyExpression,
                     SyntaxKind.AndExpression,
                     SyntaxKind.AndAlsoExpression,
                     SyntaxKind.OrExpression,
                     SyntaxKind.ExclusiveOrExpression,
                     SyntaxKind.OrElseExpression
                    Return True
            End Select

            Return False
        End Function
    End Module
End Namespace
