///|
/// A fixed interval budget or an axis-dependent automatic interval budget.
pub(all) enum MaxNBins {
  Auto
  Count(Int32)
} derive(Debug, Eq)

///|
pub extend MaxNBins with @debug.Debug::{to_repr}

///|
pub extend MaxNBins with Eq::{equal, not_equal}

///|
/// Mutable MaxN state. Step inputs remain borrowed; the extended staircase is a snapshot.
pub struct MaxNLocator {
  priv mut nbins : MaxNBins
  priv mut steps : TickValues
  priv mut extended_steps : Array[Double]
  priv mut integer : Bool
  priv mut symmetric : Bool
  priv mut prune : String?
  priv mut min_n_ticks : Int32
  priv mut tick_space_provider : (() -> Double)?
  priv mut view_interval_provider : (() -> (Double, Double) raise @core.DataError)?
}

///|
fn maxn_default_steps() -> TickValues {
  TickValues::from_array([1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0, 6.0, 8.0, 10.0])
}

///|
fn maxn_validate_steps(steps : TickValues) -> TickValues raise @core.DataError {
  let length = steps.length()
  if length == 0 {
    raise @core.DataError::InvalidTicks(
      message="maxn locator steps have no first or last element",
    )
  }
  for index in 1.. 10.0 || first < 1.0 {
    raise @core.DataError::InvalidTicks(
      message="maxn locator steps must be increasing between one and ten",
    )
  }
  if first == 1.0 && last == 10.0 {
    return steps
  }
  steps.with_endpoints(first != 1.0, last != 10.0)
}

///|
fn maxn_staircase(steps : TickValues) -> Array[Double] raise @core.DataError {
  let result : Array[Double] = []
  for index in 0..<(steps.length() - 1) {
    result.push(0.1 * steps.get(index))
  }
  for index in 0.. MaxNLocator raise @core.DataError {
  let defaults = maxn_default_steps()
  let locator = {
    nbins: MaxNBins::Count(10),
    steps: defaults,
    extended_steps: maxn_staircase(defaults),
    integer: false,
    symmetric: false,
    prune: None,
    min_n_ticks: 2,
    tick_space_provider: None,
    view_interval_provider: None,
  }
  locator.set_params(
    nbins=nbins.unwrap_or(MaxNBins::Count(10)),
    steps~,
    integer~,
    symmetric~,
    prune~,
    min_n_ticks~,
  )
  locator
}

///|
/// Preserve the source update order; later validation errors retain earlier updates.
/// Explicit None resets steps/prune, while omission leaves each value unchanged.
pub fn MaxNLocator::set_params(
  self : MaxNLocator,
  nbins? : MaxNBins,
  steps? : TickValues?,
  integer? : Bool,
  symmetric? : Bool,
  prune? : String?,
  min_n_ticks? : Int32,
) -> Unit raise @core.DataError {
  if nbins is Some(value) {
    self.nbins = value
  }
  if symmetric is Some(value) {
    self.symmetric = value
  }
  if prune is Some(value) {
    if value is Some(text) &&
      text != "lower" &&
      text != "upper" &&
      text != "both" {
      raise @core.DataError::InvalidTicks(
        message="maxn locator prune must be lower, upper, both, or None",
      )
    }
    self.prune = value
  }
  if min_n_ticks is Some(value) {
    self.min_n_ticks = Int32::max(1, value)
  }
  if steps is Some(value) {
    self.steps = match value {
      Some(steps) => maxn_validate_steps(steps)
      None => maxn_default_steps()
    }
    self.extended_steps = maxn_staircase(self.steps)
  }
  if integer is Some(value) {
    self.integer = value
  }
}

///|
pub fn MaxNLocator::nbins(self : MaxNLocator) -> MaxNBins {
  self.nbins
}

///|
pub fn MaxNLocator::steps(self : MaxNLocator) -> TickValues {
  self.steps
}

///|
/// Return an independent copy of the internally captured extended staircase.
pub fn MaxNLocator::extended_steps(self : MaxNLocator) -> Array[Double] {
  self.extended_steps.copy()
}

///|
pub fn MaxNLocator::integer(self : MaxNLocator) -> Bool {
  self.integer
}

///|
pub fn MaxNLocator::symmetric(self : MaxNLocator) -> Bool {
  self.symmetric
}

///|
pub fn MaxNLocator::prune(self : MaxNLocator) -> String? {
  self.prune
}

///|
pub fn MaxNLocator::min_n_ticks(self : MaxNLocator) -> Int32 {
  self.min_n_ticks
}

///|
pub fn MaxNLocator::tick_space_provider(self : MaxNLocator) -> (() -> Double)? {
  self.tick_space_provider
}

///|
pub fn MaxNLocator::view_interval_provider(
  self : MaxNLocator,
) -> (() -> (Double, Double) raise @core.DataError)? {
  self.view_interval_provider
}

///|
pub fn MaxNLocator::bind_tick_space(
  self : MaxNLocator,
  provider : () -> Double,
) -> Unit {
  self.tick_space_provider = Some(provider)
}

///|
pub fn MaxNLocator::bind_view_interval(
  self : MaxNLocator,
  provider : () -> (Double, Double) raise @core.DataError,
) -> Unit {
  self.view_interval_provider = Some(provider)
}

///|
/// Share step buffers, captured staircase and providers; later assignments stay independent.
pub fn MaxNLocator::copy(self : MaxNLocator) -> MaxNLocator {
  {
    nbins: self.nbins,
    steps: self.steps,
    extended_steps: self.extended_steps,
    integer: self.integer,
    symmetric: self.symmetric,
    prune: self.prune,
    min_n_ticks: self.min_n_ticks,
    tick_space_provider: self.tick_space_provider,
    view_interval_provider: self.view_interval_provider,
  }
}