///|
fn parse_decimal_int(value : String) -> Int? {
  if value == "" {
    return None
  }
  for ch in value {
    if ch < '0' || ch > '9' {
      return None
    }
  }
  let parsed : Int? = Some(@strconv.from_str(value)) catch { _ => None }
  parsed
}

///|
fn month_number(value : String) -> Int? {
  match value {
    "Jan" => Some(1)
    "Feb" => Some(2)
    "Mar" => Some(3)
    "Apr" => Some(4)
    "May" => Some(5)
    "Jun" => Some(6)
    "Jul" => Some(7)
    "Aug" => Some(8)
    "Sep" => Some(9)
    "Oct" => Some(10)
    "Nov" => Some(11)
    "Dec" => Some(12)
    _ => None
  }
}

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

///|
fn 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 is_leap_year(year) { 29 } else { 28 }
    _ => 0
  }
}

///|
fn leap_days_before_year(year : Int) -> Int64 {
  let previous = (year - 1).to_int64()
  previous / 4 - previous / 100 + previous / 400
}

///|
fn days_before_month(year : Int, month : Int) -> Int {
  let regular = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]
  let base = regular[month - 1]
  if month > 2 && is_leap_year(year) {
    base + 1
  } else {
    base
  }
}

///|
fn civil_time_to_timestamp(
  year : Int,
  month : Int,
  day : Int,
  hour : Int,
  minute : Int,
  second : Int,
) -> Timestamp? {
  if year < 1970 ||
    year > 9999 ||
    month < 1 ||
    month > 12 ||
    day < 1 ||
    day > days_in_month(year, month) ||
    hour < 0 ||
    hour > 23 ||
    minute < 0 ||
    minute > 59 ||
    second < 0 ||
    second > 60 {
    return None
  }
  let years = (year - 1970).to_int64()
  let leap_days = leap_days_before_year(year) - leap_days_before_year(1970)
  let days = years * 365 +
    leap_days +
    days_before_month(year, month).to_int64() +
    (day - 1).to_int64()
  let seconds = days * 86_400 +
    hour.to_int64() * 3_600 +
    minute.to_int64() * 60 +
    second.to_int64()
  Some(Timestamp::from_seconds(seconds))
}

///|
fn http_date_separators_valid(value : String) -> Bool {
  value.code_unit_at(3) == ',' &&
  value.code_unit_at(4) == ' ' &&
  value.code_unit_at(7) == ' ' &&
  value.code_unit_at(11) == ' ' &&
  value.code_unit_at(16) == ' ' &&
  value.code_unit_at(19) == ':' &&
  value.code_unit_at(22) == ':' &&
  value.code_unit_at(25) == ' '
}

///|
fn valid_weekday(value : String) -> Bool {
  match value {
    "Mon" | "Tue" | "Wed" | "Thu" | "Fri" | "Sat" | "Sun" => true
    _ => false
  }
}

///|
/// Parse the IMF-fixdate form emitted by modern HTTP senders:
/// `Sun, 06 Nov 1994 08:49:37 GMT`.
///
/// Obsolete HTTP-date forms belong in optional protocol adapters; the cache
/// core intentionally implements only this narrow interoperable representation.
pub fn parse_http_date(value : String) -> Timestamp? {
  let text = value.trim(chars=" \t").to_owned()
  if text.length() != 29 || !http_date_separators_valid(text) {
    return None
  }
  if !valid_weekday(text.sub(start=0, end=3).to_owned()) {
    return None
  }
  if text.sub(start=26, end=29).to_owned() != "GMT" {
    return None
  }
  let day = parse_decimal_int(text.sub(start=5, end=7).to_owned())
  let month = month_number(text.sub(start=8, end=11).to_owned())
  let year = parse_decimal_int(text.sub(start=12, end=16).to_owned())
  let hour = parse_decimal_int(text.sub(start=17, end=19).to_owned())
  let minute = parse_decimal_int(text.sub(start=20, end=22).to_owned())
  let second = parse_decimal_int(text.sub(start=23, end=25).to_owned())
  match (year, month, day, hour, minute, second) {
    (Some(y), Some(m), Some(d), Some(h), Some(min), Some(sec)) =>
      civil_time_to_timestamp(y, m, d, h, min, sec)
    _ => None
  }
}