///|
/// A calendar date paired with a wall-clock time, with no time zone.
pub struct IsoDateTime {
  /// The date part.
  date : IsoDate
  /// The time part.
  time : IsoTime
} derive(Eq, Compare, Debug)

///|
pub impl Show for IsoDateTime with fn output(self, logger) {
  logger.write_string("\{self.date}T\{self.time}")
}

///|
/// Creates a date-time without checking that it is within range.
fn IsoDateTime::new_unchecked(date : IsoDate, time : IsoTime) -> IsoDateTime {
  { date, time }
}

///|
/// Creates a date-time, rejecting one outside the range Temporal supports.
fn IsoDateTime::new(
  date : IsoDate,
  time : IsoTime,
) -> IsoDateTime raise TemporalError {
  if !iso_datetime_within_limits(date, time) {
    raise RangeError("date-time is outside the ISO date-time limits")
  }
  { date, time }
}

///|
/// Returns the date part.
pub fn IsoDateTime::date(self : IsoDateTime) -> IsoDate {
  self.date
}

///|
/// Returns the time part.
pub fn IsoDateTime::time(self : IsoDateTime) -> IsoTime {
  self.time
}

///|
/// Raises unless the date-time is within the supported range.
fn IsoDateTime::check_within_limits(
  self : IsoDateTime,
) -> Unit raise TemporalError {
  if !iso_datetime_within_limits(self.date, self.time) {
    raise RangeError("date-time is outside the ISO date-time limits")
  }
}

///|
/// `ISODateTimeWithinLimits`.
///
/// The bound is the instant range widened by one day at each end, because a
/// wall-clock time may sit up to a day outside the instant range and still be
/// reachable once a time zone offset is applied.
fn iso_datetime_within_limits(date : IsoDate, time : IsoTime) -> Bool {
  if epoch_days_from_gregorian_date(date.year, date.month, date.day).abs() >
    MAX_EPOCH_DAYS {
    return false
  }
  let ns = utc_epoch_nanoseconds(date, time)
  let max = ns_max_instant.add(i128_ns_per_day)
  let min = ns_min_instant.sub(i128_ns_per_day)
  min.compare(ns) < 0 && max.compare(ns) > 0
}

///|
/// The epoch nanoseconds of a date and time read as UTC.
fn utc_epoch_nanoseconds(date : IsoDate, time : IsoTime) -> @int128.Int128 {
  let epoch_ms = epoch_days_to_epoch_ms(
    date.to_epoch_days(),
    time.to_epoch_ms(),
  )
  @int128.of_int64(epoch_ms)
  .mul(i128_million)
  .add(@int128.of_int(time.microsecond * 1000 + time.nanosecond))
}

///|
/// Returns the epoch nanoseconds of this date-time read as UTC.
fn IsoDateTime::as_nanoseconds(self : IsoDateTime) -> @int128.Int128 {
  utc_epoch_nanoseconds(self.date, self.time)
}

///|
/// `GetISOPartsFromEpoch`: splits epoch nanoseconds into a date and time,
/// after shifting by `offset` nanoseconds.
fn IsoDateTime::from_epoch_nanoseconds(
  epoch_nanoseconds : @int128.Int128,
  offset : Int64,
) -> IsoDateTime {
  // Split off the sub-millisecond part first so the bulk of the arithmetic can
  // happen in the 64-bit millisecond domain.
  let remainder_nanos = epoch_nanoseconds.rem_euclid(i128_million)
  let epoch_millis = epoch_nanoseconds
    .sub(remainder_nanos)
    .div_euclid(i128_million)
    .to_int64_saturating()
  let (year, month, day) = ymd_from_epoch_milliseconds(epoch_millis)
  let hour = rem_euclid(div_euclid(epoch_millis, MS_PER_HOUR), 24L)
  let minute = rem_euclid(div_euclid(epoch_millis, MS_PER_MINUTE), 60L)
  let second = rem_euclid(div_euclid(epoch_millis, 1000L), 60L)
  let millis = rem_euclid(epoch_millis, 1000L)
  let micros = remainder_nanos.div_euclid(i128_thousand)
  let nanos = remainder_nanos.rem_euclid(i128_thousand)
  IsoDateTime::balance(
    year,
    month,
    day.to_int64(),
    hour,
    minute,
    second,
    millis,
    micros,
    nanos.add(@int128.of_int64(offset)),
  )
}

///|
/// Balances unbalanced date and time fields into a valid date-time.
fn IsoDateTime::balance(
  year : Int,
  month : Int,
  day : Int64,
  hour : Int64,
  minute : Int64,
  second : Int64,
  millisecond : Int64,
  microsecond : @int128.Int128,
  nanosecond : @int128.Int128,
) -> IsoDateTime {
  let (overflow_days, time) = IsoTime::balance(
    hour, minute, second, millisecond, microsecond, nanosecond,
  )
  let epoch_days = iso_date_to_epoch_days(year, month, 1) +
    day -
    1L +
    overflow_days
  let (year, month, day) = ymd_from_epoch_days(epoch_days)
  IsoDateTime::new_unchecked(IsoDate::new_unchecked(year, month, day), time)
}

///|
/// `RoundISODateTime`: rounds the time part, carrying any whole days into the
/// date.
fn IsoDateTime::round(
  self : IsoDateTime,
  options : ResolvedRoundingOptions,
) -> IsoDateTime raise TemporalError {
  let (rounded_days, rounded_time) = self.time.round(options)
  let date = IsoDate::try_balance(
    self.date.year,
    self.date.month,
    self.date.day.to_int64() + rounded_days,
  )
  IsoDateTime::new(date, rounded_time)
}

///|
/// `DifferenceISODateTime`: the duration from this date-time to `other`.
fn IsoDateTime::diff(
  self : IsoDateTime,
  other : IsoDateTime,
  largest_unit : DateTimeUnit,
) -> InternalDurationRecord raise TemporalError {
  let time_duration = self.time.diff(other.time)
  let time_sign = time_duration.sign().to_int()
  let date_sign = other.date.compare(self.date).compare(0)
  // When the time part runs opposite to the date part, borrow a day from the
  // date so both parts end up with the same sign.
  let (adjusted_date, time_duration) = if time_sign == -date_sign {
    (
      IsoDate::balance(
        other.date.year,
        other.date.month,
        other.date.day + time_sign,
      ),
      time_duration.add_days(-time_sign.to_int64()),
    )
  } else {
    (other.date, time_duration)
  }
  let date_largest_unit = largest_unit.larger(Day)
  let date_diff = self.date.diff_iso_date(adjusted_date, date_largest_unit)
  // If the caller asked for a time unit, the whole date difference has to be
  // folded back into the time part.
  let (days, time_duration) = if largest_unit == date_largest_unit {
    (date_diff.days, time_duration)
  } else {
    (0L, time_duration.add_days(date_diff.days))
  }
  InternalDurationRecord::new(
    DateDuration::new_unchecked(
      date_diff.years,
      date_diff.months,
      date_diff.weeks,
      days,
    ),
    time_duration,
  )
}