///|
/// Retained locations support live lists, fixed-shape views and numeric snapshots.
pub(all) enum FormatterLocations {
  Sequence(Array[Double])
  Integers(Array[@bigint.BigInt])
  NdArray(TickValues)
  Typed(NumericArray)
}

///|
pub fn FormatterLocations::length(self : FormatterLocations) -> Int32 {
  match self {
    Sequence(values) => values.length()
    Integers(values) => values.length()
    NdArray(values) => values.length()
    Typed(values) => values.length()
  }
}

///|
/// Return the ndarray dtype, or None for a retained Python-style sequence.
pub fn FormatterLocations::dtype(self : FormatterLocations) -> String? {
  match self {
    Sequence(_) | Integers(_) => None
    NdArray(values) => Some(if values.is_int64() { "int64" } else { "float64" })
    Typed(values) => Some(values.dtype())
  }
}

///|
pub fn FormatterLocations::get(
  self : FormatterLocations,
  index : Int32,
) -> Double raise @core.DataError {
  match self {
    Sequence(values) => {
      let index = if index < 0 {
        Int64::from_int(values.length()) + Int64::from_int(index)
      } else {
        Int64::from_int(index)
      }
      if index < 0L || index >= Int64::from_int(values.length()) {
        raise @core.DataError::InvalidTicks(
          message="formatter location index is out of bounds",
        )
      }
      values[index.to_int()]
    }
    Integers(values) => {
      let normalized = if index < 0 {
        Int64::from_int(values.length()) + Int64::from_int(index)
      } else {
        Int64::from_int(index)
      }
      if normalized < 0L || normalized >= Int64::from_int(values.length()) {
        raise @core.DataError::InvalidTicks(
          message="formatter location index is out of bounds",
        )
      }
      percent_python_integer_double(values[normalized.to_int()])
    }
    NdArray(values) => values.get(index)
    Typed(values) => values.get(index)
  }
}

///|
/// Read a stored numeric value without rounding integer storage through Double.
/// Unsigned storage projects its exact value to Integer in the six-origin carrier.
pub fn FormatterLocations::get_value(
  self : FormatterLocations,
  index : Int32,
) -> ScalarNumber raise @core.DataError {
  match self {
    Sequence(_) => ScalarNumber::Number(self.get(index))
    Integers(values) => {
      let normalized = if index < 0 {
        Int64::from_int(values.length()) + Int64::from_int(index)
      } else {
        Int64::from_int(index)
      }
      if normalized < 0L || normalized >= Int64::from_int(values.length()) {
        raise @core.DataError::InvalidTicks(
          message="formatter location index is out of bounds",
        )
      }
      ScalarNumber::Integer(values[normalized.to_int()])
    }
    NdArray(values) =>
      if values.is_int64() {
        ScalarNumber::NumpyInt64(values.get_int64(index))
      } else {
        ScalarNumber::NumpyFloat64(values.get(index))
      }
    Typed(values) => values.get_value(index)
  }
}

///|
/// Scalar domains returned by a live axis view, separate from tick locations.
pub(all) enum FormatterViewInterval {
  Sequence(Array[Double])
  Integers(Array[@bigint.BigInt])
  NdArray(TickValues)
}

///|
pub fn FormatterViewInterval::length(self : FormatterViewInterval) -> Int32 {
  match self {
    Sequence(values) => values.length()
    Integers(values) => values.length()
    NdArray(values) => values.length()
  }
}

///|
/// Stable typed axis identity; dummy metadata does not expose a dynamic Axis.
pub struct FormatterAxisBinding {
  priv minpos : Double?
  priv mut view_provider : (() -> FormatterViewInterval raise @core.DataError)?
  priv mut stored_view : FormatterViewInterval?
  priv mut short_delta_provider : ((Double) -> Double raise @core.DataError)?
  priv mut transform_linthresh_provider : (() -> ScalarNumber? raise @core.DataError)?
}

///|
pub impl @debug.Debug for FormatterAxisBinding with fn to_repr(self) {
  @debug.Repr::Repr(("FormatterAxisBinding", self.minpos))
}

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

///|
pub impl Eq for FormatterAxisBinding with fn equal(self, other) {
  physical_equal(self, other)
}

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

///|
pub fn FormatterAxisBinding::new() -> FormatterAxisBinding {
  {
    minpos: None,
    view_provider: None,
    stored_view: None,
    short_delta_provider: None,
    transform_linthresh_provider: None,
  }
}

///|
/// Bind the data distance to the two neighboring display pixels.
pub fn FormatterAxisBinding::bind_short_format_delta(
  self : FormatterAxisBinding,
  provider : (Double) -> Double raise @core.DataError,
) -> Unit {
  self.short_delta_provider = Some(provider)
}

///|
pub fn FormatterAxisBinding::clear_short_format_delta(
  self : FormatterAxisBinding,
) -> Unit {
  self.short_delta_provider = None
}

///|
/// None selects the formatter's ordinary unnamed-axis fallback.
pub fn FormatterAxisBinding::short_format_delta(
  self : FormatterAxisBinding,
  value : Double,
) -> Double? raise @core.DataError {
  match self.short_delta_provider {
    Some(provider) => Some(provider(value))
    None => None
  }
}

///|
/// Read the current interval once on each formatter call.
pub fn FormatterAxisBinding::view_interval(
  self : FormatterAxisBinding,
) -> FormatterViewInterval raise @core.DataError {
  match self.view_provider {
    Some(provider) => provider()
    None =>
      match self.stored_view {
        Some(view) => view
        None =>
          raise @core.DataError::InvalidTicks(
            message="AttributeError: axis binding has no get_view_interval provider",
          )
      }
  }
}

///|
/// Bind a live view source to this stable axis identity.
pub fn FormatterAxisBinding::bind_view_interval(
  self : FormatterAxisBinding,
  provider : () -> FormatterViewInterval raise @core.DataError,
) -> Unit {
  self.view_provider = Some(provider)
}

///|
/// Retain the supplied interval object, replacing a callback if present.
pub fn FormatterAxisBinding::set_view_interval(
  self : FormatterAxisBinding,
  view : FormatterViewInterval,
) -> Unit {
  self.view_provider = None
  self.stored_view = Some(view)
}

///|
pub fn FormatterAxisBinding::is_dummy(self : FormatterAxisBinding) -> Bool {
  self.minpos is Some(_)
}

///|
pub fn FormatterAxisBinding::dummy_minpos(
  self : FormatterAxisBinding,
) -> Double? {
  self.minpos
}

///|
priv struct FixedFormatterState {
  mut seq : Array[String]
  mut offset : String
}

///|
priv struct FuncFormatterState {
  mut callback : (Double, Int32?) -> String raise @core.DataError
  mut offset : String
}

///|
priv struct StringFormatterState {
  mut fmt : String
}

///|
priv enum FormatterKind {
  Abstract
  Null
  Fixed(FixedFormatterState)
  Func(FuncFormatterState)
  FormatStr(StringFormatterState)
  StrMethod(StringFormatterState)
  Percent(PercentFormatterState)
  Scalar(ScalarFormatterState)
  Eng(EngFormatterState)
  Log(LogFormatterState)
  Logit(LogitFormatterState)
}

///|
/// A live formatter object. Copies share buffers and retain independent slots.
pub struct BaseFormatter {
  priv kind : FormatterKind
  priv mut locations : FormatterLocations
  priv mut axis : FormatterAxisBinding?
}

///|
let formatter_default_locations : Array[Double] = []

///|
fn named_formatter_with_kind(kind : FormatterKind) -> BaseFormatter {
  {
    kind,
    locations: FormatterLocations::Sequence(formatter_default_locations),
    axis: None,
  }
}

///|
/// Construct the base object; its formatting primitive remains unimplemented.
pub fn BaseFormatter::new() -> BaseFormatter {
  named_formatter_with_kind(FormatterKind::Abstract)
}

///|
pub fn BaseFormatter::locations(self : BaseFormatter) -> FormatterLocations {
  self.locations
}

///|
/// Borrow a numeric list with its live mutable length.
pub fn BaseFormatter::set_locs(
  self : BaseFormatter,
  locations : Array[Double],
) -> Unit {
  try! self.set_locs_checked(locations)
}

///|
/// Retain the input before performing a derived formatter's preparation.
pub fn BaseFormatter::set_locs_checked(
  self : BaseFormatter,
  locations : Array[Double],
) -> Unit raise @core.DataError {
  if self.kind is Log(state) {
    log_named_prepare(self, state)
    return
  }
  if self.kind is Logit(state) {
    logit_named_prepare(self, state, LogitInput::from_array(locations))
    return
  }
  self.locations = FormatterLocations::Sequence(locations)
  match self.kind {
    Scalar(state) => scalar_named_prepare(self, state)
    Eng(state) => eng_named_prepare(self, state)
    _ => ()
  }
}

///|
/// Retain a numeric view and its original shape, dtype, buffer, and stride.
pub fn BaseFormatter::set_locs_view(
  self : BaseFormatter,
  locations : TickValues,
) -> Unit {
  try! self.set_locs_view_checked(locations)
}

///|
pub fn BaseFormatter::set_locs_view_checked(
  self : BaseFormatter,
  locations : TickValues,
) -> Unit raise @core.DataError {
  if self.kind is Log(state) {
    log_named_prepare(self, state)
    return
  }
  if self.kind is Logit(state) {
    logit_named_prepare(self, state, LogitInput::from_view(locations))
    return
  }
  self.locations = FormatterLocations::NdArray(locations)
  match self.kind {
    Scalar(state) => scalar_named_prepare(self, state)
    Eng(state) => eng_named_prepare(self, state)
    _ => ()
  }
}

///|
pub fn BaseFormatter::axis(self : BaseFormatter) -> FormatterAxisBinding? {
  self.axis
}

///|
pub fn BaseFormatter::set_axis(
  self : BaseFormatter,
  axis : FormatterAxisBinding?,
) -> Unit {
  self.axis = axis
}

///|
/// Allocate a dummy identity only while unbound, retaining arbitrary minpos.
pub fn BaseFormatter::create_dummy_axis(
  self : BaseFormatter,
  minpos~ : Double = 0.0,
) -> Unit {
  if self.axis is None {
    self.axis = Some({
      minpos: Some(minpos),
      view_provider: None,
      short_delta_provider: None,
      transform_linthresh_provider: None,
      stored_view: Some(
        FormatterViewInterval::Integers([
          @bigint.BigInt::from_int(0),
          @bigint.BigInt::from_int(1),
        ]),
      ),
    })
  }
}

///|
pub fn BaseFormatter::get_offset(self : BaseFormatter) -> String {
  try! self.get_offset_checked()
}

///|
/// Preserve deferred errors from derived offset formatting.
pub fn BaseFormatter::get_offset_checked(
  self : BaseFormatter,
) -> String raise @core.DataError {
  match self.kind {
    Abstract
    | Null
    | FormatStr(_)
    | StrMethod(_)
    | Percent(_)
    | Log(_)
    | Logit(_) => ""
    Fixed(state) => state.offset
    Func(state) => state.offset
    Scalar(state) => scalar_named_offset(self, state)
    Eng(state) => eng_named_offset(self, state)
  }
}

///|
pub fn BaseFormatter::copy(self : BaseFormatter) -> BaseFormatter {
  let kind = match self.kind {
    Abstract => FormatterKind::Abstract
    Null => FormatterKind::Null
    Fixed(state) =>
      FormatterKind::Fixed({ seq: state.seq, offset: state.offset, })
    Func(state) =>
      FormatterKind::Func({ callback: state.callback, offset: state.offset, })
    FormatStr(state) => FormatterKind::FormatStr({ fmt: state.fmt, })
    StrMethod(state) => FormatterKind::StrMethod({ fmt: state.fmt, })
    Percent(state) =>
      FormatterKind::Percent({
        xmax: state.xmax,
        decimals: state.decimals,
        symbol: state.symbol,
        is_latex: state.is_latex,
      })
    Scalar(state) => FormatterKind::Scalar(scalar_formatter_copy_state(state))
    Eng(state) =>
      FormatterKind::Eng({
        scalar: scalar_formatter_copy_state(state.scalar),
        unit: state.unit,
        places: state.places,
        sep: state.sep,
        prefixes: state.prefixes,
      })
    Log(state) =>
      FormatterKind::Log({
        style: state.style,
        base: state.base,
        label_only_base: state.label_only_base,
        minor_thresholds: state.minor_thresholds,
        linthresh: state.linthresh,
        sublabels: state.sublabels,
      })
    Logit(state) =>
      FormatterKind::Logit({
        use_overline: state.use_overline,
        one_half: state.one_half,
        minor: state.minor,
        minor_threshold: state.minor_threshold,
        minor_number: state.minor_number,
        labelled: state.labelled,
      })
  }
  { kind, locations: self.locations, axis: self.axis, }
}

///|
/// Retain a Python integer list without converting its scalar origins to NumPy.
pub fn BaseFormatter::set_locs_integers_checked(
  self : BaseFormatter,
  locations : Array[@bigint.BigInt],
) -> Unit raise @core.DataError {
  if self.kind is Log(state) {
    log_named_prepare(self, state)
    return
  }
  if self.kind is Logit(state) {
    logit_named_prepare(
      self,
      state,
      LogitInput::from_keys(
        locations.map(value => LogitKey::new(ScalarNumber::Integer(value))),
      ),
    )
    return
  }
  self.locations = FormatterLocations::Integers(locations)
  match self.kind {
    Scalar(state) => scalar_named_prepare(self, state)
    Eng(state) => eng_named_prepare(self, state)
    _ => ()
  }
}