///|
/// Mutable fixed spacing and offset. Adapters share this handle; copy() is independent.
pub struct MultipleLocator {
  priv mut base : Double
  priv mut offset : Double
}

///|
fn validate_multiple(
  base : Double,
  offset : Double,
) -> Unit raise @core.DataError {
  if base.is_nan() || base.is_inf() || base <= 0.0 {
    raise @core.DataError::InvalidTicks(
      message="multiple locator base must be finite and positive",
    )
  }
  if offset.is_nan() || offset.is_inf() {
    raise @core.DataError::InvalidTicks(
      message="multiple locator offset must be finite",
    )
  }
}

///|
/// Fixed spacing with a phase offset, retaining outer neighboring ticks.
///
/// ```mbt check
/// test {
///   let locator = @ticker.MultipleLocator::new(base=2.0, offset=1.0)
///   let adapter = locator.as_locator()
///   assert_eq(locator.tick_values(0.0, 5.0), [-1.0, 1.0, 3.0, 5.0, 7.0])
///   locator.set_params(base=4.0)
///   assert_eq(adapter.multiple_locator().unwrap().base(), 4.0)
///   assert_eq(locator.copy().offset(), 1.0)
/// }
/// ```
pub fn MultipleLocator::new(
  base~ : Double = 1.0,
  offset~ : Double = 0.0,
) -> MultipleLocator raise @core.DataError {
  validate_multiple(base, offset)
  { base, offset, }
}

///|
pub fn MultipleLocator::base(self : MultipleLocator) -> Double {
  self.base
}

///|
pub fn MultipleLocator::offset(self : MultipleLocator) -> Double {
  self.offset
}

///|
/// Validate typed parameters before replacing either value. Omitted values stay unchanged.
pub fn MultipleLocator::set_params(
  self : MultipleLocator,
  base? : Double,
  offset? : Double,
) -> Unit raise @core.DataError {
  let base = base.unwrap_or(self.base)
  let offset = offset.unwrap_or(self.offset)
  validate_multiple(base, offset)
  self.base = base
  self.offset = offset
}

///|
pub fn MultipleLocator::copy(self : MultipleLocator) -> MultipleLocator {
  { base: self.base, offset: self.offset, }
}

///|
/// Ascending ticks including neighboring points, matching source tick_values.
pub fn MultipleLocator::tick_values(
  self : MultipleLocator,
  lower : Double,
  upper : Double,
) -> Array[Double] raise @core.DataError {
  @tick_grid.ticks(lower, upper, self.base, offset=self.offset)
}

///|
// transforms._nonsingular, preserving binary64 operation order and zero threshold.
fn locator_nonsingular(
  lower : Double,
  upper : Double,
  expander : Double,
  tiny~ : Double = 1.0e-15,
) -> (Double, Double) {
  if lower.is_nan() || lower.is_inf() || upper.is_nan() || upper.is_inf() {
    return (-expander, expander)
  }
  let mut first = Double::min(lower, upper)
  let mut last = Double::max(lower, upper)
  let largest = Double::max(Double::abs(first), Double::abs(last))
  if largest < 1.0e6 / tiny * 2.2250738585072014e-308 {
    first = -expander
    last = expander
  } else if last - first <= largest * tiny {
    first -= expander * Double::abs(first)
    last += expander * Double::abs(last)
  }
  (first, last)
}

///|
pub fn MultipleLocator::nonsingular(
  _self : MultipleLocator,
  lower : Double,
  upper : Double,
) -> (Double, Double) {
  locator_nonsingular(lower, upper, 0.05)
}

///|
/// Read axes.autolimit_mode at each call; round each original endpoint before sorting.
pub fn MultipleLocator::view_limits(
  self : MultipleLocator,
  lower : Double,
  upper : Double,
) -> (Double, Double) {
  let (first, last) = if @config.axes_autolimit_mode() == "round_numbers" {
    let mut first = @tick_grid.le(lower - self.offset, self.base) * self.base +
      self.offset
    let mut last = @tick_grid.ge(upper - self.offset, self.base) * self.base +
      self.offset
    if first == last {
      first -= 1.0
      last += 1.0
    }
    (first, last)
  } else {
    (lower, upper)
  }
  locator_nonsingular(first, last, 0.001)
}

///|
pub fn MultipleLocator::as_locator(self : MultipleLocator) -> Locator {
  Multiple(self)
}

///|
pub fn MultipleLocator::as_minor_locator(
  self : MultipleLocator,
) -> MinorLocator {
  Multiple(self)
}

///|
pub fn Locator::multiple_locator(self : Locator) -> MultipleLocator? {
  match self {
    Multiple(value) => Some(value)
    _ => None
  }
}

///|
pub fn MinorLocator::multiple_locator(self : MinorLocator) -> MultipleLocator? {
  match self {
    Multiple(value) => Some(value)
    _ => None
  }
}

///|
pub fn multiple_locator(
  base~ : Double = 1.0,
  offset~ : Double = 0.0,
) -> Locator raise @core.DataError {
  MultipleLocator::new(base~, offset~).as_locator()
}