///|
/// Set a copied date formatter while preserving locators and fixed ticks/labels.
/// The formatter's timezone affects labels independently of the tick locator.
/// Use Axis::set_date_timezone/offset to update both labels and tick positions.
pub fn Axis::set_date_formatter(
  self : Axis,
  formatter : @date.DateFormatter,
) -> Unit {
  self.tickers.major_formatter_kind = Date
  self.tickers.major_formatter_default = false
  self.tickers.date_format = Some(formatter.format())
  self.tickers.named_formatter = None
  self.tickers.formatter = None
  self.tickers.fixed_labels = None
  self.tickers.date_formatter = Some(formatter.copy())
  self.tickers.date_formatter_default = false
}

///|
/// Return a copy of the configured date formatter, if any.
pub fn Axis::date_formatter(self : Axis) -> @date.DateFormatter? {
  if self.tickers.major_formatter_kind != Date {
    return None
  }
  self.tickers.date_formatter.map(formatter => {
    formatter.with_locator_unit(self.date_formatter_unit([], None, false))
  })
}

///|
fn Axis::sync_default_date_formatter(self : Axis) -> Unit {
  if !self.tickers.date_formatter_default || self.date_timezone_is_default {
    return
  }
  self.tickers.date_formatter = self.tickers.date_formatter.map(formatter => {
    match self.date_timezone {
      Some(timezone) => formatter.with_timezone(timezone)
      None => try! formatter.with_offset(self.date_offset_minutes)
    }
  })
}

///|
fn Axis::date_formatter_unit(
  self : Axis,
  values : Array[Double],
  view : @core.Limits?,
  minor : Bool,
) -> Double {
  let limits = match view {
    Some(_) => view
    None => self.current_view_limits()
  }
  let limits = limits.map(bounds => {
    self.date_nonsingular_bounds(bounds.lower(), bounds.upper())
  })
  let kind = if minor {
    self.tickers.minor_locator_kind
  } else {
    self.tickers.major_locator_kind
  }
  if kind != Date {
    return 1.0
  }
  let concrete = if minor {
    self.tickers.minor_date_locator
  } else {
    self.tickers.date_locator
  }
  if concrete is Some(_) {
    if concrete is Some(locator) {
      if locator.automatic_locator() is Some(auto) {
        match limits {
          Some(bounds) =>
            ignore(auto.get_locator_for_range(bounds.lower(), bounds.upper())) catch {
              _ => ()
            }
          None => ()
        }
      }
      return locator.unit_scale()
    }
  }
  if minor {
    return 1.0
  }
  if self.tickers.automatic_date_locator is Some(auto) {
    match limits {
      Some(bounds) =>
        ignore(auto.get_locator_for_range(bounds.lower(), bounds.upper())) catch {
          _ => ()
        }
      None =>
        if values.length() >= 2 {
          ignore(
            auto.get_locator_for_range(values[0], values[values.length() - 1]),
          ) catch {
            _ => ()
          }
        }
    }
    return auto.unit_scale()
  }
  1.0
}

///|
// Matplotlib's date locators supply ascending ticks even on an inverted axis.
// This port returns reversed generated ticks, so normalize just that path for
// the batch formatter and restore label order afterwards. Fixed ticks retain
// their explicit caller order, as with upstream FixedLocator.
fn Axis::date_formatter_values(
  self : Axis,
  values : Array[Double],
  view : @core.Limits?,
) -> (Array[Double], Bool, Bool) {
  let effective_view = match view {
    Some(_) => view
    None => self.limits
  }
  let inverted = match effective_view {
    Some(limits) => limits.lower() > limits.upper()
    None => self.inverted
  }
  let reversed = self.tickers.major_locator_kind == Date &&
    values.length() > 1 &&
    values[0] > values[values.length() - 1]
  (if reversed { values.rev() } else { values }, inverted, reversed)
}