///|
/// A parsed MIME Content-Type: a type/subtype plus ordered parameters
/// (RFC 2045 section 5.1).
pub struct ContentType {
  type_ : String
  subtype : String
  params : Array[(String, String)]
} derive(Eq, Debug)

///|
pub fn ContentType::new(type_ : String, subtype : String) -> ContentType {
  { type_, subtype, params: [] }
}

///|
pub fn ContentType::type_name(self : ContentType) -> String {
  self.type_
}

///|
pub fn ContentType::subtype_name(self : ContentType) -> String {
  self.subtype
}

///|
pub fn ContentType::params(self : ContentType) -> Array[(String, String)] {
  self.params
}

///|
/// Add (or replace) a parameter like `boundary=...` or `charset=utf-8`.
pub fn ContentType::set_param(
  self : ContentType,
  name : String,
  value : String,
) -> Unit {
  for i = 0; i < self.params.length(); i = i + 1 {
    let (n, _) = self.params[i]
    if n.to_lower() == name.to_lower() {
      self.params[i] = (name, value)
      return
    }
  }
  self.params.push((name, value))
}

///|
pub fn ContentType::get_param(self : ContentType, name : String) -> String? {
  for pair in self.params {
    let (n, v) = pair
    if n.to_lower() == name.to_lower() {
      return Some(v)
    }
  }
  None
}

///|
pub fn ContentType::is_multipart(self : ContentType) -> Bool {
  self.type_.to_lower() == "multipart"
}

///|
/// Validate a token used for the type, subtype and parameter names
/// (RFC 2045 section 5.1: any printable ASCII except tspecials).
fn is_token(s : String) -> Bool {
  if s.is_empty() {
    return false
  }
  for b in string_to_bytes(s) {
    let c = b.to_int()
    let special = c == 0x28 ||
      c == 0x29 ||
      c == 0x2C ||
      c == 0x2F ||
      c == 0x3A ||
      c == 0x3B ||
      c == 0x3C ||
      c == 0x3D ||
      c == 0x3E ||
      c == 0x3F ||
      c == 0x40 ||
      c == 0x5B ||
      c == 0x5C ||
      c == 0x5D ||
      c == 0x22
    if c < 33 || c > 126 || special {
      return false
    }
  }
  true
}

///|
/// Render the Content-Type header value, e.g.
/// `multipart/mixed; boundary="abc"`.
pub fn ContentType::to_string(self : ContentType) -> String {
  let buf = Buffer::Buffer()
  buf.write_string_utf16le(
    "\{self.type_.to_lower()}/\{self.subtype.to_lower()}",
  )
  for pair in self.params {
    let (name, value) = pair
    buf.write_string_utf16le("; \{name.to_lower()}=")
    if needs_quoted_param(value) {
      buf.write_string_utf16le("\"\{value}\"")
    } else {
      buf.write_string_utf16le(value)
    }
  }
  buf.to_string()
}

///|
fn needs_quoted_param(value : String) -> Bool {
  for b in string_to_bytes(value) {
    let c = b.to_int()
    if c < 33 || c > 126 {
      return true
    }
  }
  false
}

///|
/// Parse a Content-Type header value into a `ContentType`.
pub fn ContentType::parse(input : String) -> ContentType raise MailFailure {
  let s = input.trim().to_owned()
  guard !s.is_empty() else {
    raise MailFailure::of(Mime, MM_MIME_005, "empty content-type")
  }
  let semi = index_of(s, ";")
  let base = if semi >= 0 { s[0:semi].trim().to_owned() } else { s }
  let slash = index_of(base, "/")
  guard slash > 0 else {
    raise MailFailure::of(Mime, MM_MIME_005, "content-type missing '/'")
  }
  let type_ = base[0:slash].trim().to_owned()
  let subtype = base[slash + 1:base.length()].trim().to_owned()
  guard is_token(type_) && is_token(subtype) else {
    raise MailFailure::of(Mime, MM_MIME_005, "invalid content-type: \{s}")
  }
  let ct = ContentType::new(type_, subtype)
  if semi >= 0 {
    parse_params(ct, s[semi + 1:s.length()].to_owned())
  }
  ct
}

///|
fn parse_params(ct : ContentType, rest : String) -> Unit raise MailFailure {
  let items = split_semicolons(rest)
  for item in items {
    let item = item.trim().to_owned()
    if item.is_empty() {
      continue
    }
    let eq = index_of(item, "=")
    guard eq > 0 else {
      raise MailFailure::of(
        Mime,
        MM_MIME_005,
        "malformed content-type parameter: \{item}",
      )
    }
    let name = item[0:eq].trim().to_owned()
    let value = item[eq + 1:item.length()].trim().to_owned()
    let value_bytes = string_to_bytes(value)
    let value = if value_bytes.length() >= 2 && value_bytes[0] == b'"' {
      // strip surrounding quotes
      value[1:value.length() - 1].to_owned()
    } else {
      value
    }
    ct.set_param(name, value)
  }
}

///|
/// Split parameter text on `;` outside quoted strings.
fn split_semicolons(s : String) -> Array[String] {
  let items = []
  let bytes = string_to_bytes(s)
  let mut start = 0
  let mut in_quote = false
  for i = 0; i < bytes.length(); i = i + 1 {
    if bytes[i] == b'"' {
      in_quote = !in_quote
    } else if bytes[i] == b';' && !in_quote {
      items.push(s[start:i].to_owned())
      start = i + 1
    }
  }
  items.push(s[start:bytes.length()].to_owned())
  items
}