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

'-----------------------------------------------------------------------------
' Contains the definition of the Scanner, which produces tokens from text 
'-----------------------------------------------------------------------------

Imports System.Text
Imports Microsoft.CodeAnalysis.PooledObjects
Imports Microsoft.CodeAnalysis.Text
Imports Microsoft.CodeAnalysis.VisualBasic.Symbols
Imports Microsoft.CodeAnalysis.VisualBasic.Syntax

Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax

    Partial Friend Class Scanner
        ''' <summary>
        ''' page represents a cached array of chars.
        ''' </summary>
        Private Class Page
            ''' <summary>
            ''' where page maps in the stream. Used to validate pages
            ''' </summary>
            Friend _pageStart As Integer

            ''' <summary>
            ''' page's buffer
            ''' </summary>
            Friend ReadOnly _arr As Char()

            Private ReadOnly _pool As ObjectPool(Of Page)
            Private Sub New(pool As ObjectPool(Of Page))
                _pageStart = -1
                _arr = New Char(s_PAGE_SIZE - 1) {}
                _pool = pool
            End Sub

            Friend Sub Free()
                _pageStart = -1
                _pool.Free(Me)
            End Sub

            Private Shared ReadOnly s_poolInstance As ObjectPool(Of Page) = CreatePool()
            Private Shared Function CreatePool() As ObjectPool(Of Page)
                Dim pool As ObjectPool(Of Page) = Nothing
                pool = New ObjectPool(Of Page)(Function() New Page(pool), 128)
                Return pool
            End Function
            Friend Shared Function GetInstance() As Page
                Dim instance = s_poolInstance.Allocate()
                Return instance
            End Function
        End Class

        ''' <summary>
        ''' current page we are reading.
        ''' </summary>
        Private _curPage As Page
        Private ReadOnly _pages(s_PAGE_NUM - 1) As Page

        Private Const s_PAGE_NUM_SHIFT = 2
        Private Const s_PAGE_NUM = CInt(2 ^ s_PAGE_NUM_SHIFT)
        Private Const s_PAGE_NUM_MASK = s_PAGE_NUM - 1

        Private Const s_PAGE_SHIFT = 11
        Private Const s_PAGE_SIZE = CInt(2 ^ s_PAGE_SHIFT)
        Private Const s_PAGE_MASK = s_PAGE_SIZE - 1
        Private Const s_NOT_PAGE_MASK = Not s_PAGE_MASK

        Private ReadOnly _buffer As SourceText
        Private ReadOnly _bufferLen As Integer

        ' created on demand. we may not need it
        Private _builder As StringBuilder

        ''' <summary>
        ''' gets a page for the position.
        ''' will initialize it if we have cache miss
        ''' </summary>
        Private Function GetPage(position As Integer) As Page
            Dim pageNum = (position >> s_PAGE_SHIFT) And s_PAGE_NUM_MASK

            Dim p = _pages(pageNum)
            Dim pageStart = position And s_NOT_PAGE_MASK

            If p Is Nothing Then
                p = Page.GetInstance
                _pages(pageNum) = p
            End If

            If p._pageStart <> pageStart Then
                _buffer.CopyTo(pageStart, p._arr, 0, Math.Min(_bufferLen - pageStart, s_PAGE_SIZE))
                p._pageStart = pageStart
            End If

            _curPage = p
            Return p
        End Function

        ' PERF CRITICAL
        Private Function Peek(skip As Integer) As Char
            Debug.Assert(CanGet(skip))
            Debug.Assert(skip >= -MaxCharsLookBehind)

            Dim position = _lineBufferOffset
            Dim page = _curPage
            position += skip

            Dim ch = page._arr(position And s_PAGE_MASK)

            Dim start = page._pageStart
            Dim expectedStart = position And s_NOT_PAGE_MASK

            If start <> expectedStart Then
                page = GetPage(position)
                ch = page._arr(position And s_PAGE_MASK)
            End If

            Return ch
        End Function

        ' PERF CRITICAL
        Friend Function Peek() As Char
            Dim page = _curPage
            Dim position = _lineBufferOffset
            Dim ch = page._arr(position And s_PAGE_MASK)

            Dim start = page._pageStart
            Dim expectedStart = position And s_NOT_PAGE_MASK

            If start <> expectedStart Then
                page = GetPage(position)
                ch = page._arr(position And s_PAGE_MASK)
            End If

            Return ch
        End Function

        Friend Function GetChar() As String
            Return Intern(Peek())
        End Function

        Friend Function GetText(start As Integer, length As Integer) As String
            Dim page = _curPage
            Dim offsetInPage = start And s_PAGE_MASK

            If page._pageStart = (start And s_NOT_PAGE_MASK) AndAlso
                offsetInPage + length < s_PAGE_SIZE Then

                Return Intern(page._arr, offsetInPage, length)
            End If
            Return GetTextSlow(start, length)
        End Function

        Friend Function GetTextNotInterned(start As Integer, length As Integer) As String
            Dim page = _curPage
            Dim offsetInPage = start And s_PAGE_MASK

            If page._pageStart = (start And s_NOT_PAGE_MASK) AndAlso
                offsetInPage + length < s_PAGE_SIZE Then
                Dim arr() As Char = page._arr

                ' Always intern CR+LF since it occurs so frequently
                If length = 2 AndAlso arr(offsetInPage) = ChrW(13) AndAlso arr(offsetInPage + 1) = ChrW(10) Then
                    Return vbCrLf
                End If

                Return New String(arr, offsetInPage, length)
            End If
            Return GetTextSlow(start, length, suppressInterning:=True)
        End Function

        Private Function GetTextSlow(start As Integer, length As Integer, Optional suppressInterning As Boolean = False) As String
            Dim textOffset = start And s_PAGE_MASK

            Dim page = GetPage(start)
            If textOffset + length < s_PAGE_SIZE Then
                If suppressInterning Then
                    Return New String(page._arr, textOffset, length)
                Else
                    Return Intern(page._arr, textOffset, length)
                End If
            End If

            ' make a string builder that is big enough, but not too big
            If _builder Is Nothing Then
                _builder = New StringBuilder(Math.Min(length, 1024))
            End If

            Dim cnt = Math.Min(length, s_PAGE_SIZE - textOffset)
            _builder.Append(page._arr, textOffset, cnt)

            Dim dst = cnt
            length -= cnt
            start += cnt

            Do
                page = GetPage(start)
                cnt = Math.Min(length, s_PAGE_SIZE)
                _builder.Append(page._arr, 0, cnt)
                dst += cnt
                length -= cnt
                start += cnt
            Loop While length > 0

            Dim result As String
            If suppressInterning Then
                result = _builder.ToString
            Else
                result = _stringTable.Add(_builder)
            End If
            If result.Length < 1024 Then
                _builder.Clear()
            Else
                _builder = Nothing
            End If
            Return result
        End Function
    End Class
End Namespace
