///|
/// Backend-neutral document view model: the backing text model plus line
/// projection, decorations, and coordinate conversion. Long-lived like
/// Monaco's `ViewModel` (`viewModelImpl.ts`): constructed once at model
/// attach and updated in place through `on_configuration_changed` and
/// `on_did_change_content_or_injected_text`. Like Monaco, the line collection
/// is always `ViewModelLinesFromProjectedModel` — no-wrap documents stay
/// cheap through per-line `Identity` projections, not through a swapped
/// collection.
pub struct ViewModel {
  model : @model.TextModel
  mut options : ViewModelOptions
  lines : ViewModelLinesFromProjectedModel
  /// Outgoing-only cursor/ViewZones half of Monaco's
  /// `ViewModelEventDispatcher`. The mixed browser-view collector remains in
  /// the browser tier.
  cursor_event_dispatcher : CursorEventDispatcher
  /// Borrowed exact handle returned by TextModel attachment. ModelData alone
  /// owns its detach lifetime; ViewModel only publishes visible ranges.
  priv attached_view : @model.AttachedViewImpl?
  /// ViewModel's earlier scroll listener publishes unstable visible ranges
  /// before the root's later scroll/render listener.
  priv mut visible_lines_subscription : @base_common.Disposable?
  /// Constructor-owned model token listener (`viewModelImpl.ts:510-523`).
  /// Dispose it before the outgoing dispatcher so no late callback can
  /// enqueue into a retired ViewModel.
  priv mut model_tokens_subscription : @base_common.Disposable?
  /// The cursor state, owned per view model like Monaco's `_cursor`
  /// (`viewModelImpl.ts:65,124`): a new model starts with a fresh cursor at
  /// the origin, and reflows re-point it internally
  /// (`_cursor.onLineMappingChanged`, `viewModelImpl.ts:287`).
  cursor : @cursor.CursorsController
  /// The scroll/layout model, owned per view model like Monaco's
  /// `public readonly viewLayout` (`viewModelImpl.ts:64,126`): a new model
  /// starts at the scroll origin structurally. The viewport box is seeded
  /// from the widget's measured dimensions after construction (Monaco's
  /// `ViewLayout` reads `EditorOption.layoutInfo` from the configuration).
  view_layout : @view_layout.ViewLayout
  mut decorations : ViewModelDecorations
  coordinates_converter : CoordinatesConverter
  /// Per-source hidden-area sets, merged before they reach the line
  /// collection — Monaco's `hiddenAreasModel` (`viewModelImpl.ts:70`).
  priv hidden_areas_model : HiddenAreasModel
  /// The last merged hidden-area union, for `setHiddenAreas`'s no-change
  /// short-circuit — Monaco's `previousHiddenAreas` (`viewModelImpl.ts:71`).
  priv mut previous_hidden_areas : Array[@base_common.Range]
  /// Registration returned by `TextModel::register_view_model`
  /// (`viewModelImpl.ts:169`). Disposing it performs the source's
  /// `unregisterViewModel(this)` without relying on object identity.
  priv mut model_registration : @base_common.Disposable?
}

///|
/// Creates a view model with default no-wrap options.
pub fn ViewModel::ViewModel(model : @model.TextModel) -> ViewModel {
  ViewModel::with_options(model, ViewModelOptions::no_wrap())
}

///|
/// Creates a view model with wrapping/tab options — the line-collection
/// construction of Monaco's `ViewModel` constructor
/// (`viewModelImpl.ts:98-122`).
pub fn ViewModel::with_options(
  model : @model.TextModel,
  options : ViewModelOptions,
  smooth_scrolling~ : Bool = false,
  schedule_at_next_animation_frame~ : (() -> Unit) -> @base_common.Disposable = view_model_no_animation_scheduler,
  now~ : () -> Double = view_model_zero_clock,
  attached_view? : @model.AttachedViewImpl,
  emit_view_tokens_changed~ : (Array[ViewTokensChangedRange]) -> Unit = view_model_no_token_event_sink,
) -> ViewModel {
  let lines = ViewModelLinesFromProjectedModel(
    model,
    font_info=options.font_info,
    tab_size=options.tab_size,
    wrapping_column=options.soft_wrap_column,
    wrapping_indent=options.wrapping_indent,
  )
  let coordinates_converter = lines.create_coordinates_converter()
  // Monaco's member order: `_lines` → `coordinatesConverter` → `_cursor` →
  // `viewLayout` (`viewModelImpl.ts:122-126`) → `_decorations` (`:146`).
  let cursor_event_dispatcher = CursorEventDispatcher::CursorEventDispatcher()
  let cursor = @cursor.CursorsController(
    model,
    coordinates_converter.cursor_context(),
  )
  let view_layout = @view_layout.ViewLayout(
    line_height=options.font_info.line_height,
    line_count=lines.get_view_line_count(),
    smooth_scrolling~,
    schedule_at_next_animation_frame~,
    now~,
  )
  let decorations = ViewModelDecorations(
    options.editor_id,
    model,
    filter_validation_decorations(options.render_validation_decorations),
    lines,
    coordinates_converter,
  )
  let view_model : ViewModel = {
    model,
    options,
    lines,
    cursor_event_dispatcher,
    attached_view,
    visible_lines_subscription: None,
    model_tokens_subscription: None,
    cursor,
    view_layout,
    decorations,
    coordinates_converter,
    hidden_areas_model: HiddenAreasModel(),
    previous_hidden_areas: [],
    model_registration: None,
  }
  // `this.model.registerViewModel(this)` — the constructor tail
  // (`viewModelImpl.ts:169`). The callback registration keeps the model
  // package independent of this concrete type.
  view_model.model_registration = Some(
    model.register_view_model(
      e => view_model.on_did_change_content_or_injected_text(e~),
      e => view_model.emit_content_change_event(e),
    ),
  )
  view_model.visible_lines_subscription = Some(
    view_layout.on_did_scroll(change => {
      if change.scroll_top_changed {
        view_model.handle_visible_lines_changed()
      }
    }),
  )
  // Preserve `viewModelImpl.ts:510-523`: convert every inclusive model range
  // through the live projection, finish browser ViewEvent delivery, then
  // enqueue the original model event through the outgoing-only dispatcher.
  view_model.model_tokens_subscription = Some(
    model.on_did_change_tokens(event => {
      let view_ranges : Array[ViewTokensChangedRange] = []
      for model_range in event.ranges {
        let view_start = view_model.model_position_to_view_position(
          Position(model_range.from_line_number, 1),
        )
        let view_end = view_model.model_position_to_view_position(
          Position(
            model_range.to_line_number,
            model.get_line_max_column(model_range.to_line_number),
          ),
        )
        view_ranges.push({
          from_line_number: view_start.line_number,
          to_line_number: view_end.line_number,
        })
      }
      emit_view_tokens_changed(view_ranges)
      view_model.emit_model_tokens_changed(event)
    }),
  )
  view_model
}

///|
/// Detaches this view model from its model — the live half of Monaco's
/// `ViewModel.dispose` (`viewModelImpl.ts:172-181`):
/// `this.model.unregisterViewModel(this)`. The outgoing dispatcher owns its
/// registered cursor listeners and is retired here before layout teardown.
/// Called from the Viewer's `ModelData::dispose`.
pub fn ViewModel::dispose(self : ViewModel) -> Unit {
  if self.visible_lines_subscription is Some(subscription) {
    subscription.dispose()
  }
  self.visible_lines_subscription = None
  if self.model_tokens_subscription is Some(subscription) {
    subscription.dispose()
  }
  self.model_tokens_subscription = None
  self.cursor_event_dispatcher.dispose()
  self.lines.dispose()
  self.view_layout.dispose()
  if self.model_registration is Some(registration) {
    registration.dispose()
  }
  self.model_registration = None
}

///|
fn view_model_no_animation_scheduler(
  _callback : () -> Unit,
) -> @base_common.Disposable {
  @base_common.Disposable::from(() => ())
}

///|
fn view_model_zero_clock() -> Double {
  0.0
}

///|
/// Applies changed options to the live view model — Monaco's
/// `ViewModel._onConfigurationChanged` (`viewModelImpl.ts:274`) over the one
/// options struct: `_lines.setWrappingSettings` + `setTabSize` update the
/// collection in place, and a changed decoration filter input
/// (`renderValidationDecorations`, `editorId`) rotates the decoration
/// provider (Monaco's `_decorations.reset()` — the viewer's provider closes
/// over the filter at construction). Returns whether the view-line mapping
/// changed (Monaco emits `ViewFlushedEvent`/`ViewLineMappingChangedEvent`),
/// so the caller re-flushes the layout.
pub fn ViewModel::on_configuration_changed(
  self : ViewModel,
  options : ViewModelOptions,
) -> Bool {
  let previous = self.options
  if previous == options {
    return false
  }
  self.options = options
  let wrapping_changed = self.lines.set_wrapping_settings(
    options.font_info,
    options.soft_wrap_column,
    options.wrapping_indent,
  )
  let tab_size_changed = self.lines.set_tab_size(options.tab_size)
  let mapping_changed = wrapping_changed || tab_size_changed
  if mapping_changed {
    self.decorations.on_line_mapping_changed()
    // `this._cursor.onLineMappingChanged(...)` (`viewModelImpl.ts:287`): the
    // view model re-points its own cursor, re-projecting the selection from
    // its model positions.
    self.cursor.set_context(self.coordinates_converter.cursor_context())
  }
  if previous.editor_id != options.editor_id ||
    previous.render_validation_decorations !=
    options.render_validation_decorations {
    self.decorations = ViewModelDecorations(
      options.editor_id,
      self.model,
      filter_validation_decorations(options.render_validation_decorations),
      self.lines,
      self.coordinates_converter,
    )
  }
  mapping_changed
}

///|
/// Content or injected text changed — Monaco's
/// `onDidChangeContentOrInjectedText` (`viewModelImpl.ts:319-457`, called
/// directly by the text model), reduced to the two forms the viewer
/// produces. `e = None` is the `ModelInjectedTextChangedEvent` arm (the
/// union member is not ported — `text_model_events.mbt` header): Monaco
/// re-runs the line-breaks computer per changed line, the viewer reprojects
/// whole. `e = Some(_)` is the `InternalModelContentChangeEvent` arm, whose
/// only constructed change today is `Flush` (`:356-363`); the per-line
/// branches (`:364-408`) are the tier-2 lane. The root Viewer brackets the
/// reduced whole-document projection and emits the corresponding typed
/// `ViewFlushed`/mapping/decoration events through its View collector.
pub fn ViewModel::on_did_change_content_or_injected_text(
  self : ViewModel,
  e? : @model.InternalModelContentChangeEvent,
) -> Unit {
  match e {
    None => {
      self.lines.on_injected_text_changed()
      self.decorations.reset()
      // The reprojection changed the view-line mapping; re-point the cursor
      // (Monaco's injected-text arm updates cursor view positions through
      // `_cursor.onModelContentChanged`'s `setStates` re-project).
      self.cursor.set_context(self.coordinates_converter.cursor_context())
    }
    Some(e) => {
      for change in e.raw_content_changed_event.changes {
        match change {
          ModelRawFlush => {
            // The `Flush` branch (`viewModelImpl.ts:356-363`):
            // `_lines.onModelFlushed()` → `ViewFlushedEvent` (emitted by the
            // root source collector) → `_decorations.reset()` →
            // `viewLayout.onFlushed(lineCount)`. The custom-line-heights
            // argument is N-A (uniform line height).
            self.lines.on_model_flushed()
            self.decorations.reset()
            self.view_layout.on_flushed(self.lines.get_view_line_count())
          }
          // LinesDeleted/LinesInserted/LineChanged (`:364-404`): tier-2
          // (never constructed today). EOLChanged (`:405-408`): no-op in
          // the source; the EOL lane is a recorded scope cut.
          _ => ()
        }
      }
      // `_lines.acceptVersionId` (`:412-414`): N-A — the wholesale-rebuild
      // collection keeps no version assertion state.
      // Custom line heights (`:417-426`): N-A (uniform line height).
      // `viewLayout.onHeightMaybeChanged()` (`:428`).
      self.view_layout.on_height_maybe_changed() |> ignore
      // The `hadModelLineChangeThatChangedLineMapping` branch (`:430-435`)
      // is tier-2-only: a flush sets `hadOtherModelChange` instead.
      // `_viewportStart` invalidate + recovery (`:441-454`): scope cut 3 —
      // the recovery is gated on >= 2 attached editors; the single-editor
      // scroll clamps through the layout like Monaco's single-editor case.
      // `_configuration.setModelLineCount`/`_updateConfigurationViewLineCountNow`
      // (`:443-444`): the Viewer re-derives wrap facts in its content
      // listener.
    }
  }
  // `_handleVisibleLinesChanged` (`viewModelImpl.ts:456`) runs after the
  // projection/layout update so background priority observes current ranges.
  self.handle_visible_lines_changed()
}

///|
/// The text model's loop-two callback — `emitContentChangeEvent`
/// (`viewModelImpl.ts:462-469`). The outgoing `ModelContentChangedEvent` is
/// re-fired by the Viewer from the model's public emitter (the viewer's
/// outgoing-event seam), so the live half here is
/// `_cursor.onModelContentChanged`.
fn ViewModel::emit_content_change_event(
  self : ViewModel,
  e : @model.InternalModelContentChangeEvent,
) -> Unit {
  self.emit_cursor_state_change(self.cursor.on_model_content_changed(e))
  |> ignore
}

///|
/// The line collection for this view model.
pub fn ViewModel::lines(self : ViewModel) -> ViewModelLinesFromProjectedModel {
  self.lines
}

///|
/// ModelDecorationOptions buckets for this view model.
pub fn ViewModel::decorations(self : ViewModel) -> ViewModelDecorations {
  self.decorations
}

///|
/// Concrete coordinate converter owned by this view model. Callers erase it
/// only at lower-tier boundaries that genuinely retain heterogeneous
/// converters, such as `CursorContext`.
pub fn ViewModel::coordinates_converter(
  self : ViewModel,
) -> CoordinatesConverter {
  self.coordinates_converter
}

///|
/// Snaps a view position at injected-text/wrap boundaries per the affinity —
/// `ViewModel.normalizePosition` (`viewModelImpl.ts:1281-1283`).
pub fn ViewModel::normalize_position(
  self : ViewModel,
  position : @base_common.Position,
  affinity : @model.PositionAffinity,
) -> @base_common.Position {
  self.lines.normalize_position(position, affinity)
}

///|
/// Number of view lines.
pub fn ViewModel::line_count(self : ViewModel) -> Int32 {
  self.lines.get_view_line_count()
}

///|
/// The injected-text source index at a view position, or `-1`
/// for source text — the viewer's index-valued form of Monaco's
/// `ViewModel.getInjectedTextAt` (`viewModelImpl.ts`, delegating to the line
/// projection).
pub fn ViewModel::injected_text_index_at(
  self : ViewModel,
  position : @base_common.Position,
) -> Int32 {
  self.lines.injected_text_index_at(position.line_number, position.column - 1)
}

///|
/// Source document version.
pub fn ViewModel::version(self : ViewModel) -> Int32 {
  self.model.version
}

///|
/// Source document language id.
pub fn ViewModel::language_id(self : ViewModel) -> String {
  self.model.language_id
}

///|
/// Clamps a 1-based half-open view-line window to the document, the same
/// clamping the viewport-data builder applies (`viewport_data_from_view_model`)
/// so window consumers (the DOM recycler, hit tests) agree on the rendered
/// range.
pub fn ViewModel::clamp_window(
  self : ViewModel,
  window : @view_layout.LineWindow,
) -> @view_layout.LineWindow {
  let start = window.start.clamp(min=1, max=self.line_count())
  let end = window.end.clamp(min=start + 1, max=self.line_count() + 1)
  { start, end, }
}

///|
/// Monaco `ViewModel.getLineContent`: the text of a view line.
pub fn ViewModel::get_line_content(
  self : ViewModel,
  line_number : Int32,
) -> String {
  // Text-only callers must not enter `getViewLineData`, whose token field is
  // a passive store read. Monaco's `getViewLineContent` delegates directly to
  // the projection (`viewModelImpl.ts` / `viewModelLines.ts`).
  self.lines.get_view_line_content(line_number)
}

///|
/// Monaco `ViewModel.getLineLength`: the text length of a view line.
pub fn ViewModel::get_line_length(
  self : ViewModel,
  line_number : Int32,
) -> Int32 {
  self.lines.get_view_line_content(line_number).length()
}

///|
/// Monaco `ViewModel.getInjectedTextAt` (`viewModelImpl.ts`): the injected
/// text at a view position, or `None` on source text. The object is
/// re-derived from the position's model line (the projections retain only
/// per-column indices); `source_index` is the decoration's `attached_index`,
/// the same value `injected_text_index_at` reports.
pub fn ViewModel::get_injected_text_at(
  self : ViewModel,
  position : @base_common.Position,
) -> InjectedText? {
  let index = self.injected_text_index_at(position)
  if index < 0 {
    return None
  }
  let info = self.lines.get_view_line_info(position.line_number)
  for text in injected_text_for_model_line(self.model, info.model_line_number) {
    if text.source_index == index {
      return Some(text)
    }
  }
  None
}