Option Strict On
Option Explicit On

Imports System
Imports System.Diagnostics

Namespace Nexamas.UI.TextInput

    ' CONTRACT:
    ' MASTextCompositionState represents IME pre-edit/composition text.
    '
    ' It is NOT the committed document text.
    ' It must not enter undo history until committed through the controller.
    ' It must not replace MASTextInputState as source of truth.
    Friend NotInheritable Class MASTextCompositionState

#Region "Fields"

        Private _isActive As Boolean
        Private _text As String = String.Empty
        Private _selectionStart As Integer
        Private _selectionLength As Integer
        Private _version As Integer

#End Region

#Region "Properties"

        Friend ReadOnly Property IsActive As Boolean
            Get
                Return _isActive
            End Get
        End Property

        Friend ReadOnly Property Text As String
            Get
                Return _text
            End Get
        End Property

        Friend ReadOnly Property SelectionStart As Integer
            Get
                Return _selectionStart
            End Get
        End Property

        Friend ReadOnly Property SelectionLength As Integer
            Get
                Return _selectionLength
            End Get
        End Property

        Friend ReadOnly Property Version As Integer
            Get
                Return _version
            End Get
        End Property

        Friend ReadOnly Property HasSelection As Boolean
            Get
                Return _selectionLength > 0
            End Get
        End Property

#End Region

#Region "Operations"

        Friend Function BeginComposition(Optional value As String = "",
                                         Optional selectionStart As Integer = 0,
                                         Optional selectionLength As Integer = 0) As Boolean

            Dim normalized As String = If(value, String.Empty)
            Dim safeStart As Integer = ClampIndex(selectionStart, normalized.Length)
            Dim safeLength As Integer = ClampLength(safeStart, selectionLength, normalized.Length)

            If _isActive AndAlso
               String.Equals(_text, normalized, StringComparison.Ordinal) AndAlso
               _selectionStart = safeStart AndAlso
               _selectionLength = safeLength Then

                Return False
            End If

            _isActive = True
            _text = normalized
            _selectionStart = safeStart
            _selectionLength = safeLength
            BumpVersion()
            ValidateInvariants()

            Return True
        End Function

        Friend Function UpdateComposition(value As String,
                                          selectionStart As Integer,
                                          selectionLength As Integer) As Boolean

            If Not _isActive Then
                Return BeginComposition(value, selectionStart, selectionLength)
            End If

            Dim normalized As String = If(value, String.Empty)
            Dim safeStart As Integer = ClampIndex(selectionStart, normalized.Length)
            Dim safeLength As Integer = ClampLength(safeStart, selectionLength, normalized.Length)

            If String.Equals(_text, normalized, StringComparison.Ordinal) AndAlso
               _selectionStart = safeStart AndAlso
               _selectionLength = safeLength Then

                Return False
            End If

            _text = normalized
            _selectionStart = safeStart
            _selectionLength = safeLength
            BumpVersion()
            ValidateInvariants()

            Return True
        End Function

        Friend Function CancelComposition() As Boolean
            If Not _isActive AndAlso _text.Length = 0 Then Return False

            _isActive = False
            _text = String.Empty
            _selectionStart = 0
            _selectionLength = 0
            BumpVersion()
            ValidateInvariants()

            Return True
        End Function

        Friend Function TakeCommitText(Optional overrideText As String = Nothing) As String
            If overrideText IsNot Nothing Then
                Return overrideText
            End If

            Return _text
        End Function

#End Region

#Region "Helpers"

        Private Sub BumpVersion()
            If _version = Integer.MaxValue Then
                _version = 1
            Else
                _version += 1
            End If
        End Sub

        Private Shared Function ClampIndex(value As Integer, maxLen As Integer) As Integer
            If maxLen < 0 Then maxLen = 0
            If value < 0 Then Return 0
            If value > maxLen Then Return maxLen
            Return value
        End Function

        Private Shared Function ClampLength(start As Integer,
                                            length As Integer,
                                            maxLen As Integer) As Integer
            If length <= 0 Then Return 0
            If start < 0 Then start = 0
            If start > maxLen Then Return 0

            Dim available As Integer = maxLen - start
            If length > available Then Return available
            Return length
        End Function

        <Conditional("DEBUG")>
        Private Sub ValidateInvariants()
            Debug.Assert(_text IsNot Nothing, "Composition invariant failed: Text must never be Nothing.")
            Debug.Assert(_selectionStart >= 0 AndAlso _selectionStart <= _text.Length,
                         "Composition invariant failed: SelectionStart outside composition text bounds.")
            Debug.Assert(_selectionLength >= 0,
                         "Composition invariant failed: SelectionLength must be >= 0.")
            Debug.Assert(_selectionStart + _selectionLength <= _text.Length,
                         "Composition invariant failed: selection range exceeds composition text length.")
        End Sub

#End Region

    End Class

End Namespace
