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

Namespace Microsoft.CodeAnalysis.VisualBasic

    ''' <summary>
    ''' Binder used to bind statements inside With blocks. 
    ''' </summary>
    Friend NotInheritable Class WithBlockBinder
        Inherits BlockBaseBinder

        ''' <summary> Reference to a With statement syntax this binder is created for </summary>
        Private ReadOnly _withBlockSyntax As WithBlockSyntax

        ''' <summary> Reference to an expression from With statement </summary>
        Private ReadOnly Property Expression As ExpressionSyntax
            Get
                Return Me._withBlockSyntax.WithStatement.Expression
            End Get
        End Property

        ''' <summary> 
        ''' Holds information needed by With block to properly bind 
        ''' references to With block expression placeholder
        ''' </summary>
        Private _withBlockInfo As WithBlockInfo = Nothing

        Friend ReadOnly Property Info As WithBlockInfo
            Get
                Return _withBlockInfo
            End Get
        End Property

        ''' <summary> 
        ''' True if there were references to the With statement expression 
        ''' placeholder which prevent ByRef local from being used 
        ''' </summary>
        Friend ReadOnly Property ExpressionIsAccessedFromNestedLambda As Boolean
            Get
                Debug.Assert(Me._withBlockInfo IsNot Nothing)
                Return Me._withBlockInfo.ExpressionIsAccessedFromNestedLambda
            End Get
        End Property

        ''' <summary>
        ''' With statement expression placeholder is a bound node being used in initial binding
        ''' to represent with statement expression. In lowering it is to be replaced with
        ''' the lowered expression which will actually be emitted.
        ''' </summary>
        Friend ReadOnly Property ExpressionPlaceholder As BoundValuePlaceholderBase
            Get
                Debug.Assert(Me._withBlockInfo IsNot Nothing)
                Return Me._withBlockInfo.ExpressionPlaceholder
            End Get
        End Property

        ''' <summary>
        ''' A draft version of initializers which will be used in this With statement. 
        ''' Initializers are expressions which are used to capture expression in the current
        ''' With statement; they can be empty in some cases like if the expression is a local 
        ''' variable of value type.
        ''' 
        ''' Note, the initializers returned by this property are 'draft' because they are 
        ''' generated based on initial bound tree, the real initializers will be generated 
        ''' in lowering based on lowered expression form.
        ''' </summary>
        Friend ReadOnly Property DraftInitializers As ImmutableArray(Of BoundExpression)
            Get
                Debug.Assert(Me._withBlockInfo IsNot Nothing)
                Return Me._withBlockInfo.DraftInitializers
            End Get
        End Property

        ''' <summary>
        ''' A draft version of placeholder substitute which will be used in this With statement. 
        ''' 
        ''' Note, the placeholder substitute returned by this property is 'draft' because it is
        ''' generated based on initial bound tree, the real substitute will be generated in lowering 
        ''' based on lowered expression form.
        ''' </summary>
        Friend ReadOnly Property DraftPlaceholderSubstitute As BoundExpression
            Get
                Debug.Assert(Me._withBlockInfo IsNot Nothing)
                Return Me._withBlockInfo.DraftSubstitute
            End Get
        End Property

        ''' <summary> Holds information needed by With block to properly bind 
        ''' references to With block expression, placeholder, etc... </summary>
        Friend Class WithBlockInfo

            Public Sub New(originalExpression As BoundExpression,
                           expressionPlaceholder As BoundValuePlaceholderBase,
                           draftSubstitute As BoundExpression,
                           draftInitializers As ImmutableArray(Of BoundExpression),
                           capturedLvalueByRefCallOrProperty As BoundExpression,
                           diagnostics As ReadOnlyBindingDiagnostic(Of AssemblySymbol))

                Debug.Assert(originalExpression IsNot Nothing)
                Debug.Assert(expressionPlaceholder IsNot Nothing AndAlso (expressionPlaceholder.Kind = BoundKind.WithLValueExpressionPlaceholder OrElse expressionPlaceholder.Kind = BoundKind.WithRValueExpressionPlaceholder))
                Debug.Assert(draftSubstitute IsNot Nothing)
                Debug.Assert(Not draftInitializers.IsDefault)

                Me.OriginalExpression = originalExpression
                Me.ExpressionPlaceholder = expressionPlaceholder
                Me.DraftSubstitute = draftSubstitute
                Me.CapturedLvalueByRefCallOrProperty = capturedLvalueByRefCallOrProperty
                Me.DraftInitializers = draftInitializers
                Me.Diagnostics = diagnostics
            End Sub

            ''' <summary> Original bound expression from With statement </summary>
            Public ReadOnly OriginalExpression As BoundExpression

            ''' <summary> Bound placeholder expression if used, otherwise Nothing </summary>
            Public ReadOnly ExpressionPlaceholder As BoundValuePlaceholderBase

            ''' <summary> Diagnostics produced while binding the expression </summary>
            Public ReadOnly Diagnostics As ReadOnlyBindingDiagnostic(Of AssemblySymbol)

            ''' <summary> 
            ''' Draft initializers for With statement, is based on initial binding tree 
            ''' and is only to be used for warnings generation as well as for flow analysis 
            ''' and semantic API; real initializers will be re-calculated in lowering
            ''' </summary>
            Public ReadOnly DraftInitializers As ImmutableArray(Of BoundExpression)

            ''' <summary> 
            ''' Draft substitute for With expression placeholder, is based on initial 
            ''' binding tree and is only to be used for warnings generation as well as 
            ''' for flow analysis and semantic API; real substitute will be re-calculated 
            ''' in lowering
            ''' </summary>
            Public ReadOnly DraftSubstitute As BoundExpression

            Public ReadOnly CapturedLvalueByRefCallOrProperty As BoundExpression

            Public ReadOnly Property ExpressionIsAccessedFromNestedLambda As Boolean
                Get
                    Return Me._exprAccessedFromNestedLambda = ThreeState.True
                End Get
            End Property

            Public Sub RegisterAccessFromNestedLambda()
                If Me._exprAccessedFromNestedLambda <> ThreeState.True Then
                    Dim oldValue = Interlocked.CompareExchange(Me._exprAccessedFromNestedLambda, ThreeState.True, ThreeState.Unknown)
                    Debug.Assert(oldValue = ThreeState.Unknown OrElse oldValue = ThreeState.True)
                End If
            End Sub

            Private _exprAccessedFromNestedLambda As Integer = ThreeState.Unknown

            ''' <summary>
            ''' If With statement expression is being used from nested lambda there are some restrictions
            ''' to the usage of Me reference in this expression. As these restrictions are only to be checked 
            ''' in few scenarios, this flag is being calculated lazily.
            ''' </summary>
            Public Function ExpressionHasByRefMeReference(recursionDepth As Integer) As Boolean
                If Me._exprHasByRefMeReference = ThreeState.Unknown Then
                    ' Analyze the expression which will be used instead of placeholder
                    Dim value As Boolean = ValueTypedMeReferenceFinder.HasByRefMeReference(Me.DraftSubstitute, recursionDepth)
                    Dim newValue As Integer = If(value, ThreeState.True, ThreeState.False)
                    Dim oldValue = Interlocked.CompareExchange(Me._exprHasByRefMeReference, newValue, ThreeState.Unknown)
                    Debug.Assert(newValue = oldValue OrElse oldValue = ThreeState.Unknown)
                End If

                Debug.Assert(Me._exprHasByRefMeReference <> ThreeState.Unknown)
                Return Me._exprHasByRefMeReference = ThreeState.True
            End Function

            Private _exprHasByRefMeReference As Integer = ThreeState.Unknown

        End Class

        ''' <summary> Create a new instance of With statement binder for a statement syntax provided </summary>
        Public Sub New(enclosing As Binder, syntax As WithBlockSyntax)
            MyBase.New(enclosing)

            Debug.Assert(syntax IsNot Nothing)
            Debug.Assert(syntax.WithStatement IsNot Nothing)
            Debug.Assert(syntax.WithStatement.Expression IsNot Nothing)
            Me._withBlockSyntax = syntax
        End Sub

#Region "Implementation"

        Friend Overrides Function GetWithStatementPlaceholderSubstitute(placeholder As BoundValuePlaceholderBase) As BoundExpression
            Me.EnsureExpressionAndPlaceholder()
            If placeholder Is Me.ExpressionPlaceholder Then
                Return Me.DraftPlaceholderSubstitute
            End If
            Return MyBase.GetWithStatementPlaceholderSubstitute(placeholder)
        End Function

        Private Sub EnsureExpressionAndPlaceholder()

            If Me._withBlockInfo Is Nothing Then
                ' Because we cannot guarantee that diagnostics will be freed we 
                ' don't allocate this diagnostics bag from a pool
                Dim diagnostics = BindingDiagnosticBag.GetInstance()

                ' Bind the expression as a value
                Dim boundExpression As BoundExpression = Me.ContainingBinder.BindValue(Me.Expression, diagnostics)

                ' NOTE: If the expression is not an l-value we should make an r-value of it
                If Not boundExpression.IsLValue Then
                    boundExpression = Me.MakeRValue(boundExpression, diagnostics)
                Else
                    Dim propertyAccess = TryCast(boundExpression, BoundPropertyAccess)

                    If propertyAccess IsNot Nothing Then
                        WarnOnRecursiveAccess(propertyAccess, PropertyAccessKind.Get, diagnostics)
                        boundExpression = propertyAccess.SetAccessKind(PropertyAccessKind.Get)
                    End If
                End If

                ' Prepare draft substitute/initializers for expression placeholder;
                ' note that those substitute/initializers will be based on initial bound 
                ' form of the original expression captured without using ByRef locals
                Dim result As WithExpressionRewriter.Result =
                    (New WithExpressionRewriter(Me._withBlockSyntax.WithStatement)).AnalyzeWithExpression(Me.ContainingMember, boundExpression,
                                                                 doNotUseByRefLocal:=True,
                                                                 isDraftRewrite:=True,
                                                                 binder:=Me.ContainingBinder,
                                                                 preserveIdentityOfLValues:=True)

                ' Create a placeholder if needed
                Dim placeholder As BoundValuePlaceholderBase = Nothing
                If boundExpression.IsLValue OrElse boundExpression.IsMeReference Then
                    placeholder = New BoundWithLValueExpressionPlaceholder(Me.Expression, boundExpression.Type)
                Else
                    placeholder = New BoundWithRValueExpressionPlaceholder(Me.Expression, boundExpression.Type)
                End If
                placeholder.SetWasCompilerGenerated()

                ' It is assumed that the binding result in case of race should still be the same in all racing threads, 
                ' so if the following call fails we can just drop the bound node and diagnostics on the floor
                Interlocked.CompareExchange(Me._withBlockInfo,
                                            New WithBlockInfo(boundExpression, placeholder,
                                                              result.Expression, result.Initializers, result.CapturedLvalueByRefCallOrProperty, diagnostics.ToReadOnlyAndFree()),
                                            Nothing)
            End If

            Debug.Assert(Me._withBlockInfo IsNot Nothing)
        End Sub

        ''' <summary>
        ''' A bound tree walker which search for a bound Me and MyClass references of value type. 
        ''' Is being only used for calculating the value of 'ExpressionHasByRefMeReference'
        ''' </summary>
        Private Class ValueTypedMeReferenceFinder
            Inherits BoundTreeWalkerWithStackGuardWithoutRecursionOnTheLeftOfBinaryOperator

            Private Sub New(recursionDepth As Integer)
                MyBase.New(recursionDepth)
            End Sub

            Private _found As Boolean = False

            Public Shared Function HasByRefMeReference(expression As BoundExpression, recursionDepth As Integer) As Boolean
                Dim walker As New ValueTypedMeReferenceFinder(recursionDepth)
                walker.Visit(expression)
                Return walker._found
            End Function

            Public Overrides Function Visit(node As BoundNode) As BoundNode
                If Not _found Then
                    Return MyBase.Visit(node)
                End If

                Return Nothing
            End Function

            Public Overrides Function VisitMeReference(node As BoundMeReference) As BoundNode
                Dim type As TypeSymbol = node.Type
                Debug.Assert(Not type.IsTypeParameter)
                Debug.Assert(type.IsValueType)
                Me._found = True
                Return Nothing
            End Function

            Public Overrides Function VisitMyClassReference(node As BoundMyClassReference) As BoundNode
                Dim type As TypeSymbol = node.Type
                Debug.Assert(Not type.IsTypeParameter)
                Debug.Assert(type.IsValueType)
                Me._found = True
                Return Nothing
            End Function

        End Class

#End Region

#Region "With block binding"

        Protected Overrides Function CreateBoundWithBlock(node As WithBlockSyntax, boundBlockBinder As Binder, diagnostics As BindingDiagnosticBag) As BoundStatement
            Debug.Assert(node Is Me._withBlockSyntax)

            ' Bind With statement expression
            Me.EnsureExpressionAndPlaceholder()

            ' We need to take care of possible diagnostics that might be produced 
            ' by EnsureExpressionAndPlaceholder call, note that this call might have
            ' been before in which case the diagnostics were stored in '_withBlockInfo'
            ' See also comment in PrepareBindingOfOmittedLeft(...)
            diagnostics.AddRange(Me._withBlockInfo.Diagnostics, allowMismatchInDependencyAccumulation:=True)

            Dim result = New BoundWithStatement(node,
                                                Me._withBlockInfo.OriginalExpression,
                                                boundBlockBinder.BindBlock(node, node.Statements, diagnostics).MakeCompilerGenerated(),
                                                Me)
            If Me._withBlockInfo.CapturedLvalueByRefCallOrProperty IsNot Nothing Then
                Dim containingMethod = TryCast(ContainingMember, MethodSymbol)

                If (containingMethod IsNot Nothing AndAlso (containingMethod.IsIterator OrElse containingMethod.IsAsync)) OrElse
                   result.Binder.ExpressionIsAccessedFromNestedLambda Then
                    ReportDiagnostic(diagnostics, Me._withBlockInfo.CapturedLvalueByRefCallOrProperty.Syntax, ERRID.ERR_UnsupportedRefReturningCallInWithStatement)
                End If
            End If

            Return result
        End Function

#End Region

#Region "Other Overrides"

        ''' <summary> Asserts that the node is NOT from With statement expression </summary>
        <Conditional("DEBUG")>
        Private Sub AssertExpressionIsNotFromStatementExpression(node As SyntaxNode)
            While node IsNot Nothing
                Debug.Assert(node IsNot Me.Expression)
                node = node.Parent
            End While
        End Sub

#If DEBUG Then

        Public Overrides Function BindStatement(node As StatementSyntax, diagnostics As BindingDiagnosticBag) As BoundStatement
            AssertExpressionIsNotFromStatementExpression(node)
            Return MyBase.BindStatement(node, diagnostics)
        End Function

        Public Overrides Function GetBinder(node As SyntaxNode) As Binder
            AssertExpressionIsNotFromStatementExpression(node)
            Return MyBase.GetBinder(node)
        End Function

#End If

        Private Sub PrepareBindingOfOmittedLeft(node As VisualBasicSyntaxNode, diagnostics As BindingDiagnosticBag, accessingBinder As Binder)
            AssertExpressionIsNotFromStatementExpression(node)
            Debug.Assert((node.Kind = SyntaxKind.SimpleMemberAccessExpression) OrElse
                         (node.Kind = SyntaxKind.DictionaryAccessExpression) OrElse
                         (node.Kind = SyntaxKind.XmlAttributeAccessExpression) OrElse
                         (node.Kind = SyntaxKind.XmlElementAccessExpression) OrElse
                         (node.Kind = SyntaxKind.XmlDescendantAccessExpression) OrElse
                         (node.Kind = SyntaxKind.ConditionalAccessExpression))

            Me.EnsureExpressionAndPlaceholder()
            ' NOTE: In case the call above produced diagnostics they were stored in 
            '       '_withBlockInfo' and will be reported in later call to CreateBoundWithBlock(...)

            Dim info As WithBlockInfo = Me._withBlockInfo

            If Me.ContainingMember IsNot accessingBinder.ContainingMember Then
                ' The expression placeholder from With statement may be captured
                info.RegisterAccessFromNestedLambda()
            End If

        End Sub

        Protected Friend Overrides Function TryBindOmittedLeftForMemberAccess(node As MemberAccessExpressionSyntax,
                                                                              diagnostics As BindingDiagnosticBag,
                                                                              accessingBinder As Binder,
                                                                              <Out> ByRef wholeMemberAccessExpressionBound As Boolean) As BoundExpression
            PrepareBindingOfOmittedLeft(node, diagnostics, accessingBinder)

            wholeMemberAccessExpressionBound = False
            Return Me._withBlockInfo.ExpressionPlaceholder
        End Function

        Protected Overrides Function TryBindOmittedLeftForDictionaryAccess(node As MemberAccessExpressionSyntax,
                                                                           accessingBinder As Binder,
                                                                           diagnostics As BindingDiagnosticBag) As BoundExpression
            PrepareBindingOfOmittedLeft(node, diagnostics, accessingBinder)
            Return Me._withBlockInfo.ExpressionPlaceholder
        End Function

        Protected Overrides Function TryBindOmittedLeftForConditionalAccess(node As ConditionalAccessExpressionSyntax, accessingBinder As Binder, diagnostics As BindingDiagnosticBag) As BoundExpression
            PrepareBindingOfOmittedLeft(node, diagnostics, accessingBinder)
            Return Me._withBlockInfo.ExpressionPlaceholder
        End Function

        Protected Friend Overrides Function TryBindOmittedLeftForXmlMemberAccess(node As XmlMemberAccessExpressionSyntax,
                                                                                 diagnostics As BindingDiagnosticBag,
                                                                                 accessingBinder As Binder) As BoundExpression
            PrepareBindingOfOmittedLeft(node, diagnostics, accessingBinder)
            Return Me._withBlockInfo.ExpressionPlaceholder
        End Function

        Friend Overrides ReadOnly Property Locals As ImmutableArray(Of LocalSymbol)
            Get
                Return ImmutableArray(Of LocalSymbol).Empty
            End Get
        End Property

#End Region

    End Class

End Namespace

