Option Strict On
Option Explicit On

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

Namespace Nexamas.UI.TextInput

    ' CONTRACT:
    ' MASTextUndoManager owns undo/redo history only.
    '
    ' It stores undoable operations as units:
    ' - Undo restores BeforeSnapshot.
    ' - Redo restores AfterSnapshot.
    '
    ' It must not know UI, renderer, layout, keyboard, mouse, or Skia.
    ' It may only capture and restore MASTextInputState snapshots.
    Friend NotInheritable Class MASTextUndoManager

        Private ReadOnly _undoStack As New Stack(Of MASTextUndoUnit)()
        Private ReadOnly _redoStack As New Stack(Of MASTextUndoUnit)()

        Private Const HISTORY_LIMIT As Integer = 256
        Private Const MERGE_TIMEOUT_MS As Integer = 900

        Private _mergeOpen As Boolean
        Private _lastEditKind As MASTextEditKind = MASTextEditKind.Programmatic
        Private _lastEditTick As Integer

        Friend ReadOnly Property CanUndo As Boolean
            Get
                Return _undoStack.Count > 0
            End Get
        End Property

        Friend ReadOnly Property CanRedo As Boolean
            Get
                Return _redoStack.Count > 0
            End Get
        End Property

        Friend ReadOnly Property UndoCount As Integer
            Get
                Return _undoStack.Count
            End Get
        End Property

        Friend ReadOnly Property RedoCount As Integer
            Get
                Return _redoStack.Count
            End Get
        End Property

        Friend Sub BreakMerge()
            _mergeOpen = False
            _lastEditKind = MASTextEditKind.Programmatic
            _lastEditTick = 0
        End Sub

        Friend Sub Clear()
            _undoStack.Clear()
            _redoStack.Clear()
            BreakMerge()
        End Sub

        Friend Function Execute(state As MASTextInputState,
                                kind As MASTextEditKind,
                                action As Func(Of Boolean)) As Boolean

            If state Is Nothing Then Return False
            If action Is Nothing Then Return False

            Dim before As MASTextInputSnapshot = CreateSnapshot(state)
            Dim beforeVersion As Integer = state.Version

            Dim changed As Boolean = action.Invoke()
            If Not changed Then Return False

            Dim afterSnap As MASTextInputSnapshot = CreateSnapshot(state)
            If before.Equals(afterSnap) Then Return False

            Debug.Assert(state.Version <> beforeVersion,
                         "Undo contract failed: undoable text operation changed snapshot without changing State.Version.")

            Dim merged As Boolean = TryMerge(kind, before, afterSnap)

            If Not merged Then
                PushUndoUnit(New MASTextUndoUnit(kind, before, afterSnap))
            End If

            _redoStack.Clear()
            _mergeOpen = CanContinueMerge(kind)
            _lastEditKind = kind
            _lastEditTick = Nexamas.UI.Timing.MASInteractionClock.TickCount()

            ValidateStacks()
            Return True
        End Function

        Friend Function Undo(state As MASTextInputState) As Boolean
            If state Is Nothing Then Return False
            If _undoStack.Count <= 0 Then Return False

            BreakMerge()

            Dim unit As MASTextUndoUnit = _undoStack.Pop()
            RestoreSnapshot(state, unit.BeforeSnapshot)
            _redoStack.Push(unit)

            ValidateStacks()
            Return True
        End Function

        Friend Function Redo(state As MASTextInputState) As Boolean
            If state Is Nothing Then Return False
            If _redoStack.Count <= 0 Then Return False

            BreakMerge()

            Dim unit As MASTextUndoUnit = _redoStack.Pop()
            RestoreSnapshot(state, unit.AfterSnapshot)
            _undoStack.Push(unit)

            ValidateStacks()
            Return True
        End Function

        Friend Function ExecuteProgrammatic(state As MASTextInputState,
                                            action As Func(Of Boolean)) As Boolean

            BreakMerge()
            Return Execute(state, MASTextEditKind.Programmatic, action)
        End Function

        ' Compatibility helper for older controller code.
        ' Prefer ExecuteProgrammatic for new code.
        Friend Sub PushProgrammaticSnapshot(state As MASTextInputState)
            If state Is Nothing Then Return

            BreakMerge()
            PushUndoUnit(New MASTextUndoUnit(
                kind:=MASTextEditKind.Programmatic,
                beforeSnapshot:=CreateSnapshot(state),
                afterSnapshot:=CreateSnapshot(state)))

            _redoStack.Clear()
        End Sub

        Private Function TryMerge(kind As MASTextEditKind,
                                  before As MASTextInputSnapshot,
                                  afterSnap As MASTextInputSnapshot) As Boolean

            If _undoStack.Count <= 0 Then Return False
            If Not ShouldMerge(kind, before, afterSnap) Then Return False

            Dim top As MASTextUndoUnit = _undoStack.Peek()
            top.AfterSnapshot = afterSnap
            top.LastMergedTick = Nexamas.UI.Timing.MASInteractionClock.TickCount()

            Return True
        End Function

        Private Function ShouldMerge(kind As MASTextEditKind,
                                     before As MASTextInputSnapshot,
                                     afterSnap As MASTextInputSnapshot) As Boolean

            If kind <> MASTextEditKind.Typing Then Return False
            If Not _mergeOpen Then Return False
            If _lastEditKind <> MASTextEditKind.Typing Then Return False
            If _undoStack.Count <= 0 Then Return False

            Dim nowTick As Integer = Nexamas.UI.Timing.MASInteractionClock.TickCount()
            Dim dt As Integer = nowTick - _lastEditTick
            If dt < 0 OrElse dt > MERGE_TIMEOUT_MS Then Return False

            Dim top As MASTextUndoUnit = _undoStack.Peek()
            If top.Kind <> MASTextEditKind.Typing Then Return False
            If top.AfterSnapshot Is Nothing Then Return False

            ' Merge only continuous simple typing.
            ' Selection replacement, paste, delete, and caret jumps intentionally break merge.
            If before.SelectionLength <> 0 Then Return False
            If top.AfterSnapshot.SelectionLength <> 0 Then Return False
            If Not top.AfterSnapshot.Equals(before) Then Return False
            If afterSnap.Text.Length <= before.Text.Length Then Return False
            If afterSnap.CaretIndex < before.CaretIndex Then Return False

            Return True
        End Function

        Private Shared Function CanContinueMerge(kind As MASTextEditKind) As Boolean
            Return kind = MASTextEditKind.Typing
        End Function

        Private Sub PushUndoUnit(unit As MASTextUndoUnit)
            If unit Is Nothing Then Return

            If unit.BeforeSnapshot.Equals(unit.AfterSnapshot) Then Return

            If _undoStack.Count > 0 Then
                Dim top As MASTextUndoUnit = _undoStack.Peek()
                If top.BeforeSnapshot.Equals(unit.BeforeSnapshot) AndAlso
                   top.AfterSnapshot.Equals(unit.AfterSnapshot) Then
                    Return
                End If
            End If

            _undoStack.Push(unit)
            TrimUndoStackIfNeeded()
        End Sub

        Private Sub TrimUndoStackIfNeeded()
            If _undoStack.Count <= HISTORY_LIMIT Then Return

            Dim arr As MASTextUndoUnit() = _undoStack.ToArray()
            _undoStack.Clear()

            Dim keep As Integer = Math.Min(HISTORY_LIMIT, arr.Length)
            For i As Integer = keep - 1 To 0 Step -1
                _undoStack.Push(arr(i))
            Next
        End Sub

        Private Shared Function CreateSnapshot(state As MASTextInputState) As MASTextInputSnapshot
            Return New MASTextInputSnapshot(
                text:=state.Text,
                caretIndex:=state.CaretIndex,
                selectionStart:=state.SelectionStart,
                selectionLength:=state.SelectionLength,
                selectionAnchor:=state.SelectionAnchor)
        End Function

        Private Shared Sub RestoreSnapshot(state As MASTextInputState,
                                           snapshot As MASTextInputSnapshot)
            If state Is Nothing OrElse snapshot Is Nothing Then Return
            state.ApplySnapshot(snapshot)
        End Sub

        <Conditional("DEBUG")>
        Private Sub ValidateStacks()
            Debug.Assert(_undoStack.Count <= HISTORY_LIMIT,
                         "Undo contract failed: undo stack exceeded history limit.")

            For Each unit As MASTextUndoUnit In _undoStack
                Debug.Assert(unit IsNot Nothing, "Undo contract failed: undo unit is Nothing.")
                If unit Is Nothing Then Continue For

                Debug.Assert(unit.BeforeSnapshot IsNot Nothing,
                             "Undo contract failed: before snapshot is Nothing.")
                Debug.Assert(unit.AfterSnapshot IsNot Nothing,
                             "Undo contract failed: after snapshot is Nothing.")
                Debug.Assert(Not unit.BeforeSnapshot.Equals(unit.AfterSnapshot),
                             "Undo contract failed: no-op undo unit was stored.")
            Next
        End Sub

    End Class

End Namespace
