///|
pub fn base64_encode(data : Bytes) -> String {
  let base64 = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
  let buf = StringBuilder::new()
  let len = data.length()
  let rem = len % 3
  for i = 0; i < len - rem; i = i + 3 {
    let b0 = data[i].to_int()
    let b1 = data[i + 1].to_int()
    let b2 = data[i + 2].to_int()
    let x0 = base64[(b0 & 0xFC) >> 2]
    let x1 = base64[((b0 & 0x03) << 4) | ((b1 & 0xF0) >> 4)]
    let x2 = base64[((b1 & 0x0F) << 2) | ((b2 & 0xC0) >> 6)]
    let x3 = base64[b2 & 0x3F]
    buf.write_char(x0.to_char())
    buf.write_char(x1.to_char())
    buf.write_char(x2.to_char())
    buf.write_char(x3.to_char())
  }
  if rem == 1 {
    let b0 = data[len - 1].to_int()
    let x0 = base64[(b0 & 0xFC) >> 2]
    let x1 = base64[(b0 & 0x03) << 4]
    buf.write_char(x0.to_char())
    buf.write_char(x1.to_char())
    buf.write_char('=')
    buf.write_char('=')
  } else if rem == 2 {
    let b0 = data[len - 2].to_int()
    let b1 = data[len - 1].to_int()
    let x0 = base64[(b0 & 0xFC) >> 2]
    let x1 = base64[((b0 & 0x03) << 4) | ((b1 & 0xF0) >> 4)]
    let x2 = base64[(b1 & 0x0F) << 2]
    buf.write_char(x0.to_char())
    buf.write_char(x1.to_char())
    buf.write_char(x2.to_char())
    buf.write_char('=')
  }
  buf.to_string()
}

///|
pub fn base64_decode(s : String) -> Bytes {
  let base64 = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
  let input_length = s.length()
  let output_length = input_length / 4 * 3 -
    (match s {
      [.., '=', '='] => 2
      [.., '='] => 1
      _ => 0
    })
  let buf = Array::new(capacity=output_length)
  let rev_table : Array[Byte] = Array::make(256, 0)
  for j = 0; j < 64; j = j + 1 {
    rev_table[base64[j].to_int()] = j.to_byte()
  }
  let get_char = fn(i : Int) -> Byte {
    if s.get_char(i).unwrap() == '=' {
      0
    } else {
      rev_table[s.get_char(i).unwrap().to_int()]
    }
  }
  for i = 0; i < input_length; i = i + 4 {
    let b0 = get_char(i)
    let b1 = get_char(i + 1)
    let b2 = get_char(i + 2)
    let b3 = get_char(i + 3)
    let triplet = (b0.to_int() << 18) |
      (b1.to_int() << 12) |
      (b2.to_int() << 6) |
      b3.to_int()
    for j = 2; j >= 0 && buf.length() < output_length; j = j - 1 {
      buf.push((triplet >> (j * 8)).to_byte())
    }
  }
  Bytes::from_array(buf)
}

///|
test "decode/encode" {
  let data = b"Hello, World!"
  let encoded = base64_encode(data)
  inspect(encoded, content="SGVsbG8sIFdvcmxkIQ==")
  let decoded = base64_decode(encoded)
  inspect(
    decoded,
    content=(
      #|b"Hello, World!"
    ),
  )
  @debug.assert_eq(data, decoded)
}

///|
test "decode/encode emoji" {
  let data = b"\xf0\x9f\x98\x80" // 😀
  let encoded = base64_encode(data)
  inspect(encoded, content="8J+YgA==")
  let decoded = base64_decode(encoded)
  inspect(
    decoded,
    content=(
      #|b"\xf0\x9f\x98\x80"
    ),
  )
  @debug.assert_eq(data, decoded)
}