///|
fn[A] relative_prefer(first : A?, second : A?) -> A? {
  match first {
    Some(_) => first
    None => second
  }
}

///|
fn relative_sub_years(first : Int64, second : Int64) -> Int64 raise DateError {
  if (second > 0L && first < -9223372036854775808L + second) ||
    (second < 0L && first > 9223372036854775807L + second) {
    raise DateError::InvalidDate(message="relative years overflow Int64")
  }
  first - second
}

///|
fn RelativeDelta::rebuild(
  self : RelativeDelta,
  years : Int64,
  months : Int64,
  days : Double,
  hours : Double,
  minutes : Double,
  seconds : Double,
  microseconds : Double,
) -> RelativeDelta raise DateError {
  RelativeDelta::new(
    years~,
    months~,
    days~,
    hours~,
    minutes~,
    seconds~,
    microseconds~,
    leapdays=self.leapdays,
    year?=self.year,
    month?=self.month,
    day?=self.day,
    hour?=self.hour,
    minute?=self.minute,
    second?=self.second,
    microsecond?=self.microsecond,
    on_weekday?=self.weekday,
  )
}

///|
/// Add relative components; the right operand supplies non-None absolute fields
/// and weekday, and supplies leapdays when nonzero. Inputs remain unchanged.
pub fn RelativeDelta::add(
  self : RelativeDelta,
  other : RelativeDelta,
) -> RelativeDelta raise DateError {
  let absolute = {
    ..self,
    year: relative_prefer(other.year, self.year),
    month: relative_prefer(other.month, self.month),
    day: relative_prefer(other.day, self.day),
    hour: relative_prefer(other.hour, self.hour),
    minute: relative_prefer(other.minute, self.minute),
    second: relative_prefer(other.second, self.second),
    microsecond: relative_prefer(other.microsecond, self.microsecond),
    weekday: relative_prefer(other.weekday, self.weekday),
    leapdays: if other.leapdays != 0.0 {
      other.leapdays
    } else {
      self.leapdays
    },
  }
  let (years, months) = relative_checked_calendar(
    @bigint.BigInt::from_int64(self.years) +
    @bigint.BigInt::from_int64(other.years),
    @bigint.BigInt::from_int(self.months + other.months),
  )
  absolute.rebuild(
    years,
    months,
    self.days + other.days,
    self.hours + other.hours,
    self.minutes + other.minutes,
    self.seconds + other.seconds,
    self.microseconds + other.microseconds,
  )
}

///|
/// Subtract relative fields. Absolute fields/weekday prefer the left operand;
/// leapdays also prefer the left nonzero value, as in dateutil.__sub__.
pub fn RelativeDelta::subtract(
  self : RelativeDelta,
  other : RelativeDelta,
) -> RelativeDelta raise DateError {
  let absolute = {
    ..self,
    year: relative_prefer(self.year, other.year),
    month: relative_prefer(self.month, other.month),
    day: relative_prefer(self.day, other.day),
    hour: relative_prefer(self.hour, other.hour),
    minute: relative_prefer(self.minute, other.minute),
    second: relative_prefer(self.second, other.second),
    microsecond: relative_prefer(self.microsecond, other.microsecond),
    weekday: relative_prefer(self.weekday, other.weekday),
    leapdays: if self.leapdays != 0.0 {
      self.leapdays
    } else {
      other.leapdays
    },
  }
  let (years, months) = relative_checked_calendar(
    @bigint.BigInt::from_int64(self.years) -
    @bigint.BigInt::from_int64(other.years),
    @bigint.BigInt::from_int(self.months - other.months),
  )
  absolute.rebuild(
    years,
    months,
    self.days - other.days,
    self.hours - other.hours,
    self.minutes - other.minutes,
    self.seconds - other.seconds,
    self.microseconds - other.microseconds,
  )
}

///|
pub fn RelativeDelta::add_timedelta(
  self : RelativeDelta,
  delta : TimeDelta,
) -> RelativeDelta raise DateError {
  self.rebuild(
    self.years,
    Int64::from_int(self.months),
    self.days + delta.days.to_double(),
    self.hours,
    self.minutes,
    self.seconds + delta.seconds.to_double(),
    self.microseconds + delta.microseconds.to_double(),
  )
}

///|
/// Negate only relative fields. Absolute fields, leapdays and weekday retain
/// their meaning even when a delta is used in datetime subtraction.
pub fn RelativeDelta::negated(
  self : RelativeDelta,
) -> RelativeDelta raise DateError {
  self.rebuild(
    relative_sub_years(0L, self.years),
    -Int64::from_int(self.months),
    -self.days,
    -self.hours,
    -self.minutes,
    -self.seconds,
    -self.microseconds,
  )
}

///|
pub fn RelativeDelta::abs(
  self : RelativeDelta,
) -> RelativeDelta raise DateError {
  let years = if self.years < 0L {
    relative_sub_years(0L, self.years)
  } else {
    self.years
  }
  self.rebuild(
    years,
    Int64::from_int(self.months).abs(),
    Double::abs(self.days),
    Double::abs(self.hours),
    Double::abs(self.minutes),
    Double::abs(self.seconds),
    Double::abs(self.microseconds),
  )
}

///|
/// Return a copy with the weeks property replaced, retaining the old day
/// remainder. This property setter does not normalize other relative fields.
pub fn RelativeDelta::with_weeks(
  self : RelativeDelta,
  weeks : Double,
) -> RelativeDelta raise DateError {
  relative_finite(weeks)
  let days = self.days - self.weeks() * 7.0 + weeks * 7.0
  relative_finite(days)
  { ..self, days, }
}

///|
pub fn RelativeDelta::is_zero(self : RelativeDelta) -> Bool {
  self.years == 0L &&
  self.months == 0 &&
  self.days == 0.0 &&
  self.leapdays == 0.0 &&
  self.hours == 0.0 &&
  self.minutes == 0.0 &&
  self.seconds == 0.0 &&
  self.microseconds == 0.0 &&
  self.year is None &&
  self.month is None &&
  self.day is None &&
  self.weekday is None &&
  self.hour is None &&
  self.minute is None &&
  self.second is None &&
  self.microsecond is None
}

///|
pub impl Eq for RelativeDelta with fn equal(self, other) {
  let weekdays = match (self.weekday, other.weekday) {
    (None, None) => true
    (Some(a), Some(b)) => {
      let first = a.nth.unwrap_or(1)
      let second = b.nth.unwrap_or(1)
      a.weekday == b.weekday &&
      (if first == 0 { 1 } else { first }) ==
      (if second == 0 { 1 } else { second })
    }
    _ => false
  }
  weekdays &&
  self.years == other.years &&
  self.months == other.months &&
  self.days == other.days &&
  self.leapdays == other.leapdays &&
  self.hours == other.hours &&
  self.minutes == other.minutes &&
  self.seconds == other.seconds &&
  self.microseconds == other.microseconds &&
  self.year == other.year &&
  self.month == other.month &&
  self.day == other.day &&
  self.hour == other.hour &&
  self.minute == other.minute &&
  self.second == other.second &&
  self.microsecond == other.microsecond
}

///|
pub extend RelativeDelta with Eq::{equal, not_equal}

///|
pub fn DateTime::sub_relative_delta(
  self : DateTime,
  delta : RelativeDelta,
) -> DateTime raise DateError {
  self.add_relative_delta(delta.negated())
}

///|
// Scaling first converts every relative field to an integer. Keep those
// temporary integers exact through carries, including cancellation of values
// larger than binary64's exact-integer range, before exposing Double day fields.
fn relative_double_integer(value : Double) -> @bigint.BigInt raise DateError {
  relative_finite(value)
  let bits = value.reinterpret_as_uint64()
  let encoded_exponent = ((bits >> 52) & 2047UL).to_int()
  let fraction = bits & 4503599627370495UL
  let (mantissa, exponent) = if encoded_exponent == 0 {
    (fraction, -1074)
  } else {
    (fraction + 4503599627370496UL, encoded_exponent - 1023 - 52)
  }
  let number = @bigint.BigInt::from_uint64(mantissa)
  let integer = if exponent >= 0 {
    number.shl(exponent)
  } else {
    number.shr(-exponent)
  }
  if bits >> 63 != 0UL {
    integer.neg()
  } else {
    integer
  }
}

///|
/// Scale through float multiplication and truncate each relative component
/// independently, including for factor=1. Absolute fields/leapdays/weekday
/// persist. Integer carries are exact before storing typed day/time components.
pub fn RelativeDelta::scaled(
  self : RelativeDelta,
  factor : Double,
) -> RelativeDelta raise DateError {
  relative_finite(factor)
  let mut years = relative_double_integer(self.years.to_double() * factor)
  let months = relative_double_integer(self.months.to_double() * factor)
  let mut days = relative_double_integer(self.days * factor)
  let mut hours = relative_double_integer(self.hours * factor)
  let mut minutes = relative_double_integer(self.minutes * factor)
  let mut seconds = relative_double_integer(self.seconds * factor)
  let microseconds = relative_double_integer(self.microseconds * factor)
  let million = @bigint.BigInt::from_int(1000000)
  let sixty = @bigint.BigInt::from_int(60)
  let twenty_four = @bigint.BigInt::from_int(24)
  let twelve = @bigint.BigInt::from_int(12)
  seconds = seconds + microseconds / million
  let microseconds = microseconds % million
  minutes = minutes + seconds / sixty
  let seconds = seconds % sixty
  hours = hours + minutes / sixty
  let minutes = minutes % sixty
  days = days + hours / twenty_four
  let hours = hours % twenty_four
  years = years + months / twelve
  let months = months % twelve
  if years.compare_int64(-9223372036854775808L) < 0 ||
    years.compare_int64(9223372036854775807L) > 0 {
    raise DateError::InvalidDate(message="scaled relative years overflow Int64")
  }
  let days = @string.parse_double(days.to_string()) catch {
    _ =>
      raise DateError::InvalidDate(
        message="scaled relative days overflow Double",
      )
  }
  relative_finite(days)
  self.rebuild(
    years.to_int64(),
    months.to_int64(),
    days,
    hours.to_int().to_double(),
    minutes.to_int().to_double(),
    seconds.to_int().to_double(),
    microseconds.to_int().to_double(),
  )
}

///|
/// Divide through the original reciprocal-and-multiply path. Each relative
/// component is truncated independently; absolute fields and leapdays persist.
pub fn RelativeDelta::divided(
  self : RelativeDelta,
  factor : Double,
) -> RelativeDelta raise DateError {
  if factor == 0.0 {
    raise DateError::InvalidDate(
      message="cannot divide a relative delta by zero",
    )
  }
  self.scaled(1.0 / factor)
}

///|
fn relative_checked_calendar(
  years : @bigint.BigInt,
  months : @bigint.BigInt,
) -> (Int64, Int64) raise DateError {
  let twelve = @bigint.BigInt::from_int(12)
  let years = years + months / twelve
  if years.compare_int64(-9223372036854775808L) < 0 ||
    years.compare_int64(9223372036854775807L) > 0 {
    raise DateError::InvalidDate(
      message="relative calendar arithmetic overflows Int64",
    )
  }
  (years.to_int64(), (months % twelve).to_int64())
}