///|
/// Quoted-Printable encoding (RFC 2045 section 6.7), implemented from scratch.
///
/// Rules applied:
///   - Tab and space must be encoded when at the end of a line.
///   - `=` must always be encoded as `=3D`.
///   - Printable ASCII 33..126 except `=` is passed through.
///   - All other bytes (control and >127) are encoded as `=XX`.
///   - Lines are limited to 76 characters including any trailing soft break
///     (MM_ENCOD_001); a soft break is a trailing `=`.
///   - Line endings are CRLF.
pub fn qp_encode(input : String) -> String {
  let src = normalize_crlf(input)
  let bytes = string_to_bytes(src)
  let out = FixedArray::make(bytes.length() * 4 + 8, b'\x00')
  let mut j = 0
  let mut line_col = 0
  let mut i = 0
  while i < bytes.length() {
    let b = bytes[i]
    if b == b'\r' {
      // line terminator: emit CRLF, reset column.
      out[j] = b'\r'
      out[j + 1] = b'\n'
      j += 2
      line_col = 0
      i += 2
      continue
    }
    let encoded = needs_encoding(bytes, i)
    // if the encoded form does not fit in 76 columns (reserving one character
    // for a possible trailing `=` soft break), emit a soft break
    let token_len = if encoded { 3 } else { 1 }
    if line_col + token_len > 75 {
      out[j] = b'='
      out[j + 1] = b'\r'
      out[j + 2] = b'\n'
      j += 3
      line_col = 0
    }
    if encoded {
      let v = b.to_int()
      out[j] = b'='
      out[j + 1] = to_hex_upper(v >> 4)
      out[j + 2] = to_hex_upper(v & 0xf)
      j += 3
      line_col += 3
    } else {
      out[j] = b
      j += 1
      line_col += 1
    }
    i += 1
  }
  bytes_to_string(Bytes::from_iter(out.iter())[0:j].to_owned())
}

///|
/// A byte must be encoded when it is `=`, a control byte (<33), a byte >=127,
/// or a space/tab that sits at the end of a line (before CRLF or EOF).
fn needs_encoding(bytes : Bytes, i : Int) -> Bool {
  let b = bytes[i]
  if b == b'=' {
    return true
  }
  let c = b.to_int()
  if c < 33 || c > 126 {
    return true
  }
  if (b == b' ' || b == b'\t') &&
    (i + 1 >= bytes.length() || bytes[i + 1] == b'\r') {
    return true
  }
  false
}

///|
/// Decode Quoted-Printable text back to a string. Soft line breaks (`=\r\n`)
/// are removed; `=XX` escapes are decoded. Raises on malformed input.
pub fn qp_decode(input : String) -> String raise MailFailure {
  let bytes = string_to_bytes(input)
  let out = FixedArray::make(bytes.length(), b'\x00')
  let mut j = 0
  let mut i = 0
  while i < bytes.length() {
    let b = bytes[i]
    if b == b'=' {
      guard i + 2 < bytes.length() else {
        raise MailFailure::encode("dangling '=' in quoted-printable")
      }
      if bytes[i + 1] == b'\r' && bytes[i + 2] == b'\n' {
        // soft line break: skip
        i += 3
        continue
      }
      if bytes[i + 1] == b'\n' {
        i += 2
        continue
      }
      let hi = hex_nibble(bytes[i + 1])
      let lo = hex_nibble(bytes[i + 2])
      out[j] = ((hi << 4) | lo).to_byte()
      j += 1
      i += 3
    } else if b == b'\r' && i + 1 < bytes.length() && bytes[i + 1] == b'\n' {
      out[j] = b'\r'
      out[j + 1] = b'\n'
      j += 2
      i += 2
    } else {
      out[j] = b
      j += 1
      i += 1
    }
  }
  bytes_to_string(Bytes::from_iter(out.iter())[0:j].to_owned())
}

///|
fn hex_nibble(b : Byte) -> Int raise MailFailure {
  let c = b.to_int()
  if c >= 0x30 && c <= 0x39 {
    return c - 0x30
  }
  if c >= 0x41 && c <= 0x46 {
    return c - 0x41 + 10
  }
  if c >= 0x61 && c <= 0x66 {
    return c - 0x61 + 10
  }
  raise MailFailure::encode("invalid hex in quoted-printable")
}