// 1:1 port of Monaco's `viewModel/viewModelDecorations.ts` (whole file) plus
// the `viewModelLines.ts:923` `getDecorationsInRange` slice its context
// closure delegates to, from checked-in vscode submodule (decoration plan Phase 3 — this
// replaces the previous simplified eager-conversion `ViewModelDecorations`).
//
// Deviations:
// - The editor configuration reads (`filterValidationDecorations(options)`,
//   `filterFontDecorations(options)`) are resolved once at construction: the
//   view model rotates its decoration provider when those options change
//   (`ViewModel::on_configuration_changed`), so the per-call read has no
//   observable difference. Font decorations are out of scope, so that filter
//   is constantly `false`.
// - `getMinimapDecorationsInRange` is DEFERRED with the minimap view part
//   (plan out-of-scope list); `onlyMinimapDecorations` is carried through the
//   context signature (it only forwards to the out-of-scope bracket provider
//   in the source).
// - `dispose` is folded into `reset` (no dispose lifecycle in the readonly
//   view model; both bodies are identical in the source).

///|
/// `filterValidationDecorations` (`editorOptions.ts`): `editable` defers to
/// `readOnly`, which is constantly `true` for this viewer.
pub fn filter_validation_decorations(
  option : @editor_api.RenderValidationDecorations,
) -> Bool {
  match option {
    Editable => true // editable → readOnly; the viewer is always readonly
    On => false
    Off => true
  }
}

///|
/// `ViewModelDecorations` (`viewModelDecorations.ts:16`): the lazy,
/// viewport-cached decoration provider for one view model.
struct ViewModelDecorations {
  computer : InlineModelDecorationsComputer
  mut cached_model_decorations_resolver : ViewDecorationsCollection?
  mut cached_model_decorations_resolver_view_range : @base_common.Range?
}

///|
/// Constructor (`viewModelDecorations.ts:27-37`): the context closure hands
/// the computer model decorations for a view range via the lines collection.
fn ViewModelDecorations::ViewModelDecorations(
  editor_id : Int32,
  model : @model.TextModel,
  filter_validation : Bool,
  lines : ViewModelLinesFromProjectedModel,
  coordinates_converter : CoordinatesConverter,
) -> ViewModelDecorations {
  let context : InlineModelContext = {
    get_model_decorations: (
      view_range,
      only_minimap_decorations,
      only_margin_decorations,
    ) => {
      view_model_lines_get_decorations_in_range(
        lines, model, coordinates_converter, view_range, editor_id, filter_validation,
        only_minimap_decorations, only_margin_decorations,
      )
    },
  }
  {
    computer: InlineModelDecorationsComputer(
      context, model, coordinates_converter,
    ),
    cached_model_decorations_resolver: None,
    cached_model_decorations_resolver_view_range: None,
  }
}

///|
/// `_clearCachedModelDecorationsResolver` (`viewModelDecorations.ts:39-42`).
fn ViewModelDecorations::clear_cached_model_decorations_resolver(
  self : ViewModelDecorations,
) -> Unit {
  self.cached_model_decorations_resolver = None
  self.cached_model_decorations_resolver_view_range = None
}

///|
/// `reset` (`viewModelDecorations.ts:49-52`; `dispose` has the same body).
fn ViewModelDecorations::reset(self : ViewModelDecorations) -> Unit {
  self.computer.reset()
  self.clear_cached_model_decorations_resolver()
}

///|
/// `onModelDecorationsChanged` (`viewModelDecorations.ts:54-57`).
pub fn ViewModelDecorations::on_model_decorations_changed(
  self : ViewModelDecorations,
) -> Unit {
  self.computer.on_model_decorations_changed()
  self.clear_cached_model_decorations_resolver()
}

///|
/// `onLineMappingChanged` (`viewModelDecorations.ts:59-63`).
fn ViewModelDecorations::on_line_mapping_changed(
  self : ViewModelDecorations,
) -> Unit {
  self.computer.on_line_mapping_changed()
  self.clear_cached_model_decorations_resolver()
}

///|
/// `getDecorationsViewportData` (`viewModelDecorations.ts:69-77`): one-entry
/// cache keyed by the exact view range.
pub fn ViewModelDecorations::get_decorations_viewport_data(
  self : ViewModelDecorations,
  view_range : @base_common.Range,
) -> ViewDecorationsCollection {
  let mut cache_is_valid = self.cached_model_decorations_resolver is Some(_)
  cache_is_valid = cache_is_valid &&
    self.cached_model_decorations_resolver_view_range is Some(cached_range) &&
    cached_range == view_range
  if !cache_is_valid {
    self.cached_model_decorations_resolver = Some(
      self.compute_decorations(view_range),
    )
    self.cached_model_decorations_resolver_view_range = Some(view_range)
  }
  self.cached_model_decorations_resolver.unwrap()
}

///|
fn ViewModelDecorations::compute_decorations(
  self : ViewModelDecorations,
  view_range : @base_common.Range,
) -> ViewDecorationsCollection {
  self.computer.get_decorations(view_range, false, false)
}

///|
/// `getDecorationsInRange` (`viewModelLines.ts:923-975`): view-range query
/// against the model with hidden-line splitting. The fast path
/// covers the no-hidden-lines case; otherwise visible model-line runs are
/// fetched separately, sorted, and deduplicated. Like the source, the
/// split-path requests drop the `onlyMarginDecorations` argument.
fn view_model_lines_get_decorations_in_range(
  lines : ViewModelLinesFromProjectedModel,
  model : @model.TextModel,
  converter : CoordinatesConverter,
  range : @base_common.Range,
  owner_id : Int32,
  filter_out_validation : Bool,
  only_minimap_decorations : Bool,
  only_margin_decorations : Bool,
) -> Array[@model.ModelDecoration] {
  // onlyMinimapDecorations only forwards to the out-of-scope bracket
  // provider in the source's model.getDecorationsInRange.
  ignore(only_minimap_decorations)
  let model_start = converter.view_position_to_model_position(
    Position(range.start_line_number, range.start_column),
  )
  let model_end = converter.view_position_to_model_position(
    Position(range.end_line_number, range.end_column),
  )
  if model_end.line_number - model_start.line_number <=
    range.end_line_number - range.start_line_number {
    // most likely there are no hidden lines => fast path
    // fetch decorations from column 1 to cover the case of wrapped lines that
    // have whole line decorations at column 1
    return model.get_decorations_in_range(
      Range(model_start.line_number, 1, model_end.line_number, model_end.column),
      owner_id~,
      filter_out_validation~,
      only_margin_decorations~,
    )
  }
  let result : Array[@model.ModelDecoration] = []
  let model_start_line_index = model_start.line_number - 1
  let model_end_line_index = model_end.line_number - 1
  let mut req_start : @base_common.Position? = None
  for model_line_index in model_start_line_index..<=model_end_line_index {
    if lines.model_position_is_visible(model_line_index + 1) {
      // merge into previous request
      if req_start is None {
        req_start = Some(
          Position(
            model_line_index + 1,
            if model_line_index == model_start_line_index {
              model_start.column
            } else {
              1
            },
          ),
        )
      }
    } else if req_start is Some(start) {
      // hit invisible line => flush request
      let max_line_column = model.snapshot.get_line_max_column(model_line_index)
      result.append(
        model.get_decorations_in_range(
          Range(
            start.line_number,
            start.column,
            model_line_index,
            max_line_column,
          ),
          owner_id~,
          filter_out_validation~,
        ),
      )
      req_start = None
    }
  }
  if req_start is Some(start) {
    result.append(
      model.get_decorations_in_range(
        Range(
          start.line_number,
          start.column,
          model_end.line_number,
          model_end.column,
        ),
        owner_id~,
        filter_out_validation~,
      ),
    )
    req_start = None
  }
  result.sort_by((a, b) => {
    let res = @base_common.Range::compare_ranges_using_starts(a.range, b.range)
    if res == 0 {
      // JS `<` on decoration ids; ids share one `;` prefix, where
      // MoonBit's length-first compare and JS lexicographic order agree for
      // the numeric suffixes only when lengths match — compare code units.
      @base_common.compare(a.id, b.id)
    } else {
      res
    }
  })

  // Eliminate duplicate decorations that might have intersected our visible
  // ranges multiple times
  let final_result : Array[@model.ModelDecoration] = []
  for dec in result; last_dec_id = "" {
    let dec_id = dec.id
    if last_dec_id == dec_id {
      continue
    }
    final_result.push(dec)
    continue dec_id
  }
  final_result
}