///|
/// Explicit awareness at rrulewrapper's boundary. DateTime continues to store
/// ordinary aware instants; a naive rule date stores only its local fields.
pub struct RRuleDateTime {
  priv value : DateTime
  priv timezone : DateTimeZone?
  priv fold : Bool
}

///|
fn rrule_wall_date(value : DateTime, truncate~ : Bool = false) -> DateTime {
  {
    ..value,
    microsecond: if truncate {
      0
    } else {
      value.microsecond
    },
    offset_minutes: 0,
    offset_seconds: 0,
    abbreviation: None,
    timezone: None,
  }
}

///|
/// Drop the timezone without changing local fields.
pub fn RRuleDateTime::naive(value : DateTime) -> RRuleDateTime {
  { value: rrule_wall_date(value), timezone: None, fold: false, }
}

///|
/// Retain the instant and its rule-zone identity, or capture a fixed-offset
/// snapshot when the DateTime has no retained rule zone.
pub fn RRuleDateTime::aware(value : DateTime) -> RRuleDateTime {
  let zone = match value.timezone {
    Some(zone) => zone
    None =>
      {
        ..(try! fixed_timezone_seconds(value.offset_seconds)),
        initial_abbreviation: value.abbreviation,
      }
  }
  {
    value: arithmetic_zoned_date(
      value,
      zone,
      value.offset_seconds,
      value.abbreviation,
    ),
    timezone: Some(zone),
    fold: zone.infer_rule_fold(value),
  }
}

///|
/// Attach a zone to local fields. Imaginary gaps use the post-transition offset;
/// fold selects dateutil wall-time behavior (earlier/later for ordinary folds).
/// This does not convert through numeric date coordinates or read the epoch.
pub fn RRuleDateTime::from_local(
  value : DateTime,
  timezone : DateTimeZone,
  fold~ : Bool = false,
) -> RRuleDateTime raise DateError {
  let wall = rrule_wall_date(value)
  let mut selected = timezone.resolve_arithmetic_local(wall, fold~)
  if fold && !timezone.dateutil_footer {
    let wall_microseconds = locator_microseconds(wall)
    let mut latest : Int64? = None
    for offset in timezone.offsets() {
      let instant = wall_microseconds - Int64::from_int(offset) * 1000000L
      let (active, name) = timezone.offset_info_at_microseconds(instant)
      if active == offset && (latest is None || instant > latest.unwrap()) {
        latest = Some(instant)
        selected = arithmetic_zoned_date(wall, timezone, offset, name)
      }
    }
  }
  { value: selected, timezone: Some(timezone), fold, }
}

///|
pub fn RRuleDateTime::is_naive(self : RRuleDateTime) -> Bool {
  self.timezone is None
}

///|
/// Local fields plus an exact offset for aware values. Naive values use offset
/// zero as a field carrier; retain RRuleDateTime when awareness matters.
pub fn RRuleDateTime::date(self : RRuleDateTime) -> DateTime {
  self.value
}

///|
pub fn RRuleDateTime::timezone(self : RRuleDateTime) -> DateTimeZone? {
  self.timezone
}

///|
fn RRuleDateTime::validate_ordinal(
  self : RRuleDateTime,
) -> Unit raise DateError {
  if !self.is_naive() {
    let instant = rruleset_instant(self.value)
    if instant < -62_135_596_800_000_000L || instant >= 253_402_300_800_000_000L {
      raise DateError::InvalidDate(
        message="rule date UTC instant is outside years 1..9999",
      )
    }
  }
}

///|
/// Convert to numeric dates, checking aware UTC overflow before reading epoch.
pub fn RRuleDateTime::to_ordinal(
  self : RRuleDateTime,
) -> Double raise DateError {
  self.validate_ordinal()
  self.value.to_ordinal()
}

///|
fn RRuleDateTime::wall_in(
  self : RRuleDateTime,
  zone : DateTimeZone,
) -> DateTime raise DateError {
  match self.timezone {
    None => rrule_wall_date(self.value)
    Some(current) if physical_equal(current, zone) =>
      rrule_wall_date(self.value)
    Some(_) => rrule_wall_date(self.value.with_timezone(zone))
  }
}

///|
fn attach_rule_timezone(
  value : DateTime,
  timezone : DateTimeZone?,
) -> RRuleDateTime raise DateError {
  match timezone {
    Some(zone) => RRuleDateTime::from_local(value, zone)
    None => RRuleDateTime::naive(value)
  }
}

///|
fn DateTimeZone::infer_rule_fold(self : DateTimeZone, value : DateTime) -> Bool {
  if self.dateutil_footer &&
    self.footer is Some(rule) &&
    rule.dst_offset is Some(dst) &&
    dst < rule.std_offset {
    return false
  }
  let instant = rruleset_instant(value)
  for offset in self.offsets() {
    let candidate = locator_microseconds(value) -
      Int64::from_int(offset) * 1000000L
    let (active, _) = self.offset_info_at_microseconds(candidate) catch {
      _ => continue
    }
    if active == offset && candidate < instant {
      return true
    }
  }
  false
}