///|
/// A wall-clock time, as the `[[ISOHour]]` through `[[ISONanosecond]]`
/// internal slots.
///
/// The fields are declared in descending significance so that the derived
/// comparison orders times chronologically.
pub struct IsoTime {
  /// The hour, in `0 ..= 23`.
  hour : Int
  /// The minute, in `0 ..= 59`.
  minute : Int
  /// The second, in `0 ..= 59`. Temporal has no leap seconds.
  second : Int
  /// The millisecond, in `0 ..= 999`.
  millisecond : Int
  /// The microsecond within the millisecond, in `0 ..= 999`.
  microsecond : Int
  /// The nanosecond within the microsecond, in `0 ..= 999`.
  nanosecond : Int
} derive(Eq, Compare, Debug)

///|
pub impl Show for IsoTime with fn output(self, logger) {
  let buf = StringBuilder::new()
  write_time(
    buf,
    self.hour,
    self.minute,
    self.second,
    self.subsecond_nanoseconds(),
    Auto,
    true,
  )
  logger.write_string(buf.to_string())
}

///|
/// Midnight, `00:00:00`.
pub let iso_time_midnight : IsoTime = {
  hour: 0,
  minute: 0,
  second: 0,
  millisecond: 0,
  microsecond: 0,
  nanosecond: 0,
}

///|
/// Noon, `12:00:00`.
///
/// Range checks use noon as the reference time so that a date at either end of
/// the supported range is judged by its middle rather than its edge.
let iso_time_noon : IsoTime = { ..iso_time_midnight, hour: 12 }

///|
/// Returns the hour.
pub fn IsoTime::hour(self : IsoTime) -> Int {
  self.hour
}

///|
/// Returns the minute.
pub fn IsoTime::minute(self : IsoTime) -> Int {
  self.minute
}

///|
/// Returns the second.
pub fn IsoTime::second(self : IsoTime) -> Int {
  self.second
}

///|
/// Returns the millisecond.
pub fn IsoTime::millisecond(self : IsoTime) -> Int {
  self.millisecond
}

///|
/// Returns the microsecond within the millisecond.
pub fn IsoTime::microsecond(self : IsoTime) -> Int {
  self.microsecond
}

///|
/// Returns the nanosecond within the microsecond.
pub fn IsoTime::nanosecond(self : IsoTime) -> Int {
  self.nanosecond
}

///|
/// Returns the whole sub-second part as a nanosecond count, in `0 ..= 999999999`.
fn IsoTime::subsecond_nanoseconds(self : IsoTime) -> Int {
  self.millisecond * 1_000_000 + self.microsecond * 1_000 + self.nanosecond
}

///|
/// `RegulateTime`: creates a time, clamping or rejecting out-of-range fields.
fn IsoTime::new_with_overflow(
  hour : Int,
  minute : Int,
  second : Int,
  millisecond : Int,
  microsecond : Int,
  nanosecond : Int,
  overflow : Overflow,
) -> IsoTime raise TemporalError {
  match overflow {
    Constrain =>
      {
        hour: hour.max(0).min(23),
        minute: minute.max(0).min(59),
        second: second.max(0).min(59),
        millisecond: millisecond.max(0).min(999),
        microsecond: microsecond.max(0).min(999),
        nanosecond: nanosecond.max(0).min(999),
      }
    Reject => {
      if !is_valid_iso_time(
          hour, minute, second, millisecond, microsecond, nanosecond,
        ) {
        raise RangeError("time fields are out of range")
      }
      { hour, minute, second, millisecond, microsecond, nanosecond }
    }
  }
}

///|
/// Returns whether the components name a valid wall-clock time.
pub fn is_valid_iso_time(
  hour : Int,
  minute : Int,
  second : Int,
  millisecond : Int,
  microsecond : Int,
  nanosecond : Int,
) -> Bool {
  hour >= 0 &&
  hour <= 23 &&
  minute >= 0 &&
  minute <= 59 &&
  second >= 0 &&
  second <= 59 &&
  millisecond >= 0 &&
  millisecond <= 999 &&
  microsecond >= 0 &&
  microsecond <= 999 &&
  nanosecond >= 0 &&
  nanosecond <= 999
}

///|
/// `BalanceTime`: carries out-of-range components upward, returning the number
/// of whole days that overflowed along with the balanced time.
///
/// The sub-second inputs are 128-bit because a normalized time duration can
/// contribute more nanoseconds than an `Int64` holds.
fn IsoTime::balance(
  hour : Int64,
  minute : Int64,
  second : Int64,
  millisecond : Int64,
  microsecond : @int128.Int128,
  nanosecond : @int128.Int128,
) -> (Int64, IsoTime) {
  let (carry, nanosecond) = nanosecond.div_rem_euclid(i128_thousand)
  let microsecond = microsecond.add(carry)
  let (carry, microsecond) = microsecond.div_rem_euclid(i128_thousand)
  // From here the values are bounded by the carries above, so 64-bit
  // arithmetic suffices for the rest of the cascade.
  let millisecond = millisecond + carry.to_int64_saturating()
  let (carry, millisecond) = div_mod(millisecond, 1000L)
  let (carry, second) = div_mod(second + carry, 60L)
  let (carry, minute) = div_mod(minute + carry, 60L)
  let (days, hour) = div_mod(hour + carry, 24L)
  (
    days,
    {
      hour: hour.to_int(),
      minute: minute.to_int(),
      second: second.to_int(),
      millisecond: millisecond.to_int(),
      microsecond: microsecond.to_int64_saturating().to_int(),
      nanosecond: nanosecond.to_int64_saturating().to_int(),
    },
  )
}

///|
/// `DifferenceTime`: the signed time duration from this time to `other`.
fn IsoTime::diff(self : IsoTime, other : IsoTime) -> TimeDuration {
  TimeDuration::from_components(
    (other.hour - self.hour).to_int64(),
    (other.minute - self.minute).to_int64(),
    (other.second - self.second).to_int64(),
    (other.millisecond - self.millisecond).to_int64(),
    @int128.of_int(other.microsecond - self.microsecond),
    @int128.of_int(other.nanosecond - self.nanosecond),
  )
}

///|
/// `AddTime`: adds a normalized time duration, returning the day overflow.
fn IsoTime::add(self : IsoTime, duration : TimeDuration) -> (Int64, IsoTime) {
  IsoTime::balance(
    self.hour.to_int64(),
    self.minute.to_int64(),
    self.second.to_int64() + duration.seconds(),
    self.millisecond.to_int64(),
    @int128.of_int(self.microsecond),
    @int128.of_int(self.nanosecond + duration.subseconds()),
  )
}

///|
/// `IsoTimeToEpochMs`: milliseconds since midnight.
fn IsoTime::to_epoch_ms(self : IsoTime) -> Int64 {
  self.hour.to_int64() * MS_PER_HOUR +
  self.minute.to_int64() * MS_PER_MINUTE +
  self.second.to_int64() * 1000L +
  self.millisecond.to_int64()
}

///|
/// `RoundTime`: rounds to a multiple of the resolved smallest unit, returning
/// the whole days that rounding carried out of the time.
fn IsoTime::round(
  self : IsoTime,
  options : ResolvedRoundingOptions,
) -> (Int64, IsoTime) raise TemporalError {
  // The quantity being rounded is everything strictly below the smallest
  // unit's own place, expressed in nanoseconds.
  let quantity = match options.smallest_unit {
    Day | Hour => @int128.of_int64(self.to_nanoseconds_since_midnight())
    Minute =>
      @int128.of_int64(
        self.to_nanoseconds_since_midnight() -
        self.hour.to_int64() * NS_PER_HOUR,
      )
    Second =>
      @int128.of_int64(
        self.second.to_int64() * NS_PER_SECOND +
        self.subsecond_nanoseconds().to_int64(),
      )
    Millisecond => @int128.of_int(self.subsecond_nanoseconds())
    Microsecond => @int128.of_int(self.microsecond * 1000 + self.nanosecond)
    Nanosecond => @int128.of_int(self.nanosecond)
    _ => raise RangeError("invalid smallestUnit value for time rounding")
  }
  let length = temporal_unwrap(
    options.smallest_unit.as_nanoseconds(),
    "time unit length",
  )
  let increment = @int128.of_int(options.increment.get()).mul(
    @int128.of_int64(length),
  )
  let rounded = IncrementRounder::from_signed_num(quantity, increment)
    .round(options.rounding_mode)
    .div(@int128.of_int64(length))
    .to_int64_saturating()
  // Rebuild the time from the fields above the smallest unit plus the rounded
  // count of that unit, letting `balance` carry any overflow.
  match options.smallest_unit {
    Day => (rounded, iso_time_midnight)
    Hour => IsoTime::balance(rounded, 0L, 0L, 0L, @int128.zero, @int128.zero)
    Minute =>
      IsoTime::balance(
        self.hour.to_int64(),
        rounded,
        0L,
        0L,
        @int128.zero,
        @int128.zero,
      )
    Second =>
      IsoTime::balance(
        self.hour.to_int64(),
        self.minute.to_int64(),
        rounded,
        0L,
        @int128.zero,
        @int128.zero,
      )
    Millisecond =>
      IsoTime::balance(
        self.hour.to_int64(),
        self.minute.to_int64(),
        self.second.to_int64(),
        rounded,
        @int128.zero,
        @int128.zero,
      )
    Microsecond =>
      IsoTime::balance(
        self.hour.to_int64(),
        self.minute.to_int64(),
        self.second.to_int64(),
        self.millisecond.to_int64(),
        @int128.of_int64(rounded),
        @int128.zero,
      )
    Nanosecond =>
      IsoTime::balance(
        self.hour.to_int64(),
        self.minute.to_int64(),
        self.second.to_int64(),
        self.millisecond.to_int64(),
        @int128.of_int(self.microsecond),
        @int128.of_int64(rounded),
      )
    _ => raise AssertError("unreachable smallest unit in time rounding")
  }
}

///|
/// Returns the nanoseconds elapsed since midnight.
fn IsoTime::to_nanoseconds_since_midnight(self : IsoTime) -> Int64 {
  self.hour.to_int64() * NS_PER_HOUR +
  self.minute.to_int64() * NS_PER_MINUTE +
  self.second.to_int64() * NS_PER_SECOND +
  self.subsecond_nanoseconds().to_int64()
}