///|
pub fn parse_duration(input : String) -> Result[DurationValue, DurationError] {
  Ok(parse_duration_or_raise(input)) catch {
    err => Err(err)
  }
}

///|
fn parse_duration_or_raise(input : String) -> DurationValue raise DurationError {
  let value = trim_ascii(input)
  if value.length() == 0 {
    raise EmptyDuration
  }
  let mut negative = false
  let mut i = 0
  if value[0] == 45 {
    negative = true
    i = 1
  }
  if i >= value.length() || value[i] != 80 {
    raise MissingDurationPrefix
  }
  i += 1
  let mut weeks = 0
  let mut days = 0
  let mut hours = 0
  let mut minutes = 0
  let mut seconds = 0
  let mut in_time = false
  let mut saw_value = false
  while i < value.length() {
    if value[i] == 84 {
      if in_time {
        raise BadDurationDesignator("T")
      }
      in_time = true
      i += 1
    } else {
      let number_start = i
      while i < value.length() && value[i] >= 48 && value[i] <= 57 {
        i += 1
      }
      if number_start == i {
        raise BadDurationNumber(value[number_start:value.length()].to_owned())
      }
      let number_text = value[number_start:i].to_owned()
      let amount = parse_duration_int(number_text)
      if i >= value.length() {
        raise BadDurationDesignator(number_text)
      }
      let designator = value[i]
      if designator == 87 {
        if in_time || days > 0 || hours > 0 || minutes > 0 || seconds > 0 {
          raise MixedWeekAndDateTime
        }
        if weeks > 0 {
          raise BadDurationDesignator("W")
        }
        weeks = amount
      } else if designator == 68 {
        if weeks > 0 {
          raise MixedWeekAndDateTime
        }
        if in_time || days > 0 {
          raise BadDurationDesignator("D")
        }
        days = amount
      } else if designator == 72 {
        if weeks > 0 {
          raise MixedWeekAndDateTime
        }
        if !in_time || hours > 0 {
          raise BadDurationDesignator("H")
        }
        hours = amount
      } else if designator == 77 {
        if weeks > 0 {
          raise MixedWeekAndDateTime
        }
        if !in_time || minutes > 0 {
          raise BadDurationDesignator("M")
        }
        minutes = amount
      } else if designator == 83 {
        if weeks > 0 {
          raise MixedWeekAndDateTime
        }
        if !in_time || seconds > 0 {
          raise BadDurationDesignator("S")
        }
        seconds = amount
      } else {
        raise BadDurationDesignator(value[i:i + 1].to_owned())
      }
      saw_value = true
      i += 1
    }
  }
  if !saw_value {
    raise EmptyDurationBody
  }
  if in_time && hours == 0 && minutes == 0 && seconds == 0 {
    raise EmptyDurationBody
  }
  { negative, weeks, days, hours, minutes, seconds }
}

///|
pub fn render_duration(value : DurationValue) -> String {
  let b = StringBuilder::new()
  if value.negative {
    b.write_string("-")
  }
  b.write_string("P")
  if value.weeks > 0 {
    b.write_string(value.weeks.to_string())
    b.write_string("W")
    return b.to_string()
  }
  if value.days > 0 {
    b.write_string(value.days.to_string())
    b.write_string("D")
  }
  if value.hours > 0 || value.minutes > 0 || value.seconds > 0 {
    b.write_string("T")
    if value.hours > 0 {
      b.write_string(value.hours.to_string())
      b.write_string("H")
    }
    if value.minutes > 0 {
      b.write_string(value.minutes.to_string())
      b.write_string("M")
    }
    if value.seconds > 0 {
      b.write_string(value.seconds.to_string())
      b.write_string("S")
    }
  }
  if value.days == 0 &&
    value.hours == 0 &&
    value.minutes == 0 &&
    value.seconds == 0 {
    b.write_string("T0S")
  }
  b.to_string()
}

///|
pub fn normalize_duration(input : String) -> Result[String, DurationError] {
  match parse_duration(input) {
    Ok(value) => Ok(render_duration(value))
    Err(err) => Err(err)
  }
}

///|
pub fn duration_from_seconds(seconds : Int) -> DurationValue {
  let mut remaining = seconds
  let mut negative = false
  if remaining < 0 {
    negative = true
    remaining = 0 - remaining
  }
  let days = remaining / 86400
  remaining = remaining - days * 86400
  let hours = remaining / 3600
  remaining = remaining - hours * 3600
  let minutes = remaining / 60
  remaining = remaining - minutes * 60
  { negative, weeks: 0, days, hours, minutes, seconds: remaining }
}

///|
pub fn duration_from_minutes(minutes : Int) -> DurationValue {
  duration_from_seconds(minutes * 60)
}

///|
pub fn DurationValue::total_seconds_abs(self : DurationValue) -> Int {
  self.weeks * 604800 +
  self.days * 86400 +
  self.hours * 3600 +
  self.minutes * 60 +
  self.seconds
}

///|
pub fn DurationValue::signed_seconds(self : DurationValue) -> Int {
  if self.negative {
    0 - self.total_seconds_abs()
  } else {
    self.total_seconds_abs()
  }
}

///|
pub fn DurationValue::is_zero(self : DurationValue) -> Bool {
  self.total_seconds_abs() == 0
}

///|
pub fn DurationValue::has_week_form(self : DurationValue) -> Bool {
  self.weeks > 0
}

///|
pub fn DurationValue::has_date_form(self : DurationValue) -> Bool {
  self.days > 0
}

///|
pub fn DurationValue::has_time_form(self : DurationValue) -> Bool {
  self.hours > 0 || self.minutes > 0 || self.seconds > 0
}

///|
pub fn DurationValue::is_negative(self : DurationValue) -> Bool {
  self.negative && !self.is_zero()
}

///|
pub fn DurationValue::is_positive(self : DurationValue) -> Bool {
  !self.negative && !self.is_zero()
}

///|
pub fn DurationValue::is_short_alarm_lead(self : DurationValue) -> Bool {
  let seconds = self.total_seconds_abs()
  seconds > 0 && seconds <= 3600
}

///|
pub fn DurationValue::is_long_alarm_lead(self : DurationValue) -> Bool {
  self.total_seconds_abs() > 86400
}

///|
pub fn DurationValue::human_label(self : DurationValue) -> String {
  let b = StringBuilder::new()
  if self.negative && !self.is_zero() {
    b.write_string("before ")
  } else if !self.is_zero() {
    b.write_string("after ")
  }
  if self.weeks > 0 {
    b.write_string(self.weeks.to_string())
    b.write_string(" week")
    if self.weeks != 1 {
      b.write_string("s")
    }
    return b.to_string()
  }
  let mut wrote = false
  wrote = write_label_part(b, self.days, "day", wrote)
  wrote = write_label_part(b, self.hours, "hour", wrote)
  wrote = write_label_part(b, self.minutes, "minute", wrote)
  wrote = write_label_part(b, self.seconds, "second", wrote)
  if !wrote {
    b.write_string("0 seconds")
  }
  b.to_string()
}

///|
pub fn compare_duration(a : DurationValue, b : DurationValue) -> Int {
  let left = a.signed_seconds()
  let right = b.signed_seconds()
  if left < right {
    -1
  } else if left > right {
    1
  } else {
    0
  }
}

///|
pub fn is_duration_text(input : String) -> Bool {
  parse_duration(input) is Ok(_)
}

///|
pub fn trigger_is_relative(input : String) -> Bool {
  is_duration_text(input)
}

///|
pub fn trigger_is_absolute(input : String) -> Bool {
  let value = trim_ascii(input)
  is_datetime(value) || is_date(value)
}

///|
pub fn trigger_kind(input : String) -> String {
  if trigger_is_relative(input) {
    "relative"
  } else if trigger_is_absolute(input) {
    "absolute"
  } else {
    "invalid"
  }
}

///|
fn write_label_part(
  b : StringBuilder,
  amount : Int,
  unit : String,
  wrote : Bool,
) -> Bool {
  if amount <= 0 {
    return wrote
  }
  if wrote {
    b.write_string(" ")
  }
  b.write_string(amount.to_string())
  b.write_string(" ")
  b.write_string(unit)
  if amount != 1 {
    b.write_string("s")
  }
  true
}

///|
fn parse_duration_int(raw : String) -> Int raise DurationError {
  let value = trim_ascii(raw)
  if value.length() == 0 {
    raise BadDurationNumber(raw)
  }
  let mut total = 0
  let mut i = 0
  while i < value.length() {
    let code = value[i].to_int()
    if code < 48 || code > 57 {
      raise BadDurationNumber(raw)
    }
    total = total * 10 + code - 48
    i += 1
  }
  total
}