///|
/// Errors raised by `mails` and reported to the user without a stack trace.
pub suberror MailsError {
  MailsError(String)
}

///|
pub fn mails_error(msg : String) -> MailsError {
  MailsError(msg)
}

///|
pub fn trim(s : StringView) -> String {
  s.trim().to_owned()
}

///|
pub fn is_blank(s : String) -> Bool {
  trim(s) == ""
}

///|
pub fn base64_encode(data : Bytes) -> String {
  @base64.encode(data)
}

///|
pub fn base64_decode(text : String) -> Bytes raise MailsError {
  @base64.decode(text) catch {
    _ => raise MailsError("invalid base64 data")
  }
}

///|
pub fn bytes_concat(parts : ArrayView[Bytes]) -> Bytes {
  let mut len = 0
  for part in parts {
    len = len + part.length()
  }
  let out = Array::make(len, b'\x00')
  let mut offset = 0
  for part in parts {
    for i in 0.. String {
  @utf8.decode_lossy(data)
}

///|
pub fn string_to_bytes(text : String) -> Bytes {
  @utf8.encode(text)
}

///|
/// Parse a human duration such as `30s`, `5m`, `1h` or `250ms` into ms.
pub fn parse_duration_ms(text : String) -> Int32? {
  let t = trim(text).to_lower()
  if t == "" {
    return None
  }
  let mut digits = ""
  let mut unit = ""
  for c in t {
    if c >= '0' && c <= '9' {
      if unit != "" {
        return None
      }
      digits = digits + c.to_string()
    } else if c == ' ' || c == '_' {
      continue
    } else {
      unit = unit + c.to_string()
    }
  }
  if digits == "" {
    return None
  }
  let number = @string.parse_int(digits) catch { _ => return None }
  let factor = match unit {
    "s" | "sec" | "secs" => 1000
    "ms" | "msec" | "msecs" => 1
    "m" | "min" | "mins" => 60_000
    "h" | "hr" | "hrs" => 3_600_000
    "d" | "day" | "days" => 86_400_000
    _ => return None
  }
  Some(number * factor)
}

///|
/// Format milliseconds for log messages.
pub fn format_duration(ms : Int32) -> String {
  if ms >= 86_400_000 && ms % 86_400_000 == 0 {
    "\{ms / 86_400_000}d"
  } else if ms >= 3_600_000 && ms % 3_600_000 == 0 {
    "\{ms / 3_600_000}h"
  } else if ms >= 60_000 && ms % 60_000 == 0 {
    "\{ms / 60_000}m"
  } else if ms >= 1000 && ms % 1000 == 0 {
    "\{ms / 1000}s"
  } else {
    "\{ms}ms"
  }
}

///|
/// Escape a string for use inside an IMAP quoted string (RFC 3501).
pub fn imap_quote(text : String) -> String {
  let out = StringBuilder()
  out.write_char('"')
  for c in text {
    if c == '\\' || c == '"' {
      out.write_char('\\')
    }
    out.write_char(c)
  }
  out.write_char('"')
  out.to_string()
}

///|
/// Collapse runs of whitespace into single spaces and trim the result.
pub fn collapse_whitespace(text : String) -> String {
  let out = StringBuilder()
  let mut has_out = false
  let mut pending_space = false
  for c in text {
    if c == ' ' || c == '\t' || c == '\r' || c == '\n' {
      if has_out {
        pending_space = true
      }
    } else {
      if pending_space {
        out.write_char(' ')
        pending_space = false
      }
      out.write_char(c)
      has_out = true
    }
  }
  out.to_string()
}

///|
/// Truncate to at most `max_chars` characters, appending an ellipsis.
pub fn truncate_chars(text : String, max_chars : Int32) -> String {
  if text.char_length() <= max_chars {
    return text
  }
  let out = StringBuilder()
  let mut count = 0
  for c in text {
    if count >= max_chars {
      break
    }
    out.write_char(c)
    count = count + 1
  }
  out.write_char('…')
  out.to_string()
}

///|
pub enum LogLevel {
  Warn
  Info
  Debug
}

///|
pub let log_level : Ref[LogLevel] = Ref(Info)

///|
pub fn set_log_level(level : LogLevel) -> Unit {
  log_level.val = level
}

///|
pub fn log_error(msg : String) -> Unit {
  eprintln("mails: error: " + msg)
}

///|
pub fn log_warn(msg : String) -> Unit {
  eprintln("mails: warning: " + msg)
}

///|
pub fn log_info(msg : String) -> Unit {
  match log_level.val {
    Warn => ()
    _ => eprintln("mails: " + msg)
  }
}

///|
pub fn log_debug(msg : String) -> Unit {
  match log_level.val {
    Debug => eprintln("mails: debug: " + msg)
    _ => ()
  }
}

///|
/// Quote a string for safe interpolation into a POSIX shell command.
pub fn shell_quote(text : String) -> String {
  let out = StringBuilder()
  out.write_char('\'')
  for c in text {
    if c == '\'' {
      out.write_string("'\\''")
    } else {
      out.write_char(c)
    }
  }
  out.write_char('\'')
  out.to_string()
}

///|
/// The user-facing message of an error, without the `MailsError(...)` wrapper.
pub fn error_message(err : Error) -> String {
  match err {
    MailsError(text) => text
    ImapProtocolError(text) => text
    IdleUnsupported => "the server does not support IDLE"
    _ => err.to_string()
  }
}