///|
/// One undo/redo step as a delta: replacing `before` with `after` at
/// `start` (character indices in the pre-edit text) produced the new
/// document; undoing re-applies the replacement backwards. Entries
/// stay the size of the edit instead of the document, so keeping the
/// whole undo stack alive — and copying it per frame in the element
/// tree — costs proportional to user keystrokes, not document size.
///
/// `caret`/`selection` restore the editor position to what it was
/// before the edit when the entry is undone. The entry is symmetric:
/// moving it onto the opposite stack (the same delta with the live
/// position written into `caret`/`selection`) is all a redo step needs,
/// because `before` and `after` already describe both directions of
/// the edit.
pub(all) struct TextHistoryEntryContext {
  start : Int
  before : String
  after : String
  caret : Int
  selection : TextRange?
} derive(Eq, Debug, ToJson)

///|
pub extend TextHistoryEntryContext with Debug::{to_repr}

///|
pub extend TextHistoryEntryContext with Eq::{not_equal, equal}

///|
pub extend TextHistoryEntryContext with ToJson::{to_json}

///|
/// Current text -> text with the entry's edit applied forward (redo).
pub fn TextHistoryEntryContext::redo_text(
  self : TextHistoryEntryContext,
  text : String,
) -> String {
  text_history_apply_delta(text, self.start, self.after, self.before.length())
}

///|
/// Current text -> text with the entry's edit reverted (undo).
pub fn TextHistoryEntryContext::undo_text(
  self : TextHistoryEntryContext,
  text : String,
) -> String {
  text_history_apply_delta(text, self.start, self.before, self.after.length())
}

///|
/// Minimal prefix/suffix-anchored delta `(start, before, after)`
/// between two texts. Used by edit paths that only observe the before
/// and after documents; a single replacement stays exact, while a
/// whole-document swap degrades to the (still O(edit)) replacement of
/// the shared-affix-free middle.
pub fn text_history_delta(
  previous : String,
  next : String,
) -> (Int, String, String) {
  let before = previous.to_array()
  let after = next.to_array()
  let mut prefix = 0
  while prefix < before.length() &&
        prefix < after.length() &&
        before[prefix] == after[prefix] {
    prefix = prefix + 1
  }
  let mut suffix = 0
  while suffix < before.length() - prefix &&
        suffix < after.length() - prefix &&
        before[before.length() - 1 - suffix] ==
        after[after.length() - 1 - suffix] {
    suffix = suffix + 1
  }
  (
    prefix,
    String::from_array(before[prefix:before.length() - suffix]),
    String::from_array(after[prefix:after.length() - suffix]),
  )
}

///|
fn text_history_apply_delta(
  text : String,
  start : Int,
  replacement : String,
  removed_length : Int,
) -> String {
  let chars = text.to_array()
  let start = start.clamp(min=0, max=chars.length())
  let end = (start + removed_length).clamp(min=start, max=chars.length())
  String::from_array(chars[:start]) +
  replacement +
  String::from_array(chars[end:])
}

///|
test "delta entries apply and revert at their recorded position" {
  let entry : TextHistoryEntryContext = {
    start: 2,
    before: "cd",
    after: "XYZ",
    caret: 2,
    selection: None,
  }
  assert_eq(entry.redo_text("abcdefg"), "abXYZefg")
  assert_eq(entry.undo_text("abXYZefg"), "abcdefg")
}

///|
test "delta application clamps against out-of-range documents" {
  let entry : TextHistoryEntryContext = {
    start: 99,
    before: "x",
    after: "yz",
    caret: 0,
    selection: None,
  }
  assert_eq(entry.undo_text("ab"), "abx")
  assert_eq(entry.redo_text("ab"), "abyz")
}

///|
test "one delta serves both stacks across an undo-redo round trip" {
  let entry : TextHistoryEntryContext = {
    start: 0,
    before: "",
    after: "hello",
    caret: 0,
    selection: None,
  }
  // Undo reverts the typed word; the redo stack receives the same
  // delta with the position the undo is leaving behind, and redoing
  // it re-applies the forward edit exactly.
  let after_undo = entry.undo_text("hello")
  assert_eq(after_undo, "")
  let redo_entry = { ..entry, caret: 0, }
  assert_eq(redo_entry.redo_text(after_undo), "hello")
  assert_eq(redo_entry.undo_text("hello"), after_undo)
}

///|
test "prefix-suffix delta captures exactly the edited middle" {
  assert_eq(
    text_history_delta("hello world", "hello big world"),
    (6, "", "big "),
  )
  // The greedy trim may tie-align the shared "t"; the split is chosen
  // for correctness of the round trip, not for aesthetics.
  assert_eq(text_history_delta("one two three", "one three"), (5, "wo t", ""))
  assert_eq(text_history_delta("", "abc"), (0, "", "abc"))
  assert_eq(text_history_delta("abc", ""), (0, "abc", ""))
  // Shared prefix and suffix must not overlap into a negative span.
  assert_eq(text_history_delta("aa", "aaa"), (2, "", "a"))
}

///|
test "delta round-trips through the entry it feeds" {
  let previous = "const answer = 41"
  let next = "const answer = 42"
  let (start, before, after) = text_history_delta(previous, next)
  let entry : TextHistoryEntryContext = {
    start,
    before,
    after,
    caret: start + 19,
    selection: None,
  }
  assert_eq(entry.undo_text(next), previous)
  assert_eq(entry.redo_text(previous), next)
  assert_true(before.length() <= 1 && after.length() <= 1)
}