///|
/// Per-model-line projection, Monaco's `IModelLineProjection`
/// (`modelLineProjection.ts:16`) with the package's enum dispatch standing in
/// for interface polymorphism. `Identity` is Monaco's
/// `IdentityModelLineProjection.INSTANCE`: a visible line with no wrapping
/// and no injected text that delegates straight to the model, so the common
/// no-wrap document allocates no per-line projection data at all. `Hidden` is
/// Monaco's `HiddenModelLineProjection.INSTANCE`: a line hidden by
/// `setHiddenAreas` (folding), producing zero view lines. `Projected` is
/// Monaco's `ModelLineProjection` class, used to wrap model lines and inject
/// text; it carries its own visibility like Monaco's `_isVisible`.
///
/// Content queries on an invisible projection abort — the `Hidden` arms are
/// the port of both `HiddenModelLineProjection`'s `throw new Error('Not
/// supported')` and `ModelLineProjection._assertVisible` (an invisible
/// `Projected` is not asserted per method; only valid view lines reach these
/// accessors).
pub enum ModelLineProjection {
  Identity
  Hidden
  Projected(ModelLineProjectionData, is_visible~ : Bool)
}

///|
/// `createModelLineProjection` (`modelLineProjection.ts:52`): `None`
/// line-break data means no mapping is needed.
pub fn create_model_line_projection(
  data : ModelLineProjectionData?,
  is_visible : Bool,
) -> ModelLineProjection {
  match data {
    None => if is_visible { Identity } else { Hidden }
    Some(data) => Projected(data, is_visible~)
  }
}

///|
/// Whether this model line is visible — `isVisible`.
pub fn ModelLineProjection::is_visible(self : ModelLineProjection) -> Bool {
  match self {
    Identity => true
    Hidden => false
    Projected(_, is_visible~) => is_visible
  }
}

///|
/// Returns this projection with the given visibility — `setVisible`
/// (`modelLineProjection.ts:22`, "this invalidates the current instance"):
/// callers store the returned projection back, exactly Monaco's
/// `projections[i] = projections[i].setVisible(v)` pattern. The identity
/// singletons swap between `Identity` and `Hidden`.
pub fn ModelLineProjection::set_visible(
  self : ModelLineProjection,
  is_visible : Bool,
) -> ModelLineProjection {
  match self {
    Identity | Hidden => if is_visible { Identity } else { Hidden }
    Projected(data, ..) => Projected(data, is_visible~)
  }
}

///|
/// Number of view lines this model line produces — `getViewLineCount`:
/// zero while hidden.
pub fn ModelLineProjection::get_view_line_count(
  self : ModelLineProjection,
) -> Int32 {
  match self {
    Identity => 1
    Hidden => 0
    Projected(data, is_visible~) =>
      if is_visible {
        data.view_line_count()
      } else {
        0
      }
  }
}

///|
/// Text of one view line — `getViewLineContent`: the projected segment's
/// slice with the continuation indent prepended (`spaces(...) + r`), or the
/// model line as-is.
pub fn ModelLineProjection::get_view_line_content(
  self : ModelLineProjection,
  model : @model.TextModel,
  model_line_number : Int32,
  output_line_index : Int32,
) -> String {
  match self {
    Identity => model.snapshot.line_text(model_line_number - 1)
    Hidden => abort("Not supported")
    Projected(data, ..) => {
      let segment = data.segment_at_view_line_index(output_line_index)
      let content = data.projected_line.text[segment.start_offset:segment.end_offset].to_owned()
      if segment.wrapped_text_indent_length > 0 {
        spaces(segment.wrapped_text_indent_length) + content
      } else {
        content
      }
    }
  }
}

///|
/// Minimum one-based column of one view line — `getViewLineMinColumn`
/// (`modelLineProjection.ts:141-144`): `getMinOutputOffset + 1`, i.e. the
/// continuation indent width plus one (view columns are segment-local; the
/// segment's `start_column` is a projected-line column, a different axis).
pub fn ModelLineProjection::get_view_line_min_column(
  self : ModelLineProjection,
  output_line_index : Int32,
) -> Int32 {
  match self {
    Identity => 1
    Hidden => abort("Not supported")
    Projected(data, ..) =>
      data.segment_at_view_line_index(output_line_index).wrapped_text_indent_length +
      1
  }
}

///|
/// Maximum one-based column of one view line — `getViewLineMaxColumn`.
pub fn ModelLineProjection::get_view_line_max_column(
  self : ModelLineProjection,
  model : @model.TextModel,
  model_line_number : Int32,
  output_line_index : Int32,
) -> Int32 {
  match self {
    Identity => model.snapshot.line_text(model_line_number - 1).length() + 1
    Hidden => abort("Not supported")
    Projected(data, ..) => {
      // `getMaxOutputOffset + 1` (`modelLineProjectionData.ts:73-79`): the
      // segment's rendered length (indent included) plus one — segment-local,
      // like `get_view_line_min_column`.
      let segment = data.segment_at_view_line_index(output_line_index)
      segment.end_offset -
      segment.start_offset +
      segment.wrapped_text_indent_length +
      1
    }
  }
}

///|
/// The view-line data of one view line — `getViewLineData`. The identity arm
/// is Monaco's `IdentityModelLineProjection.getViewLineData`
/// (`modelLineProjection.ts:298`): the model line whole, tokens inflated, no
/// injected inline decorations.
pub fn ModelLineProjection::get_view_line_data(
  self : ModelLineProjection,
  model : @model.TextModel,
  model_line_number : Int32,
  output_line_index : Int32,
  base_view_line_number : Int32,
) -> ViewLineData {
  match self {
    Identity => {
      let line_tokens = model.tokenization.get_line_tokens(model_line_number)
      let content = line_tokens.get_line_content()
      {
        content,
        continues_with_wrapped_line: false,
        min_column: 1,
        max_column: content.length() + 1,
        start_visible_column: 0,
        tokens: line_tokens.inflate(),
        inline_decorations: [],
      }
    }
    Hidden => abort("Not supported")
    Projected(_, ..) => {
      let result : Array[ViewLineData?] = [None]
      ignore(
        self.get_view_lines_data(
          model,
          model_line_number,
          output_line_index,
          1,
          base_view_line_number,
          0,
          [true],
          result,
        ),
      )
      result[0].unwrap()
    }
  }
}

///|
/// Batch form of `get_view_line_data` (`modelLineProjection.ts:160-184,
/// 312-320`). A projected model line performs exactly one passive token-store
/// read after computing its injected-text decorations, including when every
/// requested output is unneeded. Identity retains its distinct early return.
fn ModelLineProjection::get_view_lines_data(
  self : ModelLineProjection,
  model : @model.TextModel,
  model_line_number : Int32,
  output_line_index : Int32,
  line_count : Int32,
  base_view_line_number : Int32,
  global_start_index : Int32,
  needed : Array[Bool],
  result : Array[ViewLineData?],
) -> Bool {
  match self {
    Identity => {
      if !needed[global_start_index] {
        result[global_start_index] = None
        return false
      }
      result[global_start_index] = Some(
        self.get_view_line_data(
          model, model_line_number, 0, base_view_line_number,
        ),
      )
      true
    }
    Hidden => abort("Not supported")
    Projected(data, is_visible~) => {
      if !is_visible {
        abort("Not supported")
      }

      // `modelLineProjection.ts:163-176`: materialize the five-callback
      // injected-decoration context and compute the per-output decorations
      // before the passive token-store read below. This ordering is observable
      // even for an all-false `needed` mask.
      let injection_offsets = injection_offsets_for_projected_line(
        data.projected_line,
      )
      let injection_options = injection_options_for_projected_line(
        data.projected_line,
      )
      let resolved_injection_options = resolve_injection_options(
        injection_options[:],
      )
      let break_offsets = data.segments.map(segment => segment.end_offset)
      let wrapped_text_indent_length = if data.segments.length() > 1 {
        data.segments[1].wrapped_text_indent_length
      } else {
        0
      }
      let context : InjectedTextInlineDecorationsComputerContext = {
        get_injection_options: _ => {
          if resolved_injection_options.length() == 0 {
            None
          } else {
            Some(resolved_injection_options)
          }
        },
        get_injection_offsets: _ => {
          if injection_offsets.length() == 0 {
            None
          } else {
            Some(injection_offsets)
          }
        },
        get_break_offsets: _ => break_offsets,
        get_wrapped_text_indent_length: _ => wrapped_text_indent_length,
        get_base_view_line_number: _ => base_view_line_number,
      }
      let prepared = prepare_projected_line_demand(
        context,
        model_line_number,
        injection_options,
        injection_offsets,
        () => model.tokenization.get_line_tokens(model_line_number),
      )
      let line_inline_decorations = prepared.line_inline_decorations
      let with_injections = prepared.line_with_injections
      for local_index in 0.. 0 {
          Some(line_inline_decorations[requested_output_line_index])
        } else {
          None
        }
        result[global_index] = Some(
          view_line_data_for_segment_with_injections(
            with_injections,
            data.segment_at_view_line_index(requested_output_line_index),
            inline_decorations,
          ),
        )
      }
      true
    }
  }
}

///|
/// Model column for a view position on one view line —
/// `getModelColumnOfViewPosition` (`translateToInputOffset`, which undoes the
/// continuation indent and clamps into the segment).
pub fn ModelLineProjection::get_model_column_of_view_position(
  self : ModelLineProjection,
  output_line_index : Int32,
  output_column : Int32,
) -> Int32 {
  match self {
    Identity => output_column
    Hidden => abort("Not supported")
    Projected(data, ..) => {
      let segment = data.segment_at_view_line_index(output_line_index)
      let width = segment.end_offset - segment.start_offset
      let projected_column = segment.start_offset +
        (output_column - 1 - segment.wrapped_text_indent_length).clamp(
          min=0,
          max=width,
        )
      data.projected_line.projected_column_to_model_column(projected_column) + 1
    }
  }
}

///|
/// View position of a model column — `getViewPositionOfModelPosition`
/// (`translateToOutputPosition(inputColumn - 1, affinity).toPosition(delta)`).
/// `delta_line_number` is the model line's first view line, so a continuation
/// segment adds its index; the view column regains the continuation indent
/// (Monaco's `getOutputPositionOfInputOffset`).
pub fn ModelLineProjection::get_view_position_of_model_position(
  self : ModelLineProjection,
  delta_line_number : Int32,
  input_column : Int32,
  affinity? : @model.PositionAffinity,
) -> @base_common.Position {
  match self {
    Identity => {
      ignore(affinity)
      Position(delta_line_number, input_column)
    }
    Hidden => abort("Not supported")
    Projected(data, ..) => {
      let model_column = (input_column - 1).clamp(min=0, max=data.line_length)
      let projected_column = data.projected_line.model_column_to_projected_column(
        model_column,
        affinity?,
      )
      let segment_index = data.view_line_index_for_projected_column(
        projected_column,
        affinity?,
      )
      let segment = data.segment_at_view_line_index(segment_index)
      Position(
        delta_line_number + segment_index,
        projected_column -
        segment.start_offset +
        1 +
        segment.wrapped_text_indent_length,
      )
    }
  }
}

///|
/// Snaps a view position at an injected-text or wrap boundary to the side the
/// affinity asks for — `normalizePosition` (`modelLineProjection.ts:237-241`)
/// over `normalizeOutputPosition` (`modelLineProjectionData.ts:169-192`).
pub fn ModelLineProjection::normalize_position(
  self : ModelLineProjection,
  output_line_index : Int32,
  position : @base_common.Position,
  affinity : @model.PositionAffinity,
) -> @base_common.Position {
  match self {
    Identity => position
    Hidden => abort("Not supported")
    Projected(data, ..) => {
      let segment = data.segment_at_view_line_index(output_line_index)
      let width = segment.end_offset - segment.start_offset
      // `outputPositionToOffsetInInputWithInjections`: undo the continuation
      // indent and clamp into the segment.
      let output_offset = (position.column -
      1 -
      segment.wrapped_text_indent_length).clamp(min=0, max=width)
      let projected = segment.start_offset + output_offset
      let normalized = data.projected_line.normalize_projected_column_around_injections(
        projected, affinity,
      )
      if normalized != projected {
        // Injected text caused a change: re-derive the view position with the
        // same affinity, relative to this model line's first view line.
        let base_view_line_number = position.line_number - output_line_index
        let segment_index = data.view_line_index_for_projected_column(
          normalized,
          affinity~,
        )
        let target = data.segment_at_view_line_index(segment_index)
        return Position(
          base_view_line_number + segment_index,
          normalized -
          target.start_offset +
          1 +
          target.wrapped_text_indent_length,
        )
      }
      // Wrap-boundary snap: `Left` at the line's min output offset moves to
      // the previous view line's max; `Right` at max moves to the next line's
      // min.
      match affinity {
        Left =>
          if output_line_index > 0 && output_offset == 0 {
            let previous = data.segment_at_view_line_index(
              output_line_index - 1,
            )
            Position(
              position.line_number - 1,
              previous.end_offset -
              previous.start_offset +
              1 +
              previous.wrapped_text_indent_length,
            )
          } else {
            position
          }
        Right =>
          if output_line_index + 1 < data.view_line_count() &&
            output_offset == width {
            Position(
              position.line_number + 1,
              1 +
              data.segment_at_view_line_index(output_line_index + 1).wrapped_text_indent_length,
            )
          } else {
            position
          }
        _ => position
      }
    }
  }
}

///|
/// The injected-text source index at a 0-based view column of
/// one view line, or `-1` for source text (including the synthetic
/// continuation indent) — the index-valued form of Monaco's
/// `getInjectedTextAt`.
pub fn ModelLineProjection::injected_text_index_at(
  self : ModelLineProjection,
  output_line_index : Int32,
  column : Int32,
) -> Int32 {
  match self {
    Identity => -1
    Hidden => abort("Not supported")
    Projected(data, ..) => {
      let segment = data.segment_at_view_line_index(output_line_index)
      let indent = segment.wrapped_text_indent_length
      if column < indent {
        return -1
      }
      let offset = segment.start_offset + (column - indent)
      if offset >= segment.end_offset {
        return -1
      }
      data.projected_line.injected_text_index_at_projected_column(offset)
    }
  }
}

///|
/// Builds one segment after the batch caller has shared the model line's
/// passive token read, injection splice, and source-shaped decoration
/// computation. The source decoration ranges carry absolute view-line numbers;
/// this local renderer adapter converts the selected line to zero-based offsets.
fn view_line_data_for_segment_with_injections(
  with_injections : @tokens.LineTokens,
  segment : ModelLineProjectionSegment,
  inline_decorations : Array[InlineDecoration]?,
) -> ViewLineData {
  let indent = segment.wrapped_text_indent_length
  let start_offset = segment.start_offset
  let end_offset = segment.end_offset
  let tokens = with_injections.slice_and_inflate(
    start_offset, end_offset, indent,
  )
  let content = tokens.get_line_content()
  let content = if indent > 0 { spaces(indent) + content } else { content }
  let inline_decorations = match inline_decorations {
    Some(decorations) =>
      decorations.map(decoration => {
        @view_layout.InlineDecoration(
          OffsetRange(
            decoration.range.start_column - 1,
            decoration.range.end_column - 1,
          ),
          decoration.inline_class_name,
        )
      })
    None => []
  }
  {
    content,
    continues_with_wrapped_line: segment.continues_with_wrapped_line,
    // Segment-local view columns (`getViewLineMinColumn`/`MaxColumn`): the
    // indent-prefixed rendered content's bounds.
    min_column: segment.wrapped_text_indent_length + 1,
    max_column: content.length() + 1,
    // The renderer's whitespace math counts code units in Int columns;
    // Monaco feeds the raw (possibly fractional) number into the same
    // formulas. Truncation only diverges when `columnsForFullWidthChar` is
    // fractional, and only for whitespace glyph tab stops.
    start_visible_column: segment.start_visible_column.to_int(),
    tokens,
    inline_decorations,
  }
}

///|
/// A run of `n` spaces — the leading indent prepended to a wrapped continuation
/// line. Monaco's `spaces(count)`.
fn spaces(n : Int32) -> String {
  " ".repeat(n.max(0))
}

///|
/// Source-shaped injection options captured by the batch context.
fn injection_options_for_projected_line(
  projected_line : ProjectedTextLine,
) -> Array[@model.InjectedTextOptions] {
  projected_line.parts.filter_map(part => {
    match part.kind {
      InjectedSegment(class_name~, ..) =>
        Some(
          InjectedTextOptions(
            projected_line.text[part.start:part.end].to_owned(),
            inline_class_name?=if class_name == "" {
              None
            } else {
              Some(class_name)
            },
          ),
        )
      SourceSegment(_) => None
    }
  })
}

///|
/// Resolves the canonical model injection options consumed by the projection
/// into the complete source fields used by the decoration computer.
fn resolve_injection_options(
  options : ArrayView[@model.InjectedTextOptions],
) -> Array[ResolvedInjectedTextOptions] {
  options.map(option => {
    ResolvedInjectedTextOptions(
      content=option.content(),
      inline_class_name?=option.inline_class_name(),
      // DEFERRED: the canonical model option has no
      // inlineClassNameAffectsLetterSpacing field.
      inline_class_name_affects_letter_spacing=false,
    )
  })
}

///|
/// Source-shaped injection offsets captured by the batch context.
fn injection_offsets_for_projected_line(
  projected_line : ProjectedTextLine,
) -> Array[Int32] {
  projected_line.parts.filter_map(part => {
    match part.kind {
      InjectedSegment(model_column~, ..) => Some(model_column)
      SourceSegment(_) => None
    }
  })
}

///|
/// Monaco's five-callback `IInjectedTextInlineDecorationsComputerContext`.
/// Keeping this literal callback shape makes ordering and the
/// `baseViewLineNumber` value directly executable in white-box tests.
priv struct ResolvedInjectedTextOptions {
  content : String
  inline_class_name : String?
  inline_class_name_affects_letter_spacing : Bool
}

///|
fn ResolvedInjectedTextOptions::ResolvedInjectedTextOptions(
  content~ : String,
  inline_class_name? : String,
  inline_class_name_affects_letter_spacing~ : Bool = false,
) -> ResolvedInjectedTextOptions {
  { content, inline_class_name, inline_class_name_affects_letter_spacing, }
}

///|
priv struct InjectedTextInlineDecorationsComputerContext {
  get_injection_options : (Int32) -> Array[ResolvedInjectedTextOptions]?
  get_injection_offsets : (Int32) -> Array[Int32]?
  get_break_offsets : (Int32) -> Array[Int32]
  get_wrapped_text_indent_length : (Int32) -> Int32
  get_base_view_line_number : (Int32) -> Int32
}

///|
/// Monaco's `InjectedTextInlineDecorationsComputer` retained at the exact
/// projection seam rather than precomputing decorations while constructing the
/// projected line.
priv struct InjectedTextInlineDecorationsComputer {
  context : InjectedTextInlineDecorationsComputerContext
}

///|
fn InjectedTextInlineDecorationsComputer::InjectedTextInlineDecorationsComputer(
  context : InjectedTextInlineDecorationsComputerContext,
) -> InjectedTextInlineDecorationsComputer {
  { context, }
}

///|
/// `inlineDecorations.ts:213-267`, including callback order, wrapped clipping,
/// continuation indent, and absolute view-line ranges.
fn InjectedTextInlineDecorationsComputer::get_inline_decorations(
  self : InjectedTextInlineDecorationsComputer,
  model_line_number : Int32,
) -> Array[Array[InlineDecoration]] {
  let injection_offsets = (self.context.get_injection_offsets)(
    model_line_number,
  )
  guard injection_offsets is Some(injection_offsets) else { [] }
  let line_inline_decorations : Array[Array[InlineDecoration]] = []
  let mut total_injected_text_length_before = 0
  let mut current_injected_offset = 0
  let injection_options = (self.context.get_injection_options)(
    model_line_number,
  )
  let break_offsets = (self.context.get_break_offsets)(model_line_number)
  for output_line_index in 0.. 0 {
      break_offsets[output_line_index - 1]
    } else {
      0
    }
    let line_end_offset_in_input_with_injections = break_offsets[output_line_index]
    while current_injected_offset < injection_offsets.length() {
      let options = injection_options.unwrap()[current_injected_offset]
      let length = options.content.length()
      let injected_text_start_offset_in_input_with_injections = injection_offsets[current_injected_offset] +
        total_injected_text_length_before
      let injected_text_end_offset_in_input_with_injections = injected_text_start_offset_in_input_with_injections +
        length
      if injected_text_start_offset_in_input_with_injections >
        line_end_offset_in_input_with_injections {
        break
      }
      if line_start_offset_in_input_with_injections <
        injected_text_end_offset_in_input_with_injections {
        if options.inline_class_name is Some(inline_class_name) {
          let wrapped_text_indent_length = (self.context.get_wrapped_text_indent_length)(
            model_line_number,
          )
          let offset = if output_line_index > 0 {
            wrapped_text_indent_length
          } else {
            0
          }
          let start = offset +
            (injected_text_start_offset_in_input_with_injections -
            line_start_offset_in_input_with_injections).max(0)
          let end = offset +
            (injected_text_end_offset_in_input_with_injections -
            line_start_offset_in_input_with_injections).min(
              line_end_offset_in_input_with_injections -
              line_start_offset_in_input_with_injections,
            )
          if start != end {
            let view_line_number = (self.context.get_base_view_line_number)(
                model_line_number,
              ) +
              output_line_index
            inline_decorations.push(
              InlineDecoration(
                Range(view_line_number, start + 1, view_line_number, end + 1),
                inline_class_name,
                if options.inline_class_name_affects_letter_spacing {
                  RegularAffectingLetterSpacing
                } else {
                  Regular
                },
              ),
            )
          }
        }
      }
      if injected_text_end_offset_in_input_with_injections <=
        line_end_offset_in_input_with_injections {
        total_injected_text_length_before += length
        current_injected_offset += 1
      } else {
        break
      }
    }
  }
  line_inline_decorations
}

///|
/// The exact ordered source seam at `modelLineProjection.ts:175-178`:
/// construct/use the decoration computer, then passively read the model-line
/// tokens, then splice injections into that one shared value. Keeping the token
/// read as the `ISimpleModel.tokenization` dependency closure gives white-box
/// tests executable ordering evidence without widening the package API.
priv struct PreparedProjectedLineDemand {
  line_inline_decorations : Array[Array[InlineDecoration]]
  line_with_injections : @tokens.LineTokens
}

///|
fn prepare_projected_line_demand(
  context : InjectedTextInlineDecorationsComputerContext,
  model_line_number : Int32,
  injection_options : Array[@model.InjectedTextOptions],
  injection_offsets : Array[Int32],
  read_line_tokens : () -> @tokens.LineTokens,
) -> PreparedProjectedLineDemand {
  let computer = InjectedTextInlineDecorationsComputer::InjectedTextInlineDecorationsComputer(
    context,
  )
  let line_inline_decorations = computer.get_inline_decorations(
    model_line_number,
  )
  let line_tokens = read_line_tokens()
  let line_with_injections = line_tokens.with_inserted(
    inserted_tokens_from_injection_data(injection_options, injection_offsets),
  )
  { line_inline_decorations, line_with_injections, }
}

///|
/// Monaco's `getLineTokensWithInjections` adapter over the same options and
/// offsets read by the decoration context.
fn inserted_tokens_from_injection_data(
  injection_options : Array[@model.InjectedTextOptions],
  injection_offsets : Array[Int32],
) -> Array[@tokens.InsertedToken] {
  let inserts : Array[@tokens.InsertedToken] = []
  for index, options in injection_options {
    inserts.push(
      InsertedToken(
        injection_offsets[index],
        options.content(),
        @tokens.LineTokens::default_token_metadata(),
      ),
    )
  }
  inserts
}