///|
/// Errors never include credentials or the raw response line.
#derive(@debug.Debug)
pub suberror NutError {
  InvalidArgument(String)
  Protocol(String)
  Server(String)
  LimitExceeded(String)
  UnexpectedEof
}

///|
/// Decode a NUT parseconf-style line without its line ending.
/// Supports quoted words, escapes, comments and equals tokens.
/// Physical newline continuation is rejected for network framing.
pub fn parse_line(line : String) -> Array[String] raise NutError {
  let out : Array[String] = []
  let mut word = StringBuilder()
  let mut active = false
  let mut quoted = false
  let mut escaped = false
  for ch in line.iter() {
    if (ch.to_int() < 32 && ch != '\t') || ch.to_int() == 127 {
      raise Protocol("control character in line")
    }
    if escaped {
      word.write_char(ch)
      active = true
      escaped = false
    } else if ch == '\\' {
      escaped = true
      active = true
    } else if quoted {
      if ch == '"' {
        out.push(word.to_string())
        word = StringBuilder()
        active = false
        quoted = false
      } else {
        word.write_char(ch)
      }
    } else if ch == '#' {
      break
    } else if ch == ' ' || ch == '\t' || ch == '=' {
      if active {
        out.push(word.to_string())
        word = StringBuilder()
        active = false
      }
      if ch == '=' {
        out.push("=")
      }
    } else if ch == '"' && !active {
      quoted = true
      active = true
    } else {
      active = true
      word.write_char(ch)
    }
  }
  if quoted || escaped {
    raise Protocol("incomplete quoted or escaped token")
  }
  if active {
    out.push(word.to_string())
  }
  out
}

///|
/// Quote an argument. Reject controls to prevent command injection.
pub fn quote_argument(value : String) -> String raise NutError {
  let out = StringBuilder()
  out.write_char('"')
  for ch in value.iter() {
    if ch.to_int() < 32 || ch.to_int() == 127 {
      raise InvalidArgument("argument contains a control character")
    }
    if ch == '"' || ch == '\\' || ch == '#' {
      out.write_char('\\')
    }
    out.write_char(ch)
  }
  out.write_char('"')
  out.to_string()
}

///|
fn check_server(tokens : Array[String]) -> Unit raise NutError {
  if tokens.length() > 0 && tokens[0] == "ERR" {
    if tokens.length() == 2 {
      let code = tokens[1]
      if code.is_empty() || code.length() > 128 {
        raise Protocol("invalid server error code")
      }
      for ch in code.iter() {
        if !(ch >= 'A' && ch <= 'Z') &&
          !(ch >= '0' && ch <= '9') &&
          ch != '-' &&
          ch != '_' {
          raise Protocol("invalid server error code")
        }
      }
      raise Server(code)
    }
    raise Protocol("malformed error response")
  }
}

///|
fn expect(
  tokens : Array[String],
  wanted : Array[String],
) -> Unit raise NutError {
  check_server(tokens)
  if tokens != wanted {
    raise Protocol("response does not match request")
  }
}

///|
pub extend NutError with @debug.Debug::{to_repr}