// Base64, for the `$bytes` representation.
//
// Encoding uses the STANDARD alphabet with the padding stripped, which is what
// the reference implementation emits and therefore what a byte-for-byte
// round-trip requires. Decoding accepts both the standard and URL-safe
// alphabets, with or without padding, because being liberal about what arrives
// costs nothing here and a server that pads is not wrong.

///|
const B64_ALPHABET : String = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

///|
let b64_digits : Array[Char] = B64_ALPHABET.iter().collect()

///|
/// The digit value of a base64 character, accepting both alphabets: `-` and `_`
/// are the URL-safe spellings of `+` and `/`.
fn b64_digit(unit : UInt16) -> Int? {
  let i = unit.to_int()
  if i >= 'A'.to_int() && i <= 'Z'.to_int() {
    Some(i - 'A'.to_int())
  } else if i >= 'a'.to_int() && i <= 'z'.to_int() {
    Some(i - 'a'.to_int() + 26)
  } else if i >= '0'.to_int() && i <= '9'.to_int() {
    Some(i - '0'.to_int() + 52)
  } else if i == '+'.to_int() || i == '-'.to_int() {
    Some(62)
  } else if i == '/'.to_int() || i == '_'.to_int() {
    Some(63)
  } else {
    None
  }
}

///|
pub fn base64_encode(bytes : Bytes) -> String {
  let b = StringBuilder::new(size_hint=(bytes.length() + 2) / 3 * 4)
  let mut i = 0
  while i + 2 < bytes.length() {
    let chunk = (bytes[i].to_int() << 16) |
      (bytes[i + 1].to_int() << 8) |
      bytes[i + 2].to_int()
    b.write_char(b64_digits[(chunk >> 18) & 0x3F])
    b.write_char(b64_digits[(chunk >> 12) & 0x3F])
    b.write_char(b64_digits[(chunk >> 6) & 0x3F])
    b.write_char(b64_digits[chunk & 0x3F])
    i = i + 3
  }
  // The tail, unpadded: two leftover bytes become three characters and one
  // becomes two, rather than four with `=` filling the gap.
  let left = bytes.length() - i
  if left == 1 {
    let chunk = bytes[i].to_int() << 16
    b.write_char(b64_digits[(chunk >> 18) & 0x3F])
    b.write_char(b64_digits[(chunk >> 12) & 0x3F])
  } else if left == 2 {
    let chunk = (bytes[i].to_int() << 16) | (bytes[i + 1].to_int() << 8)
    b.write_char(b64_digits[(chunk >> 18) & 0x3F])
    b.write_char(b64_digits[(chunk >> 12) & 0x3F])
    b.write_char(b64_digits[(chunk >> 6) & 0x3F])
  }
  b.to_string()
}

///|
/// `None` if the text is not base64. A lone trailing character is not: base64
/// has no encoding that produces one, so a 4n+1 length means truncation.
pub fn base64_decode(text : String) -> Bytes? {
  let out = []
  let mut buffer = 0
  let mut bits = 0
  let mut digits = 0
  for i = 0; i < text.length(); i = i + 1 {
    let unit = text[i]
    // Padding is accepted and contributes nothing; anything after it would be
    // caught by the length check below.
    if unit.to_int() == '='.to_int() {
      continue
    }
    guard b64_digit(unit) is Some(digit) else { return None }
    digits = digits + 1
    buffer = (buffer << 6) | digit
    bits = bits + 6
    if bits >= 8 {
      bits = bits - 8
      out.push(((buffer >> bits) & 0xFF).to_byte())
    }
  }
  // No encoding produces a group of one character: three bytes become four
  // characters, two become three and one becomes two, so a count of 4n+1 means
  // the text was truncated.
  guard digits % 4 != 1 else { return None }
  // Whatever is left must be zero-valued padding bits, and there must be fewer
  // than a whole byte of them.
  guard bits < 8 && (buffer & ((1 << bits) - 1)) == 0 else { return None }
  Some(Bytes::from_array(out))
}