///|
// Expand a local calendar candidate to every UTC instant it can represent.
// Fixed-offset locators have exactly one candidate.
fn DateLocator::ordinals_for_date(
  self : DateLocator,
  value : DateTime,
) -> Array[Double] {
  match self.timezone {
    Some(timezone) => timezone.local_ordinal_candidates(value)
    None => [value.to_ordinal()]
  }
}

///|
fn locator_push_date_ticks(
  result : Array[Double],
  locator : DateLocator,
  value : DateTime,
  first : Double,
  last : Double,
) -> Unit {
  for ordinal in locator.ordinals_for_date(value) {
    locator_push_tick(result, ordinal, first, last)
  }
}

///|
fn locator_floor_div64(value : Int64, divisor : Int64) -> Int64 {
  let quotient = value / divisor
  if value % divisor < 0L {
    quotient - 1L
  } else {
    quotient
  }
}

///|
fn locator_ceil_div64(value : Int64, divisor : Int64) -> Int64 {
  -locator_floor_div64(-value, divisor)
}

///|
fn locator_seconds(value : DateTime) -> Int64 {
  Int64::from_int(days_from_civil(value.year, value.month, value.day)) * 86400L +
  Int64::from_int(value.hour * 3600 + value.minute * 60 + value.second)
}

///|
fn locator_microseconds(value : DateTime) -> Int64 {
  locator_seconds(value) * 1_000_000L + Int64::from_int(value.microsecond)
}

///|
// Only called with microseconds inside the supported Gregorian year range.
fn locator_datetime_microseconds(
  microseconds : Int64,
  offset_seconds : Int32,
) -> DateTime {
  let day = locator_floor_div64(microseconds, 86_400_000_000L)
  let remainder = microseconds - day * 86_400_000_000L
  let (year, month, day) = civil_from_days(day.to_int())
  {
    year,
    month,
    day,
    hour: (remainder / 3_600_000_000L).to_int(),
    minute: (remainder % 3_600_000_000L / 60_000_000L).to_int(),
    second: (remainder % 60_000_000L / 1_000_000L).to_int(),
    microsecond: (remainder % 1_000_000L).to_int(),
    offset_minutes: offset_seconds / 60,
    offset_seconds,
    abbreviation: None,
    timezone: None,
  }
}

///|
// Only called with seconds inside the supported Gregorian year range.
fn locator_datetime(seconds : Int64, offset_minutes : Int32) -> DateTime {
  locator_datetime_microseconds(seconds * 1_000_000L, offset_minutes * 60)
}

///|
// Like dateutil.relativedelta, preserve the time and clamp the day when the
// target month is shorter. Returning None signals datetime under/overflow.
fn locator_shift_months(value : DateTime, months : Int32) -> DateTime? {
  let index = value.year * 12 + value.month - 1 + months
  if index < 12 || index >= 120000 {
    return None
  }
  let year = index / 12
  let month = index % 12 + 1
  Some({
    year,
    month,
    day: Int32::min(value.day, days_in_month(year, month)),
    hour: value.hour,
    minute: value.minute,
    second: value.second,
    microsecond: value.microsecond,
    offset_minutes: value.offset_minutes,
    offset_seconds: value.offset_seconds,
    abbreviation: value.abbreviation,
    timezone: value.timezone,
  })
}

///|
// RRuleLocator._create_rrule uses first - relativedelta(last, first).
// A calendar difference is whole months followed by the residual microseconds;
// replacing it with (last - first) days changes interval phases near month ends.
fn recurrence_start(first : DateTime, last : DateTime) -> DateTime {
  let mut months = (last.year - first.year) * 12 + last.month - first.month
  let last_microseconds = locator_microseconds(last)
  let mut shifted = locator_shift_months(first, months).unwrap()
  if locator_microseconds(shifted) > last_microseconds {
    months -= 1
    shifted = locator_shift_months(first, months).unwrap()
  }
  let residual = last_microseconds - locator_microseconds(shifted)
  let minimum = locator_microseconds({
    year: 1,
    month: 1,
    day: 1,
    hour: 0,
    minute: 0,
    second: 0,
    microsecond: 0,
    offset_minutes: 0,
    offset_seconds: 0,
    abbreviation: None,
    timezone: None,
  })
  let start = match locator_shift_months(first, -months) {
    Some(value) => Int64::max(locator_microseconds(value) - residual, minimum)
    None => minimum
  }
  locator_datetime_microseconds(start, first.offset_seconds)
}

///|
// Skip recurrence candidates before the visible range without changing phase.
// All arithmetic uses Int64: a valid interval may be as large as Int::MAX.
fn locator_align(first : Int64, anchor : Int64, step : Int64) -> Int64 {
  anchor + Int64::max(0L, locator_ceil_div64(first - anchor, step)) * step
}

///|
fn locator_push_tick(
  result : Array[Double],
  value : Double,
  first : Double,
  last : Double,
) -> Unit {
  if result.length() < 100000 && value >= first && value <= last {
    result.push(value)
  }
}

///|
fn locator_datetime_seconds(
  seconds : Int64,
  offset_seconds : Int32,
) -> DateTime {
  locator_datetime_microseconds(seconds * 1_000_000L, offset_seconds)
}