///|
/// Undo-history merge window for the rich-text editors.
///
/// Consecutive single-character edits of the same kind at contiguous
/// positions keep the current undo group open: instead of skipping the
/// push and trusting the group's first entry to cover them, the merge
/// rewrites the stack-top delta so one undo still reverts the whole
/// run. Multi-character edits (pastes, IME commits), newline
/// insertions, edits behind an active selection, and edits after a
/// history restore always start a fresh group.
priv struct RichTextHistoryGroupState {
  mut kind : Int
  mut start : Int
  mut after_length : Int
  mut last_push_ms : Int64
  mut expected_stack_length : Int
}

///|
let rich_text_history_group : Ref[RichTextHistoryGroupState] = Ref({
  kind: 0,
  start: -1,
  after_length: -1,
  last_push_ms: -1L,
  expected_stack_length: -1,
})

///|
/// Optional fake millisecond clock for the merge window. A negative
/// value falls back to the real wall clock; tests install a fake tick
/// so grouping never depends on wall time.
let rich_text_history_fake_clock_ms : Ref[Int64] = Ref(-1L)

///|
/// Epoch milliseconds stay Int64 all the way through: truncating them
/// into a 32-bit Int flips the sign, which silently disabled the
/// `last_push_ms >= 0` unset sentinel and with it the whole merge
/// window.
fn rich_text_history_now_ms() -> Int64 {
  let fake = rich_text_history_fake_clock_ms.val
  if fake >= 0L {
    fake
  } else {
    @env.now().reinterpret_as_int64()
  }
}

///|
/// Consecutive edits landing within this many milliseconds of each
/// other extend the open undo group.
let rich_text_history_merge_window_ms : Int = 500

///|
/// What the caller must do with the undo stack for an accepted edit:
/// push a fresh entry, or merge — rewriting the stack-top delta with
/// `kind` and the single affected character (kind `1` appends it to
/// `after`, kinds `2`/`3` extend `before`, backspace at the group's
/// left edge also shifts `start`).
priv enum RichTextHistoryAction {
  PushHistory
  MergeHistory(Int, Char)
}

///|
/// Classify an accepted edit as a single-character text change, or
/// `None` when it is anything else. Kinds: `1` inserts `character` at
/// `start` (caret typed just before it), `2` backspaces `character` at
/// `start` (caret sat right after it), `3` forward-deletes `character`
/// at `start`. Newline changes never merge, so they report `None` and
/// force their own undo entry.
fn rich_text_history_single_edit(
  previous : String,
  next : String,
  caret : Int,
  selection_active~ : Bool,
) -> (Int, Int, Char)? {
  if selection_active {
    return None
  }
  // Codepoint-length gate plus an allocation-free aligned scan: while the
  // two documents agree their UTF-16 offsets advance in lockstep, so the
  // first divergence is the edit point and its character is one
  // `get_char` away. No char-array copy of either document.
  let before_length = text_length(previous)
  let after_length = text_length(next)
  let before_units = previous.length()
  let next_units = next.length()
  if before_length + 1 == after_length {
    let mut index = 0
    let mut offset = 0
    while index < before_length &&
          offset < before_units &&
          previous.get_char(offset) == next.get_char(offset) {
      match previous.get_char(offset) {
        Some(ch) => {
          offset = offset + ch.utf16_len()
          index = index + 1
        }
        None => break
      }
    }
    match next.get_char(offset) {
      Some(inserted) =>
        if inserted == '\n' || caret != index {
          None
        } else {
          Some((1, index, inserted))
        }
      None => None
    }
  } else if after_length + 1 == before_length {
    let mut index = 0
    let mut offset = 0
    while index < after_length &&
          offset < next_units &&
          previous.get_char(offset) == next.get_char(offset) {
      match next.get_char(offset) {
        Some(ch) => {
          offset = offset + ch.utf16_len()
          index = index + 1
        }
        None => break
      }
    }
    match previous.get_char(offset) {
      Some(deleted) =>
        if deleted == '\n' {
          None
        } else if caret == index + 1 {
          Some((2, index, deleted))
        } else if caret == index {
          Some((3, index, deleted))
        } else {
          None
        }
      None => None
    }
  } else {
    None
  }
}

///|
/// Position the next same-kind edit must land on to stay in the open
/// group: one past the previous insertion, one before the previous
/// backspace, or the same spot for a repeated forward delete.
fn rich_text_history_group_delta(kind : Int) -> Int {
  if kind == 1 {
    1
  } else if kind == 2 {
    -1
  } else {
    0
  }
}

///|
/// Decide what an accepted edit does to the undo stack, updating the
/// open-group bookkeeping either way. Merging requires the group's
/// kind, expected stack length, running text length, time window, and
/// contiguity to all line up, so an undo, redo, or foreign edit in
/// between breaks the merge even without an explicit reset.
fn rich_text_history_record_edit(
  control : @core.TextControlStateContext,
  previous : String,
  next : String,
  caret : Int,
  // Classification computed from the applied edit's known range. Absent
  // means "rescan the documents"; `Some(None)` means "known to be not a
  // single-character edit".
  shape? : (Int, Int, Char)?? = None,
) -> RichTextHistoryAction {
  let now = rich_text_history_now_ms()
  let selection_active = @core.active_text_selection(control.selection)
    is Some(_)
  let edit_shape = match shape {
    Some(known) => known
    None =>
      rich_text_history_single_edit(previous, next, caret, selection_active~)
  }
  let state = rich_text_history_group.val
  let stack_length = control.undo_stack.length()
  let merges = match edit_shape {
    Some((kind, start, _)) =>
      state.kind == kind &&
      stack_length == state.expected_stack_length &&
      text_length(previous) == state.after_length &&
      state.last_push_ms >= 0L &&
      state.last_push_ms <= now &&
      now - state.last_push_ms <= rich_text_history_merge_window_ms.to_int64() &&
      start == state.start + rich_text_history_group_delta(kind)
    None => false
  }
  if merges {
    match edit_shape {
      Some((kind, start, character)) => {
        state.start = start
        // A merged edit is a single codepoint insert or delete by
        // construction (`edit_shape_at`); stepping the running length by
        // that delta avoids decoding the whole new document here.
        state.after_length = state.after_length +
          rich_text_history_shape_char_delta(kind)
        state.last_push_ms = now
        MergeHistory(kind, character)
      }
      None => PushHistory
    }
  } else {
    match edit_shape {
      Some((kind, start, _)) => {
        state.kind = kind
        state.start = start
        state.after_length = text_length(previous) +
          rich_text_history_shape_char_delta(kind)
      }
      None => {
        state.kind = 0
        state.start = -1
        // Multi-codepoint edits (paste, replacements) are the only path
        // that still decodes the new document, and they are rare.
        state.after_length = text_length(next)
      }
    }
    state.last_push_ms = now
    state.expected_stack_length = stack_length + 1
    PushHistory
  }
}

///|
/// Codepoint delta of a single-edit shape: kind 1 inserts one codepoint,
/// kinds 2 (backspace) and 3 (forward delete) remove one.
fn rich_text_history_shape_char_delta(kind : Int) -> Int {
  if kind == 1 {
    1
  } else {
    -1
  }
}

///|
/// Apply a history action to the undo stack the caller is building.
/// Merging rewrites the group's stack-top delta in place so the stack
/// keeps one entry per run while still describing the whole run
/// exactly. The merge guards guarantee a non-empty stack whose top is
/// the group entry; an empty stack defensively takes the fresh entry.
fn rich_text_history_extend_stack(
  undo_stack : Array[@core.TextHistoryEntryContext],
  action : RichTextHistoryAction,
  entry : @core.TextHistoryEntryContext,
) -> Unit {
  match action {
    PushHistory => undo_stack.push(entry)
    MergeHistory(kind, character) =>
      if undo_stack.is_empty() {
        undo_stack.push(entry)
      } else {
        let top_index = undo_stack.length() - 1
        let top = undo_stack[top_index]
        let character_text = character.to_string()
        undo_stack[top_index] = if kind == 1 {
          { ..top, after: top.after + character_text, }
        } else if kind == 2 {
          { ..top, start: top.start - 1, before: character_text + top.before, }
        } else {
          { ..top, before: top.before + character_text, }
        }
      }
  }
}

///|
/// Forget the open undo group, so the next edit starts a new one.
/// Called after a history restore: undoing must not let the following
/// edit coalesce into the group that was just undone.
fn rich_text_history_reset_group() -> Unit {
  let state = rich_text_history_group.val
  state.kind = 0
  state.start = -1
  state.after_length = -1
  state.last_push_ms = -1
  state.expected_stack_length = -1
}