// SPDX-License-Identifier: GPL-2.0-only
// Adapted from boofuzz/primitives/bit_field.py at 518c13904fc32e7f2cc88c9dec934e509062953e.

///|
pub(all) enum Endian {
  Little
  Big
} derive(Debug, Eq)

///|
pub extend Endian with @moonbitlang/core/debug.Debug::{to_repr}

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

///|
fn integer_max(width : Int) -> UInt64 raise ModelError {
  guard width == 8 || width == 16 || width == 32 || width == 64 else {
    raise Invalid("integer width must be 8, 16, 32 or 64")
  }
  if width == 64 {
    0xffffffffffffffffUL
  } else {
    (1UL << width) - 1UL
  }
}

///|
fn encode_integer(value : UInt64, width : Int, endian : Endian) -> Bytes {
  Bytes::makei(width / 8, fn(i) {
    let shift = match endian {
      Little => i * 8
      Big => width - 8 - i * 8
    }
    ((value >> shift) & 255UL).to_byte()
  })
}

///|
/// Upstream bit_field.py's sorted, deduplicated "interesting boundary"
/// candidates for the full range [0, maximum]. `maximum` is 2^width - 1;
/// the last upstream boundary 2^width contributes its ten lower neighbors.
fn integer_boundaries(maximum : UInt64) -> Array[UInt64] {
  let candidates : Array[UInt64] = []
  let boundaries : Array[UInt64] = [0UL]
  // Compute floor(2^width / divisor) without ever representing 2^64.
  for divisor in [32UL, 16UL, 8UL, 4UL, 3UL, 2UL] {
    let carry = if maximum % divisor == divisor - 1UL { 1UL } else { 0UL }
    boundaries.push(maximum / divisor + carry)
  }
  for boundary in boundaries {
    for delta in -10..<10 {
      let magnitude = (if delta < 0 { -delta } else { delta }).to_uint64()
      if delta < 0 && boundary < magnitude {
        continue
      }
      if delta >= 0 && maximum - boundary < magnitude {
        continue
      }
      let candidate = if delta < 0 {
        boundary - magnitude
      } else {
        boundary + magnitude
      }
      if candidates.last().map(fn(last) { candidate > last }).unwrap_or(true) {
        candidates.push(candidate)
      }
    }
  }
  // Last upstream boundary is 2^width, and only its ten lower neighbors fit.
  for delta in 0..<10 {
    let candidate = maximum - (9 - delta).to_uint64()
    if candidates.last().map(fn(last) { candidate > last }).unwrap_or(true) {
      candidates.push(candidate)
    }
  }
  candidates
}

///|
/// Unsigned binary integers with upstream's sorted, deduplicated +/-10 boundaries.
/// Width is in bits. ASCII, full_range and custom max_num are not supported.
pub fn Field::integer(
  value : UInt64,
  width? : Int = 8,
  endian? : Endian = Little,
  fuzzable? : Bool = true,
  fuzz_values? : Array[Bytes] = [],
) -> Field raise ModelError {
  let maximum = integer_max(width)
  guard value <= maximum else { raise Invalid("integer default out of range") }
  let candidates = integer_boundaries(maximum)
  let base : Field = {
    value: encode_integer(value, width, endian),
    count: if fuzzable {
      candidates.length()
    } else {
      0
    },
    candidate: fn(i) { encode_integer(candidates[i], width, endian) },
    candidate_length: _ => (width / 8).to_int64(),
  }
  Field::with_fuzz_values(base, fuzz_values, fuzzable)
}