///|
priv struct TextEditResult {
  text : String
  caret : Int
  selection : TextRange?
}

///|
pub(all) struct TextCommandResult {
  handled : Bool
  changed : Bool
  clipboard_text : String?
} derive(Eq, Debug, ToJson)

///|
priv enum TextCaretDirection {
  MoveLeft
  MoveRight
  MoveStart
  MoveEnd
}

///|
priv struct TextCaretMoveResult {
  caret : Int
  selection : TextRange?
}

///|
fn apply_text_edit(
  element : ElementNode,
  data : TextFieldViewSpec,
  edit : TextEditResult,
) -> ElementEventResult {
  apply_text_edit_with_history(element, data, edit, record_history=true)
}

///|
fn apply_text_edit_with_history(
  element : ElementNode,
  data : TextFieldViewSpec,
  edit : TextEditResult,
  record_history~ : Bool,
) -> ElementEventResult {
  let previous = data.value.get()
  let previous_entry = TextHistoryEntry::from_element(previous, element)
  if edit.text != data.value.get() ||
    edit.caret != element.control.caret ||
    edit.selection != element.control.selection ||
    element.control.composition is Some(_) {
    if record_history {
      record_text_history(element, previous_entry, edit.text)
    }
    data.value.set(edit.text)
    element.control.caret = edit.caret
    element.control.selection = edit.selection
    element.control.composition = None
    element.control.composition_cursor = None
    element.dirty.mark_paint()
    { changed: true, activated: false, focused: None }
  } else {
    { changed: false, activated: false, focused: None }
  }
}

///|
fn apply_text_caret_move(
  element : ElementNode,
  caret_move : TextCaretMoveResult,
) -> ElementEventResult {
  let changed = element.control.caret != caret_move.caret ||
    element.control.selection != caret_move.selection ||
    element.control.composition is Some(_) ||
    element.control.composition_cursor is Some(_)
  if changed {
    element.control.caret = caret_move.caret
    element.control.selection = caret_move.selection
    element.control.composition = None
    element.control.composition_cursor = None
    element.dirty.mark_paint()
    { changed: true, activated: false, focused: None }
  } else {
    { changed: false, activated: false, focused: None }
  }
}

///|
fn apply_multiline_text_edit(
  element : ElementNode,
  data : RichTextEditorViewSpec,
  edit : TextEditResult,
) -> ElementEventResult {
  apply_multiline_text_edit_with_history(
    element,
    data,
    edit,
    record_history=true,
  )
}

///|
fn apply_multiline_text_edit_with_history(
  element : ElementNode,
  data : RichTextEditorViewSpec,
  edit : TextEditResult,
  record_history~ : Bool,
) -> ElementEventResult {
  let previous = data.value.get()
  let previous_entry = TextHistoryEntry::from_element(previous, element)
  if edit.text != data.value.get() ||
    edit.caret != element.control.caret ||
    edit.selection != element.control.selection ||
    element.control.composition is Some(_) ||
    element.control.composition_cursor is Some(_) {
    if record_history {
      record_text_history(element, previous_entry, edit.text)
    }
    data.value.set(edit.text)
    element.control.caret = edit.caret
    element.control.selection = edit.selection
    element.control.composition = None
    element.control.composition_cursor = None
    notify_rich_text_selection_change(data, element)
    element.dirty.mark_paint()
    { changed: true, activated: false, focused: None }
  } else {
    { changed: false, activated: false, focused: None }
  }
}

///|
fn apply_searchbar_edit(
  element : ElementNode,
  data : SearchBarViewSpec,
  edit : TextEditResult,
) -> ElementEventResult {
  apply_searchbar_edit_with_history(element, data, edit, record_history=true)
}

///|
fn apply_searchbar_edit_with_history(
  element : ElementNode,
  data : SearchBarViewSpec,
  edit : TextEditResult,
  record_history~ : Bool,
) -> ElementEventResult {
  let previous = data.query.get()
  let previous_entry = TextHistoryEntry::from_element(previous, element)
  if edit.text != data.query.get() ||
    edit.caret != element.control.caret ||
    edit.selection != element.control.selection ||
    element.control.composition is Some(_) ||
    element.control.composition_cursor is Some(_) {
    if record_history {
      record_text_history(element, previous_entry, edit.text)
    }
    data.query.set(edit.text)
    element.control.caret = edit.caret
    element.control.selection = edit.selection
    element.control.composition = None
    element.control.composition_cursor = None
    element.dirty.mark_paint()
    { changed: true, activated: false, focused: None }
  } else {
    { changed: false, activated: false, focused: None }
  }
}

///|
fn TextCommandResult::ignored() -> TextCommandResult {
  { handled: false, changed: false, clipboard_text: None }
}

///|
fn TextCommandResult::handled(
  changed? : Bool = false,
  clipboard_text? : String? = None,
) -> TextCommandResult {
  { handled: true, changed, clipboard_text }
}

///|
fn record_text_history(
  element : ElementNode,
  previous : TextHistoryEntry,
  next : String,
) -> Unit {
  if previous.text != next {
    element.control.text_undo_stack.push(previous)
    element.control.text_redo_stack.clear()
  }
}

///|
fn selected_text(text : String, selection : TextRange?) -> String {
  match active_text_selection(selection) {
    Some(selection) => {
      let chars = text.to_array()
      let start = clamp_int(selection.start, 0, chars.length())
      let end = clamp_int(selection.end, start, chars.length())
      String::from_array(chars[start:end])
    }
    None => ""
  }
}

///|
fn selected_rich_text(
  text : String,
  document : RichTextDocument,
  selection : TextRange?,
) -> String {
  match active_text_selection(selection) {
    Some(selection) =>
      if rich_text_document_has_source_ranges(document) {
        selected_rich_text_document_text(document, selection)
      } else {
        selected_text(text, Some(selection))
      }
    None => ""
  }
}

///|
fn selected_rich_text_document_text(
  document : RichTextDocument,
  selection : TextRange,
) -> String {
  let lines : Array[String] = []
  for block in document.blocks {
    match selected_rich_text_block_text(block, selection) {
      Some(text) => lines.push(text)
      None => ()
    }
  }
  join_text_lines(lines)
}

///|
fn selected_rich_text_block_text(
  block : RichTextBlock,
  selection : TextRange,
) -> String? {
  match block.content_range {
    Some(content) =>
      if selection.end <= content.start || selection.start >= content.end {
        None
      } else {
        let start = clamp_int(selection.start, content.start, content.end)
        let end = clamp_int(selection.end, content.start, content.end)
        let text = selected_rich_text_runs_text(block, start, end)
        if text == "" && start != end {
          None
        } else {
          Some(text)
        }
      }
    None => None
  }
}

///|
fn selected_rich_text_runs_text(
  block : RichTextBlock,
  start : Int,
  end : Int,
) -> String {
  let parts : Array[String] = []
  for run in block.runs {
    match run.source_range {
      Some(range) =>
        if end > range.start && start < range.end {
          parts.push(
            rich_text_run_slice(
              run.text,
              clamp_int(start - range.start, 0, text_length(run.text)),
              clamp_int(end - range.start, 0, text_length(run.text)),
            ),
          )
        }
      None => ()
    }
  }
  join_text_lines(parts)
}

///|
fn rich_text_run_slice(text : String, start : Int, end : Int) -> String {
  let chars = text.to_array()
  let start = clamp_int(start, 0, chars.length())
  let end = clamp_int(end, start, chars.length())
  String::from_array(chars[start:end])
}

///|
fn join_text_lines(lines : Array[String]) -> String {
  let chars : Array[Char] = []
  for index in 0.. 0 {
      chars.push('\n')
    }
    chars.append(lines[index].to_array())
  }
  String::from_array(chars)
}

///|
fn restore_text_history(
  element : ElementNode,
  current : String,
  apply : (TextEditResult) -> ElementEventResult,
  undo~ : Bool,
) -> TextCommandResult {
  let source = if undo {
    element.control.text_undo_stack
  } else {
    element.control.text_redo_stack
  }
  if source.length() == 0 {
    return TextCommandResult::handled()
  }
  let restored = source[source.length() - 1]
  ignore(source.pop())
  let current_entry = TextHistoryEntry::from_element(current, element)
  let result = apply({
    text: restored.text,
    caret: restored.caret,
    selection: restored.selection,
  })
  if result.changed {
    if undo {
      element.control.text_redo_stack.push(current_entry)
    } else {
      element.control.text_undo_stack.push(current_entry)
    }
  }
  TextCommandResult::handled(changed=result.changed)
}

///|
fn replace_current_selection(
  text : String,
  caret : Int,
  selection : TextRange?,
  replacement : String,
) -> TextEditResult {
  let range = match selection {
    Some(selection) => selection
    None => TextRange::collapsed(caret)
  }
  replace_text_range(text, range, replacement)
}

///|
fn replace_text_range(
  text : String,
  range : TextRange,
  replacement : String,
) -> TextEditResult {
  let chars = text.to_array()
  let normalized = range.normalized()
  let start = clamp_int(normalized.start, 0, chars.length())
  let end = clamp_int(normalized.end, start, chars.length())
  let replacement_chars = replacement.to_array()
  {
    text: String::from_array(chars[:start] + replacement_chars + chars[end:]),
    caret: start + replacement_chars.length(),
    selection: None,
  }
}

///|
fn active_text_selection(selection : TextRange?) -> TextRange? {
  match selection {
    Some(selection) => {
      let normalized = selection.normalized()
      if normalized.is_collapsed() {
        None
      } else {
        Some(normalized)
      }
    }
    None => None
  }
}

///|
fn text_selection_anchor(caret : Int, selection : TextRange?) -> Int {
  match selection {
    Some(selection) =>
      if caret == selection.start {
        selection.end
      } else {
        selection.start
      }
    None => caret
  }
}

///|
fn move_text_caret(
  text : String,
  caret : Int,
  selection : TextRange?,
  direction : TextCaretDirection,
  extend~ : Bool,
) -> TextCaretMoveResult {
  let length = text_length(text)
  let caret = clamp_int(caret, 0, length)
  if extend {
    let anchor = clamp_int(text_selection_anchor(caret, selection), 0, length)
    let next = move_text_caret_index(caret, length, direction)
    {
      caret: next,
      selection: if next == anchor {
        None
      } else {
        Some(TextRange::new(start=anchor, end=next))
      },
    }
  } else {
    match active_text_selection(selection) {
      Some(selection) =>
        match direction {
          MoveLeft => { caret: selection.start, selection: None }
          MoveRight => { caret: selection.end, selection: None }
          MoveStart => { caret: 0, selection: None }
          MoveEnd => { caret: length, selection: None }
        }
      None =>
        {
          caret: move_text_caret_index(caret, length, direction),
          selection: None,
        }
    }
  }
}

///|
fn move_text_caret_index(
  caret : Int,
  length : Int,
  direction : TextCaretDirection,
) -> Int {
  match direction {
    MoveLeft => clamp_int(caret - 1, 0, length)
    MoveRight => clamp_int(caret + 1, 0, length)
    MoveStart => 0
    MoveEnd => length
  }
}

///|
fn surrounding_delete_range(
  text : String,
  caret : Int,
  before : Int,
  after : Int,
) -> TextRange {
  let length = text_length(text)
  let caret = clamp_int(caret, 0, length)
  let before = max_int(0, before)
  let after = max_int(0, after)
  TextRange::new(
    start=max_int(0, caret - before),
    end=min_int(length, caret + after),
  )
}

///|
fn text_with_composition(
  text : String,
  element : ElementNode,
  focused : Bool,
) -> String {
  if !focused {
    text
  } else {
    match element.control.composition {
      Some(composition) =>
        replace_current_selection(
          text,
          element.control.caret,
          element.control.selection,
          composition,
        ).text
      None => text
    }
  }
}

///|
fn text_field_visual_caret(
  text : String,
  element : ElementNode,
  focused : Bool,
) -> Int {
  let caret = clamp_int(element.control.caret, 0, text_length(text))
  if !focused {
    caret
  } else {
    match element.control.composition {
      Some(composition) =>
        composition_start_index(
          text,
          element.control.caret,
          element.control.selection,
        ) +
        composition_cursor_offset(element, composition)
      None => caret
    }
  }
}

///|
fn composition_start_index(
  text : String,
  caret : Int,
  selection : TextRange?,
) -> Int {
  match active_text_selection(selection) {
    Some(selection) => selection.start
    None => clamp_int(caret, 0, text_length(text))
  }
}

///|
fn composition_cursor_offset(
  element : ElementNode,
  composition : String,
) -> Int {
  let length = text_length(composition)
  match element.control.composition_cursor {
    Some(cursor) => {
      let normalized = cursor.normalized()
      clamp_int(normalized.end, 0, length)
    }
    None => length
  }
}

///|
fn text_length(text : String) -> Int {
  text.to_array().length()
}

///|
fn multiline_line_start(text : String, caret : Int) -> Int {
  let chars = text.to_array()
  let mut index = clamp_int(caret, 0, chars.length())
  while index > 0 && chars[index - 1] != '\n' {
    index = index - 1
  }
  index
}

///|
fn multiline_line_end(text : String, caret : Int) -> Int {
  let chars = text.to_array()
  let mut index = clamp_int(caret, 0, chars.length())
  while index < chars.length() && chars[index] != '\n' {
    index = index + 1
  }
  index
}

///|
fn move_multiline_caret_vertical(
  element : ElementNode,
  data : RichTextEditorViewSpec,
  direction : Int,
) -> ElementEventResult {
  let text = data.value.get()
  let chars = text.to_array()
  let current_start = multiline_line_start(text, element.control.caret)
  let current_end = multiline_line_end(text, element.control.caret)
  let column = element.control.caret - current_start
  let mut next_start = current_start
  let mut next_end = current_end
  if direction < 0 {
    if current_start == 0 {
      return { changed: false, activated: false, focused: None }
    }
    next_end = current_start - 1
    next_start = multiline_line_start(text, next_end)
  } else {
    if current_end >= chars.length() {
      return { changed: false, activated: false, focused: None }
    }
    next_start = current_end + 1
    next_end = multiline_line_end(text, next_start)
  }
  element.control.caret = min_int(next_start + column, next_end)
  element.control.selection = None
  element.control.composition = None
  element.control.composition_cursor = None
  notify_rich_text_selection_change(data, element)
  element.dirty.mark_paint()
  { changed: true, activated: false, focused: None }
}

///|
fn apply_rich_text_caret_move(
  element : ElementNode,
  data : RichTextEditorViewSpec,
  caret_move : TextCaretMoveResult,
) -> ElementEventResult {
  let result = apply_text_caret_move(element, caret_move)
  if result.changed {
    notify_rich_text_selection_change(data, element)
  }
  result
}

///|
fn notify_rich_text_selection_change(
  data : RichTextEditorViewSpec,
  element : ElementNode,
) -> Unit {
  match data.on_selection_change {
    Some(handler) => handler(element.control.caret, element.control.selection)
    None => ()
  }
}

///|
fn handle_rich_text_editor_input(
  element : ElementNode,
  data : RichTextEditorViewSpec,
  event : TextInputEvent,
) -> ElementEventResult {
  match event {
    InsertText(text) => {
      if text == "" {
        return { changed: false, activated: false, focused: None }
      }
      match rich_text_transform_insert(data, element, text) {
        Some(edit) =>
          return apply_rich_text_input_transform(element, data, edit)
        None => ()
      }
      let edit = replace_current_selection(
        data.value.get(),
        element.control.caret,
        element.control.selection,
        text,
      )
      apply_multiline_text_edit(element, data, edit)
    }
    CompositionStart => {
      element.control.composition = Some("")
      element.control.composition_cursor = None
      element.dirty.mark_paint()
      { changed: true, activated: false, focused: None }
    }
    CompositionUpdate(update) => {
      element.control.composition = if update.text == "" {
        None
      } else {
        Some(update.text)
      }
      match update.cursor {
        Some(cursor) => {
          let normalized = cursor.normalized()
          let start = clamp_int(normalized.start, 0, text_length(update.text))
          let end = clamp_int(normalized.end, 0, text_length(update.text))
          element.control.composition_cursor = Some(
            TextRange::new(start~, end~),
          )
        }
        None => element.control.composition_cursor = None
      }
      element.dirty.mark_paint()
      { changed: true, activated: false, focused: None }
    }
    CompositionEnd(text) => {
      element.control.composition = None
      element.control.composition_cursor = None
      if text == "" {
        element.control.selection = None
        element.dirty.mark_paint()
        { changed: true, activated: false, focused: None }
      } else {
        let edit = replace_current_selection(
          data.value.get(),
          element.control.caret,
          element.control.selection,
          text,
        )
        apply_multiline_text_edit(element, data, edit)
      }
    }
    DeleteRange(range) => {
      match rich_text_transform_insert(data, element, "\u{000e}") {
        Some(edit) =>
          return apply_rich_text_input_transform(element, data, edit)
        None => ()
      }
      let edit = replace_text_range(data.value.get(), range, "")
      apply_multiline_text_edit(element, data, edit)
    }
    DeleteSurrounding(before, after) => {
      let text = data.value.get()
      let signal = if before > 0 && after == 0 {
        "\u{0008}"
      } else if before == 0 && after > 0 {
        "\u{007f}"
      } else {
        ""
      }
      if signal != "" {
        match rich_text_transform_insert(data, element, signal) {
          Some(edit) =>
            return apply_rich_text_input_transform(element, data, edit)
          None => ()
        }
      }
      let range = surrounding_delete_range(
        text,
        element.control.caret,
        before,
        after,
      )
      let edit = replace_text_range(text, range, "")
      apply_multiline_text_edit(element, data, edit)
    }
  }
}

///|
fn rich_text_transform_insert(
  data : RichTextEditorViewSpec,
  element : ElementNode,
  text : String,
) -> RichTextInputTransform? {
  match data.input_transform {
    Some(transform) =>
      transform(
        data.value.get(),
        element.control.caret,
        element.control.selection,
        text,
      )
    None => None
  }
}

///|
fn apply_rich_text_input_transform(
  element : ElementNode,
  data : RichTextEditorViewSpec,
  edit : RichTextInputTransform,
) -> ElementEventResult {
  let previous = data.value.get()
  let previous_entry = TextHistoryEntry::from_element(previous, element)
  let changed = edit.text != data.value.get() ||
    edit.caret != element.control.caret ||
    edit.selection != element.control.selection ||
    element.control.composition is Some(_) ||
    element.control.composition_cursor is Some(_)
  if changed {
    record_text_history(element, previous_entry, edit.text)
    data.value.set(edit.text)
    element.control.caret = edit.caret
    element.control.selection = edit.selection
    element.control.composition = None
    element.control.composition_cursor = None
    notify_rich_text_selection_change(data, element)
    element.dirty.mark_paint()
    { changed: true, activated: false, focused: None }
  } else {
    { changed: false, activated: false, focused: None }
  }
}

///|
fn ElementNode::dispatch_text_command(
  self : ElementNode,
  target : ElementId,
  intent : CommandIntent,
  paste_text? : String? = None,
) -> TextCommandResult {
  if self.spec is ModifiedSpec({ modifier: DisabledModifier(true), .. }) {
    return TextCommandResult::ignored()
  }
  if self.id == target {
    match self.spec {
      TextFieldSpec(data) =>
        handle_text_field_command(self, data, intent, paste_text~)
      SearchBarSpec(data) =>
        handle_searchbar_command(self, data, intent, paste_text~)
      RichTextEditorSpec(data) =>
        handle_rich_text_editor_command(self, data, intent, paste_text~)
      _ => TextCommandResult::ignored()
    }
  } else {
    for child in self.children {
      let result = child.dispatch_text_command(target, intent, paste_text~)
      if result.handled {
        if result.changed {
          self.dirty.mark_paint()
        }
        return result
      }
    }
    TextCommandResult::ignored()
  }
}

///|
fn handle_text_field_command(
  element : ElementNode,
  data : TextFieldViewSpec,
  intent : CommandIntent,
  paste_text~ : String?,
) -> TextCommandResult {
  let text = data.value.get()
  match intent {
    Copy =>
      TextCommandResult::handled(
        clipboard_text=Some(selected_text(text, element.control.selection)),
      )
    Cut => {
      let copied = selected_text(text, element.control.selection)
      match active_text_selection(element.control.selection) {
        Some(selection) => {
          let result = handle_text_field_input(
            element,
            data,
            DeleteRange(selection),
          )
          TextCommandResult::handled(
            changed=result.changed,
            clipboard_text=Some(copied),
          )
        }
        None => TextCommandResult::handled(clipboard_text=Some(""))
      }
    }
    Paste =>
      match paste_text {
        Some(value) => {
          let result = handle_text_field_input(element, data, InsertText(value))
          TextCommandResult::handled(changed=result.changed)
        }
        None => TextCommandResult::handled()
      }
    Undo =>
      restore_text_history(
        element,
        text,
        edit => {
          apply_text_edit_with_history(
            element,
            data,
            edit,
            record_history=false,
          )
        },
        undo=true,
      )
    Redo =>
      restore_text_history(
        element,
        text,
        edit => {
          apply_text_edit_with_history(
            element,
            data,
            edit,
            record_history=false,
          )
        },
        undo=false,
      )
    SelectAll => {
      let result = select_text_range(element, text)
      TextCommandResult::handled(changed=result.changed)
    }
    _ => TextCommandResult::ignored()
  }
}

///|
fn handle_searchbar_command(
  element : ElementNode,
  data : SearchBarViewSpec,
  intent : CommandIntent,
  paste_text~ : String?,
) -> TextCommandResult {
  let text = data.query.get()
  match intent {
    Copy =>
      TextCommandResult::handled(
        clipboard_text=Some(selected_text(text, element.control.selection)),
      )
    Cut => {
      let copied = selected_text(text, element.control.selection)
      match active_text_selection(element.control.selection) {
        Some(selection) => {
          let result = handle_searchbar_input(
            element,
            data,
            DeleteRange(selection),
          )
          TextCommandResult::handled(
            changed=result.changed,
            clipboard_text=Some(copied),
          )
        }
        None => TextCommandResult::handled(clipboard_text=Some(""))
      }
    }
    Paste =>
      match paste_text {
        Some(value) => {
          let result = handle_searchbar_input(element, data, InsertText(value))
          TextCommandResult::handled(changed=result.changed)
        }
        None => TextCommandResult::handled()
      }
    Undo =>
      restore_text_history(
        element,
        text,
        edit => {
          apply_searchbar_edit_with_history(
            element,
            data,
            edit,
            record_history=false,
          )
        },
        undo=true,
      )
    Redo =>
      restore_text_history(
        element,
        text,
        edit => {
          apply_searchbar_edit_with_history(
            element,
            data,
            edit,
            record_history=false,
          )
        },
        undo=false,
      )
    SelectAll => {
      let result = select_text_range(element, text)
      TextCommandResult::handled(changed=result.changed)
    }
    _ => TextCommandResult::ignored()
  }
}

///|
fn handle_rich_text_editor_command(
  element : ElementNode,
  data : RichTextEditorViewSpec,
  intent : CommandIntent,
  paste_text~ : String?,
) -> TextCommandResult {
  let text = data.value.get()
  let document = (data.format)(text)
  match intent {
    Copy =>
      TextCommandResult::handled(
        clipboard_text=Some(
          selected_rich_text(text, document, element.control.selection),
        ),
      )
    Cut => {
      let copied = selected_rich_text(text, document, element.control.selection)
      match active_text_selection(element.control.selection) {
        Some(selection) => {
          let result = handle_rich_text_editor_input(
            element,
            data,
            DeleteRange(selection),
          )
          TextCommandResult::handled(
            changed=result.changed,
            clipboard_text=Some(copied),
          )
        }
        None => TextCommandResult::handled(clipboard_text=Some(""))
      }
    }
    Paste =>
      match paste_text {
        Some(value) => {
          let result = handle_rich_text_editor_input(
            element,
            data,
            InsertText(value),
          )
          TextCommandResult::handled(changed=result.changed)
        }
        None => TextCommandResult::handled()
      }
    Undo =>
      restore_text_history(
        element,
        text,
        edit => {
          apply_multiline_text_edit_with_history(
            element,
            data,
            edit,
            record_history=false,
          )
        },
        undo=true,
      )
    Redo =>
      restore_text_history(
        element,
        text,
        edit => {
          apply_multiline_text_edit_with_history(
            element,
            data,
            edit,
            record_history=false,
          )
        },
        undo=false,
      )
    SelectAll => {
      let result = select_text_range(element, text)
      TextCommandResult::handled(changed=result.changed)
    }
    _ => TextCommandResult::ignored()
  }
}