// Bytes in failure messages
//
// `Debug` shows `Bytes` as ``. Failure messages
// show a hex dump instead, as `hexdump -C` does, with 16 bytes per line.

///|
/// The value of a hex digit.
fn hex_digit(c : Char) -> Int? {
  match c {
    '0'..='9' => Some(c.to_int() - '0'.to_int())
    'a'..='f' => Some(c.to_int() - 'a'.to_int() + 10)
    'A'..='F' => Some(c.to_int() - 'A'.to_int() + 10)
    _ => None
  }
}

///|
/// The bytes in `Debug` text of `Bytes` or `BytesView`. Return `None` for
/// other text.
fn bytes_values(text : String) -> Array[Int]? {
  // Long values break over several lines.
  let text = if text.contains("\n") {
    String::from_iter(text.iter().filter(c => c != '\n' && c != ' ')).replace_all(
      old=":[",
      new=": [",
    )
  } else {
    text
  }
  let prefix = if text.has_prefix("") else { return None }
  let body = text.view(
    start_offset=prefix.length(),
    end_offset=text.length() - 2,
  )
  if body.is_empty() {
    return Some([])
  }
  let values = []
  for part in body.split(",") {
    let part = part.trim_start(chars=" ")
    if part.is_empty() {
      continue
    }
    guard part is [.. "0x", high, low] &&
      hex_digit(high) is Some(h) &&
      hex_digit(low) is Some(l) else {
      return None
    }
    values.push(h * 16 + l)
  }
  Some(values)
}

///|
/// A byte as two lower-case hex digits.
fn hex_byte(value : Int) -> String {
  let digits = "0123456789abcdef"
  "\{digits[value / 16:value / 16 + 1]}\{digits[value % 16:value % 16 + 1]}"
}

///|
/// An offset as at least two lower-case hex digits.
fn hex_offset(offset : Int) -> String {
  let text = offset.to_string(radix=16)
  if text.length() < 2 {
    "0" + text
  } else {
    text
  }
}

///|
/// A hex dump of `bytes`, 16 bytes per line, with offsets and ASCII text.
fn hex_dump(bytes : Array[Int]) -> String {
  let lines = []
  for start = 0; start < bytes.length(); start = start + 16 {
    let line = StringBuilder()
    let offset = start.to_string(radix=16)
    line.write_string("0".repeat(8 - offset.length()) + offset)
    line.write_string("  ")
    let text = StringBuilder()
    for i in 0..<16 {
      match bytes.get(start + i) {
        Some(value) => {
          line.write_string(hex_byte(value) + " ")
          text.write_char(
            if value >= 0x20 && value < 0x7f {
              Int::unsafe_to_char(value)
            } else {
              '.'
            },
          )
        }
        None => line.write_string("   ")
      }
      if i == 7 {
        line.write_string(" ")
      }
    }
    line.write_string(" |\{text}|")
    lines.push(line.to_string())
  }
  lines.join("\n")
}

///|
/// The first differing offset of two `Bytes` values in `Debug` text. Return
/// `None` when the text is not two `Bytes` values, or when they are the
/// same.
fn bytes_mismatch(expected : String, actual : String) -> String? {
  guard bytes_values(expected) is Some(expected_bytes) &&
    bytes_values(actual) is Some(actual_bytes) else {
    return None
  }
  let longest = if expected_bytes.length() > actual_bytes.length() {
    expected_bytes.length()
  } else {
    actual_bytes.length()
  }
  for i in 0.. {
        match value {
          Some(v) => "0x\{hex_byte(v)}"
          None => "nothing"
        }
      }
      return Some(
        "offset \{i} (0x\{hex_offset(i)}): expected \{describe(want)}, received \{describe(got)}",
      )
    }
  }
  None
}