///|
/// A normalized microsecond duration with Python timedelta's full day range.
/// The three components avoid overflowing a single Int64 microsecond count.
pub struct TimeDelta {
  priv days : Int32
  priv seconds : Int32
  priv microseconds : Int32
} derive(Eq, Compare, Debug)

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

///|
pub extend TimeDelta with Compare::{compare, op_lt, op_le, op_gt, op_ge}

///|
pub extend TimeDelta with @debug.Debug::{to_repr}

///|
fn delta_split(value : Int64, divisor : Int64) -> (Int64, Int64) {
  let quotient = value / divisor
  let remainder = value % divisor
  if remainder < 0L {
    (quotient - 1L, remainder + divisor)
  } else {
    (quotient, remainder)
  }
}

///|
fn normalized_timedelta(
  days : Int64,
  microseconds : Int64,
) -> TimeDelta raise DateError {
  let (extra_days, remainder) = delta_split(microseconds, 86_400_000_000L)
  let days = days + extra_days
  if days < -999999999L || days > 999999999L {
    raise DateError::InvalidDate(
      message="timedelta days are outside -999999999..999999999",
    )
  }
  {
    days: days.to_int(),
    seconds: (remainder / 1_000_000L).to_int(),
    microseconds: (remainder % 1_000_000L).to_int(),
  }
}

///|
/// Construct an exact integer-component duration. Components are normalized;
/// seconds and microseconds are nonnegative even when the duration is negative.
///
/// ```mbt check
/// test {
///   let delta = @date.timedelta(microseconds=-1L)
///   assert_eq(
///     (delta.days(), delta.seconds(), delta.microseconds()),
///     (-1, 86399, 999999),
///   )
///   assert_eq(delta.total_seconds(), -0.000001)
/// }
/// ```
pub fn timedelta(
  days~ : Int32 = 0,
  seconds~ : Int64 = 0L,
  microseconds~ : Int64 = 0L,
  milliseconds~ : Int64 = 0L,
  minutes~ : Int32 = 0,
  hours~ : Int32 = 0,
  weeks~ : Int32 = 0,
) -> TimeDelta raise DateError {
  let (second_days, second_rest) = delta_split(seconds, 86400L)
  let (micro_days, micro_rest) = delta_split(microseconds, 86_400_000_000L)
  let (milli_days, milli_rest) = delta_split(milliseconds, 86400000L)
  let (minute_days, minute_rest) = delta_split(Int64::from_int(minutes), 1440L)
  let (hour_days, hour_rest) = delta_split(Int64::from_int(hours), 24L)
  normalized_timedelta(
    Int64::from_int(days) +
    Int64::from_int(weeks) * 7L +
    second_days +
    micro_days +
    milli_days +
    minute_days +
    hour_days,
    second_rest * 1_000_000L +
    micro_rest +
    milli_rest * 1000L +
    minute_rest * 60_000_000L +
    hour_rest * 3_600_000_000L,
  )
}

///|
/// Convert a floating-point day count to a microsecond duration. Fractional
/// microseconds use round-half-to-even, matching datetime.timedelta(days=x).
/// NaN, infinity and durations outside the day range raise DateError.
///
/// ```mbt check
/// test {
///   let delta = @date.num2timedelta(1.5)
///   assert_eq((delta.days(), delta.seconds()), (1, 43200))
/// }
/// ```
pub fn num2timedelta(value : Double) -> TimeDelta raise DateError {
  if value.is_nan() ||
    value.is_inf() ||
    value <= -1000000000.0 ||
    value >= 1000000000.0 {
    raise DateError::InvalidDate(
      message="day count is not a finite representable timedelta",
    )
  }
  let days = value.to_int64()
  // The whole-day microsecond contribution is even. Rounding the fractional
  // contribution alone therefore preserves the parity of CPython's integer sum.
  let fraction = (value - days.to_double()) * 86_400_000_000.0
  normalized_timedelta(days, round_even_to_int64(fraction))
}

///|
/// Convert a floating-point day array, preserving order. Invalid elements
/// raise DateError; the input is never modified.
pub fn num2timedeltas(
  values : Array[Double],
) -> Array[TimeDelta] raise DateError {
  values.map(num2timedelta)
}

///|
pub fn TimeDelta::days(self : TimeDelta) -> Int32 {
  self.days
}

///|
pub fn TimeDelta::seconds(self : TimeDelta) -> Int32 {
  self.seconds
}

///|
pub fn TimeDelta::microseconds(self : TimeDelta) -> Int32 {
  self.microseconds
}

///|
/// Return total seconds as a float, retaining the sign of tiny negative values.
pub fn TimeDelta::total_seconds(self : TimeDelta) -> Double {
  let seconds = Int64::from_int(self.days) * 86400L +
    Int64::from_int(self.seconds)
  if seconds < 0L && self.microseconds != 0 {
    (seconds + 1L).to_double() +
    Double::from_int(self.microseconds - 1000000) / 1_000_000.0
  } else {
    seconds.to_double() + Double::from_int(self.microseconds) / 1_000_000.0
  }
}

///|
/// Add a duration in local calendar time, preserving all microsecond digits.
/// A retained rule zone resolves the new offset: folds choose the earlier
/// instant and gaps use dateutil's post-transition offset. Calendar overflow
/// raises DateError. Fixed offsets retain their original offset.
pub fn DateTime::add_timedelta(
  self : DateTime,
  delta : TimeDelta,
) -> DateTime raise DateError {
  self.add_timedelta_multiple(delta, 1L)
}

///|
fn DateTime::add_timedelta_multiple(
  self : DateTime,
  delta : TimeDelta,
  factor : Int64,
) -> DateTime raise DateError {
  let product = if factor == 0L {
    0L
  } else {
    // Any nonzero multiple of a larger duration cannot fit between two
    // supported local dates. Check this before converting the whole duration.
    if delta.days < -3652060 || delta.days > 3652060 {
      raise DateError::InvalidDate(
        message="date arithmetic overflows years 1..9999",
      )
    }
    let micros = Int64::from_int(delta.days) * 86_400_000_000L +
      Int64::from_int(delta.seconds) * 1_000_000L +
      Int64::from_int(delta.microseconds)
    if micros != 0L {
      let limit = 315_537_897_599_999_999L / micros.abs()
      if factor > limit || factor < -limit {
        raise DateError::InvalidDate(
          message="date arithmetic overflows years 1..9999",
        )
      }
    }
    micros * factor
  }
  let shifted = datetime_from_microseconds(
    locator_microseconds(self) + product,
    self.offset_seconds,
  )
  match self.timezone {
    Some(zone) => zone.resolve_arithmetic_local(shifted)
    None => shifted
  }
}