///|
priv struct MediaType {
  essence : String
  parameters : Array[(String, String)]
}

///|
fn token_char(c : Char) -> Bool {
  c.to_int() > 0x20 &&
  c.to_int() < 0x7F &&
  !"()<>@,;:\\\"/[]?=".contains(c.to_string())
}

///|
fn header_characters(a : Array[Char], path : String) -> Unit raise ParseError {
  for i, c in a {
    if c.to_int() > 0x7E ||
      (c.to_int() < 0x20 && c != '\t' && c != '\r' && c != '\n') {
      raise InvalidRequiredField(path, "invalid header character")
    }
    if c == '\r' &&
      (
        i + 2 >= a.length() ||
        a[i + 1] != '\n' ||
        (a[i + 2] != ' ' && a[i + 2] != '\t')
      ) {
      raise InvalidRequiredField(path, "invalid folding whitespace")
    }
    if c == '\n' && (i == 0 || a[i - 1] != '\r') {
      raise InvalidRequiredField(path, "bare newline in header value")
    }
  }
}

///|
fn mime_token(
  a : Array[Char],
  start : Int,
  path : String,
) -> (String, Int) raise ParseError {
  let begin = mail_cfws(a, start, path)
  let mut end = begin
  while end < a.length() && token_char(a[end]) {
    end += 1
  }
  if end == begin {
    raise InvalidRequiredField(path, "missing MIME token")
  }
  (chars_span(a, begin, end), mail_cfws(a, end, path))
}

///|
fn parse_media_type(
  value : String,
  path : String,
) -> MediaType raise ParseError {
  let a = value.to_array()
  header_characters(a, path)
  let (major, slash) = mime_token(a, 0, path)
  if slash >= a.length() || a[slash] != '/' {
    raise InvalidRequiredField(path, "expected MIME type/subtype")
  }
  let (minor, after) = mime_token(a, slash + 1, path)
  let mut pos = after
  let parameters : Array[(String, String)] = []
  while pos < a.length() {
    if a[pos] != ';' {
      raise InvalidRequiredField(path, "expected MIME parameter separator")
    }
    let (name, equal) = mime_token(a, pos + 1, path)
    if equal >= a.length() || a[equal] != '=' {
      raise InvalidRequiredField(path, "expected MIME parameter value")
    }
    pos = mail_cfws(a, equal + 1, path)
    let parameter = if pos < a.length() && a[pos] == '"' {
      pos += 1
      let out = StringBuilder()
      let mut closed = false
      while pos < a.length() {
        if a[pos] == '"' {
          pos += 1
          closed = true
          break
        }
        if a[pos] == '\\' {
          pos += 1
          if pos >= a.length() {
            raise InvalidRequiredField(path, "unfinished MIME escape")
          }
          out.write_char(a[pos])
        } else if a[pos] != '\r' && a[pos] != '\n' {
          out.write_char(a[pos])
        }
        pos += 1
      }
      if !closed {
        raise InvalidRequiredField(path, "unclosed MIME quoted value")
      }
      pos = mail_cfws(a, pos, path)
      out.to_string()
    } else {
      let (token, next) = mime_token(a, pos, path)
      pos = next
      token
    }
    let key = name.to_lower()
    // Charset names are case insensitive. Unknown parameter values retain case.
    parameters.push(
      (key, if key == "charset" { parameter.to_lower() } else { parameter }),
    )
  }
  { essence: major.to_lower() + "/" + minor.to_lower(), parameters, }
}

///|
fn same_media_type(
  left : String?,
  right : String?,
  path : String,
) -> Bool raise ParseError {
  match (left, right) {
    (None, None) => true
    (Some(l), Some(r)) => {
      let a = parse_media_type(l, path)
      let b = parse_media_type(r, path)
      if a.essence != b.essence ||
        a.parameters.length() != b.parameters.length() {
        return false
      }
      // Multiset comparison preserves even repeated parameter occurrences.
      let matched = b.parameters.map(_ => false)
      for parameter in a.parameters {
        let mut found = false
        for i, candidate in b.parameters {
          if !matched[i] && candidate == parameter {
            matched[i] = true
            found = true
            break
          }
        }
        if !found {
          return false
        }
      }
      true
    }
    _ => false
  }
}