///|
/// Source-shaped command adapters for readonly cursor movement.
///
/// Ported from
/// `vscode/src/vs/editor/common/cursor/cursorMoveCommands.ts` and the scoped
/// Home/End/Page command callers in
/// `vscode/src/vs/editor/browser/coreCommands.ts`, pinned at
/// checked-in source.
/// Exact 15-member `CursorMove.Direction` enum contract
/// (`cursorMoveCommands.ts:929-948`). The viewport members remain behaviorally
/// deferred, but retaining them here keeps the aggregate source type complete.
pub(all) enum CursorMoveDirection {
  Left
  Right
  Up
  Down
  PrevBlankLine
  NextBlankLine
  WrappedLineStart
  WrappedLineFirstNonWhitespaceCharacter
  WrappedLineColumnCenter
  WrappedLineEnd
  WrappedLineLastNonWhitespaceCharacter
  ViewPortTop
  ViewPortCenter
  ViewPortBottom
  ViewPortIfOutside
} derive(Eq, Debug)

///|
/// MoonBit representation of the source's exact 11-member
/// `SimpleMoveDirection` union (`cursorMoveCommands.ts:950-962`).
pub(all) enum SimpleMoveDirection {
  Left
  Right
  Up
  Down
  PrevBlankLine
  NextBlankLine
  WrappedLineStart
  WrappedLineFirstNonWhitespaceCharacter
  WrappedLineColumnCenter
  WrappedLineEnd
  WrappedLineLastNonWhitespaceCharacter
} derive(Eq, Debug)

///|
/// Exact `CursorMove.Unit` enum contract (`cursorMoveCommands.ts:971-978`).
/// `None` and `WrappedLine` are behaviorally scoped; Line, Character,
/// HalfLine, and FoldedLine remain explicit branch deferrals.
pub(all) enum CursorMoveUnit {
  None
  Line
  WrappedLine
  Character
  HalfLine
  FoldedLine
} derive(Eq, Debug)

///|
/// Exact typed argument contract consumed by the bound simple-move commands.
pub struct SimpleMoveArguments {
  direction : SimpleMoveDirection
  unit : CursorMoveUnit
  select : Bool
  value : Int32
} derive(Eq, Debug)

///|
pub fn SimpleMoveArguments::SimpleMoveArguments(
  direction : SimpleMoveDirection,
  unit : CursorMoveUnit,
  select : Bool,
  value : Int32,
) -> SimpleMoveArguments {
  { direction, unit, select, value, }
}

///|
/// Narrows the full Direction enum to the simple-move union. A viewport member
/// returns `None`, representing `simpleMove`'s runtime default/null arm under a
/// closed MoonBit type system.
pub fn CursorMoveDirection::as_simple(
  self : CursorMoveDirection,
) -> SimpleMoveDirection? {
  let result : SimpleMoveDirection? = match self {
    Left => Some(Left)
    Right => Some(Right)
    Up => Some(Up)
    Down => Some(Down)
    PrevBlankLine => Some(PrevBlankLine)
    NextBlankLine => Some(NextBlankLine)
    WrappedLineStart => Some(WrappedLineStart)
    WrappedLineFirstNonWhitespaceCharacter =>
      Some(WrappedLineFirstNonWhitespaceCharacter)
    WrappedLineColumnCenter => Some(WrappedLineColumnCenter)
    WrappedLineEnd => Some(WrappedLineEnd)
    WrappedLineLastNonWhitespaceCharacter =>
      Some(WrappedLineLastNonWhitespaceCharacter)
    ViewPortTop | ViewPortCenter | ViewPortBottom | ViewPortIfOutside => None
  }
  result
}

///|
/// Whether a source-shaped command produced a model-side or view-side partial
///
/// cursor state (`PartialCursorState`, `cursorCommon.ts:269-328`).
priv enum CursorMoveResult {
  ByModel(@cursor.SingleCursorState)
  ByView(@cursor.SingleCursorState)
}

///|
/// Applies a partial cursor state through the one transition/emission spine.
fn ViewModel::commit_cursor_move(
  self : ViewModel,
  result : CursorMoveResult,
  source : String,
  reason : @editor_api.CursorChangeReason,
) -> Bool {
  match result {
    ByModel(state) => self.set_model_cursor_state(state, source, reason)
    ByView(state) => self.set_view_cursor_state(state, source, reason)
  }
}

///|
/// Clamps a raw view line/column before `coordinatesConverter.validateViewPosition`.
/// It deliberately does not normalize injected-text interiors or surrogate
/// boundaries: after the model-mapping check, `Cursor._validateViewState` owns
/// that normalization and adds the exact original-column correction to the
/// leftover-visible-column residue.
fn ViewModel::validate_cursor_move_view_position(
  self : ViewModel,
  position : @base_common.Position,
) -> @base_common.Position {
  let line_number = position.line_number.clamp(min=1, max=self.line_count())
  let min_column = self.get_line_min_column(line_number)
  let max_column = self.get_line_max_column(line_number)
  let column = position.column.clamp(min=min_column, max=max_column)
  Position(line_number, column)
}

///|
/// Resolves the paired model/view command arguments exactly like
/// `CursorMoveCommands.line`/`moveTo`: validate the required model position;
/// when a view position is supplied, retain it only if its normalized
/// view-to-model result equals that model position, otherwise fall back to a
/// model-to-view conversion. An absent view argument converts directly.
fn ViewModel::resolve_cursor_move_position(
  self : ViewModel,
  model_position : @base_common.Position,
  view_position : @base_common.Position?,
) -> (@base_common.Position, @base_common.Position) {
  let model_position = self.model.validate_position(model_position)
  let view_position = match view_position {
    None =>
      self.coordinates_converter.model_position_to_view_position(model_position)
    Some(view_position) => {
      let view_position = self.validate_cursor_move_view_position(view_position)
      let computed_model_position = self.model.validate_position(
        self.coordinates_converter.view_position_to_model_position(
          view_position,
        ),
      )
      if computed_model_position == model_position {
        view_position
      } else {
        self.coordinates_converter.model_position_to_view_position(
          model_position,
        )
      }
    }
  }
  (model_position, view_position)
}

///|
/// `CursorMoveCommands.simpleMove`'s approved LTR branches
/// (`cursorMoveCommands.ts:276-353,475-538`). A zero value follows the source
/// parser's `args.value || 1` fallback. Up/Down accept only WrappedLine;
/// logical/folded lines are approved deferrals. The return value reports a
/// changed state, not whether the winning keybinding was handled: the root
///
/// registry consumes a matched command even at a document boundary.
pub fn ViewModel::move_cursor(
  self : ViewModel,
  direction : SimpleMoveDirection,
  in_selection_mode : Bool,
  value~ : Int32 = 1,
  unit~ : CursorMoveUnit = None,
  source~ : String = "keyboard",
  reason~ : @editor_api.CursorChangeReason = Explicit,
) -> Bool {
  self.move_cursor_with_arguments(
    SimpleMoveArguments(direction, unit, in_selection_mode, value),
    source~,
    reason~,
  )
}

///|
/// Full-Direction adapter that owns `simpleMove`'s runtime default/null arm.
/// The four viewport members are valid `Direction` values but not members of
/// the `SimpleMoveDirection` union, so they return without mutation here.
pub fn ViewModel::move_cursor_direction(
  self : ViewModel,
  direction : CursorMoveDirection,
  in_selection_mode : Bool,
  value~ : Int32 = 1,
  unit~ : CursorMoveUnit = None,
  source~ : String = "keyboard",
  reason~ : @editor_api.CursorChangeReason = Explicit,
) -> Bool {
  guard direction.as_simple() is Some(simple_direction) else { return false }
  self.move_cursor(
    simple_direction,
    in_selection_mode,
    value~,
    unit~,
    source~,
    reason~,
  )
}

///|
/// Consumes the exact `SimpleMoveArguments` record retained by the bound
/// commands.
pub fn ViewModel::move_cursor_with_arguments(
  self : ViewModel,
  args : SimpleMoveArguments,
  source~ : String = "keyboard",
  reason~ : @editor_api.CursorChangeReason = Explicit,
) -> Bool {
  let direction = args.direction
  let unit = args.unit
  let in_selection_mode = args.select
  let value = if args.value == 0 { 1 } else { args.value }
  let state = self.cursor.get_primary_cursor_state()
  let model = CursorMoveModel::View(self)
  let result = match direction {
    Left => {
      guard unit != HalfLine else { return false }
      ByView(
        cursor_move_left(model, state.view_state, in_selection_mode, value),
      )
    }
    Right => {
      guard unit != HalfLine else { return false }
      ByView(
        cursor_move_right(model, state.view_state, in_selection_mode, value),
      )
    }
    Up => {
      guard unit == WrappedLine else { return false }
      ByView(
        cursor_move_up(
          self.options.tab_size,
          model,
          state.view_state,
          in_selection_mode,
          value,
        ),
      )
    }
    Down => {
      guard unit == WrappedLine else { return false }
      ByView(
        cursor_move_down(
          self.options.tab_size,
          model,
          state.view_state,
          in_selection_mode,
          value,
        ),
      )
    }
    PrevBlankLine
    | NextBlankLine
    | WrappedLineStart
    | WrappedLineFirstNonWhitespaceCharacter
    | WrappedLineColumnCenter
    | WrappedLineEnd
    | WrappedLineLastNonWhitespaceCharacter => return false
  }
  self.commit_cursor_move(result, source, reason)
}

///|
/// `CursorConfiguration.pageSize` (`cursorCommon.ts:129`): exactly
///
/// `max(1, floor(layoutHeight / lineHeight) - 2)`.
pub fn ViewModel::cursor_page_size(self : ViewModel) -> Int32 {
  ((self.view_layout.dimensions().height /
  self.view_layout.line_height().to_double())
  .floor()
  .to_int() -
  2).max(1)
}

///|
/// `CursorPageUp/Down`'s dynamic page argument (`coreCommands.ts:913-936`):
/// the static `PAGE_SIZE_MARKER = -1`, an omitted argument, or an exact-zero
/// dynamic override falls back to the live cursor page size; a positive
/// override is used unchanged. Movement is wrapped-view-line
///
/// `simpleMove`, not viewport scrolling.
pub fn ViewModel::move_cursor_page(
  self : ViewModel,
  direction : SimpleMoveDirection,
  in_selection_mode : Bool,
  page_size~ : Int32 = 0,
  source~ : String = "keyboard",
  reason~ : @editor_api.CursorChangeReason = Explicit,
) -> Bool {
  guard direction == Up || direction == Down else { return false }
  let value = if page_size == -1 || page_size == 0 {
    self.cursor_page_size()
  } else {
    page_size
  }
  self.move_cursor(
    direction,
    in_selection_mode,
    value~,
    unit=WrappedLine,
    source~,
    reason~,
  )
}

///|
/// `CursorMoveCommands._moveToLineStart`
/// (`cursorMoveCommands.ts:41-76`). The first wrapped segment uses logical
/// model indentation; a continuation that is not already at its own first
///
/// nonblank column uses view-line indentation.
pub fn ViewModel::move_cursor_to_line_start(
  self : ViewModel,
  in_selection_mode : Bool,
  source~ : String = "keyboard",
  reason~ : @editor_api.CursorChangeReason = Explicit,
) -> Bool {
  let cursor = self.cursor.get_primary_cursor_state()
  let current_view_column = cursor.view_state.position.column
  let current_model_column = cursor.model_state.position.column
  let is_first_line_of_wrapped_line = current_view_column ==
    current_model_column
  let current_view_line_number = cursor.view_state.position.line_number
  let view_model = CursorMoveModel::View(self)
  let first_non_blank_column = view_model.get_line_first_non_whitespace_column(
    current_view_line_number,
  )
  let is_beginning_of_view_line = current_view_column == first_non_blank_column
  let result = if !is_first_line_of_wrapped_line && !is_beginning_of_view_line {
    ByView(
      cursor_move_to_beginning_of_line(
        view_model,
        cursor.view_state,
        in_selection_mode,
      ),
    )
  } else {
    ByModel(
      cursor_move_to_beginning_of_line(
        Model(self.model),
        cursor.model_state,
        in_selection_mode,
      ),
    )
  }
  self.commit_cursor_move(result, source, reason)
}

///|
/// `CursorMoveCommands._moveToLineEnd`
/// (`cursorMoveCommands.ts:78-117`). `sticky=true` is deliberately rejected:
///
/// the large-leftover sticky arm is the approved CMO-059 deferral.
pub fn ViewModel::move_cursor_to_line_end(
  self : ViewModel,
  in_selection_mode : Bool,
  sticky~ : Bool = false,
  source~ : String = "keyboard",
  reason~ : @editor_api.CursorChangeReason = Explicit,
) -> Bool {
  guard !sticky else { return false }
  let cursor = self.cursor.get_primary_cursor_state()
  let view_position = cursor.view_state.position
  let view_max_column = self.get_line_max_column(view_position.line_number)
  let is_end_of_view_line = view_position.column == view_max_column
  let model_position = cursor.model_state.position
  let model_max_column = self.model.get_line_max_column(
    model_position.line_number,
  )
  let is_end_line_of_wrapped_line = view_max_column - view_position.column ==
    model_max_column - model_position.column
  let result = if is_end_of_view_line || is_end_line_of_wrapped_line {
    ByModel(
      cursor_move_to_end_of_line(
        Model(self.model),
        cursor.model_state,
        in_selection_mode,
      ),
    )
  } else {
    ByView(
      cursor_move_to_end_of_line(
        View(self),
        cursor.view_state,
        in_selection_mode,
      ),
    )
  }
  self.commit_cursor_move(result, source, reason)
}

///|
/// The current reduced `WordOperations.word` seam used by pointer
/// continuation. A model word uses `TextModel.get_word_at_position`; off a
/// word, the single UTF-16 code unit under the pointer is selected, matching
/// the existing readonly pointer behavior. Full token-aware WordOperations is
///
/// the approved D6 deferral.
fn ViewModel::cursor_word_move_result(
  self : ViewModel,
  cursor : @cursor.CursorState,
  in_selection_mode : Bool,
  model_position : @base_common.Position,
) -> CursorMoveResult {
  let (start_column, end_column) = match
    self.model.get_word_at_position(model_position) {
    Some(word) => (word.start_column, word.end_column)
    None => {
      let max_column = self.model.get_line_max_column(
        model_position.line_number,
      )
      (model_position.column, (model_position.column + 1).min(max_column))
    }
  }
  if !in_selection_mode {
    let anchor = @base_common.Position(model_position.line_number, start_column)
    let active = @base_common.Position(model_position.line_number, end_column)
    return ByModel(SingleCursorState(anchor, active, Word, active))
  }
  let anchor = cursor.model_state.selection_start_start
  let active : @base_common.Position = if model_position.line_number <
    anchor.line_number ||
    (
      model_position.line_number == anchor.line_number &&
      model_position.column < anchor.column
    ) {
    Position(model_position.line_number, start_column)
  } else {
    Position(model_position.line_number, end_column)
  }
  ByModel(cursor.model_state.moved(true, active))
}

///|
/// Reduced `CursorMoveCommands.word` command entry. The required model
/// position is validated before the selected-case readonly word seam runs;
/// complete token-aware `WordOperations` remains the approved CMC-033
/// deferral.
pub fn ViewModel::move_cursor_word(
  self : ViewModel,
  model_position : @base_common.Position,
  in_selection_mode : Bool,
  source~ : String = "mouse",
  reason~ : @editor_api.CursorChangeReason = Explicit,
) -> Bool {
  let model_position = self.model.validate_position(model_position)
  let cursor = self.cursor.get_primary_cursor_state()
  self.commit_cursor_move(
    self.cursor_word_move_result(cursor, in_selection_mode, model_position),
    source,
    reason,
  )
}

///|
/// `CursorMoveCommands.line`'s continuing-selection branches
/// (`cursorMoveCommands.ts:179-230`). The supplied view position is retained
///
/// and never round-tripped through model coordinates.
fn ViewModel::cursor_line_move_result(
  self : ViewModel,
  cursor : @cursor.CursorState,
  model_position : @base_common.Position,
  view_position : @base_common.Position,
) -> CursorMoveResult {
  let entering_line_number = cursor.model_state.selection_start_start.line_number
  if model_position.line_number < entering_line_number {
    return ByView(
      cursor.view_state.moved(
        true,
        Position(view_position.line_number, 1),
        leftover_visible_columns=0,
      ),
    )
  }
  if model_position.line_number > entering_line_number {
    let line_count = self.line_count()
    let mut select_to_view_line_number = view_position.line_number + 1
    let mut select_to_view_column = 1
    if select_to_view_line_number > line_count {
      select_to_view_line_number = line_count
      select_to_view_column = self.get_line_max_column(
        select_to_view_line_number,
      )
    }
    return ByView(
      cursor.view_state.moved(
        true,
        Position(select_to_view_line_number, select_to_view_column),
        leftover_visible_columns=0,
      ),
    )
  }
  ByModel(
    cursor.model_state.moved(
      true,
      cursor.model_state.selection_start_end,
      leftover_visible_columns=0,
    ),
  )
}

///|
/// Source-shaped `CursorMoveCommands.line` entry. It accepts the required
/// model position plus the optional already-known view position, covering the
/// supplied/absent validation branches before either the new Line anchor or
/// the continuing-selection branches execute.
pub fn ViewModel::move_cursor_line(
  self : ViewModel,
  model_position : @base_common.Position,
  in_selection_mode : Bool,
  view_position? : @base_common.Position,
  source~ : String = "mouse",
  reason~ : @editor_api.CursorChangeReason = Explicit,
) -> Bool {
  let (model_position, view_position) = self.resolve_cursor_move_position(
    model_position, view_position,
  )
  let cursor = self.cursor.get_primary_cursor_state()
  let result = if !in_selection_mode {
    let line_count = self.model.get_line_count()
    let mut select_to_line_number = model_position.line_number + 1
    let mut select_to_column = 1
    if select_to_line_number > line_count {
      select_to_line_number = line_count
      select_to_column = self.model.get_line_max_column(select_to_line_number)
    }
    let start = @base_common.Position(model_position.line_number, 1)
    let active = @base_common.Position(select_to_line_number, select_to_column)
    ByModel(SingleCursorState(start, active, Line, active))
  } else {
    self.cursor_line_move_result(cursor, model_position, view_position)
  }
  self.commit_cursor_move(result, source, reason)
}

///|
/// `CoreNavigationCommands.LineSelectDrag` (`coreCommands.ts:1705-1730`)
/// always applies the Line continuation algorithm, even when Shift-gutter
/// begins from a Simple cursor. This dedicated entry therefore does not
/// dispatch on the stored anchor kind (unlike generic `move_cursor_to`). The
/// validated view position remains authoritative across wrapping and injected
/// text.
pub fn ViewModel::move_cursor_line_select_to(
  self : ViewModel,
  view_position : @base_common.Position,
  source~ : String = "mouse",
  reason~ : @editor_api.CursorChangeReason = Explicit,
) -> Bool {
  let view_position = self.validate_cursor_move_view_position(view_position)
  let model_position = self.model.validate_position(
    self.coordinates_converter.view_position_to_model_position(view_position),
  )
  self.move_cursor_line(model_position, true, view_position~, source~, reason~)
}

///|
/// Source-shaped `CursorMoveCommands.moveTo` entry. The model position is
/// required and the already-known view position is optional, so tests and
/// root command adapters can exercise both source branches directly.
pub fn ViewModel::move_cursor_to_model_position(
  self : ViewModel,
  model_position : @base_common.Position,
  in_selection_mode : Bool,
  view_position? : @base_common.Position,
  source~ : String = "keyboard",
  reason~ : @editor_api.CursorChangeReason = Explicit,
) -> Bool {
  let cursor = self.cursor.get_primary_cursor_state()
  if in_selection_mode {
    match cursor.model_state.selection_start_kind {
      Word =>
        return self.move_cursor_word(model_position, true, source~, reason~)
      Line =>
        return self.move_cursor_line(
          model_position,
          true,
          view_position?,
          source~,
          reason~,
        )
      Simple => ()
    }
  }
  let (_, view_position) = self.resolve_cursor_move_position(
    model_position, view_position,
  )
  let result = ByView(
    cursor.view_state.moved(
      in_selection_mode,
      view_position,
      leftover_visible_columns=0,
    ),
  )
  self.commit_cursor_move(result, source, reason)
}

///|
/// `CursorMoveCommands.moveTo` (`cursorMoveCommands.ts:251-274`): continuing
/// Word/Line selections dispatch by the stored anchor kind before the Simple
/// view-side move. Model validation belongs to the shared controller seam;
///
/// the supplied view position remains authoritative under wrap/injected text.
pub fn ViewModel::move_cursor_to(
  self : ViewModel,
  view_position : @base_common.Position,
  in_selection_mode : Bool,
  source~ : String = "keyboard",
  reason~ : @editor_api.CursorChangeReason = Explicit,
) -> Bool {
  let view_position = self.validate_cursor_move_view_position(view_position)
  let model_position = self.model.validate_position(
    self.coordinates_converter.view_position_to_model_position(view_position),
  )
  self.move_cursor_to_model_position(
    model_position,
    in_selection_mode,
    view_position~,
    source~,
    reason~,
  )
}

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

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

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

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

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

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

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

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