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

///|
let magic_bytes : Array[Bytes] = [
  b"\x00\x00\x81\x23", b"\x00\xfa\xca\xde", b"\x1b\xad\xb0\x02", b"\x8b\xad\xf0\x0d",
  b"\xa5\xa5\xa5\xa5", b"\xa5", b"\xab\xab\xab\xab", b"\xab\xad\xba\xbe", b"\xab\xba\xba\xbe",
  b"\xab\xad\xca\xfe", b"\xb1\x6b\x00\xb5", b"\xba\xad\xf0\x0d", b"\xba\xaa\xaa\xad",
  b"\xba\xd2\x22\x22", b"\xba\xdb\xad\xba\xdb\xad", b"\xba\xdc\x0f\xfe\xe0\xdd\xf0\x0d",
  b"\xba\xdd\xca\xfe", b"\xbb\xad\xbe\xef", b"\xbe\xef\xca\xce", b"\xc0\x00\x10\xff",
  b"\xca\xfe\xba\xbe", b"\xca\xfe\xd0\x0d", b"\xca\xfe\xfe\xed", b"\xcc\xcc\xcc\xcc",
  b"\xcd\xcd\xcd\xcd", b"\x0d\x15\xea\x5e", b"\xdd\xdd\xdd\xdd", b"\xde\xad\x10\xcc",
  b"\xde\xad\xba\xbe", b"\xde\xad\xbe\xef", b"\xde\xad\xca\xfe", b"\xde\xad\xc0\xde",
  b"\xde\xad\xfa\x11", b"\xde\xad\xf0\x0d", b"\xde\xfe\xc8\xed", b"\xde\xad\xde\xad",
  b"\xeb\xeb\xeb\xeb", b"\xfa\xde\xde\xad", b"\xfd\xfd\xfd\xfd", b"\xfe\xe1\xde\xad",
  b"\xfe\xed\xfa\xce", b"\xfe\xee\xfe\xee",
]

///|
fn byte_replacements(width : Int) -> Array[Bytes] {
  let values = match width {
    1 => [b"\x00", b"\x01", b"\x7f", b"\x80", b"\xff"]
    2 =>
      [
        b"\x00\x00", b"\x01\x00", b"\x00\x01", b"\x7f\xff", b"\xff\x7f", b"\xfe\xff",
        b"\xff\xfe", b"\xff\xff",
      ]
    _ =>
      [
        b"\x00\x00\x00\x00", b"\x00\x00\x00\x01", b"\x01\x00\x00\x00", b"\x7f\xff\xff\xff",
        b"\xff\xff\xff\x7f", b"\xfe\xff\xff\xff", b"\xff\xff\xff\xfe", b"\xff\xff\xff\xff",
      ]
  }
  for item in magic_bytes {
    if item.length() == width {
      values.push(item)
    }
  }
  values
}

///|
/// Binary mutation candidates are materialized individually on indexed access.
/// Size/max_len adjust mutations only, matching upstream; default stays verbatim.
/// Padding is restricted to exactly one byte.
pub fn Field::binary(
  value : Bytes,
  size? : Int,
  max_len? : Int,
  padding? : Bytes = b"\x00",
  fuzzable? : Bool = true,
  fuzz_values? : Array[Bytes] = [],
) -> Field raise ModelError {
  guard padding.length() == 1 else {
    raise Invalid("binary padding must be one byte")
  }
  for limit in [size, max_len] {
    if limit is Some(n) && n < 0 {
      raise Invalid("negative byte length")
    }
  }
  // Count arithmetic and repeat lengths must fit Int even before execution limits.
  guard value.length() <= 20000000 else {
    raise Invalid("binary field too large")
  }
  let replacements = [
    byte_replacements(1),
    byte_replacements(2),
    byte_replacements(4),
  ]
  let widths = [1, 2, 4]
  let mut count = 12 + magic_bytes.length()
  for i, width in widths {
    count += (value.length() - width + 1).max(0) * replacements[i].length()
  }
  let adjust = fn(candidate : Bytes) {
    match size {
      Some(n) =>
        if candidate.length() > n {
          candidate[:n].to_owned()
        } else {
          candidate + padding.repeat(n - candidate.length())
        }
      None =>
        match max_len {
          Some(n) if candidate.length() > n => candidate[:n].to_owned()
          _ => candidate
        }
    }
  }
  let base : Field = {
    value,
    count: if fuzzable {
      count
    } else {
      0
    },
    candidate_length: index => {
      let raw = if index < 9 {
        [0, 1, 1, 10, 100, 1000, 5000, 10000, 100000][index]
      } else if index < 12 {
        value.length() * [2, 10, 100][index - 9]
      } else if index < 12 + magic_bytes.length() {
        magic_bytes[index - 12].length()
      } else {
        value.length()
      }
      match size {
        Some(n) => n.to_int64()
        None => max_len.map(n => raw.min(n)).unwrap_or(raw).to_int64()
      }
    },
    candidate: fn(index) {
      let raw = if index < 9 {
        match index {
          0 => b""
          1 => b"\x00"
          2 => b"\xff"
          _ => b"A".repeat([10, 100, 1000, 5000, 10000, 100000][index - 3])
        }
      } else if index < 12 {
        let raw_length = value.length() * [2, 10, 100][index - 9]
        let cap = size.unwrap_or(max_len.unwrap_or(raw_length))
        Bytes::makei(raw_length.min(cap), i => value[i % value.length()])
      } else if index < 12 + magic_bytes.length() {
        magic_bytes[index - 12]
      } else {
        for i = 0, offset = index - 12 - magic_bytes.length(); i < 3; {
          let library = replacements[i]
          let n = (value.length() - widths[i] + 1).max(0) * library.length()
          if offset < n {
            let position = offset / library.length()
            break value[:position].to_owned() +
              library[offset % library.length()] +
              value[position + widths[i]:].to_owned()
          }
          continue i + 1, offset - n
        } nobreak {
          value
        }
      }
      adjust(raw)
    },
  }
  Field::with_fuzz_values(base, fuzz_values, fuzzable)
}