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

///|
pub(all) enum Command {
  User(String)
  Pass(String)
  Apop(String, String)
  Capa
  Stls
  Auth(String)
  Stat
  List(Int?)
  Uidl(Int?)
  Retr(Int)
  Dele(Int)
  Top(Int, Int)
  Noop
  Rset
  Quit
} derive(Debug, Eq)

///|
pub(all) enum Phase {
  Greeting
  Authorization
  TlsHandshake
  Transaction
  Closed
} derive(Debug, Eq)

///|
pub struct Reply {
  ok : Bool
  message : String
  body : Bytes
  continuation : Bool
} derive(Debug, Eq)

///|
fn Command::multiline(self : Command) -> Bool {
  match self {
    Capa | List(None) | Uidl(None) | Retr(_) | Top(_, _) => true
    _ => false
  }
}

///|
pub fn Command::encode(self : Command) -> String raise PopError {
  fn arg(s : String) -> String raise PopError {
    if s == "" {
      raise Invalid("empty argument")
    }
    for c in s.iter() {
      if c <= ' ' || c > '~' {
        raise Invalid("argument requires printable ASCII without whitespace")
      }
    }
    s
  }
  fn index(n : Int) -> String raise PopError {
    if n < 1 {
      raise Invalid("message number must be positive")
    }
    n.to_string()
  }
  let line = match self {
    User(s) => "USER " + arg(s)
    Pass(s) => "PASS " + arg(s)
    Capa => "CAPA"
    Stls => "STLS"
    Auth(mechanism) => {
      if mechanism != "PLAIN" {
        raise Invalid("only AUTH PLAIN is supported")
      }
      "AUTH PLAIN"
    }
    Apop(user, digest) => {
      if digest.length() != 32 {
        raise Invalid("APOP requires 32 hex digits")
      }
      for c in digest.iter() {
        if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) {
          raise Invalid("APOP requires lowercase hex")
        }
      }
      "APOP " + arg(user) + " " + digest
    }
    Stat => "STAT"
    List(n) =>
      "LIST" +
      (match n {
        Some(n) => " " + index(n)
        None => ""
      })
    Uidl(n) =>
      "UIDL" +
      (match n {
        Some(n) => " " + index(n)
        None => ""
      })
    Retr(n) => "RETR " + index(n)
    Dele(n) => "DELE " + index(n)
    Top(n, lines) => {
      if lines < 0 {
        raise Invalid("negative TOP line count")
      }
      "TOP " + index(n) + " " + lines.to_string()
    }
    Noop => "NOOP"
    Rset => "RSET"
    Quit => "QUIT"
  }
  if line.length() > 510 {
    raise Invalid("command too long")
  }
  line + "\r\n"
}