///|
/// Parses a cookie-date with the RFC 6265 §5.1.1 algorithm, which accepts the
/// formats servers actually send (RFC 1123, RFC 850, asctime, and their
/// variants). Returns Unix milliseconds, or `None` when the algorithm fails.
fn parse_cookie_date(text : String) -> Int64? {
  let mut time : (Int32, Int32, Int32)? = None
  let mut day : Int32? = None
  let mut month : Int32? = None
  let mut year : Int32? = None
  for token in date_tokens(text) {
    if time is None && parse_time(token) is Some(hms) {
      time = Some(hms)
    } else if day is None && leading_number(token, 1, 2) is Some(value) {
      day = Some(value)
    } else if month is None && parse_month(token) is Some(value) {
      month = Some(value)
    } else if year is None && leading_number(token, 2, 4) is Some(value) {
      year = Some(value)
    }
  }
  guard time is Some((hour, minute, second)) &&
    day is Some(day) &&
    month is Some(month) &&
    year is Some(year) else {
    return None
  }
  let year = if year >= 70 && year <= 99 {
    year + 1900
  } else if year <= 69 {
    year + 2000
  } else {
    year
  }
  guard day >= 1 &&
    day <= days_in_month(year, month) &&
    year >= 1601 &&
    hour <= 23 &&
    minute <= 59 &&
    second <= 59 else {
    return None
  }
  let days = days_from_civil(year, month, day)
  Some(
    days * 86_400_000L + (hour * 3600 + minute * 60 + second).to_int64() * 1000L,
  )
}

///|
/// Splits on the §5.1.1 delimiters: %x09 / %x20-2F / %x3B-40 / %x5B-60 /
/// %x7B-7E. Everything else, non-ASCII included, belongs to a token.
fn date_tokens(text : String) -> Array[Array[Char]] {
  let tokens = []
  let mut token = []
  for c in text {
    let n = c.to_int()
    let delimiter = n == 0x09 ||
      (n >= 0x20 && n <= 0x2F) ||
      (n >= 0x3B && n <= 0x40) ||
      (n >= 0x5B && n <= 0x60) ||
      (n >= 0x7B && n <= 0x7E)
    if delimiter {
      if !token.is_empty() {
        tokens.push(token)
        token = []
      }
    } else {
      token.push(c)
    }
  }
  if !token.is_empty() {
    tokens.push(token)
  }
  tokens
}

///|
/// Reads `min` to `max` digits from `start`; returns their value and the
/// index after them.
fn digits_at(
  token : Array[Char],
  start : Int32,
  min : Int32,
  max : Int32,
) -> (Int32, Int32)? {
  let mut value = 0
  let mut i = start
  while i < token.length() && i - start < max && token[i].is_ascii_digit() {
    value = value * 10 + (token[i].to_int() - '0'.to_int())
    i += 1
  }
  if i - start >= min {
    Some((value, i))
  } else {
    None
  }
}

///|
/// The `( non-digit *OCTET )` tail: the token ends, or the next character is
/// not a digit.
fn ends_number(token : Array[Char], at : Int32) -> Bool {
  at == token.length() || !token[at].is_ascii_digit()
}

///|
/// `min`*`max`DIGIT ( non-digit *OCTET ): day-of-month (1-2) and year (2-4).
fn leading_number(token : Array[Char], min : Int32, max : Int32) -> Int32? {
  match digits_at(token, 0, min, max) {
    Some((value, end)) if ends_number(token, end) => Some(value)
    _ => None
  }
}

///|
/// hms-time ( non-digit *OCTET ), each time-field 1*2DIGIT.
fn parse_time(token : Array[Char]) -> (Int32, Int32, Int32)? {
  guard digits_at(token, 0, 1, 2) is Some((hour, i)) &&
    i < token.length() &&
    token[i] == ':' &&
    digits_at(token, i + 1, 1, 2) is Some((minute, j)) &&
    j < token.length() &&
    token[j] == ':' &&
    digits_at(token, j + 1, 1, 2) is Some((second, k)) &&
    ends_number(token, k) else {
    return None
  }
  Some((hour, minute, second))
}

///|
/// A token whose first three characters name a month, ignoring ASCII case.
fn parse_month(token : Array[Char]) -> Int32? {
  guard token.length() >= 3 else { return None }
  let prefix = String::from_array(token[:3]).to_lower()
  let months = [
    "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov",
    "dec",
  ]
  for i, name in months {
    if name == prefix {
      return Some(i + 1)
    }
  }
  None
}

///|
fn days_in_month(year : Int32, month : Int32) -> Int32 {
  match month {
    2 =>
      if (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 {
        29
      } else {
        28
      }
    4 | 6 | 9 | 11 => 30
    _ => 31
  }
}

///|
/// Days since 1970-01-01 of a proleptic Gregorian date (Howard Hinnant's
/// `days_from_civil`), for the positive years a cookie-date can name.
fn days_from_civil(year : Int32, month : Int32, day : Int32) -> Int64 {
  let y = if month <= 2 { year - 1 } else { year }
  let era = y / 400
  let yoe = y - era * 400
  let mp = (month + 9) % 12
  let doy = (153 * mp + 2) / 5 + day - 1
  let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy
  era.to_int64() * 146_097L + doe.to_int64() - 719_468L
}