Option Strict On
Option Explicit On

Imports System
Imports System.Diagnostics
Imports Nexamas.UI.Services
Imports SkiaSharp

Namespace Nexamas.UI.TextInput

    ' CONTRACT:
    ' MASTextInputController is the only public gateway between UI controls
    ' and the internal text-input model.
    Friend NotInheritable Class MASTextInputController

#Region "Fields"

        Private ReadOnly _state As New MASTextInputState()
        Private ReadOnly _editor As New MASTextInputEditor()
        Private ReadOnly _nav As New MASTextInputNavigator()
        Private ReadOnly _undo As New MASTextUndoManager()
        Private ReadOnly _scroll As New MASTextScrollEngine()
        Private ReadOnly _composition As New MASTextCompositionState()
        Private ReadOnly _commands As MASTextCommandRunner
        Private _clipboard As IMASClipboardService

#End Region

#Region "Constructor"

        Friend Sub New()
            _commands = New MASTextCommandRunner(_undo)
        End Sub

#End Region

#Region "Properties"

        ' CONTRACT:
        ' State is exposed for inspection and snapshot building.
        ' External code must not mutate it directly.
        Friend ReadOnly Property State As MASTextInputState
            Get
                Return _state
            End Get
        End Property

        Friend ReadOnly Property Editor As MASTextInputEditor
            Get
                Return _editor
            End Get
        End Property

        Friend Property Clipboard As IMASClipboardService
            Get
                Return _clipboard
            End Get
            Set(value As IMASClipboardService)
                _clipboard = value
            End Set
        End Property

        Friend Property ScrollOffsetPx As Single
            Get
                Return _scroll.OffsetPx
            End Get
            Set(value As Single)
                _scroll.OffsetPx = value
            End Set
        End Property

        Friend ReadOnly Property CanUndo As Boolean
            Get
                Return _undo.CanUndo
            End Get
        End Property

        Friend ReadOnly Property CanRedo As Boolean
            Get
                Return _undo.CanRedo
            End Get
        End Property

        Friend ReadOnly Property UndoCount As Integer
            Get
                Return _undo.UndoCount
            End Get
        End Property

        Friend ReadOnly Property RedoCount As Integer
            Get
                Return _undo.RedoCount
            End Get
        End Property

        Friend ReadOnly Property HasActiveSelectionGesture As Boolean
            Get
                Return _state.SelectionAnchor >= 0
            End Get
        End Property

        Friend ReadOnly Property TextLength As Integer
            Get
                Return _state.Text.Length
            End Get
        End Property

        ' CONTRACT:
        ' Composition is exposed for inspection/rendering only.
        ' It is pre-edit text and must not be treated as committed State.Text.
        Friend ReadOnly Property Composition As MASTextCompositionState
            Get
                Return _composition
            End Get
        End Property

#End Region

#Region "Edge-case guards"

        ' Stage 7A:
        ' Any normal edit/navigation/undo operation must leave IME composition mode first.
        ' Composition text is pre-edit text and must not survive selection, undo, paste,
        ' programmatic text changes, or caret movement.
        Private Function CancelCompositionForExternalOperation() As Boolean
            If _composition Is Nothing Then Return False
            If Not _composition.IsActive Then Return False

            _undo.BreakMerge()
            Return _composition.CancelComposition()
        End Function

#End Region

#Region "State operations"

        Friend Function SetText(value As String) As Boolean
            Dim compositionChanged As Boolean = CancelCompositionForExternalOperation()
            Dim v As String = If(value, String.Empty)
            If String.Equals(_state.Text, v, StringComparison.Ordinal) Then Return compositionChanged

            Dim changed As Boolean = _undo.ExecuteProgrammatic(
                state:=_state,
                action:=Function()
                            Dim beforeVersion As Integer = _state.Version
                            _state.Text = v
                            Return beforeVersion <> _state.Version
                        End Function)

            If changed Then _scroll.Reset()
            Return changed OrElse compositionChanged
        End Function

        Friend Function SetReadOnly(value As Boolean) As Boolean
            Dim compositionChanged As Boolean = CancelCompositionForExternalOperation()
            If _state.ReadOnly = value Then Return compositionChanged

            _state.ReadOnly = value
            _undo.BreakMerge()

            Return True
        End Function

        Friend Function SetUseSystemPasswordChar(value As Boolean) As Boolean
            Dim compositionChanged As Boolean = CancelCompositionForExternalOperation()
            If _state.UseSystemPasswordChar = value Then Return compositionChanged

            _state.UseSystemPasswordChar = value
            _undo.BreakMerge()
            _scroll.Reset()

            Return True
        End Function

        Friend Function SetPasswordChar(value As Char) As Boolean
            Dim compositionChanged As Boolean = CancelCompositionForExternalOperation()
            If value = ControlChars.NullChar Then Return compositionChanged
            If _state.PasswordChar = value Then Return compositionChanged

            _state.PasswordChar = value
            _undo.BreakMerge()
            _scroll.Reset()

            Return True
        End Function

        Friend Function SetMaxLength(value As Integer) As Boolean
            Dim compositionChanged As Boolean = CancelCompositionForExternalOperation()
            Dim v As Integer = Math.Max(0, value)
            If _state.MaxLength = v Then Return compositionChanged

            Dim changed As Boolean = _undo.ExecuteProgrammatic(
                state:=_state,
                action:=Function()
                            Dim beforeVersion As Integer = _state.Version
                            _state.MaxLength = v
                            Return beforeVersion <> _state.Version
                        End Function)

            If changed Then _scroll.Reset()
            Return changed OrElse compositionChanged
        End Function

        Friend Function Clear() As Boolean
            Dim compositionChanged As Boolean = CancelCompositionForExternalOperation()
            If _state.Text.Length = 0 Then Return compositionChanged

            Dim changed As Boolean = _undo.ExecuteProgrammatic(
                state:=_state,
                action:=Function()
                            Dim beforeVersion As Integer = _state.Version
                            _state.ClearAll()
                            Return beforeVersion <> _state.Version
                        End Function)

            If changed Then _scroll.Reset()
            Return changed OrElse compositionChanged
        End Function

        Friend Function SelectAll() As Boolean
            Dim compositionChanged As Boolean = CancelCompositionForExternalOperation()
            Return RunCommand(New MASTextSelectAllCommand()) OrElse compositionChanged
        End Function

        Friend Function SetSelection(start As Integer, length As Integer) As Boolean
            Dim compositionChanged As Boolean = CancelCompositionForExternalOperation()
            Return RunCommand(New MASTextSetSelectionCommand(start, length)) OrElse compositionChanged
        End Function

        Friend Function ClearSelection() As Boolean
            Dim compositionChanged As Boolean = CancelCompositionForExternalOperation()
            Return RunCommand(New MASTextClearSelectionCommand()) OrElse compositionChanged
        End Function

        Friend Function EndSelectionGesture() As Boolean
            Dim compositionChanged As Boolean = CancelCompositionForExternalOperation()
            If _state.SelectionAnchor = -1 Then Return compositionChanged
            Return RunCommand(New MASTextEndSelectionGestureCommand()) OrElse compositionChanged
        End Function

        Friend Function ClampState() As Boolean
            Dim beforeVersion As Integer = _state.Version
            _state.Clamp()
            Return beforeVersion <> _state.Version
        End Function

#End Region

#Region "Caret / selection"

        Friend Function SetCaret(index As Integer) As Boolean
            Dim compositionChanged As Boolean = CancelCompositionForExternalOperation()
            Return RunCommand(New MASTextSetCaretCommand(index)) OrElse compositionChanged
        End Function

        Friend Function BeginSelection(index As Integer) As Boolean
            Dim compositionChanged As Boolean = CancelCompositionForExternalOperation()
            Return RunCommand(New MASTextBeginSelectionCommand(index)) OrElse compositionChanged
        End Function

        Friend Function ExtendSelectionTo(index As Integer) As Boolean
            Dim compositionChanged As Boolean = CancelCompositionForExternalOperation()
            Return RunCommand(New MASTextExtendSelectionCommand(index)) OrElse compositionChanged
        End Function

        Friend Function SelectWordAt(index As Integer) As Boolean
            Dim compositionChanged As Boolean = CancelCompositionForExternalOperation()
            Return RunCommand(New MASTextSelectWordCommand(index)) OrElse compositionChanged
        End Function

#End Region

#Region "Movement"

        Friend Function MoveCaretHorizontal(delta As Integer, extendSelection As Boolean) As Boolean
            Dim compositionChanged As Boolean = CancelCompositionForExternalOperation()
            Return RunCommand(New MASTextMoveCaretHorizontalCommand(delta, extendSelection)) OrElse compositionChanged
        End Function

        Friend Function MoveCaretToPreviousWord(extendSelection As Boolean) As Boolean
            Dim compositionChanged As Boolean = CancelCompositionForExternalOperation()
            Return RunCommand(New MASTextMovePreviousWordCommand(extendSelection)) OrElse compositionChanged
        End Function

        Friend Function MoveCaretToNextWord(extendSelection As Boolean) As Boolean
            Dim compositionChanged As Boolean = CancelCompositionForExternalOperation()
            Return RunCommand(New MASTextMoveNextWordCommand(extendSelection)) OrElse compositionChanged
        End Function

        Friend Function MoveCaretTo(target As Integer, extendSelection As Boolean) As Boolean
            Dim compositionChanged As Boolean = CancelCompositionForExternalOperation()
            Return RunCommand(New MASTextMoveCaretToCommand(target, extendSelection)) OrElse compositionChanged
        End Function

#End Region

#Region "Editing through commands"

        Friend Function InsertText(value As String) As Boolean
            Dim compositionChanged As Boolean = CancelCompositionForExternalOperation()
            Return RunCommand(New MASTextInsertCommand(value, MASTextEditKind.Typing)) OrElse compositionChanged
        End Function

        Friend Function PasteText(value As String) As Boolean
            Dim compositionChanged As Boolean = CancelCompositionForExternalOperation()
            Return RunCommand(New MASTextPasteCommand(value)) OrElse compositionChanged
        End Function

        Friend Function Backspace(ctrl As Boolean) As Boolean
            Dim compositionChanged As Boolean = CancelCompositionForExternalOperation()
            Return RunCommand(New MASTextBackspaceCommand(ctrl)) OrElse compositionChanged
        End Function

        Friend Function DeleteForward(ctrl As Boolean) As Boolean
            Dim compositionChanged As Boolean = CancelCompositionForExternalOperation()
            Return RunCommand(New MASTextDeleteForwardCommand(ctrl)) OrElse compositionChanged
        End Function

        Friend Function DeleteSelection() As Boolean
            Dim compositionChanged As Boolean = CancelCompositionForExternalOperation()
            Return RunCommand(New MASTextDeleteSelectionCommand()) OrElse compositionChanged
        End Function

        ' CONTRACT:
        ' All command execution must pass through this method.
        Friend Function RunCommand(command As IMASTextInputCommand) As Boolean
            Dim beforeVersion As Integer = _state.Version
            Dim beforeText As String = _state.Text

            Dim changed As Boolean = _commands.Run(
                state:=_state,
                editor:=_editor,
                navigator:=_nav,
                command:=command)

            ValidateControllerCommandResult(command, beforeVersion, beforeText, changed)
            Return changed
        End Function

        <Conditional("DEBUG")>
        Private Sub ValidateControllerCommandResult(command As IMASTextInputCommand,
                                                    beforeVersion As Integer,
                                                    beforeText As String,
                                                    changed As Boolean)

            If command Is Nothing Then Return

            Dim textChanged As Boolean =
                Not String.Equals(beforeText, _state.Text, StringComparison.Ordinal)

            If command.AffectsText Then
                Debug.Assert(Not textChanged OrElse _state.Version <> beforeVersion,
                             "Controller contract failed: text changed without State.Version change.")
            Else
                Debug.Assert(Not textChanged,
                             "Controller contract failed: non-text command changed State.Text.")
            End If

            Debug.Assert(Not changed OrElse _state.CaretIndex >= 0,
                         "Controller contract failed: invalid CaretIndex.")

            Debug.Assert(Not changed OrElse _state.CaretIndex <= _state.Text.Length,
                         "Controller contract failed: CaretIndex outside text bounds.")
        End Sub

#End Region

#Region "Undo / redo"

        Friend Function Undo() As Boolean
            Dim compositionChanged As Boolean = CancelCompositionForExternalOperation()
            Dim changed As Boolean = _undo.Undo(_state)
            If changed Then _scroll.Reset()
            Return changed OrElse compositionChanged
        End Function

        Friend Function Redo() As Boolean
            Dim compositionChanged As Boolean = CancelCompositionForExternalOperation()
            Dim changed As Boolean = _undo.Redo(_state)
            If changed Then _scroll.Reset()
            Return changed OrElse compositionChanged
        End Function

        Friend Sub ClearHistory()
            _undo.Clear()
        End Sub

#End Region

#Region "IME composition abstraction"

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

            _undo.BreakMerge()
            Return _composition.BeginComposition(value, selectionStart, selectionLength)
        End Function

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

            _undo.BreakMerge()
            Return _composition.UpdateComposition(value, selectionStart, selectionLength)
        End Function

        Friend Function CancelComposition() As Boolean
            _undo.BreakMerge()
            Return _composition.CancelComposition()
        End Function

        Friend Function CommitComposition(Optional committedText As String = Nothing) As Boolean
            Dim textToCommit As String = _composition.TakeCommitText(committedText)
            Dim hadComposition As Boolean = _composition.IsActive

            If hadComposition Then
                _composition.CancelComposition()
            End If

            _undo.BreakMerge()

            If String.IsNullOrEmpty(textToCommit) Then
                Return hadComposition
            End If

            Return InsertText(textToCommit)
        End Function

        Friend Function ProcessComposition(command As MASTextCompositionCommand) As Boolean
            If command Is Nothing Then Return False

            Select Case command.Action
                Case MASTextCompositionAction.Begin
                    Return BeginComposition(command.Text, command.SelectionStart, command.SelectionLength)

                Case MASTextCompositionAction.Update
                    Return UpdateComposition(command.Text, command.SelectionStart, command.SelectionLength)

                Case MASTextCompositionAction.Commit
                    Return CommitComposition(command.Text)

                Case MASTextCompositionAction.Cancel
                    Return CancelComposition()
            End Select

            Return False
        End Function

#End Region

#Region "Scroll"

        Friend Sub ResetScroll()
            _scroll.Reset()
        End Sub

        Friend Function EnsureCaretVisible(layoutEngine As MASTextInputLayoutEngine,
                                           contentR As SKRect,
                                           paint As SKPaint,
                                           isRtl As Boolean,
                                           displayText As String,
                                           Optional crispRendering As Boolean = True) As Boolean

            Return _scroll.EnsureCaretVisible(
                layoutEngine:=layoutEngine,
                contentR:=contentR,
                paint:=paint,
                caretIndex:=_state.CaretIndex,
                isRtl:=isRtl,
                displayText:=displayText,
                crispRendering:=crispRendering)
        End Function

        Friend Function EnsureScrollInRange(layoutEngine As MASTextInputLayoutEngine,
                                            contentR As SKRect,
                                            paint As SKPaint,
                                            isRtl As Boolean,
                                            displayText As String) As Boolean

            Return _scroll.EnsureInRange(
                layoutEngine:=layoutEngine,
                contentR:=contentR,
                paint:=paint,
                isRtl:=isRtl,
                displayText:=displayText)
        End Function

#End Region

    End Class

End Namespace
