///|
/// Low-level writers shared by every `toString` implementation.
///
/// The output format is RFC 9557 (ISO 8601 with Temporal's annotation
/// extensions), so the same year, time and fraction writers are reused across
/// dates, times, instants and zoned date-times.

///|
/// Writes a non-negative integer padded to two digits.
fn write_padded_2(buf : StringBuilder, value : Int) -> Unit {
  if value < 10 {
    buf.write_string("0")
  }
  buf.write_string(value.to_string())
}

///|
/// Writes an ISO year: four digits when in `0 ..= 9999`, otherwise the signed
/// six-digit extended form.
fn write_year(buf : StringBuilder, year : Int) -> Unit {
  if year >= 0 && year <= 9999 {
    let s = year.to_string()
    for _ in 0..<(4 - s.length()) {
      buf.write_string("0")
    }
    buf.write_string(s)
  } else {
    buf.write_string(if year < 0 { "-" } else { "+" })
    let s = year.abs().to_string()
    for _ in 0..<(6 - s.length()) {
      buf.write_string("0")
    }
    buf.write_string(s)
  }
}

///|
/// Writes the fractional-second digits of `nanoseconds`, honouring the
/// requested precision.
///
/// With `Auto` precision, trailing zeros are trimmed; with a fixed digit count
/// the field is padded or truncated to exactly that many digits.
fn write_fraction(
  buf : StringBuilder,
  nanoseconds : Int,
  precision : Precision,
) -> Unit {
  let digits = nanoseconds.to_string()
  let padded = "000000000"[:9 - digits.length()].to_owned() + digits
  let width = match precision {
    Digit(d) if d >= 0 && d <= 9 => d
    // `Auto` keeps every digit up to the last non-zero one.
    _ => {
      let mut last = 0
      for i in 0..<9 {
        if padded[i] != '0' {
          last = i + 1
        }
      }
      last
    }
  }
  buf.write_string(padded[:width].to_owned())
}

///|
/// Writes a wall-clock time, with or without `:` separators.
///
/// `Minute` precision stops after the minutes field; `Auto` omits the
/// fractional part when the sub-second value is zero.
fn write_time(
  buf : StringBuilder,
  hour : Int,
  minute : Int,
  second : Int,
  nanosecond : Int,
  precision : Precision,
  include_sep : Bool,
) -> Unit {
  write_padded_2(buf, hour)
  if include_sep {
    buf.write_string(":")
  }
  write_padded_2(buf, minute)
  if precision is Minute {
    return
  }
  if include_sep {
    buf.write_string(":")
  }
  write_padded_2(buf, second)
  if (nanosecond == 0 && precision is Auto) || precision is Digit(0) {
    return
  }
  buf.write_string(".")
  write_fraction(buf, nanosecond, precision)
}

///|
/// Writes an ISO date as `YYYY-MM-DD`.
fn write_date(buf : StringBuilder, year : Int, month : Int, day : Int) -> Unit {
  write_year(buf, year)
  buf.write_string("-")
  write_padded_2(buf, month)
  buf.write_string("-")
  write_padded_2(buf, day)
}

///|
/// Writes the `[u-ca=...]` calendar annotation, if the options call for one.
fn write_calendar_annotation(
  buf : StringBuilder,
  calendar : Calendar,
  show : DisplayCalendar,
) -> Unit {
  match show {
    Never => ()
    // `Auto` prints the annotation only when it carries information.
    Auto =>
      if calendar != Calendar::ISO {
        write_calendar(buf, calendar, false)
      }
    Always => write_calendar(buf, calendar, false)
    Critical => write_calendar(buf, calendar, true)
  }
}

///|
fn write_calendar(
  buf : StringBuilder,
  calendar : Calendar,
  critical : Bool,
) -> Unit {
  buf.write_string("[")
  if critical {
    buf.write_string("!")
  }
  buf.write_string("u-ca=")
  buf.write_string(calendar.identifier())
  buf.write_string("]")
}

///|
/// Renders the duration using the default options.
///
/// ```mbt check
/// test {
///   inspect(
///     @temporal.Duration::of(years=1, months=2, days=3).to_string(),
///     content="P1Y2M3D",
///   )
///   inspect(@temporal.duration_zero.to_string(), content="PT0S")
/// }
/// ```
pub fn Duration::to_string(self : Duration) -> String {
  try! self.to_string_with_options(ToStringRoundingOptions::default())
}

///|
/// `TemporalDurationToString`: renders the duration, first rounding the
/// sub-second part as the options require.
///
/// ```mbt check
/// test {
///   let d = @temporal.Duration::of(seconds=1, milliseconds=500)
///   inspect(
///     d.to_string_with_options(
///       @temporal.ToStringRoundingOptions::new(precision=Digit(1)),
///     ),
///     content="PT1.5S",
///   )
/// }
/// ```
pub fn Duration::to_string_with_options(
  self : Duration,
  options : ToStringRoundingOptions,
) -> String raise TemporalError {
  if options.smallest_unit is (Some(Hour) | Some(Minute)) {
    raise RangeError(
      "string rounding options cannot use an hour or minute smallestUnit",
    )
  }
  let resolved = options.resolve()
  if resolved.smallest_unit == Nanosecond &&
    resolved.increment == rounding_increment_one {
    return format_duration(self, resolved.precision)
  }
  let rounding_options = ResolvedRoundingOptions::from_to_string_options(
    resolved,
  )
  let largest = self.default_largest_unit()
  let internal = self.to_internal()
  let time = internal.time.round(rounding_options)
  let combined = InternalDurationRecord::combine(internal.date, time)
  let rounded = Duration::from_internal(combined, largest.larger(Second))
  format_duration(rounded, resolved.precision)
}

///|
/// Renders a duration in ISO 8601 form, such as `-P1Y2M3DT4H5M6.789S`.
///
/// The date part omits zero components. The time part always ends at seconds,
/// which keeps sub-second values attached to a seconds field, and a duration
/// with no components at all renders as `PT0S`.
fn format_duration(duration : Duration, precision : Precision) -> String {
  let sign = duration.sign()
  let duration = duration.abs()
  let buf = StringBuilder::new()
  if sign == Negative {
    buf.write_string("-")
  }
  buf.write_string("P")
  let has_date = duration.years != 0L ||
    duration.months != 0L ||
    duration.weeks != 0L ||
    duration.days != 0L
  if has_date {
    write_unit_if_non_zero(buf, duration.years, "Y")
    write_unit_if_non_zero(buf, duration.months, "M")
    write_unit_if_non_zero(buf, duration.weeks, "W")
    write_unit_if_non_zero(buf, duration.days, "D")
  }
  let hours = duration.hours
  let minutes = duration.minutes
  // Collapse seconds and below into one nanosecond count so that, say,
  // 1500 milliseconds renders as `1.5S` rather than as its own field.
  let sub_minute = TimeDuration::from_components(
    0L,
    0L,
    duration.seconds,
    duration.milliseconds,
    duration.microseconds,
    duration.nanoseconds,
  )
  let seconds = sub_minute.seconds().abs()
  let subseconds = sub_minute.subseconds().abs()
  // A bare `P` is not a valid duration, so a duration with no date part must
  // print a seconds field even when it is zero.
  let write_second = seconds != 0L ||
    subseconds != 0 ||
    (!has_date && hours == 0L && minutes == 0L) ||
    precision is Digit(_)
  if hours != 0L || minutes != 0L || write_second {
    buf.write_string("T")
  }
  write_unit_if_non_zero(buf, hours, "H")
  write_unit_if_non_zero(buf, minutes, "M")
  if write_second {
    buf.write_string(seconds.to_string())
    if precision is Digit(0) || (precision is Auto && subseconds == 0) {
      buf.write_string("S")
    } else {
      buf.write_string(".")
      write_fraction(buf, subseconds, precision)
      buf.write_string("S")
    }
  }
  buf.to_string()
}

///|
/// Writes `value` followed by `suffix`, unless the value is zero.
fn write_unit_if_non_zero(
  buf : StringBuilder,
  value : Int64,
  suffix : String,
) -> Unit {
  if value != 0L {
    buf.write_string(value.to_string())
    buf.write_string(suffix)
  }
}