///|
pub(all) enum Severity {
  Warning
  Error
} derive(Eq, Debug)

///|
pub extend Severity with Eq::{equal, not_equal}

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

///|
pub(all) struct Diagnostic {
  severity : Severity
  attribute : String
  message : String
} derive(Eq, Debug)

///|
pub extend Diagnostic with Eq::{equal, not_equal}

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

///|
pub(all) struct Capabilities {
  printer : Group
} derive(Eq, Debug)

///|
pub extend Capabilities with Eq::{equal, not_equal}

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

///|
pub fn Capabilities::from_response(
  response : Message,
) -> Capabilities raise IppError {
  if !response.is_success() {
    raise Protocol(status_name(response.code))
  }
  let printer = match response.find_group(4) {
    Some(group) => group
    None => raise Protocol("response has no printer attributes")
  }
  { printer, }
}

///|
pub fn Capabilities::strings(
  self : Capabilities,
  name : String,
) -> Array[String] {
  match self.printer.find(name) {
    Some(attribute) => attribute.string_values()
    None => []
  }
}

///|
pub fn Capabilities::integers(self : Capabilities, name : String) -> Array[Int] {
  match self.printer.find(name) {
    Some(attribute) => attribute.values.filter_map(fn(value) { value.as_int() })
    None => []
  }
}

///|
pub fn Capabilities::name(self : Capabilities) -> String {
  match self.printer.get_string("printer-name") {
    Some(name) => name
    None => "unnamed printer"
  }
}

///|
pub fn Capabilities::accepting_jobs(self : Capabilities) -> Bool? {
  self.printer.get_bool("printer-is-accepting-jobs")
}

///|
pub fn Capabilities::supports_operation(
  self : Capabilities,
  operation : Operation,
) -> Bool? {
  match self.printer.find("operations-supported") {
    Some(_) =>
      Some(self.integers("operations-supported").contains(operation.code()))
    None => None
  }
}

///|
pub fn Capabilities::document_formats(self : Capabilities) -> Array[String] {
  self.strings("document-format-supported")
}

///|
pub fn Capabilities::media(self : Capabilities) -> Array[String] {
  self.strings("media-supported")
}

///|
pub fn Capabilities::resolutions(self : Capabilities) -> Array[Resolution] {
  match self.printer.find("printer-resolution-supported") {
    Some(attribute) =>
      attribute.values.filter_map(fn(value) {
        match value {
          ResolutionValue(resolution) => Some(resolution)
          _ => None
        }
      })
    None => []
  }
}

///|
fn diagnostic(
  severity : Severity,
  attribute : String,
  message : String,
) -> Diagnostic {
  { severity, attribute, message, }
}

///|
fn Capabilities::check_string(
  self : Capabilities,
  diagnostics : Array[Diagnostic],
  supported_name : String,
  request_name : String,
  requested : String,
) -> Unit {
  match self.printer.find(supported_name) {
    None =>
      diagnostics.push(
        diagnostic(
          Warning,
          request_name,
          "printer did not advertise supported values",
        ),
      )
    Some(_) =>
      if !self.strings(supported_name).contains(requested) {
        diagnostics.push(
          diagnostic(Error, request_name, "unsupported value: \{requested}"),
        )
      }
  }
}

///|
fn Capabilities::check_integer(
  self : Capabilities,
  diagnostics : Array[Diagnostic],
  supported_name : String,
  request_name : String,
  requested : Int,
) -> Unit {
  match self.printer.find(supported_name) {
    None =>
      diagnostics.push(
        diagnostic(
          Warning,
          request_name,
          "printer did not advertise supported values",
        ),
      )
    Some(attribute) => {
      let supported = attribute.values.any(fn(value) {
        match value {
          Integer(n) | Enumeration(n) => n == requested
          Range(lower, upper) => requested >= lower && requested <= upper
          _ => false
        }
      })
      if !supported {
        diagnostics.push(
          diagnostic(Error, request_name, "unsupported value: \{requested}"),
        )
      }
    }
  }
}

///|
pub fn Capabilities::validate_options(
  self : Capabilities,
  options : JobOptions,
  document_bytes? : Int = 0,
) -> Array[Diagnostic] raise IppError {
  options.validate()
  if document_bytes < 0 {
    raise Invalid("negative document byte count")
  }
  let diagnostics : Array[Diagnostic] = []
  if self.accepting_jobs() == Some(false) {
    diagnostics.push(
      diagnostic(
        Error,
        "printer-is-accepting-jobs",
        "printer is not accepting jobs",
      ),
    )
  }
  if self.supports_operation(PrintJob) == Some(false) {
    diagnostics.push(
      diagnostic(Error, "operations-supported", "Print-Job is not supported"),
    )
  }
  self.check_string(
    diagnostics,
    "document-format-supported",
    "document-format",
    options.document_format,
  )
  self.check_integer(diagnostics, "copies-supported", "copies", options.copies)
  match options.media {
    Some(media) =>
      self.check_string(diagnostics, "media-supported", "media", media)
    None => ()
  }
  match options.sides {
    Some(sides) =>
      self.check_string(
        diagnostics,
        "sides-supported",
        "sides",
        sides.keyword(),
      )
    None => ()
  }
  match options.color_mode {
    Some(mode) =>
      self.check_string(
        diagnostics, "print-color-mode-supported", "print-color-mode", mode,
      )
    None => ()
  }
  match options.print_quality {
    Some(quality) =>
      self.check_integer(
        diagnostics, "print-quality-supported", "print-quality", quality,
      )
    None => ()
  }
  match options.orientation {
    Some(orientation) =>
      self.check_integer(
        diagnostics, "orientation-requested-supported", "orientation-requested",
        orientation,
      )
    None => ()
  }
  match options.number_up {
    Some(count) =>
      self.check_integer(diagnostics, "number-up-supported", "number-up", count)
    None => ()
  }
  match options.resolution {
    Some(resolution) =>
      match self.printer.find("printer-resolution-supported") {
        None =>
          diagnostics.push(
            diagnostic(
              Warning,
              "printer-resolution",
              "printer did not advertise supported resolutions",
            ),
          )
        Some(_) =>
          if !self.resolutions().contains(resolution) {
            diagnostics.push(
              diagnostic(Error, "printer-resolution", "unsupported resolution"),
            )
          }
      }
    None => ()
  }
  if !options.page_ranges.is_empty() &&
    self.printer.get_bool("page-ranges-supported") != Some(true) {
    diagnostics.push(
      diagnostic(
        Error,
        "page-ranges",
        "printer did not advertise page range support",
      ),
    )
  }
  if document_bytes > 0 &&
    self.printer.find("job-k-octets-supported") is Some(_) {
    let kibibytes = document_bytes / 1024 +
      (if document_bytes % 1024 != 0 { 1 } else { 0 })
    self.check_integer(
      diagnostics, "job-k-octets-supported", "job-k-octets", kibibytes,
    )
  }
  diagnostics
}

///|
pub fn diagnostics_have_errors(diagnostics : Array[Diagnostic]) -> Bool {
  diagnostics.any(fn(item) { item.severity == Error })
}

///|
pub fn Capabilities::require_options(
  self : Capabilities,
  options : JobOptions,
  document_bytes? : Int = 0,
) -> Unit raise IppError {
  let errors = self
    .validate_options(options, document_bytes~)
    .filter(fn(item) { item.severity == Error })
  if !errors.is_empty() {
    let details = errors
      .map(fn(item) { "\{item.attribute}: \{item.message}" })
      .join("; ")
    raise Unsupported(details)
  }
}