///|
// Preserve integer dtype through unique: its first subtraction is an Int64
// NumPy scalar subtraction, including two's-complement overflow.
fn auto_minor_unique(major : TickValues) -> TickValues raise @core.DataError {
  if major.is_int64() {
    let values = Array::makei(major.length(), index => major.get_int64(index))
    values.sort()
    let unique : Array[Int64] = []
    for value in values {
      if unique.is_empty() || value != unique[unique.length() - 1] {
        unique.push(value)
      }
    }
    return TickValues::from_int64_array(unique)
  }
  let values = major.to_array()
  let mut zero_count = 0
  let mut negative_zero = false
  let mut first_zero = 0.0
  for value in values {
    if value == 0.0 {
      if zero_count == 0 {
        first_zero = value
      }
      zero_count += 1
      if value.reinterpret_as_uint64() >> 63 != 0UL {
        negative_zero = true
      }
    }
  }
  values.sort_by((left, right) => {
    if left.is_nan() {
      if right.is_nan() {
        0
      } else {
        1
      }
    } else if right.is_nan() {
      -1
    } else if left < right {
      -1
    } else if left > right {
      1
    } else {
      0
    }
  })
  let unique : Array[Double] = []
  for value in values {
    if unique.is_empty() ||
      (
        value != unique[unique.length() - 1] &&
        !(value.is_nan() && unique[unique.length() - 1].is_nan())
      ) {
      let value = if value == 0.0 {
        // The pinned NEON sorting networks select -0. Large equal partitions
        // can keep either representative: source VQSort randomizes its pivot.
        if negative_zero && zero_count <= 16 {
          -0.0
        } else {
          first_zero
        }
      } else if value.is_nan() {
        0x7fffffffffffffffUL.reinterpret_as_int64().reinterpret_as_double()
      } else {
        value
      }
      unique.push(value)
    }
  }
  TickValues::from_array(unique)
}

///|
fn auto_minor_step(
  unique : TickValues,
  ndivs : @config.MinorTickDivisions,
) -> Double raise @core.DataError {
  let majorstep = if unique.is_int64() {
    let first = unique.get_int64(0).reinterpret_as_uint64()
    let second = unique.get_int64(1).reinterpret_as_uint64()
    (second - first).reinterpret_as_int64().to_double()
  } else {
    unique.get(1) - unique.get(0)
  }
  let divisions = match ndivs {
    Text("auto") => {
      let logarithm = @math.log10(majorstep)
      let mantissa = @math.pow(10.0, @tick_grid.divmod(logarithm, 1.0).1)
      let mut close = false
      for reference in [1.0, 2.5, 5.0, 10.0] {
        if Double::abs(mantissa - reference) <= 1.0e-8 + 1.0e-5 * reference {
          close = true
        }
      }
      if close {
        5.0
      } else {
        4.0
      }
    }
    Text(_) =>
      raise @core.DataError::InvalidTicks(
        message="AutoMinor subdivisions text is not the exact auto token",
      )
    Number(value) => value
    Integer(value) => {
      let converted = index_round_binary(value, 0)
      if converted.is_inf() {
        raise @core.DataError::InvalidTicks(
          message="AutoMinor integer subdivisions are too large to convert to float",
        )
      }
      converted
    }
  }
  majorstep / divisions
}

///|
// Python round(x) returns an arbitrary-precision integer, ties to even.
fn auto_minor_round(value : Double) -> @bigint.BigInt raise @core.DataError {
  if value.is_nan() || value.is_inf() {
    raise @core.DataError::InvalidTicks(
      message="AutoMinor grid cannot round a non-finite phase",
    )
  }
  let (integer, exponent) = index_binary_parts(value)
  if exponent >= 0 {
    return integer << exponent
  }
  let negative = integer < @bigint.BigInt::from_int(0)
  let magnitude = if negative { integer.neg() } else { integer }
  let quotient = magnitude >> -exponent
  let remainder = magnitude - (quotient << -exponent)
  let half = @bigint.BigInt::from_int(1) << (-exponent - 1)
  let rounded = if remainder > half ||
    (remainder == half && (quotient.to_uint64() & 1UL) != 0UL) {
    quotient + @bigint.BigInt::from_int(1)
  } else {
    quotient
  }
  if negative {
    rounded.neg()
  } else {
    rounded
  }
}

///|
fn auto_minor_grid(
  unique : TickValues,
  minorstep : Double,
  lower : Double,
  upper : Double,
) -> Array[Double] raise @core.DataError {
  // Python sorted preserves incoming NaN order: it swaps only on upper= 1152921504606846976.0 {
    raise @core.DataError::InvalidTicks(
      message="AutoMinor grid array exceeds supported size",
    )
  }
  if count <= 0.0 {
    return []
  }
  let length = Double::min(count, 100000.0).to_int()
  let int_min = @bigint.BigInt::from_int64(-9223372036854775808L)
  let int_max = @bigint.BigInt::from_int64(9223372036854775807L)
  let uint_max = @bigint.BigInt::from_uint64(18446744073709551615UL)
  let integer_dtype = first >= int_min &&
    first <= int_max &&
    stop >= int_min &&
    stop <= int_max
  let float_dtype = !integer_dtype &&
    first >= int_min &&
    first <= uint_max &&
    stop >= int_min &&
    stop <= uint_max
  let start_float = index_round_binary(first, 0)
  let second_float = index_round_binary(first + @bigint.BigInt::from_int(1), 0)
  let float_step = second_float - start_float
  Array::makei(length, index => {
    let phase = if float_dtype {
      if index == 0 {
        start_float
      } else if index == 1 {
        second_float
      } else {
        numeric_arange_fill(start_float, float_step, index.to_double())
      }
    } else {
      // Both int64 and object arange enumerate exact Python integers here.
      // The float multiplication converts each element separately.
      index_round_binary(first + @bigint.BigInt::from_int(index), 0)
    }
    let scaled = phase * minorstep
    scaled + origin
  })
}