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

Namespace Microsoft.CodeAnalysis.VisualBasic

    Partial Friend NotInheritable Class DiagnosticsPass
        Inherits BoundTreeWalkerWithStackGuardWithoutRecursionOnTheLeftOfBinaryOperator

        Private ReadOnly _diagnostics As DiagnosticBag
        Private ReadOnly _compilation As VisualBasicCompilation
        Private _containingSymbol As MethodSymbol
        Private _withExpressionPlaceholderMap As Dictionary(Of BoundValuePlaceholderBase, BoundWithStatement)
        Private _expressionsBeingVisited As Stack(Of BoundExpression)
        Private _insideNameof As Boolean = False

        Private _inExpressionLambda As Boolean

        Public Shared Sub IssueDiagnostics(node As BoundNode, diagnostics As DiagnosticBag, containingSymbol As MethodSymbol)
            Debug.Assert(node IsNot Nothing)
            Debug.Assert(containingSymbol IsNot Nothing)

            Try
                Dim diagnosticPass As New DiagnosticsPass(containingSymbol.DeclaringCompilation, diagnostics, containingSymbol)
                diagnosticPass.Visit(node)
            Catch ex As CancelledByStackGuardException
                ex.AddAnError(diagnostics)
            End Try
        End Sub

        Private Sub New(compilation As VisualBasicCompilation, diagnostics As DiagnosticBag, containingSymbol As MethodSymbol)
            Me._compilation = compilation
            Me._diagnostics = diagnostics
            Me._containingSymbol = containingSymbol

            Me._inExpressionLambda = False
        End Sub

        Private ReadOnly Property IsInExpressionLambda As Boolean
            Get
                Return Me._inExpressionLambda
            End Get
        End Property

        Public Overrides Function VisitQueryLambda(node As BoundQueryLambda) As BoundNode
            Dim save_containingSymbol = Me._containingSymbol
            Me._containingSymbol = node.LambdaSymbol
            Me.Visit(node.Expression)
            Me._containingSymbol = save_containingSymbol
            Return Nothing
        End Function

        Public Overrides Function VisitParameter(node As BoundParameter) As BoundNode
            Dim parameterSymbol As ParameterSymbol = node.ParameterSymbol

            If parameterSymbol.IsByRef Then
                ' cannot access ByRef parameters in Lambdas
                Dim parameterSymbolContainingSymbol As Symbol = parameterSymbol.ContainingSymbol

                If _containingSymbol IsNot parameterSymbolContainingSymbol AndAlso Not _insideNameof Then
                    ' Need to go up the chain of containers and see if the last lambda we see
                    ' is a QueryLambda, before we reach parameter's container. 
                    If Binder.IsTopMostEnclosingLambdaAQueryLambda(_containingSymbol, parameterSymbolContainingSymbol) Then
                        Binder.ReportDiagnostic(Me._diagnostics, node.Syntax, ERRID.ERR_CannotLiftByRefParamQuery1, parameterSymbol.Name)
                    Else
                        Binder.ReportDiagnostic(Me._diagnostics, node.Syntax, ERRID.ERR_CannotLiftByRefParamLambda1, parameterSymbol.Name)
                    End If
                End If
            End If

            Return MyBase.VisitParameter(node)
        End Function

        Public Overrides Function VisitMeReference(node As BoundMeReference) As BoundNode
            Dim errorId As ERRID = GetMeAccessError()

            If errorId <> ERRID.ERR_None Then
                Binder.ReportDiagnostic(Me._diagnostics, node.Syntax, errorId)
            End If

            Return MyBase.VisitMeReference(node)
        End Function

        Public Overrides Function VisitMyClassReference(node As BoundMyClassReference) As BoundNode
            Dim errorId As ERRID = GetMeAccessError()

            If errorId <> ERRID.ERR_None Then
                Binder.ReportDiagnostic(Me._diagnostics, node.Syntax, errorId)
            End If

            Return MyBase.VisitMyClassReference(node)
        End Function

        Private Function GetMeAccessError() As ERRID
            Dim meParameter As ParameterSymbol = Me._containingSymbol.MeParameter
            If meParameter IsNot Nothing AndAlso meParameter.IsByRef Then
                If _containingSymbol.MethodKind = MethodKind.LambdaMethod Then
                    ' Need to go up the chain of containers and see if the last lambda we see
                    ' is a QueryLambda, before we reach a type member. 
                    If Binder.IsTopMostEnclosingLambdaAQueryLambda(_containingSymbol, Nothing) Then
                        Return ERRID.ERR_CannotLiftStructureMeQuery
                    Else
                        Return ERRID.ERR_CannotLiftStructureMeLambda
                    End If
                End If
            End If

            Return ERRID.ERR_None
        End Function

        Public Overrides Function VisitWithRValueExpressionPlaceholder(node As BoundWithRValueExpressionPlaceholder) As BoundNode
            CheckMeAccessInWithExpression(node)
            Return MyBase.VisitWithRValueExpressionPlaceholder(node)
        End Function

        Private Sub CheckMeAccessInWithExpression(node As BoundValuePlaceholderBase)
            Dim trackedWithStatement As BoundWithStatement = Nothing

            If _withExpressionPlaceholderMap IsNot Nothing AndAlso _withExpressionPlaceholderMap.TryGetValue(node, trackedWithStatement) Then
                Dim withBlockBinder As WithBlockBinder = trackedWithStatement.Binder

                If withBlockBinder.ContainingMember IsNot _containingSymbol Then
                    ' If With statement expression is accessed from a different symbol,
                    ' we need to check for value-typed Me reference being lifted
                    If Not withBlockBinder.IsInLambda Then
                        Debug.Assert(_containingSymbol.IsLambdaMethod) ' What else can it be?

                        Dim containingType = withBlockBinder.ContainingType
                        If containingType IsNot Nothing AndAlso containingType.IsValueType AndAlso withBlockBinder.Info.ExpressionHasByRefMeReference(RecursionDepth) Then
                            Dim errorId As ERRID = GetMeAccessError()
                            If errorId <> ERRID.ERR_None Then
                                ' The placeholder node will actually use syntax from With statement and it might have some wrappers and conversions on top of it with the same syntax.
                                ' Let's use syntax of parent node instead. 
                                Dim errorSyntax As SyntaxNode = node.Syntax

                                For Each expr In _expressionsBeingVisited
                                    If expr.Syntax IsNot errorSyntax Then
                                        errorSyntax = expr.Syntax
                                        Exit For
                                    End If
                                Next

                                Binder.ReportDiagnostic(Me._diagnostics, errorSyntax, errorId)
                            End If
                        End If
                    End If
                End If
            End If
        End Sub

        Public Overrides Function VisitWithStatement(node As BoundWithStatement) As BoundNode
            Me.Visit(node.OriginalExpression)

            Dim info As WithBlockBinder.WithBlockInfo = node.Binder.Info

            If info IsNot Nothing AndAlso info.ExpressionIsAccessedFromNestedLambda Then
                If _withExpressionPlaceholderMap Is Nothing Then
                    _withExpressionPlaceholderMap = New Dictionary(Of BoundValuePlaceholderBase, BoundWithStatement)()
                    _expressionsBeingVisited = New Stack(Of BoundExpression)()
                End If

                Debug.Assert(info.ExpressionPlaceholder IsNot Nothing)
                _withExpressionPlaceholderMap.Add(info.ExpressionPlaceholder, node)
            Else
                info = Nothing
            End If

            Me.Visit(node.Body)

            If info IsNot Nothing Then
                _withExpressionPlaceholderMap.Remove(info.ExpressionPlaceholder)
            End If

            Return Nothing
        End Function

        Public Overrides Function VisitNameOfOperator(node As BoundNameOfOperator) As BoundNode
            _insideNameof = True
            Dim r = MyBase.VisitNameOfOperator(node)
            _insideNameof = False
            Return r
        End Function

        Public Overrides Function Visit(node As BoundNode) As BoundNode
            Dim pop As Boolean = False

            If _withExpressionPlaceholderMap IsNot Nothing AndAlso _withExpressionPlaceholderMap.Count > 0 Then
                Dim expr = TryCast(node, BoundExpression)

                If expr IsNot Nothing Then
                    _expressionsBeingVisited.Push(expr)
                    pop = True
                End If
            End If

            Dim result = MyBase.Visit(node)

            If pop Then
                Debug.Assert(_expressionsBeingVisited.Peek Is node)
                _expressionsBeingVisited.Pop()
            End If

            Return result
        End Function

    End Class

End Namespace
