///|
/// Set a provisional view without disabling automatic limits.
///
/// Images use this to fit their extent immediately. A later autoscale request
/// discards the provisional view and retains its direction. Explicit limits
/// take precedence. Both endpoints must be valid for this axis scale.
///
/// ```mbt check
/// test {
///   let axis = @axis.axis()
///   axis.set_auto_limits(1.5, -0.5)
///   assert_true(axis.inverted())
///   axis.request_autoscale()
///   let view = axis.view_limits(@core.limits(-2.0, 3.0))
///   assert_eq((view.lower(), view.upper()), (3.0, -2.0))
/// }
/// ```
pub fn Axis::set_auto_limits(
  self : Axis,
  lower : Double,
  upper : Double,
) -> Unit {
  if self.autoscale_enabled &&
    self.scale.is_valid(lower) &&
    self.scale.is_valid(upper) &&
    lower != upper {
    self.auto_view = Some(@core.limits(lower, upper))
    self.limits = None
    self.stored_view = @core.limits(lower, upper)
    self.autoscale_pending = false
    self.inverted = lower > upper
  }
}

///|
/// Recompute the automatic view on its next use while retaining explicit
/// limits and the axis direction.
pub fn Axis::request_autoscale(self : Axis) -> Unit {
  self.auto_view = None
  self.autoscale_pending = true
}

///|
/// Whether this axis displays larger values before smaller values.
pub fn Axis::inverted(self : Axis) -> Bool {
  self.inverted
}

///|
/// Resolve explicit or provisional limits before using an automatic range.
/// Automatic endpoints are sorted and then oriented to match the axis.
pub fn Axis::view_limits(self : Axis, automatic : @core.Limits) -> @core.Limits {
  try! self.view_limits_checked(automatic)
}

///|
/// Resolve a view while preserving named locator range errors.
pub fn Axis::view_limits_checked(
  self : Axis,
  automatic : @core.Limits,
) -> @core.Limits raise @core.DataError {
  match (self.limits, self.auto_view) {
    (Some(limits), _) | (None, Some(limits)) => limits
    (None, None) => {
      if self.tickers.major_locator_kind != Numeric {
        let bounds = self.locator_view_limits_checked(
          automatic.lower(),
          automatic.upper(),
        )
        return if self.inverted {
          @core.limits(bounds.upper(), bounds.lower())
        } else {
          bounds
        }
      }
      let (lower, upper) = match self.tickers.locator {
        Some(@ticker.Locator::NamedLogit(value)) =>
          value.view_limits(automatic.lower(), automatic.upper())
        Some(@ticker.Locator::NamedAsinh(value)) =>
          value.view_limits(automatic.lower(), automatic.upper())
        Some(@ticker.Locator::NamedSymLog(value)) =>
          value.view_limits(automatic.lower(), automatic.upper())
        Some(@ticker.Locator::NamedLog(value)) =>
          value.view_limits(automatic.lower(), automatic.upper())
        Some(@ticker.Locator::Null(value)) =>
          value.view_limits(automatic.lower(), automatic.upper())
        Some(@ticker.Locator::NamedMaxN(value)) =>
          value.view_limits(automatic.lower(), automatic.upper())
        Some(@ticker.Locator::NamedAuto(value)) =>
          value.view_limits(automatic.lower(), automatic.upper())
        Some(@ticker.Locator::NamedLinear(value)) =>
          value.view_limits(automatic.lower(), automatic.upper())
        Some(@ticker.Locator::Index(value)) =>
          value.view_limits(automatic.lower(), automatic.upper())
        Some(@ticker.Locator::NamedFixed(value)) =>
          value.view_limits(automatic.lower(), automatic.upper())
        Some(@ticker.Locator::Multiple(value)) =>
          value.view_limits(automatic.lower(), automatic.upper())
        _ =>
          (
            Double::min(automatic.lower(), automatic.upper()),
            Double::max(automatic.lower(), automatic.upper()),
          )
      }
      if self.inverted {
        @core.limits(upper, lower)
      } else {
        @core.limits(lower, upper)
      }
    }
  }
}

///|
/// Store a resolved view while retaining explicit or automatic limit selection.
/// Invalid endpoints raise DataError before changing the view. In automatic
/// mode a later request_autoscale() discards this provisional range.
///
/// ```mbt check
/// test {
///   let axis = @axis.axis()
///   try! axis.set_view_limits(-1.0, 2.0)
///   assert_true(axis.limits() is None)
///   assert_eq(axis.current_view_limits().unwrap().lower(), -1.0)
///   axis.request_autoscale()
///   assert_true(axis.current_view_limits() is None)
/// }
/// ```
pub fn Axis::set_view_limits(
  self : Axis,
  lower : Double,
  upper : Double,
) -> Unit raise @core.DataError {
  if self.scale.is_logit() {
    let view = self.logit_limit_bounds_checked(lower, upper)
    if self.limits is Some(_) {
      self.limits = Some(view)
      self.auto_view = None
    } else {
      self.auto_view = Some(view)
    }
    self.inverted = view.lower() > view.upper()
    self.stored_view = view
    self.autoscale_pending = false
    if self.tick_layout_view is Some(_) {
      self.tick_layout_view = Some((view.lower(), view.upper()))
    }
    return
  }
  if !self.scale.is_valid(lower) || !self.scale.is_valid(upper) {
    raise @core.DataError::NonFiniteLimit(axis="view")
  }
  let view = @core.limits(lower, upper)
  if self.limits is Some(_) {
    self.limits = Some(view)
    self.auto_view = None
  } else {
    self.auto_view = Some(view)
  }
  self.inverted = lower > upper
  self.stored_view = view
  self.autoscale_pending = false
  if self.tick_layout_view is Some(_) {
    self.tick_layout_view = Some((lower, upper))
  }
}

///|
/// Return a stored explicit/provisional view without computing automatic limits.
///
/// ```mbt check
/// test {
///   let axis = @axis.axis()
///   assert_true(axis.current_view_limits() is None)
///   try! axis.set_limits(2.0, -1.0)
///   assert_eq(axis.current_view_limits().unwrap().upper(), -1.0)
/// }
/// ```
pub fn Axis::current_view_limits(self : Axis) -> @core.Limits? {
  match self.limits {
    Some(_) => self.limits
    None => self.auto_view
  }
}

///|
/// Apply named numeric locators' inherited nonsingular policy before margins.
/// Other locator families retain their existing range policy.
pub fn Axis::numeric_nonsingular_bounds(
  self : Axis,
  lower : Double,
  upper : Double,
) -> @core.Limits {
  match self.tickers.locator {
    Some(@ticker.Locator::NamedLogit(value)) => {
      let (first, last) = value.nonsingular(lower, upper) catch {
        _ => (lower, upper)
      }
      @core.limits(first, last)
    }
    Some(@ticker.Locator::NamedAsinh(value)) => {
      let (first, last) = value.nonsingular(lower, upper)
      @core.limits(first, last)
    }
    Some(@ticker.Locator::NamedSymLog(value)) => {
      let (first, last) = value.nonsingular(lower, upper)
      @core.limits(first, last)
    }
    Some(@ticker.Locator::NamedLog(value)) => {
      let (first, last) = value.nonsingular(lower, upper) catch {
        _ => (lower, upper)
      }
      @core.limits(first, last)
    }
    Some(@ticker.Locator::Null(value)) => {
      let (first, last) = value.nonsingular(lower, upper)
      @core.limits(first, last)
    }
    Some(@ticker.Locator::NamedMaxN(value)) => {
      let (first, last) = value.nonsingular(lower, upper)
      @core.limits(first, last)
    }
    Some(@ticker.Locator::NamedAuto(value)) => {
      let (first, last) = value.nonsingular(lower, upper)
      @core.limits(first, last)
    }
    Some(@ticker.Locator::NamedLinear(value)) => {
      let (first, last) = value.nonsingular(lower, upper)
      @core.limits(first, last)
    }
    Some(@ticker.Locator::Index(value)) => {
      let (first, last) = value.nonsingular(lower, upper)
      @core.limits(first, last)
    }
    Some(@ticker.Locator::NamedFixed(value)) => {
      let (first, last) = value.nonsingular(lower, upper)
      @core.limits(first, last)
    }
    Some(@ticker.Locator::Multiple(value)) => {
      let (first, last) = value.nonsingular(lower, upper)
      @core.limits(first, last)
    }
    _ => @core.limits(lower, upper)
  }
}