///|
fn flush_mailbox(bytes : Array[Byte], out : StringBuilder) -> Unit {
  if !bytes.is_empty() {
    out.write_string(
      "&" +
      @base64.encode(Bytes::from_array(bytes), padding=false).replace_all(
        old="/",
        new=",",
      ) +
      "-",
    )
    bytes.clear()
  }
}

///|
/// RFC 3501 modified UTF-7, for mailbox names before UTF8=ACCEPT negotiation.
pub fn encode_mailbox(name : String) -> String raise ImapError {
  if name.length() > 4096 ||
    name.contains("\u0000") ||
    name.contains("\r") ||
    name.contains("\n") {
    raise Invalid("invalid mailbox name")
  }
  let out = StringBuilder()
  let bytes : Array[Byte] = []
  for c in name.iter() {
    let n = c.to_int()
    if n >= 32 && n <= 126 {
      flush_mailbox(bytes, out)
      if c == '&' {
        out.write_string("&-")
      } else {
        out.write_char(c)
      }
    } else if n < 65536 {
      bytes.push((n >> 8).to_byte())
      bytes.push(n.to_byte())
    } else {
      let high = 55296 + ((n - 65536) >> 10)
      let low = 56320 + ((n - 65536) & 1023)
      bytes.push((high >> 8).to_byte())
      bytes.push(high.to_byte())
      bytes.push((low >> 8).to_byte())
      bytes.push(low.to_byte())
    }
  }
  flush_mailbox(bytes, out)
  out.to_string()
}

///|
pub fn decode_mailbox(encoded : String) -> String raise ImapError {
  if encoded.length() > 16384 ||
    !encoded.iter().all(c => c.to_int() >= 32 && c.to_int() <= 126) {
    raise Invalid("invalid modified UTF-7")
  }
  let out = StringBuilder()
  let chars = encoded.to_array()
  let mut pos = 0
  while pos < chars.length() {
    if chars[pos] != '&' {
      out.write_char(chars[pos])
      pos += 1
      continue
    }
    pos += 1
    let start = pos
    while pos < chars.length() && chars[pos] != '-' {
      pos += 1
    }
    if pos == chars.length() {
      raise Invalid("unterminated modified UTF-7")
    }
    if pos == start {
      out.write_char('&')
      pos += 1
      continue
    }
    let segment = String::from_array(chars[start:pos])
    if !segment
      .iter()
      .all(c => {
        (c >= 'a' && c <= 'z') ||
        (c >= 'A' && c <= 'Z') ||
        (c >= '0' && c <= '9') ||
        c == '+' ||
        c == ','
      }) {
      raise Invalid("invalid modified base64")
    }
    let mut normal = segment.replace_all(old=",", new="/")
    while normal.length() % 4 != 0 {
      normal += "="
    }
    let bytes = @base64.decode(normal) catch {
      _ => raise Invalid("invalid modified base64")
    }
    if bytes.length() % 2 != 0 {
      raise Invalid("odd UTF-16 length")
    }
    let mut i = 0
    while i < bytes.length() {
      let mut cp = bytes[i].to_int() * 256 + bytes[i + 1].to_int()
      i += 2
      if cp >= 55296 && cp <= 56319 {
        if i + 1 >= bytes.length() {
          raise Invalid("incomplete surrogate pair")
        }
        let low = bytes[i].to_int() * 256 + bytes[i + 1].to_int()
        if low < 56320 || low > 57343 {
          raise Invalid("invalid surrogate pair")
        }
        cp = 65536 + ((cp - 55296) << 10) + low - 56320
        i += 2
      }
      let c = match cp.to_char() {
        Some(c) => c
        None => raise Invalid("invalid Unicode scalar")
      }
      out.write_char(c)
    }
    pos += 1
  }
  let decoded = out.to_string()
  if encode_mailbox(decoded) != encoded {
    raise Invalid("noncanonical modified UTF-7")
  }
  decoded
}