///|
/// Set a calendar minor locator, preserving the independent major formatter.
pub fn Axis::set_minor_date_locator(
  self : Axis,
  locator : @date.DateLocator,
) -> Unit {
  self.tickers.minor_locator_kind = Date
  self.tickers.minor_locator_default = false
  self.tickers.minor_date_locator = Some(
    match self.date_timezone {
      Some(zone) if self.date_timezone_override => locator.with_timezone(zone)
      _ => locator.copy()
    },
  )
  self.tickers.minor_locator = None
  self.bind_locator_data(minor=true)
}

///|
pub fn Axis::minor_date_locator(self : Axis) -> @date.DateLocator? {
  if self.tickers.minor_locator_kind == Date {
    self.tickers.minor_date_locator.map(value => value.copy())
  } else {
    None
  }
}

///|
/// Install a copied formatter for minor dates. Its initial locator is replaced
/// by the actual minor unit whenever labels are requested.
pub fn Axis::set_minor_date_formatter(
  self : Axis,
  formatter : @date.DateFormatter,
) -> Unit {
  self.tickers.minor_formatter_kind = Date
  self.tickers.minor_formatter_default = false
  self.tickers.minor_date_formatter = Some(formatter.copy())
  self.tickers.minor_named_formatter = None
}

///|
pub fn Axis::minor_date_formatter(self : Axis) -> @date.DateFormatter? {
  if self.tickers.minor_formatter_kind != Date {
    return None
  }
  self.tickers.minor_date_formatter.map(formatter => {
    formatter.with_locator_unit(self.date_formatter_unit([], None, true))
  })
}

///|
pub fn Axis::clear_minor_date_formatter(self : Axis) -> Unit {
  self.tickers.minor_date_formatter = None
  self.tickers.minor_formatter_kind = Numeric
  self.tickers.minor_formatter_default = false
}

///|
/// Control removal of minor locations close to major ticks. The default
/// tolerance is 1e-5 of the date view span, matching upstream linear date axes.
pub fn Axis::set_minor_overlap_removal(self : Axis, enabled : Bool) -> Unit {
  self.remove_minor_overlap = enabled
}

///|
pub fn Axis::minor_overlap_removal(self : Axis) -> Bool {
  self.remove_minor_overlap
}

///|
/// Format minor dates for a viewport. Without an explicit minor formatter,
/// labels remain empty, matching the default NullFormatter behavior.
pub fn Axis::minor_tick_labels(
  self : Axis,
  values : Array[Double],
  view? : @core.Limits,
) -> Array[String] {
  if self.tickers.minor_formatter_kind == Numeric &&
    self.tickers.minor_named_formatter is Some(formatter) {
    return formatter.format_ticks(values) catch { _ => [] }
  }
  let formatter = if self.tickers.minor_formatter_kind == Date {
    self.tickers.minor_date_formatter
  } else {
    None
  }
  match formatter {
    None => Array::make(values.length(), "")
    Some(formatter) => {
      let effective_view = match view {
        Some(_) => view
        None => self.current_view_limits()
      }
      let inverted = effective_view
        .map(limits => limits.lower() > limits.upper())
        .unwrap_or(self.inverted)
      let reversed = values.length() > 1 &&
        values[0] > values[values.length() - 1]
      let ticks = if reversed { values.rev() } else { values }
      let labels = formatter
        .with_locator_unit(
          self.date_formatter_unit(values, effective_view, true),
        )
        .format_values(ticks, inverted~)
      if reversed {
        labels.rev()
      } else {
        labels
      }
    }
  }
}

///|
fn Axis::filter_date_minor_ticks(
  self : Axis,
  ticks : Array[Double],
  major : Array[Double],
  limits : @core.Limits,
) -> Array[Double] {
  if !self.remove_minor_overlap {
    return ticks
  }
  let tolerance = Double::abs(limits.upper() - limits.lower()) * 1.0e-5
  ticks.filter(value => {
    !major.any(other => Double::abs(value - other) <= tolerance)
  })
}