///|
/// A calendar date with no time and no time zone, such as a birthday.
///
/// Reference: 
pub struct PlainDate {
  iso : IsoDate
  calendar : Calendar
} derive(Eq)

///|
pub impl Show for PlainDate with fn output(self, logger) {
  logger.write_string(self.to_string_with_options(DisplayCalendar::default()))
}

///|
pub impl Debug for PlainDate with fn to_repr(self) {
  Repr::Repr(self.to_string())
}

///|
/// Creates a date without validating it.
fn PlainDate::new_unchecked(iso : IsoDate, calendar : Calendar) -> PlainDate {
  { iso, calendar }
}

///|
/// Creates a date in the ISO calendar, rejecting one that does not exist.
///
/// ```mbt check
/// test {
///   inspect(@temporal.PlainDate::try_new(2024, 2, 29), content="2024-02-29")
/// }
/// ```
pub fn PlainDate::try_new(
  year : Int,
  month : Int,
  day : Int,
) -> PlainDate raise TemporalError {
  PlainDate::new_with_overflow(year, month, day, Reject, Calendar::ISO)
}

///|
/// Creates a date, handling out-of-range fields according to `overflow`.
pub fn PlainDate::new_with_overflow(
  year : Int,
  month : Int,
  day : Int,
  overflow : Overflow,
  calendar : Calendar,
) -> PlainDate raise TemporalError {
  let iso = IsoDate::new_with_overflow(year, month, day, overflow)
  { iso, calendar }
}

///|
/// Creates a date from an already-validated ISO date.
pub fn PlainDate::from_iso(iso : IsoDate, calendar : Calendar) -> PlainDate {
  { iso, calendar }
}

///|
/// Returns the underlying ISO date.
pub fn PlainDate::iso(self : PlainDate) -> IsoDate {
  self.iso
}

///|
/// Returns the calendar.
pub fn PlainDate::calendar(self : PlainDate) -> Calendar {
  self.calendar
}

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

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

///|
/// Returns the calendar-independent month code, such as `M02` for February.
pub fn PlainDate::month_code(self : PlainDate) -> String {
  month_code_of(self.iso.month)
}

///|
/// Returns the month code for a 1-based ISO month.
fn month_code_of(month : Int) -> String {
  if month < 10 {
    "M0\{month}"
  } else {
    "M\{month}"
  }
}

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

///|
/// Returns the ISO day of the week, Monday = 1 through Sunday = 7.
pub fn PlainDate::day_of_week(self : PlainDate) -> Int {
  iso_day_of_week(self.iso.year, self.iso.month, self.iso.day)
}

///|
/// Returns the 1-based day of the year.
pub fn PlainDate::day_of_year(self : PlainDate) -> Int {
  iso_day_of_year(self.iso.year, self.iso.month, self.iso.day)
}

///|
/// Returns the ISO week number.
pub fn PlainDate::week_of_year(self : PlainDate) -> Int {
  iso_week_of_year(self.iso.year, self.iso.month, self.iso.day).0
}

///|
/// Returns the year the ISO week belongs to, which can differ from the
/// calendar year at either end of a year.
pub fn PlainDate::year_of_week(self : PlainDate) -> Int {
  iso_week_of_year(self.iso.year, self.iso.month, self.iso.day).1
}

///|
/// Returns the number of days in the week, always 7 in the ISO calendar.
pub fn PlainDate::days_in_week(_self : PlainDate) -> Int {
  7
}

///|
/// Returns the number of days in this month.
pub fn PlainDate::days_in_month(self : PlainDate) -> Int {
  iso_days_in_month(self.iso.year, self.iso.month)
}

///|
/// Returns the number of days in this year.
pub fn PlainDate::days_in_year(self : PlainDate) -> Int {
  iso_days_in_year(self.iso.year)
}

///|
/// Returns the number of months in this year, always 12 in the ISO calendar.
pub fn PlainDate::months_in_year(_self : PlainDate) -> Int {
  12
}

///|
/// Returns whether this year is a leap year.
pub fn PlainDate::in_leap_year(self : PlainDate) -> Bool {
  is_leap_year(self.iso.year)
}

///|
/// Compares two dates chronologically.
///
/// ```mbt check
/// test {
///   let a = @temporal.PlainDate::try_new(2024, 1, 1)
///   let b = @temporal.PlainDate::try_new(2024, 6, 1)
///   inspect(a.compare(b), content="-1")
/// }
/// ```
pub impl Compare for PlainDate with fn compare(self, other) {
  self.iso.compare(other.iso)
}

///|
/// Returns whether two dates name the same day in the same calendar.
pub fn PlainDate::equals(self : PlainDate, other : PlainDate) -> Bool {
  self.iso == other.iso && self.calendar == other.calendar
}

///|
/// Returns a copy with the given fields replaced.
///
/// ```mbt check
/// test {
///   let date = @temporal.PlainDate::try_new(2024, 3, 15)
///   inspect(date.with_fields(month=1), content="2024-01-15")
/// }
/// ```
pub fn PlainDate::with_fields(
  self : PlainDate,
  year? : Int,
  month? : Int,
  day? : Int,
  overflow? : Overflow = Constrain,
) -> PlainDate raise TemporalError {
  PlainDate::new_with_overflow(
    year.unwrap_or(self.iso.year),
    month.unwrap_or(self.iso.month),
    day.unwrap_or(self.iso.day),
    overflow,
    self.calendar,
  )
}

///|
/// Returns a copy in a different calendar.
pub fn PlainDate::with_calendar(
  self : PlainDate,
  calendar : Calendar,
) -> PlainDate {
  { iso: self.iso, calendar }
}

///|
/// `CalendarDateAdd` for the ISO calendar.
fn PlainDate::calendar_date_add(
  self : PlainDate,
  duration : DateDuration,
  overflow : Overflow,
) -> PlainDate raise TemporalError {
  let result = self.iso.add_date_duration(duration, overflow)
  // The intermediate balancing allows one extra day at each end of the epoch
  // range, so the result is re-validated as a date in its own right.
  PlainDate::new_with_overflow(
    result.year,
    result.month,
    result.day,
    Reject,
    self.calendar,
  )
}

///|
/// Adds a duration to this date.
///
/// The duration's time components are truncated to whole days first, as
/// `AddDurationToDate` requires.
///
/// ```mbt check
/// test {
///   let jan31 = @temporal.PlainDate::try_new(2023, 1, 31)
///   let one_month = @temporal.Duration::of(months=1)
///   // February has no 31st, so `constrain` clamps to the end of the month.
///   inspect(jan31.add(one_month), content="2023-02-28")
/// }
/// ```
pub fn PlainDate::add(
  self : PlainDate,
  duration : Duration,
  overflow? : Overflow = Constrain,
) -> PlainDate raise TemporalError {
  self.calendar_date_add(
    duration.to_date_duration_record_without_time(),
    overflow,
  )
}

///|
/// Subtracts a duration from this date.
pub fn PlainDate::subtract(
  self : PlainDate,
  duration : Duration,
  overflow? : Overflow = Constrain,
) -> PlainDate raise TemporalError {
  self.add(duration.negated(), overflow~)
}

///|
/// `DaysUntil`: the whole days from this date to `other`.
fn PlainDate::days_until(self : PlainDate, other : PlainDate) -> Int64 {
  other.iso.to_epoch_days() - self.iso.to_epoch_days()
}

///|
/// `DifferenceDate`: the unrounded difference between two dates.
fn PlainDate::internal_diff_date(
  self : PlainDate,
  other : PlainDate,
  largest_unit : DateTimeUnit,
) -> DateDuration raise TemporalError {
  if self.iso == other.iso {
    return DateDuration::default()
  }
  // A pure day difference needs no calendar walk.
  if largest_unit is Day {
    return DateDuration::new(0L, 0L, 0L, self.days_until(other))
  }
  self.iso.diff_iso_date(other.iso, largest_unit)
}

///|
/// `DifferenceTemporalPlainDate`: the rounded difference between two dates.
fn PlainDate::diff_date(
  self : PlainDate,
  operation : DifferenceOperation,
  other : PlainDate,
  settings : DifferenceSettings,
) -> Duration raise TemporalError {
  if self.calendar != other.calendar {
    raise RangeError("cannot compare dates in different calendars")
  }
  let resolved = ResolvedRoundingOptions::from_diff_settings(
    settings,
    operation,
    UnitGroup::Date,
    Day,
    Day,
  )
  if self.iso == other.iso {
    return duration_zero
  }
  let date_diff = self.internal_diff_date(other, resolved.largest_unit)
  let mut duration = InternalDurationRecord::from_date_duration(date_diff)
  // Rounding to whole days by one is exactly what the difference already
  // produced, so the relative rounding pass can be skipped.
  let rounding_is_noop = resolved.smallest_unit is Day &&
    resolved.increment.get() == 1
  if !rounding_is_noop {
    let iso_date_time = IsoDateTime::new_unchecked(self.iso, iso_time_midnight)
    let origin_epoch_ns = iso_date_time.as_nanoseconds()
    let dest_epoch_ns = other.iso.as_nanoseconds()
    let dt = PlainDateTime::new_unchecked(iso_date_time, self.calendar)
    duration = duration.round_relative_duration(
      origin_epoch_ns,
      dest_epoch_ns,
      dt,
      None,
      utc_only_provider,
      resolved,
    )
  }
  let result = Duration::from_internal(duration, Day)
  match operation {
    Until => result
    Since => result.negated()
  }
}

///|
/// Returns the duration from this date until `other`.
///
/// ```mbt check
/// test {
///   let a = @temporal.PlainDate::try_new(2020, 1, 1)
///   let b = @temporal.PlainDate::try_new(2024, 3, 15)
///   let settings = @temporal.DifferenceSettings::new(largest_unit=Year)
///   inspect(a.until(b, settings~), content="P4Y2M14D")
/// }
/// ```
pub fn PlainDate::until(
  self : PlainDate,
  other : PlainDate,
  settings? : DifferenceSettings = DifferenceSettings::default(),
) -> Duration raise TemporalError {
  self.diff_date(Until, other, settings)
}

///|
/// Returns the duration from `other` until this date.
pub fn PlainDate::since(
  self : PlainDate,
  other : PlainDate,
  settings? : DifferenceSettings = DifferenceSettings::default(),
) -> Duration raise TemporalError {
  self.diff_date(Since, other, settings)
}

///|
/// Combines this date with a time to produce a `PlainDateTime`.
///
/// Without a time, midnight is used.
pub fn PlainDate::to_plain_date_time(
  self : PlainDate,
  time? : PlainTime,
) -> PlainDateTime raise TemporalError {
  let iso_time = match time {
    Some(t) => t.iso
    None => iso_time_midnight
  }
  PlainDateTime::from_iso(IsoDateTime::new(self.iso, iso_time), self.calendar)
}

///|
/// Renders the date as `YYYY-MM-DD`, with an optional calendar annotation.
pub fn PlainDate::to_string_with_options(
  self : PlainDate,
  display_calendar : DisplayCalendar,
) -> String {
  let buf = StringBuilder::new()
  write_date(buf, self.iso.year, self.iso.month, self.iso.day)
  write_calendar_annotation(buf, self.calendar, display_calendar)
  buf.to_string()
}

///|
/// Renders the date as `YYYY-MM-DD`.
pub fn PlainDate::to_string(self : PlainDate) -> String {
  self.to_string_with_options(DisplayCalendar::default())
}