///|
/// Source-shaped readonly cursor movement primitives.
///
/// Ported from
/// `vscode/src/vs/editor/common/cursor/cursorMoveOperations.ts` at
/// checked-in source.
///
/// The scoped surface is ordinary LTR movement with sticky tab stops off.
/// Atomic soft-tab movement, visual RTL swapping, blank-line/buffer commands,
/// and sticky End remain the explicit deferrals recorded in
/// `viewer-cursor-input-events-parity.md`.
///
/// `CursorPosition` (`cursorMoveOperations.ts:14-26`).
priv struct CursorMovePosition {
  line_number : Int32
  column : Int32
  leftover_visible_columns : Int32
}

///|
/// The two `ICursorSimpleModel` implementations used by the scoped source:
/// the text model for logical-line Home/End and the view model for wrapped
///
/// navigation (`cursorMoveCommands.ts:32-117,475-538`).
priv enum CursorMoveModel {
  Model(@model.TextModel)
  View(ViewModel)
}

///|
/// `ICursorSimpleModel.getLineCount` (`cursorCommon.ts:253-267`).
fn CursorMoveModel::get_line_count(self : CursorMoveModel) -> Int32 {
  match self {
    Model(model) => model.get_line_count()
    View(view_model) => view_model.line_count()
  }
}

///|
/// `ICursorSimpleModel.getLineContent` (`cursorCommon.ts:253-267`).
fn CursorMoveModel::get_line_content(
  self : CursorMoveModel,
  line_number : Int32,
) -> String {
  match self {
    Model(model) => model.get_line_content(line_number)
    View(view_model) => view_model.get_line_content(line_number)
  }
}

///|
/// `ICursorSimpleModel.getLineMinColumn` (`cursorCommon.ts:253-267`).
fn CursorMoveModel::get_line_min_column(
  self : CursorMoveModel,
  line_number : Int32,
) -> Int32 {
  match self {
    Model(model) => model.get_line_min_column(line_number)
    View(view_model) => view_model.get_line_min_column(line_number)
  }
}

///|
/// `ICursorSimpleModel.getLineMaxColumn` (`cursorCommon.ts:253-267`).
fn CursorMoveModel::get_line_max_column(
  self : CursorMoveModel,
  line_number : Int32,
) -> Int32 {
  match self {
    Model(model) => model.get_line_max_column(line_number)
    View(view_model) => view_model.get_line_max_column(line_number)
  }
}

///|
/// `ICursorSimpleModel.getLineFirstNonWhitespaceColumn` plus
/// `ViewModel.getLineFirstNonWhitespaceColumn`
///
/// (`cursorCommon.ts:258`, `viewModelImpl.ts:828-834`).
fn CursorMoveModel::get_line_first_non_whitespace_column(
  self : CursorMoveModel,
  line_number : Int32,
) -> Int32 {
  match self {
    Model(model) => model.get_line_first_non_whitespace_column(line_number)
    View(view_model) => {
      let result = @base_common.first_non_whitespace_index(
        view_model.get_line_content(line_number),
      )
      if result == -1 {
        0
      } else {
        result + 1
      }
    }
  }
}

///|
/// `ICursorSimpleModel.getLineLastNonWhitespaceColumn`
/// (`cursorCommon.ts:259`) for both scoped model implementations.
fn CursorMoveModel::get_line_last_non_whitespace_column(
  self : CursorMoveModel,
  line_number : Int32,
) -> Int32 {
  match self {
    Model(model) => model.get_line_last_non_whitespace_column(line_number)
    View(view_model) =>
      view_model.get_line_last_non_whitespace_column(line_number)
  }
}

///|
/// View-model side of `ICursorSimpleModel.getLineLastNonWhitespaceColumn`.
pub fn ViewModel::get_line_last_non_whitespace_column(
  self : ViewModel,
  line_number : Int32,
) -> Int32 {
  let result = @base_common.last_non_whitespace_index(
    self.get_line_content(line_number),
  )
  if result == -1 {
    0
  } else {
    result + 2
  }
}

///|
/// Keep the complete scoped interface adapter in non-test builds.
let _cursor_move_last_non_whitespace_anchor : (CursorMoveModel, Int32) -> Int32 = CursorMoveModel::get_line_last_non_whitespace_column

///|
/// `ICursorSimpleModel.normalizePosition` (`cursorCommon.ts:260`). The text
/// model has no injected-text ambiguity, so its implementation is the same
///
/// surrogate-validating clamp for every affinity.
fn CursorMoveModel::normalize_position(
  self : CursorMoveModel,
  position : @base_common.Position,
  affinity : @model.PositionAffinity,
) -> @base_common.Position {
  match self {
    Model(model) => model.validate_position(position)
    View(view_model) => view_model.normalize_position(position, affinity)
  }
}

///|
/// `CursorConfiguration.columnFromVisibleColumn`
///
/// (`cursorCommon.ts:233-247`), including its min/max clamps.
fn CursorMoveModel::column_from_visible_column(
  self : CursorMoveModel,
  line_number : Int32,
  visible_column : Int32,
  tab_size : Int32,
) -> Int32 {
  let result = @core.column_from_visible_column(
    self.get_line_content(line_number),
    visible_column,
    tab_size,
  )
  let min_column = self.get_line_min_column(line_number)
  if result < min_column {
    return min_column
  }
  let max_column = self.get_line_max_column(line_number)
  if result > max_column {
    return max_column
  }
  result
}

///|
/// `strings.prevCharLength` at the exact offset consumed by
///
/// `MoveOperations.leftPosition` (`cursorMoveOperations.ts:29-39`).
fn cursor_prev_char_length(line_content : String, offset : Int32) -> Int32 {
  if offset >= 2 &&
    @base_common.is_low_surrogate(line_content[offset - 1].to_int()) &&
    @base_common.is_high_surrogate(line_content[offset - 2].to_int()) {
    2
  } else {
    1
  }
}

///|
/// `strings.nextCharLength` at the exact offset consumed by
///
/// `MoveOperations.rightPosition` (`cursorMoveOperations.ts:110-118`).
fn cursor_next_char_length(line_content : String, offset : Int32) -> Int32 {
  if offset + 1 < line_content.length() &&
    @base_common.is_high_surrogate(line_content[offset].to_int()) &&
    @base_common.is_low_surrogate(line_content[offset + 1].to_int()) {
    2
  } else {
    1
  }
}

///|
/// `MoveOperations.leftPosition` (`cursorMoveOperations.ts:29-39`).
fn cursor_left_position(
  model : CursorMoveModel,
  position : @base_common.Position,
) -> @base_common.Position {
  if position.column > model.get_line_min_column(position.line_number) {
    Position(
      position.line_number,
      position.column -
      cursor_prev_char_length(
        model.get_line_content(position.line_number),
        position.column - 1,
      ),
    )
  } else if position.line_number > 1 {
    let new_line_number = position.line_number - 1
    Position(new_line_number, model.get_line_max_column(new_line_number))
  } else {
    position
  }
}

///|
/// `MoveOperations.clipRange` (`cursorMoveOperations.ts:100-108`).
fn cursor_clip_range(value : Int32, min : Int32, max : Int32) -> Int32 {
  if value < min {
    return min
  }
  if value > max {
    return max
  }
  value
}

///|
/// `MoveOperations.clipPositionColumn` (`cursorMoveOperations.ts:91-98`).
fn cursor_clip_position_column(
  position : @base_common.Position,
  model : CursorMoveModel,
) -> @base_common.Position {
  Position(
    position.line_number,
    cursor_clip_range(
      position.column,
      model.get_line_min_column(position.line_number),
      model.get_line_max_column(position.line_number),
    ),
  )
}

///|
/// LTR, sticky-tab-false `MoveOperations.moveLeft`
///
/// (`cursorMoveOperations.ts:60-89`).
fn cursor_move_left(
  model : CursorMoveModel,
  cursor : @cursor.SingleCursorState,
  in_selection_mode : Bool,
  no_of_columns : Int32,
) -> @cursor.SingleCursorState {
  let (line_number, column) = if cursor.has_selection() && !in_selection_mode {
    // Collapse to the beginning of the selection (`:68-72`).
    let selection = cursor.selection()
    (selection.start().line_number, selection.start().column)
  } else {
    // Pre-offset, clip before normalize(Left), then take one Unicode-scalar
    // step (`:73-82`). Atomic soft tabs are the recorded D2 deferral.
    let pos : @base_common.Position = Position(
      cursor.position.line_number,
      cursor.position.column - (no_of_columns - 1),
    )
    let normalized = model.normalize_position(
      cursor_clip_position_column(pos, model),
      Left,
    )
    let target = cursor_left_position(model, normalized)
    (target.line_number, target.column)
  }
  cursor.moved(
    in_selection_mode,
    Position(line_number, column),
    leftover_visible_columns=0,
  )
}

///|
/// `MoveOperations.rightPosition` (`cursorMoveOperations.ts:110-118`).
fn cursor_right_position(
  model : CursorMoveModel,
  line_number : Int32,
  column : Int32,
) -> @base_common.Position {
  let mut line_number = line_number
  let mut column = column
  if column < model.get_line_max_column(line_number) {
    column += cursor_next_char_length(
      model.get_line_content(line_number),
      column - 1,
    )
  } else if line_number < model.get_line_count() {
    line_number += 1
    column = model.get_line_min_column(line_number)
  }
  Position(line_number, column)
}

///|
/// LTR, sticky-tab-false `MoveOperations.moveRight`
///
/// (`cursorMoveOperations.ts:138-155`).
fn cursor_move_right(
  model : CursorMoveModel,
  cursor : @cursor.SingleCursorState,
  in_selection_mode : Bool,
  no_of_columns : Int32,
) -> @cursor.SingleCursorState {
  let (line_number, column) = if cursor.has_selection() && !in_selection_mode {
    // Collapse to the end of the selection (`:142-146`).
    let selection = cursor.selection()
    (selection.end().line_number, selection.end().column)
  } else {
    // Pre-offset, clip before normalize(Right), then take one Unicode-scalar
    // step (`:147-153`). Atomic soft tabs are the recorded D2 deferral.
    let pos : @base_common.Position = Position(
      cursor.position.line_number,
      cursor.position.column + no_of_columns - 1,
    )
    let normalized = model.normalize_position(
      cursor_clip_position_column(pos, model),
      Right,
    )
    let target = cursor_right_position(
      model,
      normalized.line_number,
      normalized.column,
    )
    (target.line_number, target.column)
  }
  cursor.moved(
    in_selection_mode,
    Position(line_number, column),
    leftover_visible_columns=0,
  )
}

///|
/// `MoveOperations.vertical` (`cursorMoveOperations.ts:157-197`). Branch
/// order, edge handling, visible-column residue, and normalization adjustment
///
/// match the source.
fn cursor_vertical(
  tab_size : Int32,
  model : CursorMoveModel,
  line_number : Int32,
  column : Int32,
  leftover_visible_columns : Int32,
  new_line_number : Int32,
  allow_move_on_edge_line : Bool,
  normalization_affinity? : @model.PositionAffinity,
) -> CursorMovePosition {
  let current_visible_column = @core.visible_column_from_column(
      model.get_line_content(line_number),
      column,
      tab_size,
    ) +
    leftover_visible_columns
  let line_count = model.get_line_count()
  let was_on_first_position = line_number == 1 && column == 1
  let was_on_last_position = line_number == line_count &&
    column == model.get_line_max_column(line_number)
  let was_at_edge_position = if new_line_number < line_number {
    was_on_first_position
  } else {
    was_on_last_position
  }
  let mut line_number = new_line_number
  let mut column = column
  if line_number < 1 {
    line_number = 1
    column = if allow_move_on_edge_line {
      model.get_line_min_column(line_number)
    } else {
      model.get_line_max_column(line_number).min(column)
    }
  } else if line_number > line_count {
    line_number = line_count
    column = if allow_move_on_edge_line {
      model.get_line_max_column(line_number)
    } else {
      model.get_line_max_column(line_number).min(column)
    }
  } else {
    column = model.column_from_visible_column(
      line_number, current_visible_column, tab_size,
    )
  }
  let mut leftover_visible_columns = if was_at_edge_position {
    0
  } else {
    current_visible_column -
    @core.visible_column_from_column(
      model.get_line_content(line_number),
      column,
      tab_size,
    )
  }
  if normalization_affinity is Some(affinity) {
    let new_position = model.normalize_position(
      Position(line_number, column),
      affinity,
    )
    leftover_visible_columns += column - new_position.column
    line_number = new_position.line_number
    column = new_position.column
  }
  { line_number, column, leftover_visible_columns, }
}

///|
/// `MoveOperations.down` (`cursorMoveOperations.ts:199-201`).
fn cursor_down(
  tab_size : Int32,
  model : CursorMoveModel,
  line_number : Int32,
  column : Int32,
  leftover_visible_columns : Int32,
  count : Int32,
  allow_move_on_last_line : Bool,
) -> CursorMovePosition {
  cursor_vertical(
    tab_size,
    model,
    line_number,
    column,
    leftover_visible_columns,
    line_number + count,
    allow_move_on_last_line,
    normalization_affinity=RightOfInjectedText,
  )
}

///|
/// `MoveOperations.moveDown` (`cursorMoveOperations.ts:203-227`), including
///
/// the exact retry-normalization cap `i++ < 10`.
fn cursor_move_down_with_retry(
  line_number : Int32,
  line_count : Int32,
  step : (Int32) -> (CursorMovePosition, Int32),
) -> CursorMovePosition {
  for i = 0 {
    let (target, normalized_line_number) = step(i)
    if normalized_line_number > line_number {
      break target
    }
    if i < 10 && line_number + i + 1 < line_count {
      continue i + 1
    }
    break target
  }
}

///|
/// `MoveOperations.moveDown` (`cursorMoveOperations.ts:203-227`). The retry
/// controller above is extracted verbatim so the source's cap is directly
/// branch-testable without exposing a product debug API.
fn cursor_move_down(
  tab_size : Int32,
  model : CursorMoveModel,
  cursor : @cursor.SingleCursorState,
  in_selection_mode : Bool,
  lines_count : Int32,
) -> @cursor.SingleCursorState {
  let (line_number, column) = if cursor.has_selection() && !in_selection_mode {
    let selection = cursor.selection()
    (selection.end().line_number, selection.end().column)
  } else {
    (cursor.position.line_number, cursor.position.column)
  }
  let target = cursor_move_down_with_retry(
    line_number,
    model.get_line_count(),
    i => {
      let target = cursor_down(
        tab_size,
        model,
        line_number + i,
        column,
        cursor.leftover_visible_columns,
        lines_count,
        true,
      )
      let normalized = model.normalize_position(
        Position(target.line_number, target.column),
        None,
      )
      (target, normalized.line_number)
    },
  )
  cursor.moved(
    in_selection_mode,
    Position(target.line_number, target.column),
    leftover_visible_columns=target.leftover_visible_columns,
  )
}

///|
/// `MoveOperations.up` (`cursorMoveOperations.ts:242-246`).
fn cursor_up(
  tab_size : Int32,
  model : CursorMoveModel,
  line_number : Int32,
  column : Int32,
  leftover_visible_columns : Int32,
  count : Int32,
  allow_move_on_first_line : Bool,
) -> CursorMovePosition {
  cursor_vertical(
    tab_size,
    model,
    line_number,
    column,
    leftover_visible_columns,
    line_number - count,
    allow_move_on_first_line,
    normalization_affinity=LeftOfInjectedText,
  )
}

///|
/// `MoveOperations.moveUp` (`cursorMoveOperations.ts:248-264`).
fn cursor_move_up(
  tab_size : Int32,
  model : CursorMoveModel,
  cursor : @cursor.SingleCursorState,
  in_selection_mode : Bool,
  lines_count : Int32,
) -> @cursor.SingleCursorState {
  let (line_number, column) = if cursor.has_selection() && !in_selection_mode {
    let selection = cursor.selection()
    (selection.start().line_number, selection.start().column)
  } else {
    (cursor.position.line_number, cursor.position.column)
  }
  let target = cursor_up(
    tab_size,
    model,
    line_number,
    column,
    cursor.leftover_visible_columns,
    lines_count,
    true,
  )
  cursor.moved(
    in_selection_mode,
    Position(target.line_number, target.column),
    leftover_visible_columns=target.leftover_visible_columns,
  )
}

///|
/// `MoveOperations.moveToBeginningOfLine`
/// (`cursorMoveOperations.ts:323-338`), including the whitespace-only `0`
///
/// sentinel and indentation toggle.
fn cursor_move_to_beginning_of_line(
  model : CursorMoveModel,
  cursor : @cursor.SingleCursorState,
  in_selection_mode : Bool,
) -> @cursor.SingleCursorState {
  let line_number = cursor.position.line_number
  let min_column = model.get_line_min_column(line_number)
  let candidate = model.get_line_first_non_whitespace_column(line_number)
  let first_non_blank_column = if candidate == 0 {
    min_column
  } else {
    candidate
  }
  let column = if cursor.position.column == first_non_blank_column {
    min_column
  } else {
    first_non_blank_column
  }
  cursor.moved(
    in_selection_mode,
    Position(line_number, column),
    leftover_visible_columns=0,
  )
}

///|
/// Sticky-false `MoveOperations.moveToEndOfLine`
/// (`cursorMoveOperations.ts:340-344`). The sticky-true large-leftover arm is
///
/// an approved deferral and is rejected by the command adapter.
fn cursor_move_to_end_of_line(
  model : CursorMoveModel,
  cursor : @cursor.SingleCursorState,
  in_selection_mode : Bool,
) -> @cursor.SingleCursorState {
  let line_number = cursor.position.line_number
  cursor.moved(
    in_selection_mode,
    Position(line_number, model.get_line_max_column(line_number)),
    leftover_visible_columns=0,
  )
}