' 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.Collections.Immutable
Imports System.Diagnostics
Imports System.Runtime.InteropServices
Imports Microsoft.CodeAnalysis.PooledObjects
Imports Microsoft.CodeAnalysis.Text
Imports Microsoft.CodeAnalysis.VisualBasic.Symbols
Imports Microsoft.CodeAnalysis.VisualBasic.Syntax
Imports TypeKind = Microsoft.CodeAnalysis.TypeKind

Namespace Microsoft.CodeAnalysis.VisualBasic

    Partial Friend Class LocalRewriter
        Public Overrides Function VisitNullableIsTrueOperator(node As BoundNullableIsTrueOperator) As BoundNode
            Debug.Assert(node.Operand.Type.IsNullableOfBoolean())

            Dim optimizableForConditionalBranch As Boolean = False
            Dim operand = VisitExpression(AdjustIfOptimizableForConditionalBranch(node.Operand, optimizableForConditionalBranch))

            If optimizableForConditionalBranch AndAlso HasValue(operand) Then
                Return NullableValueOrDefault(operand)
            End If

            If _inExpressionLambda Then
                Return node.Update(operand, node.Type)
            End If

            If HasNoValue(operand) Then
                Return New BoundLiteral(node.Syntax, ConstantValue.False, node.Type)
            End If

            Return NullableValueOrDefault(operand)
        End Function

        Private Shared Function AdjustIfOptimizableForConditionalBranch(operand As BoundExpression, <Out> ByRef optimizableForConditionalBranch As Boolean) As BoundExpression
            optimizableForConditionalBranch = False

            Dim current As BoundExpression = operand
            Do
                Select Case current.Kind
                    Case BoundKind.BinaryOperator
                        Dim binary = DirectCast(current, BoundBinaryOperator)
                        If (binary.OperatorKind And BinaryOperatorKind.IsOperandOfConditionalBranch) <> 0 Then
                            Select Case (binary.OperatorKind And BinaryOperatorKind.OpMask)
                                Case BinaryOperatorKind.AndAlso, BinaryOperatorKind.OrElse
                                    Debug.Assert((binary.OperatorKind And BinaryOperatorKind.Lifted) <> 0)
                                    optimizableForConditionalBranch = True
                                    Return binary.Update(binary.OperatorKind Or BinaryOperatorKind.OptimizableForConditionalBranch,
                                                         binary.Left, binary.Right, binary.Checked, binary.ConstantValueOpt, binary.Type)
                            End Select
                        End If

                        Return operand

                    Case BoundKind.Parenthesized
                        current = DirectCast(current, BoundParenthesized).Expression
                    Case Else
                        Return operand
                End Select
            Loop

        End Function

        Public Overrides Function VisitUserDefinedUnaryOperator(node As BoundUserDefinedUnaryOperator) As BoundNode
            If _inExpressionLambda Then
                Return node.Update(node.OperatorKind, VisitExpression(node.UnderlyingExpression), node.Type)
            End If

            If (node.OperatorKind And UnaryOperatorKind.Lifted) <> 0 Then
                Return RewriteLiftedUserDefinedUnaryOperator(node)
            End If

            Return Visit(node.UnderlyingExpression)
        End Function

        Public Overrides Function VisitUnaryOperator(node As BoundUnaryOperator) As BoundNode
            If (node.OperatorKind And UnaryOperatorKind.Lifted) = 0 OrElse _inExpressionLambda Then
                Dim result As BoundNode = MyBase.VisitUnaryOperator(node)
                If result.Kind = BoundKind.UnaryOperator Then

                    result = RewriteUnaryOperator(DirectCast(result, BoundUnaryOperator))
                End If
                Return result

            Else
                Return RewriteLiftedUnaryOperator(node)
            End If
        End Function

        Private Function RewriteUnaryOperator(node As BoundUnaryOperator) As BoundExpression
            Dim result As BoundExpression = node
            Dim kind As UnaryOperatorKind = node.OperatorKind

            If Not node.HasErrors AndAlso ((kind And UnaryOperatorKind.Lifted) = 0) AndAlso (kind <> UnaryOperatorKind.Error) AndAlso Not _inExpressionLambda Then
                Dim opType As TypeSymbol = node.Type

                If opType.IsObjectType() Then
                    result = RewriteObjectUnaryOperator(node)
                ElseIf opType.IsDecimalType() Then
                    result = RewriteDecimalUnaryOperator(node)
                End If
            End If

            Return result
        End Function

        Private Function RewriteObjectUnaryOperator(node As BoundUnaryOperator) As BoundExpression
            Debug.Assert(node.Operand.Type.IsObjectType() AndAlso node.Type.IsObjectType())

            Dim result As BoundExpression = node
            Dim opKind = (node.OperatorKind And UnaryOperatorKind.IntrinsicOpMask)

            Dim member As WellKnownMember

            If opKind = UnaryOperatorKind.Plus Then
                member = WellKnownMember.Microsoft_VisualBasic_CompilerServices_Operators__PlusObjectObject
            ElseIf opKind = UnaryOperatorKind.Minus Then
                member = WellKnownMember.Microsoft_VisualBasic_CompilerServices_Operators__NegateObjectObject
            Else
                Debug.Assert(opKind = UnaryOperatorKind.Not)
                member = WellKnownMember.Microsoft_VisualBasic_CompilerServices_Operators__NotObjectObject
            End If

            ' Call member(operand)
            Dim memberSymbol = DirectCast(Compilation.GetWellKnownTypeMember(member), MethodSymbol)

            If Not ReportMissingOrBadRuntimeHelper(node, member, memberSymbol) Then
                result = New BoundCall(node.Syntax, memberSymbol, Nothing, Nothing,
                                       ImmutableArray.Create(node.Operand), Nothing, memberSymbol.ReturnType)
            End If

            Return result
        End Function

        Private Function RewriteDecimalUnaryOperator(node As BoundUnaryOperator) As BoundExpression
            Debug.Assert(node.Operand.Type.IsDecimalType() AndAlso node.Type.IsDecimalType())

            Dim result As BoundExpression = node
            Dim opKind = (node.OperatorKind And UnaryOperatorKind.IntrinsicOpMask)

            If opKind = UnaryOperatorKind.Plus Then
                result = node.Operand
            Else
                Debug.Assert(opKind = UnaryOperatorKind.Minus)

                ' Call Decimal.Negate(operand)

                Const memberId As SpecialMember = SpecialMember.System_Decimal__NegateDecimal
                Dim memberSymbol = DirectCast(ContainingAssembly.GetSpecialTypeMember(memberId), MethodSymbol)

                If Not ReportMissingOrBadRuntimeHelper(node, memberId, memberSymbol) Then
                    result = New BoundCall(node.Syntax, memberSymbol, Nothing, Nothing,
                                           ImmutableArray.Create(node.Operand), Nothing, memberSymbol.ReturnType)
                End If
            End If

            Return result
        End Function

        Private Function RewriteLiftedUnaryOperator(node As BoundUnaryOperator) As BoundNode
            Dim operand As BoundExpression = VisitExpressionNode(node.Operand)

            ' null stays null
            If HasNoValue(operand) Then
                ' return new R?()
                Return NullableNull(operand, node.Type)
            End If

            ' we know that operand is not null, just wrap the result of unlifted op
            If HasValue(operand) Then
                ' return new R?(UnliftedOp(operand))
                Dim unliftedOp = ApplyUnliftedUnaryOp(node, NullableValueOrDefault(operand))

                Return WrapInNullable(unliftedOp, node.Type)
            End If

            ' NOTE: in some cases (like if operand is a local) we do not need the temp
            '
            ' return {.temp
            '           temp = operand
            '           iif (temp.HasValue, 
            '               new R?(UnliftedOp(operand)), 
            '               new R?)
            '        }
            Dim temp As SynthesizedLocal = Nothing
            Dim tempInit As BoundExpression = Nothing

            ' No need to capture locals in Unary since we will not eval another operand
            Dim capturedOperand = CaptureNullableIfNeeded(operand, temp, tempInit, doNotCaptureLocals:=True)

            Dim unliftedOpOnCaptured = ApplyUnliftedUnaryOp(node, NullableValueOrDefault(capturedOperand))
            Dim value As BoundExpression = MakeTernaryConditionalExpression(node.Syntax,
                                                              NullableHasValue(capturedOperand),
                                                              WrapInNullable(unliftedOpOnCaptured, node.Type),
                                                              capturedOperand)

            ' if we used a temp, arrange a sequence for it and its initialization
            If temp IsNot Nothing Then
                value = New BoundSequence(node.Syntax,
                                     ImmutableArray.Create(Of LocalSymbol)(temp),
                                     ImmutableArray.Create(tempInit),
                                     value,
                                     value.Type)
            End If

            Return value
        End Function

        Private Function RewriteLiftedUserDefinedUnaryOperator(node As BoundUserDefinedUnaryOperator) As BoundExpression
            '
            ' Lifted user defined operator has structure as the following:
            '            
            '                    |          
            '             [implicit wrap]
            '                    |
            '                  CALL
            '                    |
            '           [implicit unwrap]
            '                    |                   |
            '                OPERAND
            '
            ' Implicit unwrapping conversion if present is always O? -> O 
            ' It is encoded as a disparity between CALL argument type and parameter type of the call symbol.
            '
            ' Implicit wrapping conversion of the result, if present, is always T -> T?
            '
            ' The rewrite is:
            '   If (OPERAND.HasValue, CALL(OPERAND), Null)
            '
            ' Note that the result of the operator is nullable type. 

            Dim operand = Me.VisitExpressionNode(node.Operand)
            Dim operatorCall = node.Call

            Dim resultType = operatorCall.Type

            Debug.Assert(resultType.IsNullableType())
            Dim whenHasNoValue = NullableNull(node.Syntax, resultType)

            Debug.Assert(operand.Type.IsNullableType, "operand must be nullable")

            Dim operandHasNoValue As Boolean = HasNoValue(operand)

            ' TRIVIAL CASE
            If operandHasNoValue Then
                Return whenHasNoValue
            End If

            Dim temps As ArrayBuilder(Of LocalSymbol) = Nothing
            Dim inits As ArrayBuilder(Of BoundExpression) = Nothing

            ' PREPARE OPERAND
            Dim operandHasValue As Boolean = HasValue(operand)
            Dim callInput As BoundExpression
            Dim operandHasValueExpression As BoundExpression = Nothing

            If operandHasValue Then
                callInput = NullableValueOrDefault(operand)
            Else
                callInput = ProcessNullableOperand(operand, operandHasValueExpression, temps, inits, doNotCaptureLocals:=True)
            End If

            Debug.Assert(callInput.Type.IsSameTypeIgnoringAll(operatorCall.Method.Parameters(0).Type),
                         "operator must take unwrapped value of the operand")

            Dim whenHasValue As BoundExpression = operatorCall.Update(operatorCall.Method,
                                                                       Nothing,
                                                                       operatorCall.ReceiverOpt,
                                                                       ImmutableArray.Create(Of BoundExpression)(callInput),
                                                                       Nothing,
                                                                       operatorCall.ConstantValueOpt,
                                                                       isLValue:=operatorCall.IsLValue,
                                                                       suppressObjectClone:=operatorCall.SuppressObjectClone,
                                                                       type:=operatorCall.Method.ReturnType)

            If Not whenHasValue.Type.IsSameTypeIgnoringAll(resultType) Then
                whenHasValue = WrapInNullable(whenHasValue, resultType)
            End If

            Debug.Assert(whenHasValue.Type.IsSameTypeIgnoringAll(resultType), "result type must be same as resultType")

            ' RESULT

            If operandHasValue Then
                Debug.Assert(temps Is Nothing AndAlso inits Is Nothing)
                Return whenHasValue

            Else
                Dim condition As BoundExpression = operandHasValueExpression
                Dim result As BoundExpression = MakeTernaryConditionalExpression(node.Syntax,
                                                               condition,
                                                               whenHasValue,
                                                               whenHasNoValue)

                ' if we used a temp, arrange a sequence for it
                If temps IsNot Nothing Then
                    result = New BoundSequence(node.Syntax,
                                         temps.ToImmutableAndFree,
                                         inits.ToImmutableAndFree,
                                         result,
                                         result.Type)
                End If

                Return result
            End If

        End Function

        Private Function ApplyUnliftedUnaryOp(originalOperator As BoundUnaryOperator, operandValue As BoundExpression) As BoundExpression
            Debug.Assert(Not operandValue.Type.IsNullableType)

            'return UnliftedOP(operandValue)
            Dim unliftedOpKind = originalOperator.OperatorKind And (Not UnaryOperatorKind.Lifted)

            Return RewriteUnaryOperator(
                        New BoundUnaryOperator(originalOperator.Syntax,
                                               unliftedOpKind,
                                               operandValue,
                                               originalOperator.Checked,
                                               originalOperator.Type.GetNullableUnderlyingType))
        End Function
    End Class

End Namespace
