///|
/// A calendar date and a wall-clock time, with no time zone.
///
/// Reference: 
pub struct PlainDateTime {
  iso : IsoDateTime
  calendar : Calendar
} derive(Eq)

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

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

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

///|
/// Creates a date-time in the ISO calendar, rejecting invalid fields.
///
/// ```mbt check
/// test {
///   inspect(
///     @temporal.PlainDateTime::try_new(2025, 3, 1, hour=11, minute=16, second=10),
///     content="2025-03-01T11:16:10",
///   )
/// }
/// ```
pub fn PlainDateTime::try_new(
  year : Int,
  month : Int,
  day : Int,
  hour? : Int = 0,
  minute? : Int = 0,
  second? : Int = 0,
  millisecond? : Int = 0,
  microsecond? : Int = 0,
  nanosecond? : Int = 0,
) -> PlainDateTime raise TemporalError {
  PlainDateTime::new_with_overflow(
    year,
    month,
    day,
    hour,
    minute,
    second,
    millisecond,
    microsecond,
    nanosecond,
    Reject,
    Calendar::ISO,
  )
}

///|
/// Creates a date-time, handling out-of-range fields according to `overflow`.
pub fn PlainDateTime::new_with_overflow(
  year : Int,
  month : Int,
  day : Int,
  hour : Int,
  minute : Int,
  second : Int,
  millisecond : Int,
  microsecond : Int,
  nanosecond : Int,
  overflow : Overflow,
  calendar : Calendar,
) -> PlainDateTime raise TemporalError {
  let date = IsoDate::new_with_overflow(year, month, day, overflow)
  let time = IsoTime::new_with_overflow(
    hour, minute, second, millisecond, microsecond, nanosecond, overflow,
  )
  { iso: IsoDateTime::new(date, time), calendar }
}

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

///|
/// Returns the underlying ISO date-time.
pub fn PlainDateTime::iso(self : PlainDateTime) -> IsoDateTime {
  self.iso
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

///|
/// Returns the year the ISO week belongs to.
pub fn PlainDateTime::year_of_week(self : PlainDateTime) -> Int {
  iso_week_of_year(self.iso.date.year, self.iso.date.month, self.iso.date.day).1
}

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

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

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

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

///|
/// Compares two date-times chronologically.
pub impl Compare for PlainDateTime with fn compare(self, other) {
  self.iso.compare(other.iso)
}

///|
/// Returns whether two date-times are the same instant in the same calendar.
pub fn PlainDateTime::equals(
  self : PlainDateTime,
  other : PlainDateTime,
) -> Bool {
  self.iso == other.iso && self.calendar == other.calendar
}

///|
/// Returns the date part.
pub fn PlainDateTime::to_plain_date(self : PlainDateTime) -> PlainDate {
  PlainDate::new_unchecked(self.iso.date, self.calendar)
}

///|
/// Returns the time part.
pub fn PlainDateTime::to_plain_time(self : PlainDateTime) -> PlainTime {
  PlainTime::from_iso(self.iso.time)
}

///|
/// Returns a copy with the given fields replaced.
pub fn PlainDateTime::with_fields(
  self : PlainDateTime,
  year? : Int,
  month? : Int,
  day? : Int,
  hour? : Int,
  minute? : Int,
  second? : Int,
  millisecond? : Int,
  microsecond? : Int,
  nanosecond? : Int,
  overflow? : Overflow = Constrain,
) -> PlainDateTime raise TemporalError {
  PlainDateTime::new_with_overflow(
    year.unwrap_or(self.iso.date.year),
    month.unwrap_or(self.iso.date.month),
    day.unwrap_or(self.iso.date.day),
    hour.unwrap_or(self.iso.time.hour),
    minute.unwrap_or(self.iso.time.minute),
    second.unwrap_or(self.iso.time.second),
    millisecond.unwrap_or(self.iso.time.millisecond),
    microsecond.unwrap_or(self.iso.time.microsecond),
    nanosecond.unwrap_or(self.iso.time.nanosecond),
    overflow,
    self.calendar,
  )
}

///|
/// `AddDurationToDateTime`: adds a duration, treating each of its days as
/// exactly 24 hours.
///
/// ```mbt check
/// test {
///   let dt = @temporal.PlainDateTime::try_new(2024, 1, 31, hour=23)
///   // The time is added first: 23:00 + 2h rolls into the next day, so the
///   // date arithmetic sees one month and one day.
///   inspect(
///     dt.add(@temporal.Duration::of(months=1, hours=2)),
///     content="2024-03-01T01:00:00",
///   )
/// }
/// ```
pub fn PlainDateTime::add(
  self : PlainDateTime,
  duration : Duration,
  overflow? : Overflow = Constrain,
) -> PlainDateTime raise TemporalError {
  let internal = InternalDurationRecord::from_duration_with_24_hour_days(
    duration,
  )
  // The time part is added first; any whole days it produces join the date
  // part before the calendar arithmetic runs.
  let (days, time_result) = self.iso.time.add(internal.time)
  let date_duration = internal.date.adjust(internal.date.days + days)
  let added_date = PlainDate::new_unchecked(self.iso.date, self.calendar).calendar_date_add(
    date_duration, overflow,
  )
  {
    iso: IsoDateTime::new(added_date.iso, time_result),
    calendar: self.calendar,
  }
}

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

///|
/// `DifferencePlainDateTimeWithRounding`.
fn PlainDateTime::diff_with_rounding(
  self : PlainDateTime,
  other : PlainDateTime,
  options : ResolvedRoundingOptions,
) -> InternalDurationRecord raise TemporalError {
  if self.iso == other.iso {
    return internal_duration_zero
  }
  self.iso.check_within_limits()
  other.iso.check_within_limits()
  let diff = self.iso.diff(other.iso, options.largest_unit)
  if options.smallest_unit is Nanosecond && options.increment.get() == 1 {
    return diff
  }
  diff.round_relative_duration(
    self.iso.as_nanoseconds(),
    other.iso.as_nanoseconds(),
    self,
    None,
    utc_only_provider,
    options,
  )
}

///|
/// `DifferencePlainDateTimeWithTotal`.
fn PlainDateTime::diff_with_total(
  self : PlainDateTime,
  other : PlainDateTime,
  unit : DateTimeUnit,
) -> Double raise TemporalError {
  if self.iso == other.iso {
    return 0.0
  }
  self.iso.check_within_limits()
  other.iso.check_within_limits()
  let diff = self.iso.diff(other.iso, unit)
  if unit is Nanosecond {
    return diff.time.nanoseconds().to_double()
  }
  diff.total_relative_duration(
    self.iso.as_nanoseconds(),
    other.iso.as_nanoseconds(),
    self,
    None,
    utc_only_provider,
    unit,
  )
}

///|
/// `DifferenceTemporalPlainDateTime`.
fn PlainDateTime::diff(
  self : PlainDateTime,
  operation : DifferenceOperation,
  other : PlainDateTime,
  settings : DifferenceSettings,
) -> Duration raise TemporalError {
  if self.calendar != other.calendar {
    raise RangeError("cannot compare date-times in different calendars")
  }
  let options = ResolvedRoundingOptions::from_diff_settings(
    settings,
    operation,
    UnitGroup::DateTime,
    Day,
    Nanosecond,
  )
  if self.iso == other.iso {
    return duration_zero
  }
  let record = self.diff_with_rounding(other, options)
  let result = Duration::from_internal(record, options.largest_unit)
  match operation {
    Until => result
    Since => result.negated()
  }
}

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

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

///|
/// Rounds to a multiple of the given unit, which may be as large as a day.
pub fn PlainDateTime::round(
  self : PlainDateTime,
  options : RoundingOptions,
) -> PlainDateTime raise TemporalError {
  let resolved = ResolvedRoundingOptions::from_datetime_options(options)
  if resolved.is_noop() {
    return self
  }
  { iso: self.iso.round(resolved), calendar: self.calendar }
}

///|
/// Renders the date-time in RFC 9557 form.
pub fn PlainDateTime::to_string_with_options(
  self : PlainDateTime,
  options : ToStringRoundingOptions,
  display_calendar : DisplayCalendar,
) -> String raise TemporalError {
  let resolved = options.resolve()
  let rounding = ResolvedRoundingOptions::from_to_string_options(resolved)
  let iso = self.iso.round(rounding)
  let buf = StringBuilder::new()
  write_date(buf, iso.date.year, iso.date.month, iso.date.day)
  buf.write_string("T")
  write_time(
    buf,
    iso.time.hour,
    iso.time.minute,
    iso.time.second,
    iso.time.subsecond_nanoseconds(),
    resolved.precision,
    true,
  )
  write_calendar_annotation(buf, self.calendar, display_calendar)
  buf.to_string()
}

///|
/// Renders the date-time as `YYYY-MM-DDTHH:MM:SS`.
pub fn PlainDateTime::to_string(self : PlainDateTime) -> String {
  try! self.to_string_with_options(
    ToStringRoundingOptions::default(),
    DisplayCalendar::default(),
  )
}