///|
pub fn validate_request(request : Message) -> Unit raise IppError {
  if operation_from_code(request.code) is None {
    raise Unsupported("request operation")
  }
  if request.groups.is_empty() || request.groups[0].tag != 1 {
    raise Invalid("operation attributes must be the first group")
  }
  let operation = request.groups[0]
  validate_common(operation)
  if operation.get_string("printer-uri") is None {
    raise Invalid("missing printer-uri")
  }
  match operation.get_string("printer-uri") {
    Some(uri) => {
      let _ = PrinterUri::parse(uri)
    }
    None => ()
  }
  if request.request_id < 1 {
    raise Invalid("nonpositive request-id")
  }
  if request.groups.filter(fn(group) { group.tag == 1 }).length() != 1 {
    raise Invalid("request has repeated operation groups")
  }
  if request.groups.any(fn(group) { group.tag != 1 && group.tag != 2 }) {
    raise Invalid("request has unsupported attribute group")
  }
  if request.groups.filter(fn(group) { group.tag == 2 }).length() > 1 {
    raise Invalid("request has repeated job groups")
  }
  match operation_from_code(request.code) {
    Some(PrintJob) => {
      if request.document.is_empty() {
        raise Invalid("Print-Job requires a document")
      }
      require_document_format(operation)
    }
    Some(SendDocument) => {
      require_job_id(operation)
      require_document_format(operation)
      if operation.get_bool("last-document") is None {
        raise Invalid("Send-Document requires last-document boolean")
      }
      if request.document.is_empty() {
        raise Invalid("Send-Document requires a document")
      }
    }
    Some(CancelJob) | Some(GetJobAttributes) => {
      require_job_id(operation)
      require_no_document(request)
    }
    Some(ValidateJob) => {
      require_document_format(operation)
      require_no_document(request)
    }
    _ => require_no_document(request)
  }
}

///|
fn require_job_id(group : Group) -> Unit raise IppError {
  match group.get_int("job-id") {
    Some(value) if value > 0 => ()
    _ => raise Invalid("operation requires positive job-id")
  }
}

///|
fn require_document_format(group : Group) -> Unit raise IppError {
  match group.find("document-format") {
    Some(attribute) =>
      match attribute.first() {
        Some(MimeType(format)) if valid_mime_type(format) => ()
        _ => raise Invalid("invalid document-format tag or value")
      }
    None => raise Invalid("operation requires document-format")
  }
}

///|
fn require_no_document(message : Message) -> Unit raise IppError {
  if !message.document.is_empty() {
    raise Invalid("unexpected document data")
  }
}

///|
fn validate_common(group : Group) -> Unit raise IppError {
  if group.attributes.length() < 2 {
    raise Protocol("charset and language attributes missing")
  }
  let charset = group.attributes[0]
  let language = group.attributes[1]
  if charset.name != "attributes-charset" ||
    charset.values != [Charset("utf-8")] {
    raise Protocol("first attribute must be UTF-8 charset")
  }
  if language.name != "attributes-natural-language" ||
    language.values.length() != 1 {
    raise Protocol("second attribute must be natural language")
  }
  match language.values[0] {
    Language(value) if !value.is_empty() && value.length() <= 63 => ()
    _ => raise Protocol("invalid natural-language value")
  }
}

///|
pub fn validate_response(
  request : Message,
  response : Message,
) -> Unit raise IppError {
  if response.request_id != request.request_id {
    raise Protocol("response request-id does not match request")
  }
  if response.groups.is_empty() || response.groups[0].tag != 1 {
    raise Protocol("response must begin with operation attributes")
  }
  validate_common(response.groups[0])
  if response.groups.filter(fn(group) { group.tag == 1 }).length() != 1 {
    raise Protocol("response has repeated operation groups")
  }
  if !response.document.is_empty() {
    raise Protocol("unexpected document in response")
  }
  if response.code > 0x05ff ||
    (response.code > 0x00ff && response.code < 0x0400) {
    raise Protocol("invalid status-code class")
  }
}

///|
pub fn unsupported_attributes(response : Message) -> Array[Attribute] {
  let result : Array[Attribute] = []
  for group in response.groups {
    if group.tag == 5 {
      for attribute in group.attributes {
        result.push(attribute)
      }
    }
  }
  result
}

///|
pub fn response_status_message(response : Message) -> String {
  let label = status_name(response.code)
  match response.find_group(1) {
    Some(group) =>
      match group.get_string("status-message") {
        Some(message) => "\{label}: \{message}"
        None => label
      }
    None => label
  }
}

///|
pub fn require_success(response : Message) -> Unit raise IppError {
  if !response.is_success() {
    raise Protocol(response_status_message(response))
  }
}