///|
/// A single SMTP reply line: a 3-digit code, an optional enhanced status code
/// (RFC 3463, e.g. `2.1.0`), the text, and whether more lines follow.
pub struct ReplyLine {
  code : Int
  enhanced_code : String?
  text : String
  more : Bool
} derive(Eq, Debug)

///|
/// Parse a single SMTP reply line of the form `ddd[- ]text` where `-` means
/// more lines follow and ` ` means this is the last line.
pub fn parse_reply_line(line : String) -> ReplyLine? {
  let b = string_to_bytes(line)
  if b.length() < 3 {
    return None
  }
  let mut code = 0
  for i = 0; i < 3; i = i + 1 {
    let c = b[i].to_int()
    if c < 0x30 || c > 0x39 {
      return None
    }
    code = code * 10 + (c - 0x30)
  }
  if b.length() == 3 {
    return Some({ code, enhanced_code: None, text: "", more: false })
  }
  let sep = b[3]
  if sep != b'-' && sep != b' ' {
    return None
  }
  let more = sep == b'-'
  let text = line[4:line.length()].to_owned()
  let enhanced = parse_enhanced_code(text)
  Some({ code, enhanced_code: enhanced, text, more })
}

///|
/// Extract an enhanced status code `X.Y.Z` from the start of the text.
fn parse_enhanced_code(text : String) -> String? {
  if text.length() < 5 {
    return None
  }
  let b = string_to_bytes(text)
  let d1 = b[0]
  let d2 = b[2]
  let d3 = b[4]
  if b.length() < 5 || b[1] != b'.' || b[3] != b'.' {
    return None
  }
  if !is_digit_byte(d1) || !is_digit_byte(d2) || !is_digit_byte(d3) {
    return None
  }
  Some(text[0:5].to_owned())
}

///|
/// A fully collected server reply (all lines of a multi-line response).
pub struct ServerReply {
  code : Int
  enhanced_code : String?
  lines : Array[String]
  text : String
} derive(Eq, Debug)

///|
pub fn ServerReply::code(self : ServerReply) -> Int {
  self.code
}

///|
pub fn ServerReply::enhanced_code(self : ServerReply) -> String? {
  self.enhanced_code
}

///|
pub fn ServerReply::lines(self : ServerReply) -> Array[String] {
  self.lines
}

///|
pub fn ServerReply::text(self : ServerReply) -> String {
  self.text
}

///|
/// The reply carries a 2xx positive completion code.
pub fn ServerReply::is_positive(self : ServerReply) -> Bool {
  self.code >= 200 && self.code < 300
}

///|
/// The reply carries a 3xx intermediate code.
pub fn ServerReply::is_intermediate(self : ServerReply) -> Bool {
  self.code >= 300 && self.code < 400
}

///|
/// The reply carries a 4xx transient negative completion code.
pub fn ServerReply::is_transient(self : ServerReply) -> Bool {
  self.code >= 400 && self.code < 500
}

///|
/// The reply carries a 5xx permanent negative completion code.
pub fn ServerReply::is_permanent(self : ServerReply) -> Bool {
  self.code >= 500 && self.code < 600
}

///|
/// Build a `ServerReply` from the collected reply lines (the raw lines that
/// were received, including code prefixes).
pub fn ServerReply::from_lines(lines : Array[String]) -> ServerReply {
  let first = match parse_reply_line(lines[0]) {
    Some(r) => r
    None => { code: 0, enhanced_code: None, text: "", more: false }
  }
  let mut text_parts = []
  for i, l in lines {
    let parsed = parse_reply_line(l)
    let t = match parsed {
      Some(r) => r.text
      None => l
    }
    if i > 0 {
      text_parts.push(t)
    } else {
      text_parts = [t]
    }
  }
  let text = text_parts.join("\n")
  { code: first.code, enhanced_code: first.enhanced_code, lines, text }
}

///|
pub impl Show for ServerReply with fn output(self, logger) {
  logger.write_string(self.lines.join("\r\n"))
}

///|
/// Strip the leading `ddd[- ]` reply-code prefix from a line, returning just
/// the text (e.g. `250-8BITMIME` -> `8BITMIME`).
pub fn strip_reply_code(line : String) -> String {
  if line.length() > 4 {
    line[4:line.length()].to_owned()
  } else {
    ""
  }
}

///|
fn is_digit_byte(b : Byte) -> Bool {
  b.to_int() >= 0x30 && b.to_int() <= 0x39
}