///|
fn log_grid_numpy_power(base : Double, exponent : Double) -> Double {
  if exponent == 0.0 || base == 1.0 || (base == -1.0 && exponent.is_inf()) {
    1.0
  } else if !base.is_nan() &&
    !base.is_inf() &&
    base != 0.0 &&
    !exponent.is_nan() &&
    !exponent.is_inf() &&
    exponent == exponent.floor() {
    log_grid_integer_power(base, exponent)
  } else {
    @math.pow(base, exponent)
  }
}

///|
fn log_decade_less_equal(value : Double, base : Double) -> Double {
  if value == 0.0 {
    value
  } else if value < 0.0 {
    -log_decade_greater_equal(-value, base)
  } else {
    log_grid_numpy_power(
      base,
      (log_grid_natural_log(value) / log_grid_natural_log(base)).floor(),
    )
  }
}

///|
fn log_decade_greater_equal(value : Double, base : Double) -> Double {
  if value == 0.0 {
    value
  } else if value < 0.0 {
    -log_decade_less_equal(-value, base)
  } else {
    log_grid_numpy_power(
      base,
      (log_grid_natural_log(value) / log_grid_natural_log(base)).ceil(),
    )
  }
}

///|
fn log_decade_less(value : Double, base : Double) -> Double {
  if value < 0.0 {
    return -log_decade_greater(-value, base)
  }
  let lower = log_decade_less_equal(value, base)
  if lower == value {
    lower / base
  } else {
    lower
  }
}

///|
fn log_decade_greater(value : Double, base : Double) -> Double {
  if value < 0.0 {
    return -log_decade_less(-value, base)
  }
  let upper = log_decade_greater_equal(value, base)
  if upper == value {
    upper * base
  } else {
    upper
  }
}

///|
fn log_grid_log(value : Double, base : Double) -> Double {
  if base == 10.0 {
    @math.log10(value)
  } else if base == 2.0 {
    @math.log2(value)
  } else {
    @math.ln(value) / @math.ln(base)
  }
}

///|
priv struct LogGridDecades {
  start : @bigint.BigInt
  step : @bigint.BigInt
  length : @bigint.BigInt
  // 0=int64, 1=float64, 2=object, 3=uint64. Whole shape decides dtype.
  dtype : Int32
  arange : Bool
}

///|
fn log_grid_array_dtype(low : @bigint.BigInt, high : @bigint.BigInt) -> Int32 {
  let min_int = @bigint.BigInt::from_int64(-9223372036854775808L)
  let max_int = @bigint.BigInt::from_int64(9223372036854775807L)
  let max_uint = @bigint.BigInt::from_uint64(18446744073709551615UL)
  if low < min_int || high > max_uint {
    2
  } else if high > max_int {
    if low <= max_int {
      1
    } else {
      3
    }
  } else {
    0
  }
}

///|
fn log_grid_decades(
  start : LogGridNumber,
  stop : LogGridNumber,
  step : LogGridNumber,
  arange : Bool,
) -> LogGridDecades raise @core.DataError {
  let start = start.integer()
  let stop = stop.integer()
  let step = step.integer()
  if step.is_zero() {
    raise log_grid_error("log decade range step is zero")
  }
  let zero = @bigint.BigInt::from_int(0)
  let distance = stop - start
  let length = if arange {
    let ratio = log_grid_integer_ratio(distance, step)
    if ratio < -9223372036854775808.0 || ratio >= 1152921504606846976.0 {
      raise log_grid_error("log decade arange exceeds array size")
    }
    if ratio <= 0.0 {
      zero
    } else {
      auto_minor_round(ratio.ceil())
    }
  } else if (distance > zero && step > zero) || (distance < zero && step < zero) {
    let a = if distance < zero { distance.neg() } else { distance }
    let b = if step < zero { step.neg() } else { step }
    (a + b - @bigint.BigInt::from_int(1)) / b
  } else {
    zero
  }
  let dtype = if arange {
    let low = if start < stop {
      if start < step {
        start
      } else {
        step
      }
    } else if stop < step {
      stop
    } else {
      step
    }
    let high = if start > stop {
      if start > step {
        start
      } else {
        step
      }
    } else if stop > step {
      stop
    } else {
      step
    }
    let inferred = log_grid_array_dtype(low, high)
    if inferred == 3 {
      1
    } else {
      inferred
    }
  } else if length.is_zero() {
    1
  } else {
    let last = start + (length - @bigint.BigInt::from_int(1)) * step
    log_grid_array_dtype(
      if start < last {
        start
      } else {
        last
      },
      if start > last {
        start
      } else {
        last
      },
    )
  }
  { start, step, length, dtype, arange, }
}

///|
fn LogGridDecades::phase(self : LogGridDecades, index : Int32) -> Double {
  if self.arange && self.dtype == 1 {
    let first = index_round_binary(self.start, 0)
    let second = index_round_binary(self.start + self.step, 0)
    if index == 0 {
      first
    } else if index == 1 {
      second
    } else {
      numeric_arange_fill(first, second - first, index.to_double())
    }
  } else {
    index_round_binary(
      self.start + @bigint.BigInt::from_int(index) * self.step,
      0,
    )
  }
}

///|
fn log_grid_python_power(
  base : Double,
  exponent : Double,
) -> Double raise @core.DataError {
  if base == 0.0 && exponent < 0.0 {
    raise log_grid_error("log grid zero cannot be raised to a negative power")
  }
  let value = log_grid_numpy_power(base, exponent)
  if !base.is_inf() && !base.is_nan() && value.is_inf() {
    raise log_grid_error("log grid Python power overflows")
  }
  value
}

///|
fn log_grid_major_ticks(
  decades : LogGridDecades,
  base : Double,
) -> Array[Double] raise @core.DataError {
  if decades.length >= @bigint.BigInt::from_int(1) << 60 {
    raise log_grid_error("log grid decade array exceeds supported size")
  }
  if decades.length.is_zero() {
    return []
  }
  // Object power executes Python float power for every exponent. Validate both
  // ends before materializing a prefix, so an overflow beyond the cap survives.
  if decades.dtype == 2 {
    ignore(log_grid_python_power(base, index_round_binary(decades.start, 0)))
    let last = decades.start +
      (decades.length - @bigint.BigInt::from_int(1)) * decades.step
    ignore(log_grid_python_power(base, index_round_binary(last, 0)))
  }
  let length = if decades.length > @bigint.BigInt::from_int(100000) {
    100000
  } else {
    decades.length.to_int()
  }
  let cached_phase = [decades.phase(0).reinterpret_as_uint64()]
  let first = if decades.dtype == 2 {
    log_grid_python_power(base, decades.phase(0))
  } else {
    log_grid_numpy_power(base, decades.phase(0))
  }
  let cached_power = [first]
  Array::makei(length, index => {
    let exponent = decades.phase(index)
    if exponent.reinterpret_as_uint64() != cached_phase[0] {
      cached_phase[0] = exponent.reinterpret_as_uint64()
      cached_power[0] = if decades.dtype == 2 {
        log_grid_python_power(base, exponent)
      } else {
        log_grid_numpy_power(base, exponent)
      }
    }
    cached_power[0]
  })
}

///|
priv struct LogGridSubs {
  values : TickValues?
  first : Double
  length : Int64
}

///|
fn LogGridSubs::get(
  self : LogGridSubs,
  index : Int64,
) -> Double raise @core.DataError {
  match self.values {
    Some(values) => values.get(index.to_int())
    None => self.first + index.to_double()
  }
}

///|
fn LogGridSubs::visible_count(
  self : LogGridSubs,
  power : Double,
  lower : Double,
  upper : Double,
  stop_after : Int32,
) -> Int32 raise @core.DataError {
  match self.values {
    Some(values) => {
      let mut count = 0
      for index in 0.. {
      if power.is_nan() || power <= 0.0 || power.is_inf() {
        return 0
      }
      let boundary = fn(target : Double, strict : Bool) {
        let mut first = 0L
        let mut last = self.length
        while first < last {
          let middle = first + (last - first) / 2L
          let value = (self.first + middle.to_double()) * power
          let after = if strict { value > target } else { value >= target }
          if after {
            last = middle
          } else {
            first = middle + 1L
          }
        }
        first
      }
      Int64::min(
        boundary(upper, true) - boundary(lower, false),
        stop_after.to_int64(),
      ).to_int()
    }
  }
}

///|
fn named_log_locator_ticks(
  locator : LogLocator,
  lower : Double,
  upper : Double,
) -> Array[Double] raise @core.DataError {
  let zero = log_grid_integer(0)
  let one = log_grid_integer(1)
  let mut requested = match locator.numticks {
    Integer(value) => LogGridNumber::PythonInteger(value)
    Number(value) => PythonFloat(value)
    Text("auto") =>
      if locator.is_bound() {
        match locator.tick_space_provider {
          Some(provider) => log_grid_space(provider())
          None =>
            raise log_grid_error("bound log locator has no tick-space provider")
        }
      } else {
        log_grid_integer(9)
      }
    Text(_) => Text
  }
  let base = locator.base
  let mut lower = lower
  let mut upper = upper
  if lower <= 0.0 {
    if locator.is_bound() {
      lower = match locator.minpos_provider {
        Some(provider) => provider()
        None =>
          raise log_grid_error(
            "bound log locator has no minimum-positive provider",
          )
      }
    }
    if lower <= 0.0 || lower.is_nan() || lower.is_inf() {
      raise log_grid_error(
        "data cannot be log-scaled because all values are nonpositive",
      )
    }
  }
  if upper < lower {
    let previous = lower
    lower = upper
    upper = previous
  }
  let efmin = log_grid_log(lower, base)
  let efmax = log_grid_log(upper, base)
  let emin = LogGridNumber::PythonInteger(auto_minor_round(efmin.ceil()))
  let emax = LogGridNumber::PythonInteger(auto_minor_round(efmax.floor()))
  let available = log_grid_op(log_grid_op(emax, emin, 1), one, 0)
  let subs = match locator.subs {
    Values(values) =>
      LogGridSubs::{
        values: Some(values),
        first: 0.0,
        length: values.length().to_int64(),
      }
    Text(value) =>
      if log_grid_compare(available, log_grid_integer(10)) is Some(0 | 1) ||
        base < 3.0 {
        if value == "auto" {
          return []
        }
        LogGridSubs::{ values: None, first: 1.0, length: 1L, }
      } else {
        let first = if value == "auto" { 2.0 } else { 1.0 }
        let count = (base - first).ceil()
        if count.is_nan() ||
          count.is_inf() ||
          count < -9223372036854775808.0 ||
          count >= 1152921504606846976.0 {
          raise log_grid_error(
            "log automatic subs arange exceeds supported size",
          )
        }
        LogGridSubs::{
          values: None,
          first,
          length: if count <= 0.0 {
            0L
          } else {
            count.to_int64()
          },
        }
      }
  }
  let mut stride : LogGridNumber = one
  let decades = if @config.classic_mode() {
    stride = log_grid_max(
      log_grid_ceil(
        log_grid_divide(
          log_grid_op(available, one, 1),
          log_grid_op(requested, one, 1),
        ),
      ),
      one,
    )
    log_grid_decades(
      log_grid_op(emin, stride, 1),
      log_grid_op(log_grid_op(emax, stride, 0), one, 0),
      stride,
      true,
    )
  } else {
    stride = log_grid_op(
      log_grid_op(available, log_grid_op(requested, one, 0), 3),
      one,
      0,
    )
    let actual = log_grid_ceil(log_grid_divide(available, stride))
    if log_grid_compare(actual, requested) is Some(-1 | 0) {
      requested = actual
    } else if !(log_grid_compare(actual, log_grid_op(requested, one, 0))
      is Some(0)) {
      raise log_grid_error("log grid requested tick assertion failed")
    }
    if log_grid_compare(requested, zero) is Some(0) {
      let first = log_grid_op(emin, one, 1)
      let last = log_grid_op(emax, one, 0)
      stride = log_grid_op(last, first, 1)
      // The source uses a list here, rather than range with this stride.
      LogGridDecades::{
        start: first.integer(),
        step: stride.integer(),
        length: @bigint.BigInt::from_int(2),
        dtype: log_grid_array_dtype(first.integer(), last.integer()),
        arange: false,
      }
    } else if log_grid_compare(requested, one) is Some(0) {
      let mid = LogGridNumber::PythonInteger(
        auto_minor_round((efmin + efmax) / 2.0),
      )
      stride = log_grid_max(
        log_grid_op(mid, log_grid_op(emin, one, 1), 1),
        log_grid_op(log_grid_op(emax, one, 0), mid, 1),
      )
      let first = log_grid_op(mid, stride, 1).integer()
      let last = log_grid_op(mid, stride, 0).integer()
      LogGridDecades::{
        start: first,
        step: stride.integer(),
        length: @bigint.BigInt::from_int(3),
        dtype: log_grid_array_dtype(
          if first < last {
            first
          } else {
            last
          },
          if first > last {
            first
          } else {
            last
          },
        ),
        arange: false,
      }
    } else {
      stride = log_grid_op(
        log_grid_op(available, one, 1),
        log_grid_op(requested, one, 1),
        3,
      )
      if log_grid_compare(stride, log_grid_divide(available, requested))
        is Some(-1) {
        stride = log_grid_op(available, requested, 3)
      }
      let low = log_grid_max(
        log_grid_op(available, log_grid_op(stride, requested, 2), 1),
        zero,
      )
      let high = log_grid_min(
        log_grid_op(
          available,
          log_grid_op(stride, log_grid_op(requested, one, 1), 2),
          1,
        ),
        stride,
      )
      let mut offset = log_grid_op(log_grid_op(zero, emin, 1), stride, 4)
      if !(log_grid_compare(low, offset) is Some(-1 | 0) &&
        log_grid_compare(offset, high) is Some(-1)) {
        offset = low
      }
      log_grid_decades(
        log_grid_op(log_grid_op(emin, offset, 0), stride, 1),
        log_grid_op(log_grid_op(emax, stride, 0), one, 0),
        stride,
        false,
      )
    }
  }
  let is_minor = subs.length > 1L || (subs.length == 1L && subs.get(0L) != 1.0)
  if !is_minor {
    return log_grid_major_ticks(decades, base)
  }
  let stride_one = log_grid_compare(stride, one) is Some(0)
  let mut first_exponent = @bigint.BigInt::from_int(0)
  let mut minor_decades = @bigint.BigInt::from_int(0)
  if stride_one || log_grid_compare(available, one) is Some(-1 | 0) {
    first_exponent = log_grid_op(emin, one, 1).integer()
    minor_decades = log_grid_op(available, one, 0).integer()
    if minor_decades <= @bigint.BigInt::from_int(0) {
      raise log_grid_error("log minor concatenate needs at least one array")
    }
    ignore(log_grid_python_power(base, index_round_binary(first_exponent, 0)))
    ignore(
      log_grid_python_power(
        base,
        index_round_binary(
          first_exponent + minor_decades - @bigint.BigInt::from_int(1),
          0,
        ),
      ),
    )
  }
  let full_count = minor_decades * @bigint.BigInt::from_int64(subs.length)
  if full_count >= @bigint.BigInt::from_int(1) << 60 {
    raise log_grid_error("log minor array exceeds supported size")
  }
  // The source decides fallback using the entire grid. A prefix can contain
  // no visible tick while a later subdivision supplies the deciding one.
  if subs.length > 1L && stride_one {
    let major_count = decades.length - @bigint.BigInt::from_int(2)
    if major_count <= @bigint.BigInt::from_int(1) {
      let mut visible = @bigint.BigInt::from_int(0)
      let mut decade = @bigint.BigInt::from_int(0)
      while decade < minor_decades &&
            major_count + visible <= @bigint.BigInt::from_int(1) {
        let power = log_grid_python_power(
          base,
          index_round_binary(first_exponent + decade, 0),
        )
        let needed = (@bigint.BigInt::from_int(2) - major_count - visible).to_int()
        visible += @bigint.BigInt::from_int(
          subs.visible_count(power, lower, upper, needed),
        )
        decade += @bigint.BigInt::from_int(1)
      }
      if major_count + visible <= @bigint.BigInt::from_int(1) {
        return AutoLocator::new().tick_values(lower, upper)
      }
    }
  }
  let length = if full_count > @bigint.BigInt::from_int(100000) {
    100000
  } else {
    full_count.to_int()
  }
  let cached_decade = [-1L]
  let cached_power = [0.0]
  Array::makei(length, index => {
    let decade = index.to_int64() / subs.length
    let position = index.to_int64() % subs.length
    if decade != cached_decade[0] {
      cached_decade[0] = decade
      cached_power[0] = log_grid_python_power(
        base,
        index_round_binary(
          first_exponent + @bigint.BigInt::from_int64(decade),
          0,
        ),
      )
    }
    subs.get(position) * cached_power[0]
  })
}