///|
/// Calendar-relative changes and absolute field replacements. Fractional time
/// components remain separate until normalized or applied; months are calendar
/// months. Construction and inspection do not read the date epoch or timezone rc.
pub struct RelativeDelta {
  priv years : Int64
  priv months : Int32
  priv days : Double
  priv leapdays : Double
  priv hours : Double
  priv minutes : Double
  priv seconds : Double
  priv microseconds : Double
  priv year : Int32?
  priv month : Int32?
  priv day : Int32?
  priv hour : Int32?
  priv minute : Int32?
  priv second : Int32?
  priv microsecond : Int32?
  priv weekday : RelativeWeekday?
} derive(Debug)

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

///|
fn relative_finite(value : Double) -> Unit raise DateError {
  if value.is_nan() || value.is_inf() {
    raise DateError::InvalidDate(
      message="relative date components must be finite",
    )
  }
}

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

///|
// CPython float divmod for a nonnegative input and positive integer divisor.
// The quotient rounds the result of (value - remainder) / divisor to recover
// exact-integer division when the final floating operation is slightly inexact.
fn relative_carry(
  value : Double,
  base : Double,
  threshold : Double,
) -> (Double, Double) {
  if Double::abs(value) <= threshold {
    return (0.0, value)
  }
  let magnitude = Double::abs(value)
  let remainder = magnitude % base
  let quotient = (magnitude - remainder) / base
  let floor = quotient.floor()
  let integral = if quotient - floor > 0.5 { floor + 1.0 } else { floor }
  if value < 0.0 {
    (-integral, -remainder)
  } else {
    (integral, remainder)
  }
}

///|
/// Construct dateutil's relative/absolute calendar fields. Relative years and
/// months are Int64; fractional day/time fields are finite Double. Absolute
/// fields are stored without datetime validation until application, including
/// year/month/day=0 fallback. Use either weekday (-7..6 tuple index) or the
/// on_weekday object form. nlyearday takes precedence over a nonzero yearday.
pub fn RelativeDelta::new(
  years~ : Int64 = 0L,
  months~ : Int64 = 0L,
  days~ : Double = 0.0,
  weeks~ : Double = 0.0,
  leapdays~ : Double = 0.0,
  hours~ : Double = 0.0,
  minutes~ : Double = 0.0,
  seconds~ : Double = 0.0,
  microseconds~ : Double = 0.0,
  year? : Int32,
  month? : Int32,
  day? : Int32,
  hour? : Int32,
  minute? : Int32,
  second? : Int32,
  microsecond? : Int32,
  weekday? : Int32,
  on_weekday? : RelativeWeekday,
  yearday? : Int32,
  nlyearday? : Int32,
) -> RelativeDelta raise DateError {
  for value in [days, weeks, leapdays, hours, minutes, seconds, microseconds] {
    relative_finite(value)
  }
  let weekday = match (weekday, on_weekday) {
    (Some(_), Some(_)) =>
      raise DateError::InvalidDate(message="use either weekday or on_weekday")
    (Some(index), None) => {
      if index < -7 || index > 6 {
        raise DateError::InvalidDate(message="integer weekday must be in -7..6")
      }
      Some(RelativeWeekday::new(if index < 0 { index + 7 } else { index }))
    }
    (_, object) => object
  }
  let mut month = month
  let mut day = day
  let mut leapdays = leapdays
  let yday = if nlyearday.unwrap_or(0) != 0 {
    nlyearday.unwrap()
  } else {
    let selected = yearday.unwrap_or(0)
    if selected > 59 {
      leapdays = -1.0
    }
    selected
  }
  if yday != 0 {
    if yday > 366 {
      raise DateError::InvalidDate(message="invalid relative year day")
    }
    let limits = [31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 366]
    let mut index = 0
    while index < 11 && yday > limits[index] {
      index += 1
    }
    month = Some(index + 1)
    day = Some(yday - (if index == 0 { 0 } else { limits[index - 1] }))
  }
  let (carry, microseconds) = relative_carry(microseconds, 1000000.0, 999999.0)
  let (carry, seconds) = relative_carry(seconds + carry, 60.0, 59.0)
  let (carry, minutes) = relative_carry(minutes + carry, 60.0, 59.0)
  let (carry, hours) = relative_carry(hours + carry, 24.0, 23.0)
  let days = days + weeks * 7.0 + carry
  for value in [days, hours, minutes, seconds, microseconds] {
    relative_finite(value)
  }
  let years = relative_add_years(years, months / 12L)
  {
    years,
    months: (months % 12L).to_int(),
    days,
    leapdays,
    hours,
    minutes,
    seconds,
    microseconds,
    year,
    month,
    day,
    hour,
    minute,
    second,
    microsecond,
    weekday,
  }
}

///|
/// Integral-valued Double, matching truncation in dateutil's weeks property.
pub fn RelativeDelta::weeks(self : RelativeDelta) -> Double {
  (self.days / 7.0).trunc()
}

///|
pub fn RelativeDelta::has_time(self : RelativeDelta) -> Bool {
  self.hours != 0.0 ||
  self.minutes != 0.0 ||
  self.seconds != 0.0 ||
  self.microseconds != 0.0 ||
  self.hour is Some(_) ||
  self.minute is Some(_) ||
  self.second is Some(_) ||
  self.microsecond is Some(_)
}

///|
pub fn RelativeDelta::years(self : RelativeDelta) -> Int64 {
  self.years
}

///|
pub fn RelativeDelta::months(self : RelativeDelta) -> Int32 {
  self.months
}

///|
pub fn RelativeDelta::days(self : RelativeDelta) -> Double {
  self.days
}

///|
pub fn RelativeDelta::leapdays(self : RelativeDelta) -> Double {
  self.leapdays
}

///|
pub fn RelativeDelta::hours(self : RelativeDelta) -> Double {
  self.hours
}

///|
pub fn RelativeDelta::minutes(self : RelativeDelta) -> Double {
  self.minutes
}

///|
pub fn RelativeDelta::seconds(self : RelativeDelta) -> Double {
  self.seconds
}

///|
pub fn RelativeDelta::microseconds(self : RelativeDelta) -> Double {
  self.microseconds
}

///|
pub fn RelativeDelta::year(self : RelativeDelta) -> Int32? {
  self.year
}

///|
pub fn RelativeDelta::month(self : RelativeDelta) -> Int32? {
  self.month
}

///|
pub fn RelativeDelta::day(self : RelativeDelta) -> Int32? {
  self.day
}

///|
pub fn RelativeDelta::hour(self : RelativeDelta) -> Int32? {
  self.hour
}

///|
pub fn RelativeDelta::minute(self : RelativeDelta) -> Int32? {
  self.minute
}

///|
pub fn RelativeDelta::second(self : RelativeDelta) -> Int32? {
  self.second
}

///|
pub fn RelativeDelta::microsecond(self : RelativeDelta) -> Int32? {
  self.microsecond
}

///|
pub fn RelativeDelta::weekday(self : RelativeDelta) -> RelativeWeekday? {
  self.weekday
}