///|
pub(all) struct ToGet {
  input : ByteCursor
  position : Int
  length : Int
  crypt : PdfStreamDataCrypt
} derive(Debug, Eq, ToJson)

///|
pub(all) enum PdfStreamData {
  StreamGot(PdfBytes)
  StreamToGet(ToGet)
} derive(Debug, Eq, ToJson)

///|
pub(all) enum PdfStreamDataCrypt {
  PdfStreamDataNoCrypt
  PdfStreamDataArc4Decrypt(Int, Int, PdfBytes, Int)
  PdfStreamDataAesV2Decrypt(Int, Int, PdfBytes, Int)
  PdfStreamDataAesV3Decrypt(PdfBytes)
} derive(Debug, Eq, ToJson)

///|
pub(all) struct PdfStream {
  mut dictionary : PdfObject
  mut data : PdfStreamData
} derive(Debug, Eq, ToJson)

///|
pub(all) enum PdfObject {
  PdfNull
  PdfBoolean(Bool)
  PdfInteger(Int)
  PdfReal(Double)
  PdfString(PdfBytes)
  PdfNameObject(PdfName)
  PdfArray(Array[PdfObject])
  PdfDictionary(Array[(PdfName, PdfObject)])
  PdfStreamObject(PdfStream)
  PdfIndirect(Int)
} derive(Debug, Eq, ToJson)

///|
fn pdf_bytes_physical_copy(bytes : PdfBytes) -> PdfBytes {
  Bytes::makei(bytes.length(), i => bytes[i])
}

///|
fn ByteCursor::deep_copy(self : ByteCursor) -> ByteCursor {
  {
    data: self.data,
    position: self.position,
    offset: self.offset,
    source: self.source,
  }
}

///|
fn PdfStreamDataCrypt::deep_copy(
  self : PdfStreamDataCrypt,
) -> PdfStreamDataCrypt {
  match self {
    PdfStreamDataNoCrypt => PdfStreamDataNoCrypt
    PdfStreamDataArc4Decrypt(
      object_number,
      generation,
      file_key,
      key_length_bits
    ) =>
      PdfStreamDataArc4Decrypt(
        object_number,
        generation,
        pdf_bytes_physical_copy(file_key),
        key_length_bits,
      )
    PdfStreamDataAesV2Decrypt(
      object_number,
      generation,
      file_key,
      key_length_bits
    ) =>
      PdfStreamDataAesV2Decrypt(
        object_number,
        generation,
        pdf_bytes_physical_copy(file_key),
        key_length_bits,
      )
    PdfStreamDataAesV3Decrypt(file_key) =>
      PdfStreamDataAesV3Decrypt(pdf_bytes_physical_copy(file_key))
  }
}

///|
fn PdfStreamData::deep_copy(self : PdfStreamData) -> PdfStreamData {
  match self {
    StreamGot(bytes) => StreamGot(pdf_bytes_physical_copy(bytes))
    StreamToGet(to_get) =>
      StreamToGet({
        input: to_get.input.deep_copy(),
        position: to_get.position,
        length: to_get.length,
        crypt: to_get.crypt.deep_copy(),
      })
  }
}

///|
fn PdfObject::deep_copy(self : PdfObject) -> PdfObject {
  match self {
    PdfArray(values) => PdfArray([ for value in values => value.deep_copy() ])
    PdfDictionary(entries) =>
      PdfDictionary([ for entry in entries => (entry.0, entry.1.deep_copy()) ])
    PdfStreamObject(stream) =>
      PdfStreamObject({
        dictionary: stream.dictionary.deep_copy(),
        data: stream.data.deep_copy(),
      })
    _ => self
  }
}

///|
pub fn pdf_array(values : ArrayView[PdfObject]) -> PdfObject {
  PdfArray([ for value in values => value ])
}

///|
pub fn pdf_dictionary(entries : ArrayView[(PdfName, PdfObject)]) -> PdfObject {
  PdfDictionary([ for entry in entries => entry ])
}

///|
pub fn pdf_recurse_array(
  transform : (PdfObject) -> PdfObject,
  values : ArrayView[PdfObject],
) -> PdfObject {
  PdfArray([ for value in values => transform(value) ])
}

///|
pub fn pdf_recurse_dict(
  transform : (PdfObject) -> PdfObject,
  entries : ArrayView[(PdfName, PdfObject)],
  preserve_order? : Bool = false,
) -> PdfObject {
  let transformed : Array[(PdfName, PdfObject)] = [
    for entry in entries => (entry.0, transform(entry.1))
  ]
  if preserve_order {
    PdfDictionary(transformed)
  } else {
    PdfDictionary(transformed.rev())
  }
}

///|
pub fn pdf_stream(dictionary : PdfObject, data : PdfStreamData) -> PdfObject {
  PdfStreamObject({ dictionary, data })
}

///|
pub fn stream_to_get(input : ByteCursor, position : Int, length : Int) -> ToGet {
  { input, position, length, crypt: PdfStreamDataNoCrypt }
}

///|
let pdf_object_length_key_name : PdfName = pdf_name_of_bytes(
  @ascii.encode("/Length"),
)

///|
fn pdf_object_length_name() -> PdfName {
  pdf_object_length_key_name
}

///|
fn PdfStreamDataCrypt::is_identity(self : PdfStreamDataCrypt) -> Bool {
  match self {
    PdfStreamDataNoCrypt => true
    _ => false
  }
}

///|
fn PdfStreamDataCrypt::decrypt(
  self : PdfStreamDataCrypt,
  data : BytesView,
) -> PdfBytes raise PdfError {
  match self {
    PdfStreamDataNoCrypt => data.to_owned()
    PdfStreamDataArc4Decrypt(
      object_number,
      generation,
      file_key,
      key_length_bits
    ) =>
      pdf_arc4_crypt_object(
        object_number,
        generation,
        file_key[:],
        key_length_bits,
        data,
      )
    PdfStreamDataAesV2Decrypt(
      object_number,
      generation,
      file_key,
      key_length_bits
    ) =>
      pdf_aesv2_decrypt_object_data(
        object_number,
        generation,
        file_key[:],
        key_length_bits,
        data,
      )
    PdfStreamDataAesV3Decrypt(file_key) =>
      pdf_aesv3_decrypt_object_data(file_key[:], data)
  }
}

///|
fn pdf_stream_to_get_raw_view(to_get : ToGet) -> BytesView raise PdfError {
  let input = to_get.input()
  let start = to_get.position() + input.offset
  let length = to_get.length()
  if start < 0 {
    raise InvalidCursorPosition(to_get.position())
  }
  if length < 0 {
    raise InvalidReadLength(length)
  }
  let end = start + length
  if end > input.data.length() {
    raise EndOfInput
  }
  input.data[start:end]
}

///|
fn pdf_stream_data_view(data : PdfStreamData) -> BytesView raise PdfError {
  match data {
    StreamGot(bytes) => bytes[:]
    StreamToGet(to_get) => {
      let raw = pdf_stream_to_get_raw_view(to_get)
      if to_get.crypt().is_identity() {
        raw
      } else {
        to_get.crypt().decrypt(raw)[:]
      }
    }
  }
}

///|
fn pdf_stream_data_bytes(data : PdfStreamData) -> PdfBytes raise PdfError {
  match data {
    StreamGot(bytes) => bytes
    StreamToGet(to_get) => {
      let raw = pdf_stream_to_get_raw_view(to_get)
      if to_get.crypt().is_identity() {
        raw.to_owned()
      } else {
        to_get.crypt().decrypt(raw)
      }
    }
  }
}

///|
pub fn PdfObject::stream_bytes(self : PdfObject) -> PdfBytes raise PdfError {
  match self {
    PdfStreamObject(stream) => pdf_stream_data_bytes(stream.data)
    _ => raise ParseStreamExpected
  }
}

///|
pub fn PdfObject::get_stream(self : PdfObject) -> Unit raise PdfError {
  match self {
    PdfStreamObject(stream) =>
      match stream.data {
        StreamGot(_) => ()
        StreamToGet(_) => {
          let data = pdf_stream_data_bytes(stream.data)
          match stream.dictionary {
            PdfDictionary(_) =>
              stream.dictionary = stream.dictionary.replace_dict_entry(
                pdf_object_length_name(),
                PdfInteger(data.length()),
              )
            _ => ()
          }
          stream.data = StreamGot(data)
        }
      }
    _ => raise ParseStreamExpected
  }
}

///|
pub fn ToGet::input(self : ToGet) -> ByteCursor {
  self.input
}

///|
pub fn ToGet::position(self : ToGet) -> Int {
  self.position
}

///|
pub fn ToGet::length(self : ToGet) -> Int {
  self.length
}

///|
pub fn ToGet::crypt(self : ToGet) -> PdfStreamDataCrypt {
  self.crypt
}

///|
pub fn PdfObject::dictionary_view(
  self : PdfObject,
) -> ArrayView[(PdfName, PdfObject)]? {
  match self {
    PdfDictionary(entries) => Some(entries[:])
    PdfStreamObject(stream) =>
      match stream.dictionary {
        PdfDictionary(entries) => Some(entries[:])
        _ => None
      }
    _ => None
  }
}

///|
pub fn PdfObject::lookup_immediate(
  self : PdfObject,
  key : PdfName,
) -> PdfObject? {
  match self.dictionary_view() {
    Some(entries) =>
      for i = 0; i < entries.length(); i = i + 1 {
        let (name, value) = entries[i]
        if name == key {
          break Some(value)
        }
      } nobreak {
        None
      }
    None => None
  }
}

///|
pub fn PdfObject::find_indirect(
  self : PdfObject,
  key : PdfName,
) -> Int? raise PdfError {
  match self {
    PdfDictionary(_) =>
      match self.lookup_immediate(key) {
        Some(PdfIndirect(number)) => Some(number)
        _ => None
      }
    _ => raise DictionaryExpected
  }
}

///|
fn dict_add_entries(
  entries : ArrayView[(PdfName, PdfObject)],
  key : PdfName,
  value : PdfObject,
) -> Array[(PdfName, PdfObject)] {
  let output : Array[(PdfName, PdfObject)] = []
  for i = 0; i < entries.length(); i = i + 1 {
    let (name, old_value) = entries[i]
    if name == key {
      output.push((key, value))
      for j = i + 1; j < entries.length(); j = j + 1 {
        output.push(entries[j])
      }
      break output
    } else {
      output.push((name, old_value))
    }
  } nobreak {
    let reversed : Array[(PdfName, PdfObject)] = [(key, value)]
    for i = entries.length() - 1; i >= 0; i = i - 1 {
      reversed.push(entries[i])
    }
    reversed
  }
}

///|
fn pdf_object_unique_key_candidate(prefix : String, index : Int) -> PdfName {
  let output : Array[Byte] = [47]
  for byte in @ascii.encode(prefix) {
    output.push(byte)
  }
  for byte in @ascii.encode(index.to_string()) {
    output.push(byte)
  }
  pdf_name_of_bytes(Bytes::from_array(output))
}

///|
fn pdf_object_dictionary_has_key(
  entries : ArrayView[(PdfName, PdfObject)],
  key : PdfName,
) -> Bool {
  for entry in entries {
    if entry.0 == key {
      break true
    }
  } nobreak {
    false
  }
}

///|
pub fn PdfObject::unique_key(
  self : PdfObject,
  prefix : String,
) -> PdfName raise PdfError {
  match self.dictionary_view() {
    Some(entries) => {
      let mut index = 0
      let mut candidate = pdf_object_unique_key_candidate(prefix, index)
      while pdf_object_dictionary_has_key(entries, candidate) {
        index += 1
        candidate = pdf_object_unique_key_candidate(prefix, index)
      }
      candidate
    }
    None => raise DictionaryExpected
  }
}

///|
fn dict_replace_entries(
  entries : ArrayView[(PdfName, PdfObject)],
  key : PdfName,
  value : PdfObject,
) -> Array[(PdfName, PdfObject)] raise PdfError {
  let output : Array[(PdfName, PdfObject)] = []
  for i = 0; i < entries.length(); i = i + 1 {
    let (name, old_value) = entries[i]
    if name == key {
      output.push((key, value))
      for j = i + 1; j < entries.length(); j = j + 1 {
        output.push(entries[j])
      }
      break output
    } else {
      output.push((name, old_value))
    }
  } nobreak {
    raise DictionaryKeyNotFound(key)
  }
}

///|
fn dict_remove_entries(
  entries : ArrayView[(PdfName, PdfObject)],
  key : PdfName,
) -> Array[(PdfName, PdfObject)] {
  let output : Array[(PdfName, PdfObject)] = []
  for entry in entries {
    let (name, value) = entry
    if name != key {
      output.push((name, value))
    }
  }
  output
}

///|
pub fn PdfObject::add_dict_entry(
  self : PdfObject,
  key : PdfName,
  value : PdfObject,
) -> PdfObject raise PdfError {
  match self {
    PdfNull => PdfDictionary([(key, value)])
    PdfDictionary(entries) =>
      PdfDictionary(dict_add_entries(entries, key, value))
    PdfStreamObject(stream) => {
      stream.dictionary = stream.dictionary.add_dict_entry(key, value)
      self
    }
    _ => raise DictionaryExpected
  }
}

///|
pub fn PdfObject::replace_dict_entry(
  self : PdfObject,
  key : PdfName,
  value : PdfObject,
) -> PdfObject raise PdfError {
  match self {
    PdfDictionary(entries) =>
      PdfDictionary(dict_replace_entries(entries, key, value))
    PdfStreamObject(stream) => {
      stream.dictionary = stream.dictionary.replace_dict_entry(key, value)
      self
    }
    _ => raise DictionaryKeyNotFound(key)
  }
}

///|
pub fn PdfObject::remove_dict_entry(
  self : PdfObject,
  key : PdfName,
) -> PdfObject raise PdfError {
  match self {
    PdfDictionary(entries) => PdfDictionary(dict_remove_entries(entries, key))
    PdfStreamObject(stream) => {
      stream.dictionary = stream.dictionary.remove_dict_entry(key)
      self
    }
    _ => raise DictionaryExpected
  }
}