///|
/// Platform-independent time record used by cpdf-style strftime.
///
/// `tm_year` is years since 1900 and `tm_mon` is zero-based, matching C/Unix
/// `struct tm` and OCaml's `Unix.tm`.
pub(all) struct PdfStrftimeTime {
  tm_sec : Int
  tm_min : Int
  tm_hour : Int
  tm_mday : Int
  tm_mon : Int
  tm_year : Int
  tm_wday : Int
  tm_yday : Int
  tm_isdst : Bool
} derive(Debug, Eq, ToJson)

///|
/// Source-spelled cpdf dummy strftime time value.
pub let pdf_dummy : PdfStrftimeTime = {
  tm_sec: 0,
  tm_min: 0,
  tm_hour: 0,
  tm_mday: 1,
  tm_mon: 0,
  tm_year: 2000,
  tm_wday: 0,
  tm_yday: 0,
  tm_isdst: false,
}

///|
/// Dummy cpdf strftime time value.
pub fn pdf_strftime_dummy_time() -> PdfStrftimeTime {
  pdf_dummy
}

///|
fn pdf_strftime_weekday_full(time : PdfStrftimeTime) -> String {
  match time.tm_wday {
    0 => "Sunday"
    1 => "Monday"
    2 => "Tuesday"
    3 => "Wednesday"
    4 => "Thursday"
    5 => "Friday"
    6 => "Saturday"
    _ => "strf_AFail"
  }
}

///|
fn pdf_strftime_weekday_abbrev(time : PdfStrftimeTime) -> String {
  pdf_strftime_weekday_full(time).unsafe_substring(start=0, end=3)
}

///|
fn pdf_strftime_month_full(time : PdfStrftimeTime) -> String {
  match time.tm_mon {
    0 => "January"
    1 => "February"
    2 => "March"
    3 => "April"
    4 => "May"
    5 => "June"
    6 => "July"
    7 => "August"
    8 => "September"
    9 => "October"
    10 => "November"
    11 => "December"
    _ => "strf_Bfail"
  }
}

///|
fn pdf_strftime_month_abbrev(time : PdfStrftimeTime) -> String {
  pdf_strftime_month_full(time).unsafe_substring(start=0, end=3)
}

///|
fn pdf_strftime_pad2(value : Int, pad : String) -> String {
  let text = value.to_string()
  if text.length() == 1 {
    pad + text
  } else {
    text
  }
}

///|
fn pdf_strftime_pad3(value : Int) -> String {
  let text = value.to_string()
  match text.length() {
    1 => "00" + text
    2 => "0" + text
    _ => text
  }
}

///|
fn pdf_strftime_hour12(time : PdfStrftimeTime) -> String {
  pdf_strftime_pad2(time.tm_hour % 12, "0")
}

///|
fn pdf_strftime_ampm(time : PdfStrftimeTime) -> String {
  if time.tm_hour >= 12 {
    "p.m"
  } else {
    "a.m"
  }
}

///|
fn pdf_strftime_time(time : PdfStrftimeTime) -> String {
  pdf_strftime_pad2(time.tm_hour, "0") +
  ":" +
  pdf_strftime_pad2(time.tm_min, "0") +
  ":" +
  pdf_strftime_pad2(time.tm_sec, "0")
}

///|
fn pdf_strftime_weekday_monday_based(time : PdfStrftimeTime) -> String {
  match time.tm_wday {
    0 => "7"
    n => (n + 1).to_string()
  }
}

///|
fn pdf_strftime_year(time : PdfStrftimeTime) -> String {
  (time.tm_year + 1900).to_string()
}

///|
fn pdf_strftime_replace_all(
  text : String,
  old : String,
  new : String,
) -> String {
  let mut output = text
  while output.contains(old) {
    output = output.replace(old~, new~)
  }
  output
}

///|
fn pdf_strftime_replace_pair(
  text : String,
  time : PdfStrftimeTime,
  pair : (String, (PdfStrftimeTime) -> String),
) -> String {
  pdf_strftime_replace_all(text, pair.0, (pair.1)(time))
}

///|
let pdf_strftime_pairs : Array[(String, (PdfStrftimeTime) -> String)] = [
  ("%a", pdf_strftime_weekday_abbrev),
  ("%A", pdf_strftime_weekday_full),
  ("%b", pdf_strftime_month_abbrev),
  ("%B", pdf_strftime_month_full),
  ("%d", fn(time) { pdf_strftime_pad2(time.tm_mday, "0") }),
  ("%e", fn(time) { pdf_strftime_pad2(time.tm_mday, " ") }),
  ("%H", fn(time) { pdf_strftime_pad2(time.tm_hour, "0") }),
  ("%I", pdf_strftime_hour12),
  ("%j", fn(time) { pdf_strftime_pad3(time.tm_yday) }),
  ("%m", fn(time) { pdf_strftime_pad2(time.tm_mon + 1, "0") }),
  ("%M", fn(time) { pdf_strftime_pad2(time.tm_min, "0") }),
  ("%p", pdf_strftime_ampm),
  ("%S", fn(time) { pdf_strftime_pad2(time.tm_sec, "0") }),
  ("%T", pdf_strftime_time),
  ("%u", pdf_strftime_weekday_monday_based),
  ("%w", fn(time) { time.tm_wday.to_string() }),
  ("%Y", pdf_strftime_year),
  ("%%", fn(_) { "%" }),
]

///|
/// Format `text` using cpdf's C-style strftime subset and an explicit time.
///
/// This follows the OCaml implementation's replacement order and field
/// semantics, including zero-based `%j` and the absence of a `%s` replacement.
pub fn pdf_strftime_with_time(time : PdfStrftimeTime, text : String) -> String {
  let mut output = text
  for pair in pdf_strftime_pairs {
    output = pdf_strftime_replace_pair(output, time, pair)
  }
  output
}