///|
/// Timestamp formatting for rotated log file names.
///
/// Only the calendar/clock fields that make sense in a file name are
/// supported. Formatting is offset-aware so a daemon can render file names
/// in the host's local time while keeping the rest of its arithmetic in
/// Unix seconds.

///|
/// Days since 1970-01-01 for a civil date (Howard Hinnant's algorithm).
pub fn days_from_civil(year : Int, month : Int, day : Int) -> Int {
  let y = if month <= 2 { year - 1 } else { year }
  let era = (if y >= 0 { y } else { y - 399 }) / 400
  let yoe = y - era * 400
  let mp = if month > 2 { month - 3 } else { month + 9 }
  let doy = (153 * mp + 2) / 5 + day - 1
  let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy
  era * 146097 + doe - 719468
}

///|
/// Civil date `(year, month, day)` for a number of days since 1970-01-01.
pub fn civil_from_days(days : Int) -> (Int, Int, Int) {
  let z = days + 719468
  let era = (if z >= 0 { z } else { z - 146096 }) / 146097
  let doe = z - era * 146097
  let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365
  let y = yoe + era * 400
  let doy = doe - (365 * yoe + yoe / 4 - yoe / 100)
  let mp = (5 * doy + 2) / 153
  let d = doy - (153 * mp + 2) / 5 + 1
  let m = if mp < 10 { mp + 3 } else { mp - 9 }
  (if m <= 2 { y + 1 } else { y }, m, d)
}

///|
/// Day of week for a number of days since 1970-01-01 (0 = Sunday).
pub fn weekday_from_days(days : Int) -> Int {
  (days % 7 + 4 + 7) % 7
}

///|
/// Split a local Unix second into its calendar/clock fields.
pub fn split_timestamp(
  local_secs : Int64,
) -> (Int, Int, Int, Int, Int, Int, Int) {
  let days = (local_secs / 86400L).to_int()
  let sod = (local_secs % 86400L).to_int()
  let (y, mo, d) = civil_from_days(days)
  let hh = sod / 3600
  let mi = sod % 3600 / 60
  let ss = sod % 60
  (y, mo, d, hh, mi, ss, days)
}

///|
/// Try to render a single format token at `pos`. Returns the rendered text
/// and the token length, or `None` when no token starts here.
fn render_token(
  fmt : StringView,
  pos : Int,
  year : Int,
  month : Int,
  day : Int,
  hour : Int,
  minute : Int,
  second : Int,
  weekday : Int,
  offset_secs : Int,
) -> (String, Int)? {
  let rest = fmt.view(start_offset=pos, end_offset=fmt.length())
  let abs_offset = if offset_secs < 0 { -offset_secs } else { offset_secs }
  let offset_sign = if offset_secs < 0 { "-" } else { "+" }
  let offset_hh = pad_left((abs_offset / 3600).to_string(), 2, '0')
  let offset_mm = pad_left((abs_offset % 3600 / 60).to_string(), 2, '0')
  // Longest tokens first so that YYYY wins over YY, MM over M, ...
  let tokens : Array[(String, String)] = [
    ("YYYY", pad_left(year.to_string(), 4, '0')),
    ("MMM", month_names[month - 1]),
    ("ddd", weekday_names[weekday]),
    ("YY", pad_left((year % 100).to_string(), 2, '0')),
    ("MM", pad_left(month.to_string(), 2, '0')),
    ("DD", pad_left(day.to_string(), 2, '0')),
    ("HH", pad_left(hour.to_string(), 2, '0')),
    ("mm", pad_left(minute.to_string(), 2, '0')),
    ("ss", pad_left(second.to_string(), 2, '0')),
    ("ZZ", offset_sign + offset_hh + offset_mm),
    ("Z", offset_sign + offset_hh + ":" + offset_mm),
    ("M", month.to_string()),
    ("D", day.to_string()),
    ("H", hour.to_string()),
    ("m", minute.to_string()),
    ("s", second.to_string()),
  ]
  for token in tokens {
    let (pattern, value) = token
    if rest.has_prefix(pattern) {
      return Some((value, pattern.length()))
    }
  }
  None
}

///|
/// Short weekday names, indexed by day of week (0 = Sunday).
let weekday_names : Array[String] = [
  "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
]

///|
/// Short month names, indexed by month number minus one.
let month_names : Array[String] = [
  "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct",
  "Nov", "Dec",
]

///|
/// Format a Unix second using a subset of the moment.js-style tokens:
/// `YYYY`, `YY`, `MMM`, `MM`, `M`, `DD`, `D`, `ddd`, `HH`, `H`, `mm`, `m`,
/// `ss`, `s`, `Z` (±HH:mm) and `ZZ` (±HHmm). `[literal]` escapes text, as in
/// dayjs.
pub fn format_timestamp(
  epoch_secs : Int64,
  offset_secs : Int,
  fmt : String,
) -> String {
  let local_secs = epoch_secs + offset_secs.to_int64()
  let (year, month, day, hour, minute, second, days) = split_timestamp(local_secs)
  let weekday = weekday_from_days(days)
  let out = StringBuilder()
  let mut i = 0
  while i < fmt.length() {
    // `[literal]` escapes text, the way dayjs does, so a format can carry
    // words like `GMT` without their letters being read as tokens.
    if fmt.get_char(i) == Some('[') {
      let mut j = i + 1
      while j < fmt.length() && fmt.get_char(j) != Some(']') {
        guard fmt.get_char(j) is Some(c) else { break }
        out.write_char(c)
        j = j + 1
      }
      i = if j < fmt.length() { j + 1 } else { j }
      continue
    }
    match render_token(
      fmt, i, year, month, day, hour, minute, second, weekday, offset_secs,
    ) {
      Some((text, len)) => {
        out.write_string(text)
        i = i + len
      }
      None => {
        guard fmt.get_char(i) is Some(c) else { break }
        out.write_char(c)
        i = i + 1
      }
    }
  }
  out.to_string()
}