///|
pub(all) struct Ttl {
  seconds_value : UInt64
} derive(Eq, Debug)

///|
fn ttl_error(code : ZoneErrorCode, message : String, column : Int) -> ZoneError {
  ZoneError::new(code, message, SourceSpan::point(0, column))
}

///|
fn ttl_unit_multiplier(code : Int) -> UInt64? {
  match code {
    119 | 87 => Some(604800UL)
    100 | 68 => Some(86400UL)
    104 | 72 => Some(3600UL)
    109 | 77 => Some(60UL)
    115 | 83 => Some(1UL)
    _ => None
  }
}

///|
fn checked_ttl_add(
  total : UInt64,
  value : UInt64,
  multiplier : UInt64,
  column : Int,
) -> Result[UInt64, ZoneError] {
  let limit = 0xFFFFFFFFUL
  if value > limit / multiplier || total > limit - value * multiplier {
    Err(ttl_error(TtlOverflow, "TTL exceeds the unsigned 32-bit range", column))
  } else {
    Ok(total + value * multiplier)
  }
}

///|
pub fn Ttl::from_seconds(seconds : UInt64) -> Result[Ttl, ZoneError] {
  if seconds > 0xFFFFFFFFUL {
    Err(ttl_error(TtlOverflow, "TTL exceeds the unsigned 32-bit range", 0))
  } else {
    Ok({ seconds_value: seconds })
  }
}

///|
/// Parse a BIND-style TTL such as `3600`, `2h30m` or `1w2d`.
pub fn parse_ttl(text : String) -> Result[Ttl, ZoneError] {
  if text.length() == 0 {
    return Err(ttl_error(InvalidTtl, "TTL is empty", 0))
  }
  let mut total = 0UL
  let mut current = 0UL
  let mut digits = 0
  let mut saw_unit = false
  for index = 0; index < text.length(); index = index + 1 {
    let code = text[index].to_int()
    if code >= 48 && code <= 57 {
      let digit = (code - 48).to_uint64()
      if current > 0xFFFFFFFFUL / 10UL || current * 10UL > 0xFFFFFFFFUL - digit {
        return Err(
          ttl_error(TtlOverflow, "TTL numeric component overflows", index),
        )
      }
      current = current * 10UL + digit
      digits = digits + 1
    } else {
      let multiplier = match ttl_unit_multiplier(code) {
        Some(value) => value
        None =>
          return Err(
            ttl_error(InvalidTtl, "TTL contains an invalid unit", index),
          )
      }
      if digits == 0 {
        return Err(
          ttl_error(InvalidTtl, "TTL unit has no numeric value", index),
        )
      }
      total = match checked_ttl_add(total, current, multiplier, index) {
        Ok(value) => value
        Err(error) => return Err(error)
      }
      current = 0UL
      digits = 0
      saw_unit = true
    }
  }
  if digits == 0 {
    if saw_unit {
      Ttl::from_seconds(total)
    } else {
      Err(ttl_error(InvalidTtl, "TTL has no numeric value", 0))
    }
  } else if saw_unit {
    match checked_ttl_add(total, current, 1UL, text.length() - 1) {
      Ok(value) => Ttl::from_seconds(value)
      Err(error) => Err(error)
    }
  } else {
    Ttl::from_seconds(current)
  }
}

///|
pub fn Ttl::seconds(self : Ttl) -> UInt64 {
  self.seconds_value
}

///|
fn append_ttl_unit(
  result : String,
  remaining : UInt64,
  unit : UInt64,
  suffix : String,
) -> (String, UInt64) {
  if remaining >= unit {
    (result + (remaining / unit).to_string() + suffix, remaining % unit)
  } else {
    (result, remaining)
  }
}

///|
pub fn Ttl::to_text(self : Ttl) -> String {
  if self.seconds_value == 0UL {
    return "0"
  }
  let (weeks, after_weeks) = append_ttl_unit(
    "",
    self.seconds_value,
    604800UL,
    "w",
  )
  let (days, after_days) = append_ttl_unit(weeks, after_weeks, 86400UL, "d")
  let (hours, after_hours) = append_ttl_unit(days, after_days, 3600UL, "h")
  let (minutes, after_minutes) = append_ttl_unit(hours, after_hours, 60UL, "m")
  if after_minutes > 0UL {
    minutes + after_minutes.to_string() + "s"
  } else {
    minutes
  }
}