///|
/// A year and a month with no day, such as "the October 2030 issue".
///
/// A reference day is kept internally so that arithmetic and comparison have
/// a concrete date to work from; it is never part of the type's identity.
///
/// Reference: 
pub struct PlainYearMonth {
  iso : IsoDate
  calendar : Calendar
} derive(Eq)

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

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

///|
/// Creates a year-month, rejecting one outside the supported range.
///
/// `reference_day` chooses the internal reference day; it defaults to the
/// first of the month.
///
/// ```mbt check
/// test {
///   inspect(@temporal.PlainYearMonth::try_new(2030, 10), content="2030-10")
/// }
/// ```
pub fn PlainYearMonth::try_new(
  year : Int,
  month : Int,
  reference_day? : Int = 1,
  overflow? : Overflow = Reject,
  calendar? : Calendar = Calendar::ISO,
) -> PlainYearMonth raise TemporalError {
  if !year_month_within_limits(year, month) {
    raise RangeError("year-month is outside the supported range")
  }
  let iso = IsoDate::regulate(year, month, reference_day, overflow)
  { iso, calendar }
}

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

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

///|
/// Returns the calendar-independent month code.
pub fn PlainYearMonth::month_code(self : PlainYearMonth) -> String {
  month_code_of(self.iso.month)
}

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

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

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

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

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

///|
/// Compares two year-months chronologically.
pub impl Compare for PlainYearMonth with fn compare(self, other) {
  let c = self.iso.year.compare(other.iso.year)
  if c != 0 {
    c
  } else {
    self.iso.month.compare(other.iso.month)
  }
}

///|
/// Returns whether both name the same month in the same calendar.
pub fn PlainYearMonth::equals(
  self : PlainYearMonth,
  other : PlainYearMonth,
) -> Bool {
  self.iso == other.iso && self.calendar == other.calendar
}

///|
/// Returns a copy with the year or month replaced.
pub fn PlainYearMonth::with_fields(
  self : PlainYearMonth,
  year? : Int,
  month? : Int,
  overflow? : Overflow = Constrain,
) -> PlainYearMonth raise TemporalError {
  PlainYearMonth::try_new(
    year.unwrap_or(self.iso.year),
    month.unwrap_or(self.iso.month),
    reference_day=self.iso.day,
    overflow~,
    calendar=self.calendar,
  )
}

///|
/// Combines this year-month with a day to produce a `PlainDate`.
pub fn PlainYearMonth::to_plain_date(
  self : PlainYearMonth,
  day : Int,
) -> PlainDate raise TemporalError {
  PlainDate::new_with_overflow(
    self.iso.year,
    self.iso.month,
    day,
    Reject,
    self.calendar,
  )
}

///|
/// `AddDurationToYearMonth`: adds a duration of whole years and months.
///
/// Weeks, days and time components are rejected: a year-month has no day to
/// apply them to.
///
/// ```mbt check
/// test {
///   let ym = @temporal.PlainYearMonth::try_new(2024, 1)
///   inspect(ym.add(@temporal.Duration::of(months=13)), content="2025-02")
/// }
/// ```
pub fn PlainYearMonth::add(
  self : PlainYearMonth,
  duration : Duration,
  overflow? : Overflow = Constrain,
) -> PlainYearMonth raise TemporalError {
  let date_duration = duration.date_duration()
  if date_duration.weeks != 0L ||
    date_duration.days != 0L ||
    duration.has_time_component() {
    raise RangeError("only years and months can be added to a PlainYearMonth")
  }
  // The arithmetic is anchored to the first of the month, so a short target
  // month cannot drag the result into the previous one. The anchor is
  // range-checked: the first of the month can fall outside the supported range
  // even when the year-month itself does not.
  let anchor = IsoDate::new_with_overflow(
    self.iso.year,
    self.iso.month,
    1,
    Constrain,
  )
  let result = anchor.add_date_duration(date_duration, overflow)
  PlainYearMonth::try_new(
    result.year,
    result.month,
    overflow~,
    calendar=self.calendar,
  )
}

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

///|
/// `DifferenceTemporalPlainYearMonth`.
fn PlainYearMonth::diff(
  self : PlainYearMonth,
  operation : DifferenceOperation,
  other : PlainYearMonth,
  settings : DifferenceSettings,
) -> Duration raise TemporalError {
  if self.calendar != other.calendar {
    raise RangeError("cannot compare year-months in different calendars")
  }
  let resolved = ResolvedRoundingOptions::from_diff_settings(
    settings,
    operation,
    UnitGroup::Date,
    Year,
    Month,
  )
  if resolved.largest_unit is (Week | Day) ||
    resolved.smallest_unit is (Week | Day) {
    raise RangeError("a year-month difference cannot use weeks or days")
  }
  if self.iso.year == other.iso.year && self.iso.month == other.iso.month {
    return duration_zero
  }
  // Both sides are anchored to the first of their month so that the day
  // components cancel out. As in `add`, the anchors are range-checked.
  let start = IsoDate::new_with_overflow(
    self.iso.year,
    self.iso.month,
    1,
    Constrain,
  )
  let end = IsoDate::new_with_overflow(
    other.iso.year,
    other.iso.month,
    1,
    Constrain,
  )
  let date_diff = start.diff_iso_date(end, resolved.largest_unit)
  let mut duration = InternalDurationRecord::from_date_duration(
    DateDuration::new(date_diff.years, date_diff.months, 0L, 0L),
  )
  if !(resolved.smallest_unit is Month && resolved.increment.get() == 1) {
    let iso_date_time = IsoDateTime::new_unchecked(start, iso_time_midnight)
    let dt = PlainDateTime::new_unchecked(iso_date_time, self.calendar)
    duration = duration.round_relative_duration(
      iso_date_time.as_nanoseconds(),
      IsoDateTime::new_unchecked(end, iso_time_midnight).as_nanoseconds(),
      dt,
      None,
      utc_only_provider,
      resolved,
    )
  }
  let result = Duration::from_internal(duration, resolved.largest_unit)
  match operation {
    Until => result
    Since => result.negated()
  }
}

///|
/// Returns the duration from this year-month until `other`.
pub fn PlainYearMonth::until(
  self : PlainYearMonth,
  other : PlainYearMonth,
  settings? : DifferenceSettings = DifferenceSettings::default(),
) -> Duration raise TemporalError {
  self.diff(Until, other, settings)
}

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

///|
/// Parses a year-month from an RFC 9557 string.
///
/// ```mbt check
/// test {
///   inspect(@temporal.PlainYearMonth::of_string("2030-10"), content="2030-10")
/// }
/// ```
pub fn PlainYearMonth::of_string(
  source : String,
) -> PlainYearMonth raise TemporalError {
  let record = parse_year_month_string(source)
  let date = temporal_unwrap(record.date, "date component")
  PlainYearMonth::try_new(
    date.year,
    date.month,
    reference_day=date.day,
    calendar=calendar_of(record),
  )
}

///|
/// Renders the year-month as `YYYY-MM`.
///
/// The reference day is included when a calendar annotation is present, since
/// a non-ISO calendar needs it to reconstruct the same month.
pub fn PlainYearMonth::to_string_with_options(
  self : PlainYearMonth,
  display_calendar : DisplayCalendar,
) -> String {
  let buf = StringBuilder::new()
  write_year(buf, self.iso.year)
  buf.write_string("-")
  write_padded_2(buf, self.iso.month)
  if display_calendar is (Always | Critical) || self.calendar != Calendar::ISO {
    buf.write_string("-")
    write_padded_2(buf, self.iso.day)
  }
  write_calendar_annotation(buf, self.calendar, display_calendar)
  buf.to_string()
}

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