///|
/// Copy at most `length` document values beginning at the zero-based `start`.
///
/// Negative starts and non-positive lengths select an empty sequence. The input
/// values themselves are not modified.
pub fn slice_values(
  values : ArrayView[@model.IonValue],
  start : Int,
  length : Int,
) -> Array[@model.IonValue] {
  let selected : Array[@model.IonValue] = []
  if start < 0 || length <= 0 {
    return selected
  }
  for index, value in values {
    if index >= start && selected.length() < length {
      selected.push(value)
    }
  }
  selected
}

///|
/// Copy document values whose Ion type matches `kind`, preserving their order.
///
/// Type annotations do not affect the match, consistent with `IonValue::kind`.
pub fn filter_values_by_kind(
  values : ArrayView[@model.IonValue],
  kind : @model.IonNull,
) -> Array[@model.IonValue] {
  let selected : Array[@model.IonValue] = []
  for value in values {
    if value.kind() == kind {
      selected.push(value)
    }
  }
  selected
}

///|
/// Concatenate documents in their supplied order after enforcing sequence limits.
///
/// The returned document has no synthetic Ion version marker: version markers are
/// document framing and are emitted by `encode_binary` when configured. Nested
/// values are retained as supplied, while the top-level sequence is newly owned.
pub fn merge_documents(
  documents : ArrayView[Array[@model.IonValue]],
  limits? : @model.Limits,
) -> Array[@model.IonValue] raise @model.IonError {
  let limits = limits.unwrap_or(@model.Limits::default())
  let merged : Array[@model.IonValue] = []
  for document in documents {
    for value in document {
      if merged.length() >= limits.max_values() {
        raise sequence_limit_error(
          @model.LimitKind::Values,
          "merged document exceeds max_values",
        )
      }
      merged.push(value)
    }
  }
  validate_merged_limits(merged[:], limits)
  merged
}

///|
/// Parse a text document, select a bounded range of its values, and encode it.
///
/// Parsing enforces `input_limits`; encoding enforces the limits carried by
/// `options`. This deliberately works on complete, bounded documents rather than
/// presenting itself as a streaming API.
pub fn reencode_text_range(
  input : String,
  start : Int,
  length : Int,
  input_limits? : @model.Limits,
  options? : @model.EncodeOptions,
) -> String raise @model.IonError {
  let values = parse_text(
    input,
    limits=input_limits.unwrap_or(@model.Limits::default()),
  )
  let selected = slice_values(values[:], start, length)
  encode_text(
    selected[:],
    options=options.unwrap_or(@model.EncodeOptions::default()),
  )
}

///|
/// Parse a binary document, select a bounded range of its values, and encode it.
///
/// A selected binary range is written as one new Ion document, so output binary
/// version-marker emission is controlled by `options` rather than copied from the
/// input framing.
pub fn reencode_binary_range(
  input : Bytes,
  start : Int,
  length : Int,
  input_limits? : @model.Limits,
  options? : @model.EncodeOptions,
) -> Bytes raise @model.IonError {
  let values = parse_binary(
    input,
    limits=input_limits.unwrap_or(@model.Limits::default()),
  )
  let selected = slice_values(values[:], start, length)
  encode_binary(
    selected[:],
    options=options.unwrap_or(@model.EncodeOptions::default()),
  )
}

///|
fn validate_merged_limits(
  values : ArrayView[@model.IonValue],
  limits : @model.Limits,
) -> Unit raise @model.IonError {
  let stats = analyze(values)
  if stats.value_count() > limits.max_values() {
    raise sequence_limit_error(
      @model.LimitKind::Values,
      "merged document exceeds max_values",
    )
  }
  if stats.max_depth() > limits.max_depth() {
    raise sequence_limit_error(
      @model.LimitKind::Depth,
      "merged document exceeds max_depth",
    )
  }
  if stats.symbol_count() > limits.max_symbols() {
    raise sequence_limit_error(
      @model.LimitKind::SymbolCount,
      "merged document exceeds max_symbols",
    )
  }
  if stats.text_bytes > limits.max_text_bytes() {
    raise sequence_limit_error(
      @model.LimitKind::TextBytes,
      "merged document exceeds max_text_bytes",
    )
  }
  if stats.blob_bytes > limits.max_blob_bytes() {
    raise sequence_limit_error(
      @model.LimitKind::BlobBytes,
      "merged document exceeds max_blob_bytes",
    )
  }
  for value in values {
    if container_limit_exceeded(value, limits.max_container_items()) {
      raise sequence_limit_error(
        @model.LimitKind::ContainerItems,
        "merged document exceeds max_container_items",
      )
    }
  }
}

///|
fn container_limit_exceeded(value : @model.IonValue, limit : Int) -> Bool {
  match value {
    @model.IonValue::Annotated(_, nested) =>
      container_limit_exceeded(nested, limit)
    @model.IonValue::List(items) | @model.IonValue::Sexp(items) =>
      if items.length() > limit {
        true
      } else {
        for item in items {
          if container_limit_exceeded(item, limit) {
            return true
          }
        }
        false
      }
    @model.IonValue::Struct(fields) =>
      if fields.length() > limit {
        true
      } else {
        for field in fields {
          if container_limit_exceeded(field.1, limit) {
            return true
          }
        }
        false
      }
    _ => false
  }
}

///|
fn sequence_limit_error(
  kind : @model.LimitKind,
  message : String,
) -> @model.IonError {
  @model.IonError::Limit(kind~, offset=0, line=1, column=1, message~)
}