///|
/// A calendar period of years, months and days.
///
/// Unlike `TimeDelta`, a period has no fixed length: what a month or a year
/// amounts to in days depends on the date it is applied to. Components are
/// kept as given and carry independent signs, so `months=14` and
/// `years=1, months=2` are distinct values.
pub struct Period {
  priv years : Int32
  priv months : Int32
  priv days : Int32
} derive(Eq, Hash, @debug.Debug)

///|
pub extend Period with Eq::{equal, not_equal}

///|
pub extend Period with Hash::{hash, hash_combine}

///|
pub extend Period with @debug.Debug::{to_repr}

///|
pub impl Default for Period with fn default() {
  Period::zero()
}

///|
pub extend Period with Default::{default}

///|
/// Renders the period in the ISO 8601 date-based duration style (`P1Y2M3D`),
/// omitting zero components. The zero period is `P0D`. A negative component
/// keeps its own leading `-` (`P-1Y3D`); that per-component sign is an
/// extension beyond ISO 8601, which `parse_iso8601_period` reads back.
pub impl Show for Period with fn output(self, logger) {
  if self.is_zero() {
    logger.write_string("P0D")
  } else {
    logger.write_char('P')
    if self.years != 0 {
      logger.write_string(self.years.to_string() + "Y")
    }
    if self.months != 0 {
      logger.write_string(self.months.to_string() + "M")
    }
    if self.days != 0 {
      logger.write_string(self.days.to_string() + "D")
    }
  }
}

///|
pub extend Period with Show::{to_string, output}

///|
/// Builds a period from its components; omitted components are zero.
pub fn Period::new(
  years~ : Int32 = 0,
  months~ : Int32 = 0,
  days~ : Int32 = 0,
) -> Period {
  { years, months, days, }
}

///|
/// The period of zero years, months and days; `Default::default()` returns
/// it.
pub fn Period::zero() -> Period {
  { years: 0, months: 0, days: 0, }
}

///|
pub fn Period::years(self : Period) -> Int32 {
  self.years
}

///|
pub fn Period::months(self : Period) -> Int32 {
  self.months
}

///|
pub fn Period::days(self : Period) -> Int32 {
  self.days
}

///|
/// Whether every component is zero.
pub fn Period::is_zero(self : Period) -> Bool {
  self.years == 0 && self.months == 0 && self.days == 0
}

///|
/// The period with every component negated, or `None` if a component is
/// `Int32::min_value` and so has no representable negation.
pub fn Period::checked_neg(self : Period) -> Period? {
  Period::zero().checked_sub(self)
}

///|
/// The component-wise sum, or `None` if any component overflows.
pub fn Period::checked_add(self : Period, other : Period) -> Period? {
  combine_components(self, other, fn(a, b) { a.to_int64() + b.to_int64() })
}

///|
/// The component-wise difference, or `None` if any component overflows.
pub fn Period::checked_sub(self : Period, other : Period) -> Period? {
  combine_components(self, other, fn(a, b) { a.to_int64() - b.to_int64() })
}

///|
/// Every component multiplied by `scalar`, or `None` if any component
/// overflows.
pub fn Period::checked_mul(self : Period, scalar : Int32) -> Period? {
  let factor = Period::new(years=scalar, months=scalar, days=scalar)
  combine_components(self, factor, fn(a, b) { a.to_int64() * b.to_int64() })
}

///|
fn Period::total_months(self : Period) -> Int64 {
  self.years.to_int64() * 12L + self.months.to_int64()
}

///|
fn Period::from_months_and_days(months : Int64, days : Int64) -> Period {
  Period::new(
    years=(months / 12L).to_int(),
    months=(months % 12L).to_int(),
    days=days.to_int(),
  )
}

///|
fn combine_components(
  a : Period,
  b : Period,
  op : (Int32, Int32) -> Int64,
) -> Period? {
  match
    (
      narrow32(op(a.years, b.years)),
      narrow32(op(a.months, b.months)),
      narrow32(op(a.days, b.days)),
    ) {
    (Some(years), Some(months), Some(days)) =>
      Some(Period::new(years~, months~, days~))
    _ => None
  }
}

///|
fn narrow32(value : Int64) -> Int32? {
  if value < -2147483648L || value > 2147483647L {
    None
  } else {
    Some(value.to_int())
  }
}