///|
fn pdf_portfolio_name(text : String) -> PdfName {
  pdf_name_of_bytes(@ascii.encode(text))
}

///|
let pdf_portfolio_root_key_name : PdfName = pdf_portfolio_name("/Root")

///|
fn pdf_portfolio_root_key() -> PdfName {
  pdf_portfolio_root_key_name
}

///|
let pdf_portfolio_names_key_name : PdfName = pdf_portfolio_name("/Names")

///|
fn pdf_portfolio_names_key() -> PdfName {
  pdf_portfolio_names_key_name
}

///|
let pdf_portfolio_embedded_files_key_name : PdfName = pdf_portfolio_name(
  "/EmbeddedFiles",
)

///|
fn pdf_portfolio_embedded_files_key() -> PdfName {
  pdf_portfolio_embedded_files_key_name
}

///|
let pdf_portfolio_collection_key_name : PdfName = pdf_portfolio_name(
  "/Collection",
)

///|
fn pdf_portfolio_collection_key() -> PdfName {
  pdf_portfolio_collection_key_name
}

///|
let pdf_portfolio_view_key_name : PdfName = pdf_portfolio_name("/View")

///|
fn pdf_portfolio_view_key() -> PdfName {
  pdf_portfolio_view_key_name
}

///|
let pdf_portfolio_table_view_name : PdfName = pdf_portfolio_name("/T")

///|
fn pdf_portfolio_table_view() -> PdfName {
  pdf_portfolio_table_view_name
}

///|
let pdf_portfolio_params_key_name : PdfName = pdf_portfolio_name("/Params")

///|
fn pdf_portfolio_params_key() -> PdfName {
  pdf_portfolio_params_key_name
}

///|
let pdf_portfolio_mod_date_key_name : PdfName = pdf_portfolio_name("/ModDate")

///|
fn pdf_portfolio_mod_date_key() -> PdfName {
  pdf_portfolio_mod_date_key_name
}

///|
let pdf_portfolio_size_key_name : PdfName = pdf_portfolio_name("/Size")

///|
fn pdf_portfolio_size_key() -> PdfName {
  pdf_portfolio_size_key_name
}

///|
let pdf_portfolio_length_key_name : PdfName = pdf_portfolio_name("/Length")

///|
fn pdf_portfolio_length_key() -> PdfName {
  pdf_portfolio_length_key_name
}

///|
let pdf_portfolio_type_key_name : PdfName = pdf_portfolio_name("/Type")

///|
fn pdf_portfolio_type_key() -> PdfName {
  pdf_portfolio_type_key_name
}

///|
let pdf_portfolio_file_spec_name : PdfName = pdf_portfolio_name("/FileSpec")

///|
fn pdf_portfolio_file_spec() -> PdfName {
  pdf_portfolio_file_spec_name
}

///|
let pdf_portfolio_desc_key_name : PdfName = pdf_portfolio_name("/Desc")

///|
fn pdf_portfolio_desc_key() -> PdfName {
  pdf_portfolio_desc_key_name
}

///|
let pdf_portfolio_f_key_name : PdfName = pdf_portfolio_name("/F")

///|
fn pdf_portfolio_f_key() -> PdfName {
  pdf_portfolio_f_key_name
}

///|
let pdf_portfolio_uf_key_name : PdfName = pdf_portfolio_name("/UF")

///|
fn pdf_portfolio_uf_key() -> PdfName {
  pdf_portfolio_uf_key_name
}

///|
let pdf_portfolio_af_relationship_key_name : PdfName = pdf_portfolio_name(
  "/AFRelationship",
)

///|
fn pdf_portfolio_af_relationship_key() -> PdfName {
  pdf_portfolio_af_relationship_key_name
}

///|
let pdf_portfolio_ef_key_name : PdfName = pdf_portfolio_name("/EF")

///|
fn pdf_portfolio_ef_key() -> PdfName {
  pdf_portfolio_ef_key_name
}

///|
let pdf_portfolio_unspecified_relationship_name : PdfName = pdf_portfolio_name(
  "/Unspecified",
)

///|
fn pdf_portfolio_unspecified_relationship() -> PdfName {
  pdf_portfolio_unspecified_relationship_name
}

///|
/// One file to embed in a PDF portfolio.
///
/// `filename` is used for the portfolio name-tree key and its basename is used
/// for the file specification names. `data` is the raw embedded-file payload.
pub(all) struct PdfPortfolioEntry {
  filename : String
  data : PdfBytes
  relationship : String?
  description : String?
} derive(Debug, Eq, ToJson)

///|
/// Construct a portfolio entry, copying the supplied data view.
pub fn pdf_portfolio_entry(
  filename : String,
  data : BytesView,
  relationship? : String? = None,
  description? : String? = None,
) -> PdfPortfolioEntry {
  { filename, data: data.to_owned(), relationship, description }
}

///|
fn pdf_portfolio_is_path_separator(code_unit : UInt16) -> Bool {
  let value = code_unit.to_int()
  value == 47 || value == 92
}

///|
fn pdf_portfolio_basename(path : String) -> String {
  let mut start = 0
  for index in 0.. PdfName {
  match relationship {
    Some(text) => pdf_portfolio_name("/" + text)
    None => pdf_portfolio_unspecified_relationship()
  }
}

///|
fn pdf_portfolio_description(
  entry : PdfPortfolioEntry,
  basename : String,
) -> String {
  match entry.description {
    Some(description) => description
    None => basename
  }
}

///|
fn pdf_portfolio_pad2(value : Int) -> String {
  if value < 10 {
    "0" + value.to_string()
  } else {
    value.to_string()
  }
}

///|
fn pdf_portfolio_pad4(value : Int) -> String {
  if value < 10 {
    "000" + value.to_string()
  } else if value < 100 {
    "00" + value.to_string()
  } else if value < 1000 {
    "0" + value.to_string()
  } else {
    value.to_string()
  }
}

///|
fn pdf_portfolio_is_leap_year(year : Int) -> Bool {
  (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}

///|
fn pdf_portfolio_days_in_month(year : Int, month : Int) -> Int {
  match month {
    1 | 3 | 5 | 7 | 8 | 10 | 12 => 31
    4 | 6 | 9 | 11 => 30
    2 => if pdf_portfolio_is_leap_year(year) { 29 } else { 28 }
    _ => 30
  }
}

///|
fn pdf_portfolio_date_text_from_unix_millis(milliseconds : UInt64) -> String {
  let total_seconds = (milliseconds / (1000 : UInt64)).reinterpret_as_int64()
  let mut days = (total_seconds / 86400L).to_int()
  let seconds_of_day = (total_seconds % 86400L).to_int()
  let hour = seconds_of_day / 3600
  let minute = seconds_of_day % 3600 / 60
  let second = seconds_of_day % 60
  let mut year = 1970
  let mut year_days = if pdf_portfolio_is_leap_year(year) { 366 } else { 365 }
  while days >= year_days {
    days -= year_days
    year += 1
    year_days = if pdf_portfolio_is_leap_year(year) { 366 } else { 365 }
  }
  let mut month = 1
  while days >= pdf_portfolio_days_in_month(year, month) {
    days -= pdf_portfolio_days_in_month(year, month)
    month += 1
  }
  let day = days + 1
  "D:" +
  pdf_portfolio_pad4(year) +
  pdf_portfolio_pad2(month) +
  pdf_portfolio_pad2(day) +
  pdf_portfolio_pad2(hour) +
  pdf_portfolio_pad2(minute) +
  pdf_portfolio_pad2(second)
}

///|
fn pdf_portfolio_current_mod_date() -> PdfBytes {
  let text = match @env.get_env_var("CPDF_REPRODUCIBLE_DATES") {
    Some("true") =>
      pdf_strftime_with_time(pdf_strftime_dummy_time(), "D:%Y%m%d%H%M%S")
    _ => pdf_portfolio_date_text_from_unix_millis(@env.now())
  }
  @ascii.encode(text)
}

///|
fn pdf_portfolio_stream(
  entry : PdfPortfolioEntry,
  mod_date : PdfBytes,
) -> PdfObject {
  pdf_stream(
    PdfDictionary([
      (
        pdf_portfolio_params_key(),
        PdfDictionary([
          (pdf_portfolio_mod_date_key(), PdfString(mod_date)),
          (pdf_portfolio_size_key(), PdfInteger(entry.data.length())),
        ]),
      ),
      (pdf_portfolio_length_key(), PdfInteger(entry.data.length())),
    ]),
    StreamGot(entry.data),
  )
}

///|
fn PdfDocument::pdf_portfolio_file_spec(
  self : PdfDocument,
  entry : PdfPortfolioEntry,
  mod_date : PdfBytes,
) -> PdfObject {
  let basename = pdf_portfolio_basename(entry.filename)
  let basename_bytes = @utf8.encode(basename)
  let contents = self.add_object(pdf_portfolio_stream(entry, mod_date))
  PdfDictionary([
    (pdf_portfolio_type_key(), PdfNameObject(pdf_portfolio_file_spec())),
    (
      pdf_portfolio_desc_key(),
      PdfString(@utf8.encode(pdf_portfolio_description(entry, basename))),
    ),
    (pdf_portfolio_f_key(), PdfString(basename_bytes)),
    (pdf_portfolio_uf_key(), PdfString(basename_bytes)),
    (
      pdf_portfolio_af_relationship_key(),
      PdfNameObject(pdf_portfolio_relationship_name(entry.relationship)),
    ),
    (
      pdf_portfolio_ef_key(),
      PdfDictionary([
        (pdf_portfolio_f_key(), PdfIndirect(contents)),
        (pdf_portfolio_uf_key(), PdfIndirect(contents)),
      ]),
    ),
  ])
}

///|
fn PdfDocument::pdf_portfolio_name_tree_entries(
  self : PdfDocument,
  entries : ArrayView[PdfPortfolioEntry],
  mod_date : PdfBytes,
) -> Array[(PdfBytes, PdfObject)] {
  [
    for index, entry in entries => {
      let key = @utf8.encode(index.to_string() + "/" + entry.filename)
      let file_spec = self.add_object(
        self.pdf_portfolio_file_spec(entry, mod_date),
      )
      (key, PdfIndirect(file_spec))
    }
  ]
}

///|
fn pdf_portfolio_collection() -> PdfObject {
  PdfDictionary([
    (pdf_portfolio_view_key(), PdfNameObject(pdf_portfolio_table_view())),
  ])
}

///|
/// Build a PDF portfolio, also known as a PDF collection.
///
/// The original document is left unchanged. `mod_date` can be supplied for
/// deterministic output; otherwise the current cpdf-style date is used.
pub fn PdfDocument::portfolio(
  self : PdfDocument,
  entries : ArrayView[PdfPortfolioEntry],
  mod_date? : PdfBytes? = None,
) -> PdfDocument raise PdfError {
  ignore(self.catalog())
  let document = self.copy()
  let resolved_mod_date = match mod_date {
    Some(date) => date
    None => pdf_portfolio_current_mod_date()
  }
  let embedded_files = PdfIndirect(
    document.add_object(
      document.build_name_tree(
        document.pdf_portfolio_name_tree_entries(entries, resolved_mod_date),
      ),
    ),
  )
  document.replace_chain(
    [
      pdf_portfolio_root_key(),
      pdf_portfolio_names_key(),
      pdf_portfolio_embedded_files_key(),
    ],
    embedded_files,
  )
  document.replace_chain(
    [pdf_portfolio_root_key(), pdf_portfolio_collection_key()],
    pdf_portfolio_collection(),
  )
  document
}

///|
/// Compatibility wrapper for `PdfDocument::portfolio`.
pub fn pdf_portfolio(
  document : PdfDocument,
  entries : ArrayView[PdfPortfolioEntry],
  mod_date? : PdfBytes? = None,
) -> PdfDocument raise PdfError {
  document.portfolio(entries, mod_date~)
}