///|
/// Stable typed identity used by explicitly installed named formatters.
pub fn Axis::formatter_axis_binding(
  self : Axis,
) -> @ticker.FormatterAxisBinding {
  self.formatter_axis_binding
}

///|
pub fn Axis::formatter_warnings(self : Axis) -> Array[String] {
  self.formatter_messages.copy()
}

///|
fn Axis::check_named_formatter_install(
  self : Axis,
  formatter : @ticker.BaseFormatter,
  minor : Bool,
) -> Unit {
  self.formatter_messages.clear()
  let fixed_locator = if minor {
    self.tickers.minor_locator_kind == Numeric &&
    self.tickers.minor_locator
    is Some(
      @ticker.MinorLocator::NamedFixed(_)
      | @ticker.MinorLocator::Fixed(_)
    )
  } else {
    self.tickers.major_locator_kind == Numeric &&
    (
      self.tickers.fixed_ticks is Some(_) ||
      self.tickers.locator
      is Some(@ticker.Locator::NamedFixed(_) | @ticker.Locator::Fixed(_))
    )
  }
  if formatter.fixed_formatter() is Some(fixed) &&
    fixed.seq().length() > 0 &&
    !fixed_locator {
    self.formatter_messages.push(
      "FixedFormatter should only be used together with FixedLocator",
    )
  }
}

///|
/// Install a live checked formatter independently of the legacy pure helpers.
pub fn Axis::set_named_formatter(
  self : Axis,
  formatter : @ticker.BaseFormatter,
) -> Unit {
  self.check_named_formatter_install(formatter, false)
  self.tickers.major_formatter_kind = Numeric
  self.tickers.major_formatter_default = false
  self.tickers.named_formatter = Some(formatter)
  self.tickers.formatter = None
  self.tickers.date_formatter = None
  self.tickers.date_format = None
  self.tickers.fixed_labels = None
  self.bind_formatter_axis()
}

///|
pub fn Axis::named_formatter(self : Axis) -> @ticker.BaseFormatter? {
  if self.tickers.major_formatter_kind == Numeric {
    self.tickers.named_formatter
  } else {
    None
  }
}

///|
pub fn Axis::set_minor_named_formatter(
  self : Axis,
  formatter : @ticker.BaseFormatter,
) -> Unit {
  self.check_named_formatter_install(formatter, true)
  self.tickers.minor_formatter_kind = Numeric
  self.tickers.minor_formatter_default = false
  self.tickers.minor_named_formatter = Some(formatter)
  self.tickers.minor_date_formatter = None
  self.bind_formatter_axis(minor=true)
}

///|
pub fn Axis::minor_named_formatter(self : Axis) -> @ticker.BaseFormatter? {
  if self.tickers.minor_formatter_kind == Numeric {
    self.tickers.minor_named_formatter
  } else {
    None
  }
}

///|
pub fn Axis::clear_named_formatter(self : Axis) -> Unit {
  self.tickers.major_formatter_kind = Numeric
  self.tickers.major_formatter_default = false
  self.tickers.named_formatter = None
}

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

///|
/// Rebind installed handles after shared-axis installation or removal.
pub fn Axis::bind_formatter_axis(self : Axis, minor~ : Bool = false) -> Unit {
  let formatter = if minor {
    self.minor_named_formatter()
  } else {
    self.named_formatter()
  }
  if formatter is Some(formatter) {
    formatter.set_axis(Some(self.formatter_axis_binding))
  }
}

///|
/// Keep a locator's borrowed numeric view until the corresponding batch ends.
pub fn Axis::formatter_tick_locations_checked(
  self : Axis,
  automatic : @core.Limits,
  minor~ : Bool = false,
) -> @ticker.FormatterLocations raise @core.DataError {
  let active = if minor {
    self.minor_named_formatter()
  } else {
    self.named_formatter()
  }
  let kind = if minor {
    self.tickers.minor_locator_kind
  } else {
    self.tickers.major_locator_kind
  }
  if kind == Category {
    let values = self.tickers.category_locator_units
      .map(units => {
        Array::makei(units.labels().length(), index => {
          @bigint.BigInt::from_int(index)
        })
      })
      .unwrap_or([])
    return @ticker.FormatterLocations::Integers(values)
  }
  let formatter_kind = if minor {
    self.tickers.minor_formatter_kind
  } else {
    self.tickers.major_formatter_kind
  }
  if active is None && formatter_kind == Numeric {
    return @ticker.FormatterLocations::Sequence(
      if minor {
        self.minor_tick_values_checked(automatic)
      } else {
        self.tick_values_checked(automatic)
      },
    )
  }
  let limits = self.limits.unwrap_or(automatic)
  let previous = self.tick_layout_view
  if previous is None && !self.has_view_interval_provider() {
    self.tick_layout_view = Some((limits.lower(), limits.upper()))
  }
  defer {
    self.tick_layout_view = previous
  }
  let view = if minor {
    self.raw_formatter_minor_view_checked(limits)
  } else if kind == Date {
    self.major_tick_view_checked()
  } else if self.tickers.fixed_ticks is Some(values) {
    @ticker.TickValues::from_array(values)
  } else if self.tickers.locator is Some(@ticker.Locator::Fixed(values)) {
    @ticker.TickValues::from_array(values)
  } else if self.tickers.locator is Some(@ticker.Locator::Multiple(locator)) {
    let (lower, upper) = self.view_interval_checked()
    @ticker.TickValues::from_array(locator.tick_values(lower, upper))
  } else {
    self.major_tick_view_checked()
  }
  if minor && self.remove_minor_overlap {
    let major = self.major_tick_view_checked().to_array()
    let mask = self.numeric_minor_selection_mask(view.to_array(), major)
    return @ticker.FormatterLocations::NdArray(view.select_mask(mask))
  }
  @ticker.FormatterLocations::NdArray(view)
}

///|
fn Axis::raw_formatter_minor_view_checked(
  self : Axis,
  limits : @core.Limits,
) -> @ticker.TickValues raise @core.DataError {
  if self.tickers.minor_locator_kind == Date {
    return @ticker.TickValues::from_array(
      self.date_locator_tick_values_checked(limits, true),
    )
  }
  if self.tickers.minor_locator_kind == Category {
    return @ticker.TickValues::from_array([])
  }
  let values = match self.tickers.minor_locator {
    None => return @ticker.TickValues::from_array([])
    Some(@ticker.MinorLocator::NamedFixed(locator)) =>
      return locator.tick_values(0.0, 0.0)
    Some(@ticker.MinorLocator::Fixed(values)) =>
      return @ticker.TickValues::from_array(values)
    Some(@ticker.MinorLocator::NamedLinear(locator)) =>
      return locator.tick_values(limits.lower(), limits.upper())
    Some(@ticker.MinorLocator::Multiple(locator)) => {
      let (lower, upper) = self.view_interval_checked()
      locator.tick_values(lower, upper)
    }
    Some(@ticker.MinorLocator::NamedLogit(locator)) => locator.call()
    Some(@ticker.MinorLocator::NamedAsinh(locator)) => locator.call()
    Some(@ticker.MinorLocator::NamedSymLog(locator)) => locator.call()
    Some(@ticker.MinorLocator::NamedLog(locator)) => locator.call()
    Some(@ticker.MinorLocator::NamedAutoMinor(locator)) => locator.call()
    Some(@ticker.MinorLocator::Null(locator)) => locator.call()
    Some(@ticker.MinorLocator::Index(locator)) => locator.call()
    Some(@ticker.MinorLocator::NamedMaxN(locator)) => locator.call()
    Some(@ticker.MinorLocator::NamedAuto(locator)) => locator.call()
    Some(_) => self.minor_tick_values_checked(limits)
  }
  @ticker.TickValues::from_array(values)
}

///|
pub fn Axis::tick_labels_checked(
  self : Axis,
  values : Array[Double],
  view? : @core.Limits,
) -> Array[String] raise @core.DataError {
  if self.tickers.major_formatter_kind == Category {
    return match self.category_for_labels() {
      Some(units) => units.format_values(values)
      None => Array::make(values.length(), "")
    }
  }
  if self.tickers.major_formatter_kind == Numeric &&
    self.tickers.named_formatter is Some(formatter) {
    return formatter.format_ticks(values)
  }
  self.tick_labels_with_view(values, view)
}

///|
pub fn Axis::minor_tick_labels_checked(
  self : Axis,
  values : Array[Double],
  view? : @core.Limits,
) -> Array[String] raise @core.DataError {
  if self.tickers.minor_formatter_kind == Numeric &&
    self.tickers.minor_named_formatter is Some(formatter) {
    return formatter.format_ticks(values)
  }
  match view {
    Some(view) => self.minor_tick_labels(values, view~)
    None => self.minor_tick_labels(values)
  }
}

///|
/// Read the live major offset after its batch, without formatting a second time.
pub fn Axis::formatter_offset_label_checked(
  self : Axis,
  values : Array[Double],
  view? : @core.Limits,
) -> String raise @core.DataError {
  if self.tickers.major_formatter_kind == Numeric &&
    self.tickers.named_formatter is Some(formatter) {
    return formatter.get_offset_checked()
  }
  self.formatter_offset_label_with_view(values, view)
}