///|
pub fn escape_header(value : String) -> String raise ProtocolError {
  escape_versioned(value, V12)
}

///|
fn escape_versioned(
  value : String,
  version : Version,
) -> String raise ProtocolError {
  let chars : Array[Char] = []
  for c in value {
    match c {
      '\u0000' => raise Invalid("NUL in header")
      '\\' => {
        chars.push('\\')
        chars.push('\\')
      }
      '\r' =>
        if version == V12 {
          chars.push('\\')
          chars.push('r')
        } else {
          chars.push('\r')
        }
      '\n' => {
        chars.push('\\')
        chars.push('n')
      }
      ':' => {
        chars.push('\\')
        chars.push('c')
      }
      _ => chars.push(c)
    }
  }
  String::from_array(chars)
}

///|
pub fn unescape_header(value : String) -> String raise ProtocolError {
  unescape_versioned(value, V12)
}

///|
fn unescape_versioned(
  value : String,
  version : Version,
) -> String raise ProtocolError {
  let chars : Array[Char] = []
  let mut escaped = false
  for c in value {
    if escaped {
      chars.push(
        match c {
          'r' =>
            if version == V12 {
              '\r'
            } else {
              raise Invalid("undefined 1.1 header escape")
            }
          'n' => '\n'
          'c' => ':'
          '\\' => '\\'
          _ => raise Invalid("undefined header escape")
        },
      )
      escaped = false
    } else if c == '\\' {
      escaped = true
    } else {
      if c == '\u0000' || (c == '\r' && version == V12) || c == '\n' || c == ':' {
        raise Invalid("raw delimiter in header")
      }
      chars.push(c)
    }
  }
  if escaped {
    raise Invalid("trailing header escape")
  }
  String::from_array(chars)
}

///|
fn raw_header(
  s : String,
  key : Bool,
  version~ : Version = V12,
) -> String raise ProtocolError {
  for c in s {
    if c == '\u0000' ||
      c == '\n' ||
      (c == '\r' && version == V12) ||
      (key && c == ':') {
      raise Invalid("invalid raw header")
    }
  }
  s
}

///|
/// Emits LF and a content-length for every nonempty body (including binary NUL).
pub fn encode(frame : Frame) -> Bytes raise ProtocolError {
  encode_versioned(frame, V12)
}

///|
/// Emits LF in all versions. 1.0 headers are literal; 1.1 CR is data, never a \r escape.
pub fn encode_versioned(
  frame : Frame,
  version : Version,
) -> Bytes raise ProtocolError {
  if version == V10 && (frame.command == "NACK" || frame.command == "STOMP") {
    raise Invalid("command unavailable in STOMP 1.0")
  }
  if !valid_command(frame.command) {
    raise Invalid("unknown command")
  }
  if !body_allowed(frame.command) && frame.body.length() != 0 {
    raise Invalid("body forbidden for command")
  }
  let mut text = frame.command + "\n"
  for pair in frame.headers {
    if pair.0.length() == 0 {
      raise Invalid("empty header name")
    }
    let key = if version == V10 || unescaped(frame.command) {
      raw_header(pair.0, true, version~)
    } else {
      escape_versioned(pair.0, version)
    }
    let value = if version == V10 || unescaped(frame.command) {
      raw_header(pair.1, false, version~)
    } else {
      escape_versioned(pair.1, version)
    }
    text = text + key + ":" + value + "\n"
  }
  match frame.header("content-length") {
    Some(n) =>
      if decimal(n, 2147483647) != frame.body.length() {
        raise Invalid("length mismatch")
      }
    None =>
      if frame.body.length() > 0 {
        text = text + "content-length:" + frame.body.length().to_string() + "\n"
      }
  }
  let bytes = @utf8.encode(text + "\n").to_array()
  for b in frame.body {
    bytes.push(b)
  }
  bytes.push(b'\x00')
  Bytes::from_array(bytes)
}