///|
/// A calendar date in the ISO 8601 calendar, as the `[[ISOYear]]`,
/// `[[ISOMonth]]` and `[[ISODay]]` internal slots.
///
/// The fields are declared in descending significance so that the derived
/// comparison is the chronological one.
pub struct IsoDate {
  /// The ISO year, in `-271821 ..= 275760`.
  year : Int
  /// The month, in `1 ..= 12`.
  month : Int
  /// The day of the month, in `1 ..= 31`.
  day : Int
} derive(Eq, Compare, Debug)

///|
pub impl Show for IsoDate with fn output(self, logger) {
  let buf = StringBuilder::new()
  write_date(buf, self.year, self.month, self.day)
  logger.write_string(buf.to_string())
}

///|
/// The ISO date of the Unix epoch.
pub let iso_date_unix_epoch : IsoDate = { year: 1970, month: 1, day: 1 }

///|
/// Creates a date without checking that it exists or is in range.
fn IsoDate::new_unchecked(year : Int, month : Int, day : Int) -> IsoDate {
  { year, month, day }
}

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

///|
/// Returns the month, 1-based.
pub fn IsoDate::month(self : IsoDate) -> Int {
  self.month
}

///|
/// Returns the day of the month.
pub fn IsoDate::day(self : IsoDate) -> Int {
  self.day
}

///|
/// `RegulateISODate`: brings the fields into range according to `overflow`.
fn IsoDate::regulate(
  year : Int,
  month : Int,
  day : Int,
  overflow : Overflow,
) -> IsoDate raise TemporalError {
  match overflow {
    Constrain => {
      let month = month.max(1).min(12)
      let day = day.max(1).min(iso_days_in_month(year, month))
      { year, month, day }
    }
    Reject => {
      if !is_valid_iso_date(year, month, day) {
        raise RangeError("\{year}-\{month}-\{day} is not a valid ISO date")
      }
      { year, month, day }
    }
  }
}

///|
/// Creates a date, regulating it and then checking it is inside the range
/// Temporal supports.
fn IsoDate::new_with_overflow(
  year : Int,
  month : Int,
  day : Int,
  overflow : Overflow,
) -> IsoDate raise TemporalError {
  let date = IsoDate::regulate(year, month, day, overflow)
  date.check_within_limits()
  date
}

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

///|
/// Returns whether the date names a day that exists.
pub fn IsoDate::is_valid(self : IsoDate) -> Bool {
  is_valid_iso_date(self.year, self.month, self.day)
}

///|
/// Returns whether `year`-`month`-`day` names a day that exists.
pub fn is_valid_iso_date(year : Int, month : Int, day : Int) -> Bool {
  if month < 1 || month > 12 {
    return false
  }
  day >= 1 && day <= iso_days_in_month(year, month)
}

///|
/// `ISODateToEpochDays`: days since 1970-01-01.
pub fn IsoDate::to_epoch_days(self : IsoDate) -> Int64 {
  epoch_days_from_gregorian_date(self.year, self.month, self.day)
}

///|
/// The epoch nanoseconds of this date at midnight UTC.
fn IsoDate::as_nanoseconds(self : IsoDate) -> @int128.Int128 {
  utc_epoch_nanoseconds(self, iso_time_midnight)
}

///|
/// `ISODateToEpochDays` over unbalanced fields: `month` may be outside
/// `1 ..= 12` and `day` outside the month's length, and the excess carries.
fn iso_date_to_epoch_days(year : Int, month : Int, day : Int) -> Int64 {
  let resolved_year = year + div_euclid_i(month, 12)
  let resolved_month = rem_euclid_i(month, 12)
  epoch_days_from_gregorian_date(resolved_year, resolved_month, 1) +
  day.to_int64() -
  1L
}

///|
/// `BalanceISODate`: carries out-of-range month and day values into the year.
fn IsoDate::balance(year : Int, month : Int, day : Int) -> IsoDate {
  let epoch_days = iso_date_to_epoch_days(year, month, day)
  let (year, month, day) = ymd_from_epoch_days(epoch_days)
  { year, month, day }
}

///|
/// Balances a date while rejecting an intermediate that escapes the supported
/// epoch-day range.
///
/// `day` is an `Int64` because it accumulates a duration's day count before
/// being balanced, which can overflow an `Int` well before it overflows the
/// date range check.
fn IsoDate::try_balance(
  year : Int,
  month : Int,
  day : Int64,
) -> IsoDate raise TemporalError {
  let epoch_days = iso_date_to_epoch_days(year, month, 1) + day - 1L
  if epoch_days.abs() > MAX_EPOCH_DAYS {
    raise RangeError("epoch days exceed the maximum range")
  }
  let (year, month, day) = ymd_from_epoch_days(epoch_days)
  { year, month, day }
}

///|
/// `BalanceISOYearMonth`: carries an out-of-range month into the year.
fn balance_iso_year_month(year : Int, month : Int) -> (Int, Int) {
  (year + div_euclid_i(month - 1, 12), rem_euclid_i(month - 1, 12) + 1)
}

///|
/// `BalanceISOYearMonth` over 64-bit inputs, clamping the resulting year to
/// the `Int` range so that a wildly out-of-range duration still produces a
/// value the range check can reject.
fn balance_iso_year_month_clamped(year : Int64, month : Int64) -> (Int, Int) {
  let y = year + div_euclid(month - 1L, 12L)
  let m = rem_euclid(month - 1L, 12L) + 1L
  (y.max(-2147483648L).min(2147483647L).to_int(), m.to_int())
}

///|
/// Returns whether `year`-`month` is within the supported range.
///
/// The endpoints are partial months, so the boundary years need a month check
/// as well.
fn year_month_within_limits(year : Int, month : Int) -> Bool {
  if year < -271821 || year > 275760 {
    false
  } else if year == -271821 && month < 4 {
    false
  } else if year == 275760 && month > 9 {
    false
  } else {
    true
  }
}

///|
/// `AddISODate`: adds a date duration, regulating the intermediate year and
/// month before the days are carried.
fn IsoDate::add_date_duration(
  self : IsoDate,
  duration : DateDuration,
  overflow : Overflow,
) -> IsoDate raise TemporalError {
  // Years and months are applied first, and the day is constrained against
  // the resulting month before any day arithmetic happens. This is what makes
  // "January 31 plus one month" land on the end of February.
  let (year, month) = balance_iso_year_month_clamped(
    self.year.to_int64() + duration.years,
    self.month.to_int64() + duration.months,
  )
  let intermediate = IsoDate::new_with_overflow(year, month, self.day, overflow)
  let additional_days = duration.days + 7L * duration.weeks
  IsoDate::try_balance(
    intermediate.year,
    intermediate.month,
    intermediate.day.to_int64() + additional_days,
  )
}

///|
/// `DifferenceISODate`: the duration from this date to `other`, expressed in
/// units no larger than `largest_unit`.
fn IsoDate::diff_iso_date(
  self : IsoDate,
  other : IsoDate,
  largest_unit : DateTimeUnit,
) -> DateDuration raise TemporalError {
  let sign = -self.compare(other).compare(0)
  if sign == 0 {
    return DateDuration::default()
  }
  let mut years = 0
  let mut months = 0
  if largest_unit is (Year | Month) {
    // Start from the raw year difference rather than stepping one year at a
    // time; the answer is at most one away from it.
    let mut candidate_years = other.year - self.year
    if candidate_years != 0 {
      candidate_years = candidate_years - sign
    }
    while !iso_date_surpasses(
            IsoDate::new_unchecked(
              self.year + candidate_years,
              self.month,
              self.day,
            ),
            other,
            sign,
          ) {
      years = candidate_years
      candidate_years = candidate_years + sign
    }
    let mut candidate_months = sign
    let mut intermediate = balance_iso_year_month(
      self.year + years,
      self.month + candidate_months,
    )
    while !iso_date_surpasses(
            IsoDate::new_unchecked(intermediate.0, intermediate.1, self.day),
            other,
            sign,
          ) {
      months = candidate_months
      candidate_months = candidate_months + sign
      intermediate = balance_iso_year_month(
        intermediate.0,
        intermediate.1 + sign,
      )
    }
    if largest_unit is Month {
      months = months + years * 12
      years = 0
    }
  }
  let intermediate = balance_iso_year_month(
    self.year + years,
    self.month + months,
  )
  let constrained = IsoDate::new_with_overflow(
    intermediate.0,
    intermediate.1,
    self.day,
    Constrain,
  )
  // Whatever the calendar units did not cover is a whole number of days, so
  // the remainder is a plain epoch-day subtraction rather than another loop.
  let days = epoch_days_from_gregorian_date(other.year, other.month, other.day) -
    epoch_days_from_gregorian_date(
      constrained.year,
      constrained.month,
      constrained.day,
    )
  let (weeks, days) = if largest_unit is Week {
    (days / 7L, days % 7L)
  } else {
    (0L, days)
  }
  DateDuration::new(years.to_int64(), months.to_int64(), weeks, days)
}

///|
/// Returns whether stepping from `this` in direction `sign` has gone past
/// `other`.
fn iso_date_surpasses(this : IsoDate, other : IsoDate, sign : Int) -> Bool {
  this.compare(other).compare(0) * sign == 1
}