///|
using @ports {trait ClipboardPort, type CancelController}

///|
using @rendering {
  type FormattedString,
  type FormatSpan,
  type ConsoleFormat,
  type Screen,
  type Cell,
  wrap_text,
  get_string_width,
  render_diff,
}

///|
using @editing {
  type HistoryLog,
  type CompletionState,
  type OverloadState,
  type UndoManager,
  type Selection,
  type KillRing,
  type SearchState,
  type UndoRecord,
  type OverloadItem,
}

///|
priv struct PaneState {
  mut completion_state : CompletionState?
  mut selection : Selection?
  mut overload_state : OverloadState?
  mut search_state : SearchState?
}

///|
fn PaneState::new() -> PaneState {
  {
    completion_state: None,
    selection: None,
    overload_state: None,
    search_state: None,
  }
}

///|
struct Prompt {
  console : &ConsolePort
  clipboard : &ClipboardPort
  configuration : PromptConfiguration
  history : HistoryLog
  cancel_controller : CancelController
  mut pane_state : PaneState
  mut last_screen : Screen?
  mut render_anchor : (Int, Int)?
  undo_manager : UndoManager
  kill_ring : KillRing
}

///|
pub async fn Prompt::new(
  console : &ConsolePort,
  clipboard : &ClipboardPort,
  configuration? : PromptConfiguration = PromptConfiguration::default(),
) -> Prompt {
  let cancel_controller = CancelController::new(console)
  let history = HistoryLog::new(configuration.history)
  history.load_persistent_history()
  {
    console,
    clipboard,
    configuration,
    history,
    cancel_controller,
    pane_state: PaneState::new(),
    last_screen: None,
    render_anchor: None,
    undo_manager: UndoManager::new("", 0),
    kill_ring: KillRing::new(max_size=configuration.kill_ring_max_size),
  }
}

///|
pub async fn Prompt::read_line(self : Prompt) -> PromptResult {
  self.read_line_with_prompt(self.configuration.prompt)
}

///|
pub async fn Prompt::read_line_with_prompt(
  self : Prompt,
  prompt : FormattedString,
) -> PromptResult {
  self.cancel_controller.set_mode(CaptureAsInput)
  self.last_screen = None
  self.render_anchor = None
  self.reserve_space_for_helper_panes()
  let state = InputState::new()
  let key_bindings = self.configuration.key_bindings
  self.render_line(prompt, state.left, state.right)

  while true {
    if self.cancel_controller.take_cancellation_requested() {
      self.console.write_line("")
      self.cancel_controller.set_mode(AllowCancelSignal)
      return cancel_result()
    }

    let key_presses = read_key_presses(self.console)
    for key_press in key_presses {
      let result = self.process_key_press(
        prompt, state, key_bindings, key_press,
      )
      match result {
        Handled => continue
        NotHandled => ()
        Return(value) => return value
      }
    }
  }
  cancel_result()
}

///|
fn Prompt::reserve_space_for_helper_panes(self : Prompt) -> Unit {
  let desired_helper_rows = self.configuration.max_completion_items_count + 3
  let cursor_in_window = self.console.get_cursor_top() -
    self.console.get_window_top()
  let rows_below_cursor = self.console.get_window_height() -
    (cursor_in_window + 1)
  let extra_rows = desired_helper_rows - rows_below_cursor
  if extra_rows <= 0 {
    return
  }

  self.console.write(
    "\n".repeat(extra_rows) +
    "\u{1b}[\{extra_rows}A" +
    "\u{1b}[1G" +
    "\u{1b}[0m",
  )
}

///|
async fn Prompt::process_key_press(
  self : Prompt,
  prompt : FormattedString,
  state : InputState,
  key_bindings : KeyBindings,
  key_press : KeyPress,
) -> KeyProcessingResult {
  let transformed = (self.configuration.callbacks.transform_key_press)(
    KeyTransformContext::new(
      self.snapshot_of(state.left, state.right),
      key_press,
    ),
  )
  let info = transformed.console_key_info
  if key_bindings.cancel_prompt.matches(info) || is_control_c_key(info) {
    return Return(self.cancel_prompt(info))
  }

  if transformed.pasted_text is Some(pasted) {
    self.pane_state.completion_state = None
    self.pane_state.overload_state = None
    self.pane_state.search_state = None
    self.delete_selection(state.left, state.right)
    state.left.push_iter(pasted.iter())
    self.apply_format_input(state, transformed)
    self.track_undo(state.left, state.right)
    state.request_render()
    state.last_was_yank = false
    return self.commit_requested_render(prompt, state)
  }

  let result = self.try_handle_key_press_callbacks(state, info)
  if result is NotHandled {
    self.run_pane_pipeline(
      prompt,
      state,
      key_bindings,
      transformed,
      default_pane_pipeline(),
    )
  } else {
    result
  }
}

///|
async fn Prompt::try_handle_key_press_callbacks(
  self : Prompt,
  state : InputState,
  info : @console.ConsoleKeyInfo,
) -> KeyProcessingResult {
  let callbacks = self.configuration.callbacks.key_press_callbacks
  for callback in callbacks {
    if callback.pattern.matches(info) {
      let snapshot = self.snapshot_of(state.left, state.right)
      let result = (callback.callback)(snapshot)
      match result {
        KeyPressCallbackResult::StayOnPrompt => return Handled
        KeyPressCallbackResult::SubmitPrompt(text~) =>
          return Return(PromptResult::submitted(text, info))
      }
    }
  }
  NotHandled
}

///|
fn Prompt::cancel_prompt(
  self : Prompt,
  info : @console.ConsoleKeyInfo,
) -> PromptResult {
  self.run_cancel_prompt_handler(info)
}

///|
fn Prompt::snapshot_of(
  self : Prompt,
  left : Array[Char],
  right : Array[Char],
) -> PromptSnapshot {
  ignore(self)
  PromptSnapshot::new(line_from_buffers(left, right), left.length())
}

///|
fn Prompt::apply_snapshot(
  self : Prompt,
  state : InputState,
  snapshot : PromptSnapshot,
) -> Unit {
  self.set_line_buffers(state.left, state.right, snapshot.text)
  while state.left.length() > snapshot.caret {
    if state.left.pop() is Some(ch) {
      state.right.push(ch)
    }
  }
}

///|
fn Prompt::normalize_completion_span(
  self : Prompt,
  snapshot : PromptSnapshot,
  span : CompletionSpan,
) -> CompletionSpan {
  ignore(self)
  let start = if span.start < 0 {
    0
  } else if span.start > snapshot.text.length() {
    snapshot.text.length()
  } else {
    span.start
  }
  let end = if span.end() < start {
    start
  } else if span.end() > snapshot.text.length() {
    snapshot.text.length()
  } else {
    span.end()
  }
  CompletionSpan::new(start, end - start)
}

///|
async fn Prompt::apply_format_input(
  self : Prompt,
  state : InputState,
  key_press : KeyPress,
) -> Unit {
  let snapshot = self.snapshot_of(state.left, state.right)
  let (text, caret) = (self.configuration.callbacks.format_input)(
    FormatInputContext::new(snapshot, key_press),
  )
  self.apply_snapshot(state, PromptSnapshot::new(text, caret))
}

///|
fn Prompt::replace_completion_span(
  self : Prompt,
  state : InputState,
  span : CompletionSpan,
  replacement_text : String,
) -> Unit {
  let snapshot = self.snapshot_of(state.left, state.right)
  let normalized = self.normalize_completion_span(snapshot, span)
  let prefix = snapshot.text.sub(end=normalized.start).to_string()
  let suffix = snapshot.text.sub(start=normalized.end()).to_string()
  let next = "\{prefix}\{replacement_text}\{suffix}"
  let next_caret = prefix.length() + replacement_text.length()
  self.apply_snapshot(state, PromptSnapshot::new(next, next_caret))
}

///|
fn Prompt::handle_search_mode(
  self : Prompt,
  state : InputState,
  key_bindings : KeyBindings,
  info : @console.ConsoleKeyInfo,
) -> KeyProcessingResult {
  self.run_search_mode_handler(state, key_bindings, info)
}

///|
async fn Prompt::handle_navigation_modes(
  self : Prompt,
  state : InputState,
  key_bindings : KeyBindings,
  info : @console.ConsoleKeyInfo,
) -> KeyProcessingResult {
  self.run_navigation_modes_handler(state, key_bindings, info)
}

///|
fn Prompt::handle_search_trigger(
  self : Prompt,
  state : InputState,
  key_bindings : KeyBindings,
  info : @console.ConsoleKeyInfo,
) -> KeyProcessingResult {
  self.run_search_trigger_handler(state, key_bindings, info)
}

///|
fn Prompt::handle_selection_clipboard(
  self : Prompt,
  state : InputState,
  key_bindings : KeyBindings,
  info : @console.ConsoleKeyInfo,
) -> KeyProcessingResult {
  self.run_selection_clipboard_handler(state, key_bindings, info)
}

///|
fn Prompt::handle_paste(
  self : Prompt,
  state : InputState,
  key_bindings : KeyBindings,
  info : @console.ConsoleKeyInfo,
) -> KeyProcessingResult {
  self.run_paste_handler(state, key_bindings, info)
}

///|
fn Prompt::handle_kill_ring(
  self : Prompt,
  state : InputState,
  key_bindings : KeyBindings,
  info : @console.ConsoleKeyInfo,
) -> KeyProcessingResult {
  self.run_kill_ring_handler(state, key_bindings, info)
}

///|
fn Prompt::handle_selection_movement_keys(
  self : Prompt,
  state : InputState,
  key_bindings : KeyBindings,
  info : @console.ConsoleKeyInfo,
) -> KeyProcessingResult {
  self.run_selection_movement_handler(state, key_bindings, info)
}

///|
fn Prompt::handle_undo_redo(
  self : Prompt,
  state : InputState,
  key_bindings : KeyBindings,
  info : @console.ConsoleKeyInfo,
) -> KeyProcessingResult {
  self.run_undo_redo_handler(state, key_bindings, info)
}

///|
async fn Prompt::handle_completion_and_history(
  self : Prompt,
  state : InputState,
  key_bindings : KeyBindings,
  key_press : KeyPress,
) -> KeyProcessingResult {
  self.run_completion_and_history_handler(state, key_bindings, key_press)
}

///|
async fn Prompt::handle_submit_or_cancel(
  self : Prompt,
  state : InputState,
  key_bindings : KeyBindings,
  key_press : KeyPress,
) -> KeyProcessingResult {
  self.run_submit_or_cancel_handler(state, key_bindings, key_press)
}

///|
async fn Prompt::handle_edit_keys(
  self : Prompt,
  state : InputState,
  key_bindings : KeyBindings,
  key_press : KeyPress,
) -> KeyProcessingResult {
  self.run_edit_keys_handler(state, key_bindings, key_press)
}

///|
async fn Prompt::handle_character_input(
  self : Prompt,
  state : InputState,
  key_press : KeyPress,
) -> KeyProcessingResult {
  self.run_character_input_handler(state, key_press)
}

///|
fn cycle_previous_index(index : Int, length : Int) -> Int {
  if length == 0 {
    0
  } else {
    (index + length - 1) % length
  }
}

///|
fn cycle_next_index(index : Int, length : Int) -> Int {
  if length == 0 {
    0
  } else {
    (index + 1) % length
  }
}

///|
fn Prompt::handle_selection_movement(
  self : Prompt,
  left : Array[Char],
  right : Array[Char],
  delta : Int,
) -> Unit {
  let old_pos = left.length()
  if delta < 0 {
    for _ in 0..<-delta {
      if left.pop() is Some(ch) {
        right.push(ch)
      }
    }
  } else {
    for _ in 0.. self.pane_state.selection = Some(Selection::new(old_pos, new_pos))
    Some(sel) => {
      sel.set_active(new_pos)
      if sel.anchor == sel.active {
        self.pane_state.selection = None
      }
    }
  }
}

///|
fn Prompt::delete_selection(
  self : Prompt,
  left : Array[Char],
  right : Array[Char],
) -> Unit {
  if self.pane_state.selection is Some(sel) {
    let (start, end) = sel.range()
    let text = line_from_buffers(left, right)
    let chars = text.to_array()
    left.clear()
    right.clear()
    for i in 0....end {
      right.push(chars[i])
    }
    self.pane_state.selection = None
  }
}

///|
fn Prompt::move_word_left(
  _self : Prompt,
  left : Array[Char],
  right : Array[Char],
) -> Bool {
  let mut moved = false
  while left.length() > 0 && !is_word_character(left[left.length() - 1]) {
    if left.pop() is Some(ch) {
      right.push(ch)
      moved = true
    }
  }
  while left.length() > 0 && is_word_character(left[left.length() - 1]) {
    if left.pop() is Some(ch) {
      right.push(ch)
      moved = true
    }
  }
  moved
}

///|
fn Prompt::move_word_right(
  _self : Prompt,
  left : Array[Char],
  right : Array[Char],
) -> Bool {
  let mut moved = false
  while right.length() > 0 && !is_word_character(right[right.length() - 1]) {
    if right.pop() is Some(ch) {
      left.push(ch)
      moved = true
    }
  }
  while right.length() > 0 && is_word_character(right[right.length() - 1]) {
    if right.pop() is Some(ch) {
      left.push(ch)
      moved = true
    }
  }
  moved
}

///|
fn Prompt::delete_word_left(
  self : Prompt,
  left : Array[Char],
  right : Array[Char],
) -> Bool {
  if self.pane_state.selection is Some(_) {
    self.delete_selection(left, right)
    return true
  }

  let mut deleted = false
  while left.length() > 0 && !is_word_character(left[left.length() - 1]) {
    ignore(left.pop())
    deleted = true
  }
  while left.length() > 0 && is_word_character(left[left.length() - 1]) {
    ignore(left.pop())
    deleted = true
  }
  deleted
}

///|
fn Prompt::delete_word_right(
  self : Prompt,
  left : Array[Char],
  right : Array[Char],
) -> Bool {
  if self.pane_state.selection is Some(_) {
    self.delete_selection(left, right)
    return true
  }

  let mut deleted = false
  while right.length() > 0 && !is_word_character(right[right.length() - 1]) {
    ignore(right.pop())
    deleted = true
  }
  while right.length() > 0 && is_word_character(right[right.length() - 1]) {
    ignore(right.pop())
    deleted = true
  }
  deleted
}

///|
fn Prompt::track_undo(
  self : Prompt,
  left : Array[Char],
  right : Array[Char],
) -> Unit {
  self.undo_manager.track(line_from_buffers(left, right), left.length())
}

///|
fn Prompt::apply_undo_record(
  _self : Prompt,
  left : Array[Char],
  right : Array[Char],
  record : UndoRecord,
) -> Unit {
  left.clear()
  right.clear()
  let chars = record.text.to_array()
  for i in 0....record.cursor_pos {
    right.push(chars[i])
  }
}

///|
fn Prompt::set_line_buffers(
  _self : Prompt,
  left : Array[Char],
  right : Array[Char],
  text : String,
) -> Unit {
  left.clear()
  right.clear()
  left.push_iter(text.iter())
}

///|
fn is_word_character(ch : Char) -> Bool {
  ch.is_ascii_alphabetic() || ch.is_ascii_digit() || ch == '_'
}

///|
fn is_control_c_key(info : @console.ConsoleKeyInfo) -> Bool {
  (info.key is C && info.control) || (info.key_char == '\u0003' && info.control)
}

///|
pub fn Prompt::take_execution_cancellation_requested(self : Prompt) -> Bool {
  self.cancel_controller.take_cancellation_requested()
}

///|
pub async fn Prompt::dispose(self : Prompt) -> Unit {
  self.history.save_persistent_history_full()
  self.cancel_controller.dispose()
}

///|
fn line_from_buffers(left : Array[Char], right : Array[Char]) -> String {
  let chars = Array::new(capacity=left.length() + right.length())
  chars.append(left)
  right.rev_each(ch => chars.push(ch))
  String::from_array(chars)
}

///|
async fn Prompt::render_line(
  self : Prompt,
  prompt : FormattedString,
  left : Array[Char],
  right : Array[Char],
) -> Unit {
  let width = self.console.get_buffer_width()
  let max_items = self.configuration.max_completion_items_count

  // 1. Determine Prompt Text & Style
  let active_prompt = match self.pane_state.search_state {
    Some(s) =>
      FormattedString::new("(reverse-i-search)'\{s.query}': ", spans=[
        FormatSpan::new(0, 18, ConsoleFormat::new(bold=true, foreground=Cyan)),
      ])
    None => prompt
  }
  let active_prompt_width = active_prompt.width()

  // 2. Word Wrapping
  let raw_line = line_from_buffers(left, right)
  let full_text = active_prompt.text + raw_line
  let wrapped = wrap_text(
    full_text,
    active_prompt_width + get_string_width(String::from_array(left)),
    width,
  )

  // 2. Calculate heights
  let raw_completion_items_count = match self.pane_state.completion_state {
    Some(state) =>
      if state.items.length() < max_items {
        state.items.length()
      } else {
        max_items
      }
    None => 0
  }
  let raw_overload_items_count = match self.pane_state.overload_state {
    Some(state) =>
      if state.items.length() < 3 {
        state.items.length()
      } else {
        3
      }
    None => 0
  }

  let cursor_in_window = self.console.get_cursor_top() -
    self.console.get_window_top()
  let rows_below_cursor = self.console.get_window_height() -
    (cursor_in_window + 1)
  let input_rows_below_cursor = wrapped.lines.length() - wrapped.cursor_row - 1
  let helper_rows_budget = rows_below_cursor - input_rows_below_cursor
  let helper_rows_budget = if helper_rows_budget > 0 {
    helper_rows_budget
  } else {
    0
  }

  let overload_items_count = if raw_overload_items_count < helper_rows_budget {
    raw_overload_items_count
  } else {
    helper_rows_budget
  }
  let completion_rows_budget = helper_rows_budget - overload_items_count
  let completion_items_count = if raw_completion_items_count <
    completion_rows_budget {
    raw_completion_items_count
  } else {
    completion_rows_budget
  }

  let height = wrapped.lines.length() +
    completion_items_count +
    overload_items_count
  let new_screen = Screen::new(height, width)

  // 4. Draw Input Lines
  let spans = if self.configuration.use_colors {
    (self.configuration.callbacks.highlight_callback)(
      PromptSnapshot::new(raw_line, left.length()),
    )
  } else {
    []
  }
  spans.sort_by_key(span => span.start)

  let active_prompt_spans = active_prompt.spans.copy()
  active_prompt_spans.sort_by_key(span => span.start)

  for r in 0..= span.start &&
              char_index_on_line < span.start + span.length {
              format = span.format
              break
            }
          }
        } else {
          // Apply input text styles
          let i = if r == 0 {
            char_index_on_line - active_prompt_width
          } else {
            char_index_on_line
          }
          for span in spans {
            if i >= span.start && i < span.start + span.length {
              format = span.format
              break
            }
          }
          if self.pane_state.selection is Some(sel) {
            if sel.contains(i) {
              format = format.with_background(
                self.configuration.selection_background,
              )
            }
          }
        }
      }

      new_screen.rows[r][line_col] = Cell::new(ch, format)
      let w = get_string_width(ch.to_string())
      if w == 2 && line_col + 1 < width {
        new_screen.rows[r][line_col + 1] = Cell::new('\u{0000}', format)
      }
      line_col = line_col + w
      char_index_on_line = char_index_on_line + 1
    }
  }
  new_screen.set_cursor(wrapped.cursor_row, wrapped.cursor_col)

  // 5. Draw Overloads & Completions
  let mut next_row = wrapped.lines.length()
  if self.pane_state.overload_state is Some(state) {
    for i in 0..= state.items.length() {
        break
      }
      let item = state.items[item_index]
      let is_selected = item_index == state.selected_index
      let format = if is_selected && self.configuration.use_colors {
        ConsoleFormat::new(bold=true, underline=true)
      } else {
        ConsoleFormat::default()
      }
      let prefix = if is_selected { "> " } else { "  " }
      new_screen.draw_string(
        next_row + i,
        0,
        prefix + item.display_text,
        format,
      )
      if is_selected && item.description != "" {
        new_screen.draw_string(
          next_row + i,
          30,
          " │ " + item.description,
          ConsoleFormat::default(),
        )
      }
    }
  }

  // 6. Render Diff
  let (base_row, base_col) = match self.render_anchor {
    Some(anchor) => anchor
    None => {
      let anchor = (
        self.console.get_cursor_top(),
        self.console.get_cursor_left(),
      )
      self.render_anchor = Some(anchor)
      anchor
    }
  }
  let old_screen_val = self.last_screen.unwrap_or_else(() => {
    Screen::new(0, width)
  })
  render_diff(
    self.console,
    new_screen,
    old_screen_val,
    base_row + 1,
    base_col + 1,
  )
  self.last_screen = Some(new_screen)
}

///|
fn cancel_result() -> PromptResult {
  let key_info = @console.ConsoleKeyInfo::new(
    '\u0003',
    @console.C,
    false,
    false,
    true,
  )
  PromptResult::cancelled(key_info)
}

///|
pub struct PromptResult {
  is_success : Bool
  text : String
  submit_key_info : @console.ConsoleKeyInfo
}

///|
pub fn PromptResult::submitted(
  text : String,
  submit_key_info : @console.ConsoleKeyInfo,
) -> PromptResult {
  { is_success: true, text, submit_key_info }
}

///|
pub fn PromptResult::cancelled(
  submit_key_info : @console.ConsoleKeyInfo,
) -> PromptResult {
  { is_success: false, text: "", submit_key_info }
}