///|
pub suberror IrcError {
  Invalid(String)
} derive(Debug)

///|
pub(all) struct Tag {
  key : String
  value : String?
} derive(Debug, Eq)

///|
pub(all) struct Message {
  tags : Array[Tag]
  prefix : String?
  command : String
  params : Array[String]
} derive(Debug, Eq)

///|
fn unescape(s : String) -> String {
  let mut out = ""
  let mut escaped = false
  for c in s.iter() {
    if escaped {
      out += match c {
        ':' => ";"
        's' => " "
        'r' => "\r"
        'n' => "\n"
        _ => c.to_string()
      }
      escaped = false
    } else if c == '\\' {
      escaped = true
    } else {
      out += c.to_string()
    }
  }
  out
}

///|
fn escape(s : String) -> String {
  let mut out = ""
  for c in s.iter() {
    out += match c {
      ';' => "\\:"
      ' ' => "\\s"
      '\\' => "\\\\"
      '\r' => "\\r"
      '\n' => "\\n"
      _ => c.to_string()
    }
  }
  out
}

///|
fn key_valid(s : String) -> Bool {
  if s == "" || s == "+" {
    return false
  }
  for c in s.iter() {
    if !(c >= 'a' && c <= 'z') &&
      !(c >= 'A' && c <= 'Z') &&
      !(c >= '0' && c <= '9') &&
      c != '-' &&
      c != '.' &&
      c != '/' &&
      c != '+' {
      return false
    }
  }
  true
}

///|
pub fn parse(line : String) -> Message raise IrcError {
  if !valid_unicode(line) {
    raise Invalid("ill-formed UTF-16 input")
  }
  let text = if line.has_suffix("\r\n") {
    line[:line.length() - 2].to_owned()
  } else {
    line
  }
  if text.contains("\r") || text.contains("\n") || text.contains("\u0000") {
    raise Invalid("forbidden line character")
  }
  let mut rest = text
  let tags : Array[Tag] = []
  if rest.has_prefix("@") {
    let end = match rest.find(" ") {
      Some(v) => v
      None => raise Invalid("tags without command")
    }
    if @utf8.encode(rest[:end]).length() > 4094 {
      raise Invalid("tag section too long")
    }
    for entry in rest[1:end].split(";") {
      let raw = entry.to_owned()
      let (key, value) = match raw.find("=") {
        Some(p) => (raw[:p].to_owned(), Some(unescape(raw[p + 1:].to_owned())))
        None => (raw, None)
      }
      if !key_valid(key) {
        raise Invalid("invalid tag key")
      }
      let tag : Tag = { key, value, }
      let mut replaced = false
      for i in 0.. 510 {
    raise Invalid("IRC base message exceeds 512 bytes including CRLF")
  }
  let mut prefix = None
  if rest.has_prefix(":") {
    let end = match rest.find(" ") {
      Some(v) => v
      None => raise Invalid("prefix without command")
    }
    if end == 1 {
      raise Invalid("empty prefix")
    }
    prefix = Some(rest[1:end].to_owned())
    rest = rest[end + 1:].to_owned()
  }
  rest = rest.trim_start(chars=" ").to_owned()
  let end = rest.find(" ").unwrap_or(rest.length())
  let command = rest[:end].to_owned()
  if command == "" {
    raise Invalid("empty command")
  }
  let mut letters = true
  let mut numbers = command.length() == 3
  for c in command.iter() {
    if !(c >= 'a' && c <= 'z') && !(c >= 'A' && c <= 'Z') {
      letters = false
    }
    if c < '0' || c > '9' {
      numbers = false
    }
  }
  if !letters && !numbers {
    raise Invalid("invalid command")
  }
  rest = rest[end:].trim_start(chars=" ").to_owned()
  let params = []
  while rest != "" {
    if params.length() == 15 {
      raise Invalid("more than 15 parameters")
    }
    if rest.has_prefix(":") {
      params.push(rest[1:].to_owned())
      break
    }
    let end = rest.find(" ").unwrap_or(rest.length())
    params.push(rest[:end].to_owned())
    rest = rest[end:].trim_start(chars=" ").to_owned()
  }
  { tags, prefix, command: command.to_upper(), params, }
}

///|
pub fn Message::encode(self : Message) -> String raise IrcError {
  let mut out = ""
  if !self.tags.is_empty() {
    let parts = []
    for tag in self.tags {
      if !key_valid(tag.key) {
        raise Invalid("invalid tag key")
      }
      parts.push(
        tag.key +
        (match tag.value {
          Some(v) => "=" + escape(v)
          None => ""
        }),
      )
    }
    out += "@" + parts.join(";") + " "
  }
  if self.prefix is Some(prefix) {
    if prefix == "" || prefix.contains(" ") {
      raise Invalid("invalid prefix")
    }
    out += ":" + prefix + " "
  }
  if self.command == "" || self.command.contains(" ") {
    raise Invalid("invalid command")
  }
  out += self.command
  for i in 0..