Option Strict On
Option Explicit On

Imports System
Imports System.Collections.Generic
Imports System.Diagnostics

Namespace Nexamas.UI.TextInput

    ' CONTRACT:
    ' MASTextMultilineDocument is a read-only logical view over text.
    '
    ' It owns:
    ' - Normalized text view
    ' - Logical line ranges
    ' - Mapping index -> line
    '
    ' It must not:
    ' - Mutate MASTextInputState
    ' - Perform wrapping
    ' - Render
    ' - Handle keyboard/mouse input
    Friend NotInheritable Class MASTextMultilineDocument

        Private Shared ReadOnly Scanner As New MASTextLineBreakScanner()

        Private Sub New(text As String,
                        lines As IReadOnlyList(Of MASTextLineRange),
                        normalizedLineEndings As Boolean)

            Me.Text = If(text, String.Empty)
            Me.Lines = If(lines, Array.Empty(Of MASTextLineRange)())
            Me.NormalizedLineEndings = normalizedLineEndings

            Validate()
        End Sub

        Friend ReadOnly Property Text As String
        Friend ReadOnly Property Lines As IReadOnlyList(Of MASTextLineRange)
        Friend ReadOnly Property NormalizedLineEndings As Boolean

        Friend ReadOnly Property LineCount As Integer
            Get
                Return Lines.Count
            End Get
        End Property

        Friend Shared Function FromText(value As String,
                                        Optional normalizeLineEndings As Boolean = True) As MASTextMultilineDocument

            Dim text As String = If(value, String.Empty)

            If normalizeLineEndings Then
                text = MASTextLineBreakScanner.NormalizeLineEndings(text)
            End If

            Dim ranges As IReadOnlyList(Of MASTextLineRange) = Scanner.Scan(text)
            Return New MASTextMultilineDocument(text, ranges, normalizeLineEndings)
        End Function

        Friend Function GetLine(lineIndex As Integer) As MASTextLineRange
            If Lines.Count = 0 Then Throw New InvalidOperationException("Document has no lines.")

            If lineIndex < 0 Then lineIndex = 0
            If lineIndex >= Lines.Count Then lineIndex = Lines.Count - 1

            Return Lines(lineIndex)
        End Function

        Friend Function GetLineText(lineIndex As Integer) As String
            Return GetLine(lineIndex).GetText(Text)
        End Function

        Friend Function GetLineIndexFromTextIndex(textIndex As Integer) As Integer
            If Lines.Count = 0 Then Return 0

            Dim index As Integer = ClampTextIndex(textIndex)
            Dim lo As Integer = 0
            Dim hi As Integer = Lines.Count - 1

            While lo <= hi
                Dim mid As Integer = lo + ((hi - lo) \ 2)
                Dim line As MASTextLineRange = Lines(mid)

                If index < line.StartIndex Then
                    hi = mid - 1
                ElseIf index > line.EndIndexExclusive Then
                    lo = mid + 1
                Else
                    Return mid
                End If
            End While

            If lo <= 0 Then Return 0
            If lo >= Lines.Count Then Return Lines.Count - 1
            Return lo
        End Function

        Friend Function GetColumnFromTextIndex(textIndex As Integer) As Integer
            Dim lineIndex As Integer = GetLineIndexFromTextIndex(textIndex)
            Dim line As MASTextLineRange = GetLine(lineIndex)
            Return Math.Max(0, ClampTextIndex(textIndex) - line.StartIndex)
        End Function

        Friend Function GetTextIndexFromLineColumn(lineIndex As Integer,
                                                   column As Integer) As Integer
            Dim line As MASTextLineRange = GetLine(lineIndex)
            Dim safeColumn As Integer = column
            If safeColumn < 0 Then safeColumn = 0
            If safeColumn > line.Length Then safeColumn = line.Length

            Return line.StartIndex + safeColumn
        End Function

        Friend Function ClampTextIndex(textIndex As Integer) As Integer
            If textIndex < 0 Then Return 0
            If textIndex > Text.Length Then Return Text.Length
            Return textIndex
        End Function

        <Conditional("DEBUG")>
        Private Sub Validate()
            Debug.Assert(Text IsNot Nothing, "MultilineDocument contract failed: Text is Nothing.")
            Debug.Assert(Lines IsNot Nothing, "MultilineDocument contract failed: Lines is Nothing.")
            If Lines Is Nothing Then Return

            Debug.Assert(Lines.Count > 0, "MultilineDocument contract failed: at least one logical line is required.")

            For i As Integer = 0 To Lines.Count - 1
                Dim line As MASTextLineRange = Lines(i)
                Debug.Assert(line IsNot Nothing, "MultilineDocument contract failed: line is Nothing.")
                If line Is Nothing Then Continue For

                Debug.Assert(line.LineIndex = i, "MultilineDocument contract failed: line index mismatch.")
                Debug.Assert(line.EndIndexExclusive <= Text.Length, "MultilineDocument contract failed: line exceeds text length.")
            Next
        End Sub

    End Class

End Namespace
