///|
/// The date portion of a duration: years, months, weeks and days.
///
/// Unlike a time duration these components cannot be normalized against each
/// other, because the length of a year or a month depends on where in the
/// calendar it starts.
///
/// Spec: 
pub struct DateDuration {
  /// Whole years.
  years : Int64
  /// Whole months.
  months : Int64
  /// Whole weeks.
  weeks : Int64
  /// Whole days.
  days : Int64
} derive(Eq, Debug, Default)

///|
/// Creates a date duration without validating it.
fn DateDuration::new_unchecked(
  years : Int64,
  months : Int64,
  weeks : Int64,
  days : Int64,
) -> DateDuration {
  { years, months, weeks, days }
}

///|
/// `CreateDateDurationRecord`: creates a validated date duration.
///
/// All non-zero fields must share a sign, and each must stay inside the range
/// a `Duration` permits.
///
/// ```mbt check
/// test {
///   let d = @temporal.DateDuration::new(1, 2, 3, 4)
///   inspect(d.years(), content="1")
///   inspect(d.days(), content="4")
/// }
/// ```
pub fn DateDuration::new(
  years : Int64,
  months : Int64,
  weeks : Int64,
  days : Int64,
) -> DateDuration raise TemporalError {
  if !is_valid_duration(
      years, months, weeks, days, 0L, 0L, 0L, 0L, @int128.zero, @int128.zero,
    ) {
    raise RangeError("invalid DateDuration")
  }
  { years, months, weeks, days }
}

///|
/// Returns the years component.
pub fn DateDuration::years(self : DateDuration) -> Int64 {
  self.years
}

///|
/// Returns the months component.
pub fn DateDuration::months(self : DateDuration) -> Int64 {
  self.months
}

///|
/// Returns the weeks component.
pub fn DateDuration::weeks(self : DateDuration) -> Int64 {
  self.weeks
}

///|
/// Returns the days component.
pub fn DateDuration::days(self : DateDuration) -> Int64 {
  self.days
}

///|
/// Returns the duration with every component negated.
pub fn DateDuration::negated(self : DateDuration) -> DateDuration {
  {
    years: -self.years,
    months: -self.months,
    weeks: -self.weeks,
    days: -self.days,
  }
}

///|
/// Returns the duration with every component made non-negative.
pub fn DateDuration::abs(self : DateDuration) -> DateDuration {
  {
    years: self.years.abs(),
    months: self.months.abs(),
    weeks: self.weeks.abs(),
    days: self.days.abs(),
  }
}

///|
/// `DateDurationSign`: the sign shared by every non-zero component.
pub fn DateDuration::sign(self : DateDuration) -> Sign {
  duration_sign([self.years, self.months, self.weeks, self.days])
}

///|
/// `AdjustDateDurationRecord`: returns a copy with `days` replaced, and
/// optionally `weeks` and `months` too.
fn DateDuration::adjust(
  self : DateDuration,
  days : Int64,
  weeks? : Int64,
  months? : Int64,
) -> DateDuration {
  {
    years: self.years,
    months: months.unwrap_or(self.months),
    weeks: weeks.unwrap_or(self.weeks),
    days,
  }
}

///|
/// `DurationSign`: returns the sign of the first non-zero value, or zero.
fn duration_sign(values : Array[Int64]) -> Sign {
  for v in values {
    if v < 0L {
      return Negative
    }
    if v > 0L {
      return Positive
    }
  }
  Zero
}

///|
/// `IsValidDuration`: checks that the components agree in sign and that the
/// duration's total magnitude stays representable.
///
/// Spec: 
fn is_valid_duration(
  years : Int64,
  months : Int64,
  weeks : Int64,
  days : Int64,
  hours : Int64,
  minutes : Int64,
  seconds : Int64,
  milliseconds : Int64,
  microseconds : @int128.Int128,
  nanoseconds : @int128.Int128,
) -> Bool {
  let signums = [
    years,
    months,
    weeks,
    days,
    hours,
    minutes,
    seconds,
    milliseconds,
    microseconds.signum().to_int64(),
    nanoseconds.signum().to_int64(),
  ]
  let sign = duration_sign(signums)
  for v in signums {
    if v < 0L && sign == Positive {
      return false
    }
    if v > 0L && sign == Negative {
      return false
    }
  }
  // Years, months and weeks are stored in 32 bits by the reference
  // implementation, so their magnitude is bounded independently.
  let u32_max = 4_294_967_295L
  if years.abs() > u32_max || months.abs() > u32_max || weeks.abs() > u32_max {
    return false
  }
  // The spec defines the magnitude check over ECMAScript numbers, so the
  // sub-minute components are first snapped to the nearest representable
  // `Double`. Without this, a value one nanosecond under the limit would be
  // accepted here but rejected by an engine.
  // See .
  let seconds = round_trip_through_double_i64(seconds)
  let milliseconds = round_trip_through_double_i64(milliseconds)
  let microseconds = round_trip_through_double(microseconds)
  let nanoseconds = round_trip_through_double(nanoseconds)
  // The whole duration, expressed in nanoseconds, must stay below 2^53
  // seconds, the point past which a `Double` count of seconds loses integer
  // precision.
  let whole_ns = @int128.of_int64(days)
    .mul(i128_ns_per_day)
    .add(@int128.of_int64(hours).mul(@int128.of_int64(NS_PER_HOUR)))
    .add(@int128.of_int64(minutes).mul(@int128.of_int64(NS_PER_MINUTE)))
    .add(@int128.of_int64(seconds).mul(i128_billion))
  let sub_ns = @int128.of_int64(milliseconds)
    .mul(i128_million)
    .add(microseconds.mul(i128_thousand))
    .add(nanoseconds)
  let total = whole_ns.add(sub_ns)
  total.abs().compare(max_safe_ns_precision) < 0
}

///|
/// Rounds a value to the nearest `Double` and back, saturating if the result
/// leaves the 128-bit range.
fn round_trip_through_double(v : @int128.Int128) -> @int128.Int128 {
  match @int128.of_double(v.to_double()) {
    Some(rounded) => rounded
    None => if v.is_negative() { @int128.min_value } else { @int128.max_value }
  }
}

///|
/// Rounds a value to the nearest `Double` and back, saturating at the `Int64`
/// bounds.
fn round_trip_through_double_i64(v : Int64) -> Int64 {
  round_trip_through_double(@int128.of_int64(v)).to_int64_saturating()
}

///|
/// `2^53` seconds expressed in nanoseconds: the bound on a valid duration.
let max_safe_ns_precision : @int128.Int128 = @int128.of_int64(
  TWO_POWER_FIFTY_THREE,
).mul(i128_billion)