///|
/// A parsed indirect object with object number, generation, and payload.
pub(all) struct PdfParsedIndirectObject {
  number : Int
  generation : Int
  object : PdfObject
} derive(Debug, Eq, ToJson)

///|
fn pdf_parse_indirect_object_at(
  lexemes : ArrayView[PdfLexeme],
  index : Int,
) -> (PdfParsedIndirectObject, Int) raise @core.PdfError {
  let (object, next, complete) = pdf_parse_indirect_object_segment_at(
    lexemes, index,
  )
  ignore(complete)
  (object, next)
}

///|
/// Parse an indirect-object segment and report whether `endobj` was consumed.
pub fn pdf_parse_indirect_object_segment_at(
  lexemes : ArrayView[PdfLexeme],
  index : Int,
) -> (PdfParsedIndirectObject, Int, Bool) raise @core.PdfError {
  let first = pdf_skip_comments(lexemes, index)
  if first + 2 >= lexemes.length() {
    raise ParseIndirectObjectExpected
  }
  match (lexemes[first], lexemes[first + 1], lexemes[first + 2]) {
    (LexInt(number), LexInt(generation), LexObj) => {
      let object_start = pdf_skip_comments(lexemes, first + 3)
      if object_start < lexemes.length() && lexemes[object_start] == LexEndObj {
        ({ number, generation, object: PdfNull, }, object_start + 1, true)
      } else {
        let (object, after_object) = pdf_parse_value_at(lexemes, object_start)
        let end_position = pdf_skip_comments(lexemes, after_object)
        if end_position < lexemes.length() && lexemes[end_position] == LexEndObj {
          ({ number, generation, object, }, end_position + 1, true)
        } else {
          ({ number, generation, object, }, after_object, false)
        }
      }
    }
    _ => raise ParseIndirectObjectExpected
  }
}

///|
/// Parse an indirect-object prefix, tolerating a missing trailing `endobj`.
pub fn pdf_parse_indirect_object_prefix_lexemes(
  lexemes : ArrayView[PdfLexeme],
) -> PdfParsedIndirectObject raise @core.PdfError {
  let first = pdf_skip_comments(lexemes, 0)
  if first + 2 >= lexemes.length() {
    raise ParseIndirectObjectExpected
  }
  match (lexemes[first], lexemes[first + 1], lexemes[first + 2]) {
    (LexInt(number), LexInt(generation), LexObj) => {
      let object_start = pdf_skip_comments(lexemes, first + 3)
      let object = if object_start < lexemes.length() &&
        lexemes[object_start] == LexEndObj {
        PdfNull
      } else {
        let (object, _) = pdf_parse_value_at(lexemes, object_start)
        object
      }
      { number, generation, object, }
    }
    _ => raise ParseIndirectObjectExpected
  }
}

///|
/// Parses one indirect object from lexemes.
///
/// Raises `@core.PdfError::ParseIndirectObjectExpected` if the input does not start
/// with an indirect object header.
pub fn pdf_parse_indirect_object_lexemes(
  lexemes : ArrayView[PdfLexeme],
) -> PdfParsedIndirectObject raise @core.PdfError {
  let (object, _) = pdf_parse_indirect_object_at(lexemes, 0)
  object
}

///|
/// Parses one indirect object from owned PDF bytes.
pub fn pdf_parse_indirect_object_from_bytes(
  data : @core.PdfBytes,
) -> PdfParsedIndirectObject raise @core.PdfError {
  pdf_parse_indirect_object_from_view(data)
}

///|
/// Parses one indirect object from a byte view.
pub fn pdf_parse_indirect_object_from_view(
  data : BytesView,
) -> PdfParsedIndirectObject raise @core.PdfError {
  pdf_parse_indirect_object_lexemes(pdf_lex_view(data))
}

///|
/// Parses every complete indirect object from a lexeme sequence.
///
/// Raises `@core.PdfError::ParseIndirectObjectExpected` when an expected indirect
/// object header is malformed.
pub fn pdf_parse_indirect_objects(
  lexemes : ArrayView[PdfLexeme],
) -> Array[PdfParsedIndirectObject] raise @core.PdfError {
  let objects : Array[PdfParsedIndirectObject] = Array(
    capacity=(lexemes.length() + 3) / 4,
  )
  let mut current = 0
  while current < lexemes.length() {
    current = pdf_skip_comments(lexemes, current)
    if current < lexemes.length() {
      let (object, next) = pdf_parse_indirect_object_at(lexemes, current)
      objects.push(object)
      current = next
    }
  }
  objects
}

///|
/// Parse complete indirect-object segments and return a trailing prefix if present.
pub fn pdf_parse_indirect_object_segments(
  lexemes : ArrayView[PdfLexeme],
) -> (Array[PdfParsedIndirectObject], PdfParsedIndirectObject?) raise @core.PdfError {
  let objects : Array[PdfParsedIndirectObject] = Array(
    capacity=(lexemes.length() + 3) / 4,
  )
  let mut prefix : PdfParsedIndirectObject? = None
  let mut current = 0
  let mut done = false
  while !done && current < lexemes.length() {
    current = pdf_skip_comments(lexemes, current)
    if current < lexemes.length() {
      let (object, next, complete) = pdf_parse_indirect_object_segment_at(
        lexemes, current,
      )
      if complete {
        objects.push(object)
        current = next
      } else {
        let next_object = pdf_skip_comments(lexemes, next)
        if pdf_lexeme_indirect_object_header_at(lexemes, next_object) {
          objects.push(object)
          current = next_object
        } else {
          prefix = Some(object)
          done = true
        }
      }
    }
  }
  (objects, prefix)
}