///|
/// Normalize every line ending to CRLF: `\r\n` stays, a lone `\r` becomes
/// `\r\n`, and a lone `\n` becomes `\r\n` (MM_CRLF_001).
pub fn normalize_crlf(s : String) -> String {
  let b = string_to_bytes(s)
  let out = FixedArray::make(b.length() * 2, b'\x00')
  let mut i = 0
  let mut j = 0
  while i < b.length() {
    if b[i] == b'\r' {
      if i + 1 < b.length() && b[i + 1] == b'\n' {
        out[j] = b'\r'
        out[j + 1] = b'\n'
        i += 2
        j += 2
      } else {
        out[j] = b'\r'
        out[j + 1] = b'\n'
        i += 1
        j += 2
      }
    } else if b[i] == b'\n' {
      out[j] = b'\r'
      out[j + 1] = b'\n'
      i += 1
      j += 2
    } else {
      out[j] = b[i]
      i += 1
      j += 1
    }
  }
  bytes_to_string(Bytes::from_iter(out.iter())[0:j].to_owned())
}

///|
/// Split text into lines on `\n`, stripping a trailing `\r`. A trailing
/// newline does not produce an empty final line.
pub fn to_lines(s : String) -> Array[String] {
  let lines = []
  let b = string_to_bytes(s)
  let mut start = 0
  let mut saw_newline = false
  for i = 0; i < b.length(); i = i + 1 {
    if b[i] == b'\n' {
      saw_newline = true
      let end = if i > 0 && b[i - 1] == b'\r' { i - 1 } else { i }
      lines.push(bytes_to_string(b[start:end].to_owned()))
      start = i + 1
    }
  }
  if start < b.length() || (start == 0 && b.length() == 0) {
    lines.push(bytes_to_string(b[start:b.length()].to_owned()))
  } else if start == b.length() && !saw_newline {
    lines.push("")
  }
  lines
}

///|
/// Join lines with CRLF.
pub fn join_crlf(lines : Array[String]) -> String {
  let buf = Buffer::Buffer()
  for i, line in lines {
    if i > 0 {
      buf.write_string_utf16le("\r\n")
    }
    buf.write_string_utf16le(line)
  }
  buf.to_string()
}

///|
/// Dot-stuff every line of `s`: a line starting with `.` gets a leading `.`
/// prepended (MM_DOT_001). Operates on the already CRLF-normalized text.
pub fn dot_stuff(s : String) -> String {
  let b = string_to_bytes(s)
  let out = FixedArray::make(b.length() + 8, b'\x00')
  let mut j = 0
  let mut at_line_start = true
  for i = 0; i < b.length(); i = i + 1 {
    if at_line_start && b[i] == b'.' {
      out[j] = b'.'
      j += 1
    }
    out[j] = b[i]
    j += 1
    at_line_start = b[i] == b'\n'
  }
  bytes_to_string(Bytes::from_iter(out.iter())[0:j].to_owned())
}

///|
/// Check that every line of `s` (split on LF) is at most `max` bytes. Used to
/// enforce the RFC 5322 998-octet hard limit before transmission (MM_ENCOD_005).
pub fn check_line_lengths(s : String, max : Int) -> Unit raise MailFailure {
  let lines = to_lines(s)
  for line in lines {
    if string_to_bytes(line).length() > max {
      raise MailFailure::content_length(
        "line length \{string_to_bytes(line).length()} exceeds limit \{max}",
      )
    }
  }
}