///|
/// A span of time, such as "2 hours and 30 minutes" or "3 years, 2 months".
///
/// A duration is not anchored to any point on the timeline. Its date
/// components (years, months, weeks, days) and time components (hours through
/// nanoseconds) are kept separate because the length of a year or month
/// depends on where it starts, so converting between the two groups requires a
/// reference point.
///
/// Every non-zero component of a valid duration shares the same sign.
///
/// Reference: 
pub struct Duration {
  years : Int64
  months : Int64
  weeks : Int64
  days : Int64
  hours : Int64
  minutes : Int64
  seconds : Int64
  milliseconds : Int64
  microseconds : @int128.Int128
  nanoseconds : @int128.Int128
} derive(Eq)

///|
/// A duration of zero length.
pub let duration_zero : Duration = {
  years: 0,
  months: 0,
  weeks: 0,
  days: 0,
  hours: 0,
  minutes: 0,
  seconds: 0,
  milliseconds: 0,
  microseconds: @int128.zero,
  nanoseconds: @int128.zero,
}

///|
pub impl Default for Duration with fn default() {
  duration_zero
}

///|
pub impl Show for Duration with fn output(self, logger) {
  // The default options never round, so this cannot fail.
  logger.write_string(
    try! self.to_string_with_options(ToStringRoundingOptions::default()),
  )
}

///|
pub impl Debug for Duration with fn to_repr(self) {
  Repr::Repr(self.to_string())
}

///|
/// `CreateTemporalDuration`: creates a validated duration.
///
/// Raises a `RangeError` when the components disagree in sign or the total
/// magnitude is not representable.
///
/// ```mbt check
/// test {
///   let d = @temporal.Duration::of(weeks=2, days=3)
///   inspect(d, content="P2W3D")
/// }
/// ```
pub fn Duration::new(
  years : Int64,
  months : Int64,
  weeks : Int64,
  days : Int64,
  hours : Int64,
  minutes : Int64,
  seconds : Int64,
  milliseconds : Int64,
  microseconds : @int128.Int128,
  nanoseconds : @int128.Int128,
) -> Duration raise TemporalError {
  if !is_valid_duration(
      years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds,
      nanoseconds,
    ) {
    raise RangeError("duration is not valid")
  }
  {
    years,
    months,
    weeks,
    days,
    hours,
    minutes,
    seconds,
    milliseconds,
    microseconds,
    nanoseconds,
  }
}

///|
/// Creates a duration from date and time components given as `Int`.
///
/// This is the ergonomic form for the common case; use [`Duration::new`] when
/// a component needs the full `Int64` or 128-bit range.
///
/// ```mbt check
/// test {
///   let d = @temporal.Duration::of(hours=2, minutes=30)
///   inspect(d, content="PT2H30M")
/// }
/// ```
pub fn Duration::of(
  years? : Int64 = 0,
  months? : Int64 = 0,
  weeks? : Int64 = 0,
  days? : Int64 = 0,
  hours? : Int64 = 0,
  minutes? : Int64 = 0,
  seconds? : Int64 = 0,
  milliseconds? : Int64 = 0,
  microseconds? : Int64 = 0,
  nanoseconds? : Int64 = 0,
) -> Duration raise TemporalError {
  Duration::new(
    years,
    months,
    weeks,
    days,
    hours,
    minutes,
    seconds,
    milliseconds,
    @int128.of_int64(microseconds),
    @int128.of_int64(nanoseconds),
  )
}

///|
/// Creates a duration with only date components.
pub fn Duration::from_date_duration(
  date : DateDuration,
) -> Duration raise TemporalError {
  Duration::new(
    date.years,
    date.months,
    date.weeks,
    date.days,
    0L,
    0L,
    0L,
    0L,
    @int128.zero,
    @int128.zero,
  )
}

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

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

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

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

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

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

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

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

///|
/// Returns the microseconds component.
pub fn Duration::microseconds(self : Duration) -> @int128.Int128 {
  self.microseconds
}

///|
/// Returns the nanoseconds component.
pub fn Duration::nanoseconds(self : Duration) -> @int128.Int128 {
  self.nanoseconds
}

///|
/// `DurationSign`: the sign shared by every non-zero component.
///
/// ```mbt check
/// test {
///   inspect(@temporal.Duration::of(days=-1).sign(), content="-1")
///   inspect(@temporal.duration_zero.sign(), content="0")
/// }
/// ```
pub fn Duration::sign(self : Duration) -> Sign {
  duration_sign(self.fields_signum())
}

///|
/// Returns the sign of each component, in descending magnitude order.
fn Duration::fields_signum(self : Duration) -> Array[Int64] {
  [
    self.years.compare(0L).to_int64(),
    self.months.compare(0L).to_int64(),
    self.weeks.compare(0L).to_int64(),
    self.days.compare(0L).to_int64(),
    self.hours.compare(0L).to_int64(),
    self.minutes.compare(0L).to_int64(),
    self.seconds.compare(0L).to_int64(),
    self.milliseconds.compare(0L).to_int64(),
    self.microseconds.signum().to_int64(),
    self.nanoseconds.signum().to_int64(),
  ]
}

///|
/// Returns whether every component is zero.
pub fn Duration::is_zero(self : Duration) -> Bool {
  self.sign() == Zero
}

///|
/// Returns the duration with every component negated.
///
/// ```mbt check
/// test {
///   inspect(@temporal.Duration::of(days=1, hours=2).negated(), content="-P1DT2H")
/// }
/// ```
pub fn Duration::negated(self : Duration) -> Duration {
  {
    years: -self.years,
    months: -self.months,
    weeks: -self.weeks,
    days: -self.days,
    hours: -self.hours,
    minutes: -self.minutes,
    seconds: -self.seconds,
    milliseconds: -self.milliseconds,
    microseconds: self.microseconds.neg(),
    nanoseconds: self.nanoseconds.neg(),
  }
}

///|
/// Returns the duration with every component made non-negative.
pub fn Duration::abs(self : Duration) -> Duration {
  {
    years: self.years.abs(),
    months: self.months.abs(),
    weeks: self.weeks.abs(),
    days: self.days.abs(),
    hours: self.hours.abs(),
    minutes: self.minutes.abs(),
    seconds: self.seconds.abs(),
    milliseconds: self.milliseconds.abs(),
    microseconds: self.microseconds.abs(),
    nanoseconds: self.nanoseconds.abs(),
  }
}

///|
/// Returns the date components as a [`DateDuration`].
///
/// This is lossy: any time components are dropped.
pub fn Duration::date_duration(self : Duration) -> DateDuration {
  DateDuration::new_unchecked(self.years, self.months, self.weeks, self.days)
}

///|
/// Returns whether the duration has any non-zero time component.
fn Duration::has_time_component(self : Duration) -> Bool {
  self.hours != 0L ||
  self.minutes != 0L ||
  self.seconds != 0L ||
  self.milliseconds != 0L ||
  !self.microseconds.is_zero() ||
  !self.nanoseconds.is_zero()
}

///|
/// `DefaultTemporalLargestUnit`: the largest unit with a non-zero component.
fn Duration::default_largest_unit(self : Duration) -> DateTimeUnit {
  let signums = self.fields_signum()
  for i, v in signums {
    if v != 0L {
      return unit_from_table_index(i)
    }
  }
  Nanosecond
}

///|
/// `ToInternalDurationRecord`: splits the duration into calendar and
/// normalized time parts.
fn Duration::to_internal(self : Duration) -> InternalDurationRecord {
  InternalDurationRecord::combine(
    self.date_duration(),
    TimeDuration::from_duration(self),
  )
}

///|
/// `TemporalDurationFromInternal`: expands a normalized time duration back
/// into components no larger than `largest_unit`.
fn Duration::from_internal(
  record : InternalDurationRecord,
  largest_unit : DateTimeUnit,
) -> Duration raise TemporalError {
  let sign = record.time.sign().to_multiplier().to_int64()
  let mut nanoseconds = record.time.nanoseconds().abs()
  let mut days = 0L
  let mut hours = 0L
  let mut minutes = 0L
  let mut seconds = 0L
  let mut milliseconds = 0L
  let mut microseconds = @int128.zero
  // Each arm carries the split one unit further up than the arm below it.
  if largest_unit >= Microsecond {
    let (q, r) = nanoseconds.div_rem_euclid(i128_thousand)
    microseconds = q
    nanoseconds = r
  }
  if largest_unit >= Millisecond {
    let (q, r) = microseconds.div_rem_euclid(i128_thousand)
    milliseconds = q.to_int64_saturating()
    microseconds = r
  }
  if largest_unit >= Second {
    let (q, r) = div_mod(milliseconds, 1000L)
    seconds = q
    milliseconds = r
  }
  if largest_unit >= Minute {
    let (q, r) = div_mod(seconds, 60L)
    minutes = q
    seconds = r
  }
  if largest_unit >= Hour {
    let (q, r) = div_mod(minutes, 60L)
    hours = q
    minutes = r
  }
  if largest_unit >= Day {
    let (q, r) = div_mod(hours, 24L)
    days = q
    hours = r
  }
  Duration::new(
    record.date.years,
    record.date.months,
    record.date.weeks,
    record.date.days + days * sign,
    hours * sign,
    minutes * sign,
    seconds * sign,
    milliseconds * sign,
    microseconds.mul(@int128.of_int64(sign)),
    nanoseconds.mul(@int128.of_int64(sign)),
  )
}

///|
/// `ToDateDurationRecordWithoutTime`: keeps the calendar components and
/// truncates the time part to whole days.
fn Duration::to_date_duration_record_without_time(
  self : Duration,
) -> DateDuration raise TemporalError {
  InternalDurationRecord::from_duration_with_24_hour_days(self).to_date_duration_record_without_time()
}

///|
/// Adds two durations.
///
/// Neither operand may have a calendar component, because combining years or
/// months requires a reference point; use `PlainDate::add` or
/// `ZonedDateTime::add` for that.
///
/// ```mbt check
/// test {
///   let a = @temporal.Duration::of(minutes=45)
///   let b = @temporal.Duration::of(hours=1)
///   inspect(a.add(b), content="PT1H45M")
/// }
/// ```
pub fn Duration::add(
  self : Duration,
  other : Duration,
) -> Duration raise TemporalError {
  let largest_unit = self
    .default_largest_unit()
    .larger(other.default_largest_unit())
  // Adding calendar units would need a reference date to resolve their length.
  if largest_unit.is_calendar_unit() {
    raise RangeError(
      "adding durations with years, months or weeks requires a relativeTo reference",
    )
  }
  let combined = InternalDurationRecord::from_duration_with_24_hour_days(self).time.add(
    InternalDurationRecord::from_duration_with_24_hour_days(other).time,
  )
  Duration::from_internal(
    InternalDurationRecord::combine(DateDuration::default(), combined),
    largest_unit,
  )
}

///|
/// Subtracts `other` from this duration.
///
/// The same calendar-component restriction as [`Duration::add`] applies.
pub fn Duration::subtract(
  self : Duration,
  other : Duration,
) -> Duration raise TemporalError {
  self.add(other.negated())
}