///|
/// Whether automatic view updates are enabled, independently of stored limits.
pub fn Axis::autoscale_on(self : Axis) -> Bool {
  self.autoscale_enabled
}

///|
/// Enable or disable automatic updates without requesting or changing a view.
pub fn Axis::set_autoscale_on(self : Axis, enabled : Bool) -> Unit {
  self.autoscale_enabled = enabled
  // Keep the legacy explicit/provisional projection useful without conflating
  // it with the authoritative viewport or changing the pending request.
  if enabled {
    if self.limits is Some(view) {
      self.limits = None
      self.auto_view = Some(view)
    }
  } else {
    self.limits = Some(self.stored_view)
    self.auto_view = None
  }
}

///|
/// Whether this coordinate has an outstanding automatic view request.
pub fn Axis::autoscale_stale(self : Axis) -> Bool {
  self.autoscale_pending
}

///|
/// Consume a request before the checked computation, including a failing one.
pub fn Axis::clear_autoscale_request(self : Axis) -> Unit {
  self.autoscale_pending = false
}

///|
/// The stored viewport, without resolving pending updates or render projections.
pub fn Axis::stored_view_limits(self : Axis) -> @core.Limits {
  self.stored_view
}

///|
/// Compare current viewport endpoints to the initial interval, as Bbox does.
/// Returning to the original interval restores the unmutated state.
pub fn Axis::view_mutated(self : Axis) -> Bool {
  self.stored_view.lower() != 0.0 || self.stored_view.upper() != 1.0
}

///|
/// Bind the owning panel's lazy view update without changing ticker ownership.
pub fn Axis::bind_autoscale_view(
  self : Axis,
  provider : () -> Unit raise @core.DataError,
) -> Unit {
  self.autoscale_view_provider = Some(provider)
}

///|
/// Resolve the owning panel's outstanding shared view requests.
pub fn Axis::resolve_autoscale_view(self : Axis) -> Unit raise @core.DataError {
  if self.autoscale_view_provider is Some(provider) {
    provider()
  }
}

///|
/// Whether the owning panel supplies authoritative automatic view resolution.
pub fn Axis::has_autoscale_view_provider(self : Axis) -> Bool {
  self.autoscale_view_provider is Some(_)
}

///|
/// Whether installed tickers support the shared numeric-coordinate range pipeline.
pub fn Axis::uses_numeric_autoscale(_self : Axis) -> Bool {
  true
}

///|
/// Bind shared raw DataBox minima, distinct from locator/Axis minimum queries.
pub fn Axis::bind_autoscale_minpos(
  self : Axis,
  provider : () -> Array[Double] raise @core.DataError,
) -> Unit {
  self.autoscale_minpos_provider = Some(provider)
}

///|
/// Supply raw shared minima for an IR graph without replacing Scene data.
pub fn Axis::bind_render_autoscale_minpos(
  self : Axis,
  provider : () -> Array[Double] raise @core.DataError,
) -> Unit {
  self.render_autoscale_minpos_provider = Some(provider)
}

///|
fn Axis::autoscale_minpos_checked(
  self : Axis,
  fallback : (() -> Array[Double] raise @core.DataError)?,
) -> Array[Double] raise @core.DataError {
  match self.autoscale_minpos_provider {
    Some(provider) => provider()
    None =>
      match fallback {
        Some(provider) => provider()
        None =>
          match self.render_autoscale_minpos_provider {
            Some(provider) => provider()
            // Standalone Axis contexts can model both data queries with one binding.
            None => self.shared_minpos_checked()
          }
      }
  }
}

///|
fn Axis::autoscale_nonsingular(
  self : Axis,
  lower : Double,
  upper : Double,
) -> (Double, Double) raise @core.DataError {
  self.locator_nonsingular(lower, upper)
}

///|
fn autoscale_locator_view_limits(
  locator : @ticker.Locator?,
  lower : Double,
  upper : Double,
) -> @core.Limits raise @core.DataError {
  let (first, last) = match locator {
    Some(@ticker.Locator::NamedLogit(value)) => value.view_limits(lower, upper)
    Some(@ticker.Locator::NamedAsinh(value)) => value.view_limits(lower, upper)
    Some(@ticker.Locator::NamedSymLog(value)) => value.view_limits(lower, upper)
    Some(@ticker.Locator::NamedLog(value)) => value.view_limits(lower, upper)
    Some(@ticker.Locator::Null(value)) => value.view_limits(lower, upper)
    Some(@ticker.Locator::NamedMaxN(value)) => value.view_limits(lower, upper)
    Some(@ticker.Locator::NamedAuto(value)) => value.view_limits(lower, upper)
    Some(@ticker.Locator::NamedLinear(value)) => value.view_limits(lower, upper)
    Some(@ticker.Locator::Index(value)) => value.view_limits(lower, upper)
    Some(@ticker.Locator::NamedFixed(value)) => value.view_limits(lower, upper)
    Some(@ticker.Locator::Multiple(value)) => value.view_limits(lower, upper)
    _ => @ticker.NullLocator::new().view_limits(lower, upper)
  }
  @core.limits(first, last)
}

///|
fn Axis::locator_view_limits_checked(
  self : Axis,
  lower : Double,
  upper : Double,
) -> @core.Limits raise @core.DataError {
  let locator = if self.tickers.major_locator_kind == Numeric {
    self.tickers.locator
  } else {
    None
  }
  autoscale_locator_view_limits(locator, lower, upper)
}

///|
/// Compute one selected automatic coordinate through the source range pipeline.
/// `endpoints` are shared raw data intervals, independent of views and scales.
/// The result precedes set_bound's view reads, finalization and peer propagation.
/// No-data mutated views return None before reading locators or minimum data.
pub fn Axis::autoscale_bounds_checked(
  self : Axis,
  endpoints : Array[Double],
  margin~ : Double,
  stickies~ : Array[Double],
  tight~ : Bool,
  data_minpos~ : (() -> Array[Double] raise @core.DataError)? = None,
  tight_provider~ : (() -> Bool)? = None,
  log_stickies~ : Bool? = None,
) -> @core.Limits? raise @core.DataError {
  if !self.autoscale_enabled {
    return None
  }
  let logarithmic_stickies = log_stickies.unwrap_or(
    self.coordinate_scale_name() == "log",
  )
  let mut lower = 1.0 / 0.0
  let mut upper = -1.0 / 0.0
  let mut finite = false
  for value in endpoints {
    if !value.is_nan() && !value.is_inf() {
      if !finite {
        lower = value
        upper = value
      } else {
        if value < lower {
          lower = value
        }
        if value > upper {
          upper = value
        }
      }
      finite = true
    }
  }
  if !finite {
    if self.view_mutated() {
      return None
    }
    lower = -1.0 / 0.0
    upper = 1.0 / 0.0
  }
  // The source keeps this locator object across later data/provider callbacks.
  // Final Axis._set_lim independently fetches the then-current major locator.
  let locator = if self.tickers.major_locator_kind == Numeric {
    self.tickers.locator
  } else {
    None
  }
  let (lower, upper) = self.autoscale_nonsingular(lower, upper)
  let minpos_values = self.autoscale_minpos_checked(data_minpos)
  if minpos_values.is_empty() {
    raise @core.DataError::InvalidTicks(
      message="shared axis has no minimum positive data",
    )
  }
  // Python min keeps its first value when later comparisons are unordered.
  let mut minpos = minpos_values[0]
  for value in minpos_values {
    if value < minpos {
      minpos = value
    }
  }
  let mut lower_sticky : Double? = None
  let mut upper_sticky : Double? = None
  let tolerance = 1.0e-5 * Double::abs(upper - lower)
  for value in stickies {
    // Only LogScale removes nonpositive sticky edges. Other scales keep them.
    if !logarithmic_stickies || value > 0.0 {
      if value < lower + tolerance {
        lower_sticky = Some(
          match lower_sticky {
            Some(previous) => Double::max(previous, value)
            None => value
          },
        )
      }
      if value >= upper - tolerance {
        upper_sticky = Some(
          match upper_sticky {
            Some(previous) => Double::min(previous, value)
            None => value
          },
        )
      }
    }
  }
  // Scale transforms are immutable values; acquiring their forward/inverse
  // pair has no callback. The range limiter still precedes their evaluation.
  let scale = self.scale
  let function_scale = self.function_scale
  let transform = self.coordinate_transform()
  let inverse = self.coordinate_inverse()
  let (lower, upper) = scale.limit_range_for_scale(lower, upper, minpos)
  // LogScale's default LogTransform clips nonpositive inputs at -1000.
  // Raw finite minpos overrides can reach this branch after the scale limiter.
  let transformed_lower = if function_scale is Some(_) {
    transform(lower)
  } else if scale.is_log() && lower <= 0.0 {
    -1000.0
  } else {
    scale.transform(lower)
  }
  let transformed_upper = if function_scale is Some(_) {
    transform(upper)
  } else if scale.is_log() && upper <= 0.0 {
    -1000.0
  } else {
    scale.transform(upper)
  }
  let padding = (transformed_upper - transformed_lower) * margin
  let padding = if padding.is_nan() || padding.is_inf() { 0.0 } else { padding }
  // InvertedLogTransform uses exp(x * ln(base)), with a separately rounded
  // product. Replacing that chain with pow(base, x) changes source results.
  let lower = if function_scale is Some(_) {
    inverse(transformed_lower - padding)
  } else if scale.is_log() {
    @math.exp((transformed_lower - padding) * @math.ln(10.0))
  } else {
    scale.inverse(transformed_lower - padding)
  }
  let upper = if function_scale is Some(_) {
    inverse(transformed_upper + padding)
  } else if scale.is_log() {
    @math.exp((transformed_upper + padding) * @math.ln(10.0))
  } else {
    scale.inverse(transformed_upper + padding)
  }
  let lower = match lower_sticky {
    Some(value) => if value > lower { value } else { lower }
    None => lower
  }
  let upper = match upper_sticky {
    Some(value) => if value < upper { value } else { upper }
    None => upper
  }
  let tight = match tight_provider {
    Some(provider) => provider()
    None => tight
  }
  Some(
    if tight {
      @core.limits(lower, upper)
    } else {
      autoscale_locator_view_limits(locator, lower, upper)
    },
  )
}

///|
/// Finalize already oriented set_bound endpoints while preserving auto-enable.
/// The owning panel controls lazy view reads and source-ordered sibling emits.
pub fn Axis::set_bound_limits_checked(
  self : Axis,
  lower : Double,
  upper : Double,
  automatic~ : Bool? = None,
) -> @core.Limits raise @core.DataError {
  if !@core.limits(lower, upper).is_finite() {
    raise @core.DataError::NonFiniteLimit(axis="axis")
  }
  let (lower, upper) = if self.coordinate_scale_name() == "log" &&
    (lower <= 0.0 || upper <= 0.0) {
    let (old_lower, old_upper) = self.lifecycle_view_interval_checked()
    (
      if lower <= 0.0 {
        old_lower
      } else {
        lower
      },
      if upper <= 0.0 {
        old_upper
      } else {
        upper
      },
    )
  } else {
    (lower, upper)
  }
  let reversed = lower > upper
  let (lower, upper) = self.autoscale_nonsingular(lower, upper)
  let (lower, upper) = self.scale.limit_range_for_scale(
    lower,
    upper,
    self.get_minpos(),
  )
  let (low, high) = if upper < lower { (upper, lower) } else { (lower, upper) }
  let view = if reversed {
    @core.limits(high, low)
  } else {
    @core.limits(low, high)
  }
  self.stored_view = view
  self.inverted = reversed
  self.autoscale_pending = false
  if automatic is Some(enabled) {
    self.autoscale_enabled = enabled
  }
  if self.autoscale_enabled {
    self.limits = None
    self.auto_view = Some(view)
  } else {
    self.limits = Some(view)
    self.auto_view = None
  }
  if self.tick_layout_view is Some(_) {
    self.tick_layout_view = Some((view.lower(), view.upper()))
  }
  view
}