///|
priv enum XRef {
  Plain(Int, Int)
  Stream(Int, Int)
}

///|
priv enum XRefLine {
  Invalid
  Section(Int, Int) // second Int (length) is unused but required for parsing
  Valid(Int, Int)
  Free(Int, Int) // both Ints are unused but required for parsing
  InObjectStream(Int, Int)
  StreamFree(Int, Int) // constructed but not read in current implementation
  XRefNull // constructed but not read in current implementation
  Finished
}

///|
fn parse_digits(chars : StringView, start : Int, len : Int) -> Int? {
  if start < 0 || len < 0 || start + len > chars.length() {
    return None
  }
  let mut value = 0
  for i in start..<(start + len) {
    let c = chars[i]
    if c < '0' || c > '9' {
      return None
    }
    value = value * 10 + (c.to_int() - '0'.to_int())
  }
  Some(value)
}

///|
fn count_leading_spaces(chars : StringView) -> Int {
  let mut i = 0
  for ch in chars {
    if ch == ' ' {
      i = i + 1
    } else {
      break
    }
  }
  i
}

///|
fn split_fields(line : String) -> Array[String] {
  let parts = Array::new()
  for part in line.split(" ") {
    let s = part.to_string()
    if s != "" {
      parts.push(s)
    }
  }
  parts
}

///|
fn parse_uint_string(s : String) -> Int? {
  if s == "" {
    return None
  }
  let mut value = 0
  for c in s {
    if c < '0' || c > '9' {
      return None
    }
    value = value * 10 + (c.to_int() - '0'.to_int())
  }
  Some(value)
}

///|
fn read_xref_line(input : @pdfio.Input) -> XRefLine raise {
  let pos = (input.pos_in)()
  @pdfsyntax.PdfSyntax::new().dropwhite(input)
  let line = @pdfsyntax.PdfSyntax::new().input_line(input)
  if line == "xref" || line == "xref " {
    return read_xref_line(input)
  }
  let chars = line[:]
  let offset_opt = if chars.length() >= 18 {
    parse_digits(chars, 0, 10)
  } else {
    None
  }
  let gen_opt = if chars.length() >= 18 {
    parse_digits(chars, 11, 5)
  } else {
    None
  }
  match (offset_opt, gen_opt) {
    (Some(offset), Some(gen)) =>
      if chars.length() > 17 {
        let mark = chars[17]
        if mark is 'n' {
          return Valid(offset, gen)
        } else if mark is 'f' {
          return Free(offset, gen)
        } else {
          return Invalid
        }
      } else {
        return Invalid
      }
    _ => ()
  }
  if chars.length() > 17 {
    let mark = chars[17]
    if mark is 'n' && parse_digits(chars, 0, 10) is Some(0) {
      return Free(0, 0)
    }
  }
  if line.has_prefix("trailer") {
    let leading_spaces = count_leading_spaces(chars)
    (input.seek_in)(pos + 7 + leading_spaces)
    return Finished
  }
  let parts = split_fields(line)
  match parts {
    ["xref", first, second, ..] | [first, second, ..] =>
      match (parse_uint_string(first), parse_uint_string(second)) {
        (Some(s), Some(l)) => return Section(s, l)
        _ => ()
      }
    _ => ()
  }
  Invalid
}

///|
fn read_xref(input : @pdfio.Input) -> Array[(Int, XRef)] raise {
  fn fail_read() -> Unit raise {
    let message = @pdf.input_pdferror(input, "Could not read x-ref table")
    raise @pdf.PdfError::Msg(message)
  }

  let xrefs = Array::new()
  let mut finished = false
  let mut objnumber = 0
  while !finished {
    match read_xref_line(input) {
      Invalid => fail_read()
      Valid(offset, gen) => {
        xrefs.push((objnumber, XRef::Plain(offset, gen)))
        objnumber = objnumber + 1
      }
      Free(offset, gen) => {
        ignore(offset)
        ignore(gen)
        objnumber = objnumber + 1
      }
      Section(start, len) => {
        ignore(len)
        objnumber = start
      }
      Finished => finished = true
      _ => ()
    }
  }
  xrefs
}

///|
/// local methods
fn[A] Array::get_slice(
  self : Array[A],
  start : Int,
  finish : Int,
) -> ArrayView[A]? {
  if start < 0 || finish > self.length() || start > finish {
    return None
  }
  Some(self[start:finish])
}

///|
test "Array:get_slice" {
  let data = [1, 2, 3, 4, 6]
  debug_inspect(data.get_slice(0, 0), content="Some()")
  debug_inspect(data.get_slice(0, 3), content="Some()")
  debug_inspect(data.get_slice(1, 4), content="Some()")
  debug_inspect(
    data.get_slice(0, 5),
    content="Some()",
  )
  debug_inspect(data.get_slice(-1, 3), content="None")
}

///|
fn read_xref_line_stream(
  input : @pdfio.Input,
  w1 : Int,
  w2 : Int,
  w3 : Int,
) -> XRefLine raise {
  fn read_field(bytes : Int) -> Int raise {
    if bytes == 0 {
      return 0
    }
    let mut value = 0
    for _ in 0.. StreamFree(f2, f3)
    1 => Valid(f2, f3)
    2 => InObjectStream(f2, f3)
    _ => XRefNull
  }
}

///|

///|
fn read_xref_stream(
  ctx : PdfRead,
  input : @pdfio.Input,
) -> (Array[(Int, XRef)], Int, @pdf.PdfObject) raise {
  let original_pos = (input.pos_in)()
  let err = @pdf.PdfError::Msg(@pdf.input_pdferror(input, "Bad xref stream"))
  let dict_level : Ref[Int] = { val: 0 }
  let array_level : Ref[Int] = { val: 0 }
  let xrefstream_objectnumber = match
    @pdfsyntax.PdfSyntax::new().lex_next(
      dict_level,
      array_level,
      false,
      input,
      Array::new(),
      true,
      _ => [],
    ) {
    LexInt(i) => i
    _ => {
      (ctx.logger)("couldn't lex object number\n")
      raise err
    }
  }
  if ctx.read_debug {
    (ctx.logger)(
      "Object number of this xref stream is \{xrefstream_objectnumber}\n",
    )
  }
  (input.seek_in)(original_pos)
  fn lex_untilstream(
    input : @pdfio.Input,
    read_stream_data : Bool,
  ) -> Array[@pdfgenlex.Token] {
    let lexemes = Array::new()
    let dict_level : Ref[Int] = { val: 0 }
    let array_level : Ref[Int] = { val: 0 }
    let mut done = false
    while !done {
      let t = @pdfsyntax.PdfSyntax::new().lex_next(
        dict_level,
        array_level,
        true,
        input,
        lexemes,
        read_stream_data,
        _ => [],
      )
      match t {
        StopLexing | LexNone => done = true
        _ => lexemes.push(t)
      }
    }
    lexemes
  }

  let dictlex = lex_untilstream(input, true)
  if dictlex.length() < 2 {
    raise err
  }
  let (objnum, gen) = match dictlex {
    [LexInt(o), LexInt(g), ..] => (o, g)
    _ => raise err
  }
  let stream_token = @pdfsyntax.PdfSyntax::new().lex_stream(
    input,
    dictlex,
    _ => [],
    true,
  )
  let tokens = Array::new()
  for t in dictlex {
    tokens.push(t)
  }
  tokens.push(stream_token)
  tokens.push(LexEndStream)
  tokens.push(LexEndObj)
  let (_, parsed) = @pdfsyntax.PdfSyntax::new().parse(tokens)
  let stream_obj = match parsed {
    @pdf.PdfObject::Stream(_) => parsed
    _ => raise err
  }
  @pdfcodec.PdfCodec::new().decode_pdfstream(@pdf.Pdf::empty(), stream_obj)
  stream_obj.getstream()
  let (w1, w2, w3) = match @pdf.Pdf::empty().lookup_direct("/W", stream_obj) {
    Some(Array([Integer(a), Integer(b), Integer(c)])) => (a, b, c)
    _ => raise err
  }
  let raw_input = match stream_obj {
    Stream(r) =>
      match r.val {
        (_, @pdf.Stream::Got(bytes)) => @pdfio.Input::of_bytes(bytes)
        _ => raise err
      }
    _ => raise err
  }
  let xrefs_raw = Array::new()
  try {
    while true {
      xrefs_raw.push(read_xref_line_stream(raw_input, w1, w2, w3))
    }
  } catch {
    _ => ()
  }
  let starts_and_lens = match
    @pdf.Pdf::empty().lookup_direct("/Index", stream_obj) {
    Some(Array(elts)) => {
      // PDF spec: /Index is an array of (start, count) pairs, so it must be even-length.
      let pairs = Array::new()
      // TODO(upstream): `;` should be optional
      for i = 0; i < elts.length(); {
        match elts.get_slice(i, i + 2) {
          Some([Integer(s), Integer(l)]) => {
            pairs.push((s, l))
            continue i + 2
          }
          _ =>
            raise @pdf.PdfError::Msg(
              @pdf.input_pdferror(input, "Bad /Index entry"),
            )
        }
      }
      pairs
    }
    Some(_) =>
      raise @pdf.PdfError::Msg(@pdf.input_pdferror(input, "Unknown /Index"))
    None => {
      let size = match @pdf.Pdf::empty().lookup_direct("/Size", stream_obj) {
        Some(@pdf.PdfObject::Integer(s)) => s
        _ =>
          raise @pdf.PdfError::Msg(
            @pdf.input_pdferror(input, "Missing /Size in xref dict"),
          )
      }
      [(0, size)]
    }
  }
  let xrefs = Array::new()
  let mut raw_pos = 0
  for pair in starts_and_lens {
    let (start, len) = pair
    let mut objnumber = start
    let mut i = 0
    while i < len {
      match xrefs_raw.get(raw_pos) {
        None =>
          raise @pdf.PdfError::Msg(
            @pdf.input_pdferror(input, "Bad xref stream"),
          )
        Some(Valid(offset, gen)) =>
          xrefs.push((objnumber, XRef::Plain(offset, gen)))
        Some(InObjectStream(stream, index)) =>
          xrefs.push((objnumber, XRef::Stream(stream, index)))
        Some(StreamFree(offset, gen)) => {
          ignore(offset)
          ignore(gen)
        }
        Some(XRefNull) => ()
        _ => ()
      }
      objnumber = objnumber + 1
      raw_pos = raw_pos + 1
      i = i + 1
    }
  }
  (input.seek_in)(original_pos)
  let _ = objnum
  let _ = gen
  (xrefs, xrefstream_objectnumber, stream_obj)
}

///|
fn read_int_from_stream(input : @pdfio.Input) -> Int raise {
  @pdfsyntax.PdfSyntax::new().dropwhite(input)
  match @pdfsyntax.PdfSyntax::new().lex_number(input) {
    LexInt(i) => i
    _ => {
      let message = @pdf.input_pdferror(input, "objstm offset problem")
      raise @pdf.PdfError::Msg(message)
    }
  }
}

///|
fn parse_object_stream(
  input : @pdfio.Input,
  xrefs : Map[Int, XRef],
  objstm : Int,
  user_password : String?,
  owner_password : String?,
  partial_pdf : @pdf.Pdf,
  indexes? : Array[Int],
) -> Array[(Int, @pdf.PdfObject)] raise {
  let lexemes = lex_object_for_xref(input, xrefs, true, objstm)
  let (_, stmobj) = @pdfsyntax.PdfSyntax::new().parse(lexemes)
  let gen = match xrefs.get(objstm) {
    Some(XRef::Plain(_, g)) => g
    _ => 0
  }
  let stmobj = @pdfcrypt.PdfCrypt::new().decrypt_single_stream(
    user_password, owner_password, partial_pdf, objstm, gen, stmobj,
  )
  match stmobj {
    @pdf.PdfObject::Stream(_) => ()
    _ => {
      let message = @pdf.input_pdferror(
        input, "lex_stream_object: not a stream",
      )
      raise @pdf.PdfError::Msg(message)
    }
  }
  @pdfcodec.PdfCodec::new().decode_pdfstream(partial_pdf, stmobj)
  stmobj.getstream()
  let (dict, stream_bytes) = match stmobj {
    @pdf.PdfObject::Stream(r) => r.val
    _ => raise @pdf.PdfError::Msg(@pdf.input_pdferror(input, "Bad objstm"))
  }
  let n = match @pdf.Pdf::empty().lookup_direct("/N", dict) {
    Some(@pdf.PdfObject::Integer(v)) => v
    _ => raise @pdf.PdfError::Msg(@pdf.input_pdferror(input, "malformed /N"))
  }
  let first = match @pdf.Pdf::empty().lookup_direct("/First", dict) {
    Some(@pdf.PdfObject::Integer(v)) => v
    _ =>
      raise @pdf.PdfError::Msg(@pdf.input_pdferror(input, "malformed /First"))
  }
  let raw = match stream_bytes {
    @pdf.Stream::Got(bytes) => bytes
    _ =>
      raise @pdf.PdfError::Msg(
        @pdf.input_pdferror(input, "couldn't decode objstream"),
      )
  }
  let stm_input = @pdfio.Input::of_bytes(raw)
  let rawnums = Array::new()
  let mut i = 0
  while i < n * 2 {
    rawnums.push(read_int_from_stream(stm_input))
    i = i + 1
  }
  let (use_indexes, sorted_indexes) = match indexes {
    Some(values) => {
      let ordered = values.copy()
      ordered.sort_by((a, b) => a - b)
      (true, ordered)
    }
    None => (false, Array::new())
  }
  let objects = Array::new()
  let mut idx = 0
  let mut current_index = 0
  let mut wanted_pos = 0
  while idx < rawnums.length() {
    if use_indexes && wanted_pos >= sorted_indexes.length() {
      break
    }
    let objnum = match rawnums.get(idx) {
      Some(value) => value
      None => raise @pdf.PdfError::Msg(@pdf.input_pdferror(input, "Bad objstm"))
    }
    let offset = match rawnums.get(idx + 1) {
      Some(value) => value
      None => raise @pdf.PdfError::Msg(@pdf.input_pdferror(input, "Bad objstm"))
    }
    let should_parse = if use_indexes {
      let want = match sorted_indexes.get(wanted_pos) {
        Some(value) => value
        None => break
      }
      if current_index == want {
        wanted_pos = wanted_pos + 1
        true
      } else {
        false
      }
    } else {
      true
    }
    if should_parse {
      (stm_input.seek_in)(offset + first)
      let lexemes = @pdfsyntax.PdfSyntax::new().lex_object_at(
        true,
        stm_input,
        true,
        _ => [],
      )
      let (_, obj) = @pdfsyntax.PdfSyntax::new().parse(lexemes)
      objects.push((objnum, obj))
    }
    current_index = current_index + 1
    idx = idx + 2
  }
  objects
}

///|
fn build_pdf_objects(
  ctx : PdfRead,
  input : @pdfio.Input,
  xref_entries : Array[(Int, XRef)],
  read_stream_data : Bool,
  trailerdict : @pdf.PdfObject,
  user_password : String?,
  owner_password : String?,
) -> @pdf.PdfObjects {
  let xrefs : Map[Int, XRef] = Map::new()
  for pair in xref_entries {
    xrefs.set(pair.0, pair.1)
  }
  let objects : @pdf.PdfObjMap = Map::new()
  let object_stream_ids : Map[Int, Int] = Map::new()
  let stream_id_seen : Map[Int, Bool] = Map::new()
  let stream_groups : Map[Int, Array[(Int, Int)]] = Map::new()
  let mut max_obj = 0
  let stream_ids = Array::new()
  for pair in xref_entries {
    let objnum = pair.0
    let xref = pair.1
    if objnum > max_obj {
      max_obj = objnum
    }
    match xref {
      XRef::Plain(_, gen) =>
        objects.set(objnum, ({ val: @pdf.ObjectData::ToParse }, gen))
      XRef::Stream(stream_id, _index) => {
        if stream_id_seen.get(stream_id) is None {
          stream_id_seen.set(stream_id, true)
          stream_ids.push(stream_id)
        }
        object_stream_ids.set(objnum, stream_id)
        let entries = match stream_groups.get(stream_id) {
          Some(existing) => existing
          None => {
            let created = Array::new()
            stream_groups.set(stream_id, created)
            created
          }
        }
        entries.push((objnum, _index))
      }
    }
  }
  fn parse_obj(num : Int) -> @pdf.PdfObject {
    try {
      let lexemes = lex_object_for_xref(input, xrefs, read_stream_data, num)
      let (_, obj) = @pdfsyntax.PdfSyntax::new().parse(lexemes)
      obj
    } catch {
      _ => {
        if ctx.read_debug {
          (ctx.logger)("parse_obj failed for object \{num}\n")
        }
        @pdf.PdfObject::Null
      }
    }
  }

  let partial_pdf : @pdf.Pdf = {
    major: 0,
    minor: 0,
    root: 0,
    objects: {
      max_obj_num: max_obj,
      parse: Some(parse_obj),
      objects,
      object_stream_ids,
    },
    trailerdict,
    was_linearized: false,
    saved_encryption: None,
  }
  if read_stream_data {
    for stream_id in stream_ids {
      let parsed = parse_object_stream(
        input, xrefs, stream_id, user_password, owner_password, partial_pdf,
      ) catch {
        _ => {
          (ctx.logger)("Warning: unable to parse object stream \{stream_id}\n")
          Array::new()
        }
      }
      for pair in parsed {
        let objnum = pair.0
        let obj = pair.1
        objects.set(
          objnum,
          ({ val: @pdf.ObjectData::ParsedAlreadyDecrypted(obj) }, 0),
        )
        object_stream_ids.set(objnum, stream_id)
      }
    }
  } else {
    let themap : Map[Int, Array[Int]] = Map::new()
    for group in stream_groups {
      let entries = group.1
      for entry in entries {
        let objnum = entry.0
        let index = entry.1
        let indexes = match themap.get(objnum) {
          Some(existing) => existing
          None => {
            let created = Array::new()
            themap.set(objnum, created)
            created
          }
        }
        indexes.push(index)
      }
    }
    fn parse_stream(
      stream_objnum : Int,
      indexes : Array[Int],
    ) -> Array[(Int, (Ref[@pdf.ObjectData], Int))] {
      try {
        let parsed = parse_object_stream(
          input,
          xrefs,
          stream_objnum,
          user_password,
          owner_password,
          partial_pdf,
          indexes~,
        )
        let out = Array::new(capacity=parsed.length())
        for pair in parsed {
          out.push(
            (
              pair.0,
              (Ref::new(@pdf.ObjectData::ParsedAlreadyDecrypted(pair.1)), 0),
            ),
          )
        }
        out
      } catch {
        _ => Array::new()
      }
    }

    for group in stream_groups {
      let stream_id = group.0
      let entries = group.1
      for entry in entries {
        let objnum = entry.0
        let index = entry.1
        objects.set(
          objnum,
          (
            {
              val: @pdf.ObjectData::ToParseFromObjectStream(
                themap, stream_id, index, parse_stream,
              ),
            },
            0,
          ),
        )
      }
    }
  }
  { max_obj_num: max_obj, parse: Some(parse_obj), objects, object_stream_ids }
}