///|
fn pdf_id_name() -> @core.PdfName {
  @core.pdf_name_of_bytes(b"/ID")
}

///|
fn pdf_reproducible_ids_enabled() -> Bool {
  @env.get_env_var("CAMLPDF_REPRODUCIBLE_IDS") == Some("true")
}

///|
fn pdf_id_ocaml_string_of_float(value : Double) -> String {
  if value.is_nan() {
    "nan"
  } else if value.is_pos_inf() {
    "inf"
  } else if value.is_neg_inf() {
    "-inf"
  } else {
    let text = value.to_string()
    if !text.contains(".") && !text.contains("e") && !text.contains("E") {
      text + "."
    } else {
      text
    }
  }
}

///|
fn pdf_id_seed(path : String, time_text : () -> String) -> String {
  if pdf_reproducible_ids_enabled() {
    "camlpdf"
  } else {
    path + time_text()
  }
}

///|
fn pdf_generate_id_bytes_from_text(
  path : String,
  time_text : () -> String,
) -> @core.PdfBytes {
  let seed = pdf_id_seed(path, time_text)
  @crypt_core.pdf_md5_digest(@ascii.encode(seed))
}

///|
fn pdf_generate_current_id_bytes(path : String) -> @core.PdfBytes {
  pdf_generate_id_bytes_from_text(path, fn() { @env.now().to_string() })
}

///|
fn pdf_id_object(digest : @core.PdfBytes) -> @syntax.PdfObject {
  PdfArray([PdfString(digest), PdfString(digest)])
}

///|
fn pdf_generate_current_id(path : String) -> @syntax.PdfObject {
  pdf_id_object(pdf_generate_current_id_bytes(path))
}

///|
/// Generates a PDF trailer `/ID` array from a path and caller-supplied time
/// source.
///
/// The digest construction follows CamlPDF's path-plus-time behavior. When
/// `CAMLPDF_REPRODUCIBLE_IDS=true`, the seed is fixed so tests and
/// reproducible builds can produce stable IDs.
pub fn pdf_generate_id(
  path : String,
  get_time : () -> Double,
) -> @syntax.PdfObject {
  let digest = pdf_generate_id_bytes_from_text(path, fn() {
    pdf_id_ocaml_string_of_float(get_time())
  })
  pdf_id_object(digest)
}

///|
/// Replaces this document's trailer `/ID` entry.
///
/// Raises `@core.PdfError::DictionaryExpected` if the trailer is not a dictionary.
pub fn PdfDocument::change_id(
  self : PdfDocument,
  path : String,
  get_time : () -> Double,
) -> Unit raise @core.PdfError {
  match self.trailer_dict() {
    PdfDictionary(_) =>
      self.set_trailer_dict(
        self
        .trailer_dict()
        .add_dict_entry(pdf_id_name(), pdf_generate_id(path, get_time)),
      )
    _ => raise DictionaryExpected
  }
}

///|
fn PdfDocument::copy_with_current_id(
  self : PdfDocument,
  path : String,
) -> PdfDocument raise @core.PdfError {
  let output = self.copy()
  match output.trailer_dict() {
    PdfDictionary(_) => {
      output.set_trailer_dict(
        output
        .trailer_dict()
        .add_dict_entry(pdf_id_name(), pdf_generate_current_id(path)),
      )
      output
    }
    _ => raise DictionaryExpected
  }
}