///|
pub(all) enum ChangeKind {
  Added
  Removed
  Changed
} derive(Eq, Debug)

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

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

///|
pub(all) struct CapabilityChange {
  attribute : String
  kind : ChangeKind
  before : Array[IppValue]
  after : Array[IppValue]
  transient : Bool
} derive(Eq, Debug)

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

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

///|
pub fn volatile_printer_attribute(name : String) -> Bool {
  match name {
    "printer-up-time"
    | "printer-current-time"
    | "printer-state"
    | "printer-state-reasons"
    | "printer-state-message"
    | "queued-job-count"
    | "printer-state-change-time"
    | "printer-state-change-date-time"
    | "printer-config-change-time"
    | "printer-config-change-date-time"
    | "printer-is-accepting-jobs" => true
    _ => false
  }
}

///| Multi-valued supported attributes are treated as multisets, not sequences.

///|
/// Duplicate values remain significant. Collection member order is retained.
fn same_values(left : Array[IppValue], right : Array[IppValue]) -> Bool {
  if left.length() != right.length() {
    return false
  }
  let used = FixedArray::make(right.length(), false)
  for value in left {
    let mut found = false
    for index, candidate in right {
      if !used[index] && candidate == value {
        used[index] = true
        found = true
        break
      }
    }
    if !found {
      return false
    }
  }
  true
}

///|
pub fn compare_capabilities(
  before : Capabilities,
  after : Capabilities,
  include_volatile? : Bool = false,
) -> Array[CapabilityChange] {
  let changes : Array[CapabilityChange] = []
  let seen : Map[String, Bool] = Map([])
  for attribute in before.printer.attributes {
    seen[attribute.name] = true
    let transient = volatile_printer_attribute(attribute.name)
    if transient && !include_volatile {
      continue
    }
    match after.printer.find(attribute.name) {
      None =>
        changes.push({
          attribute: attribute.name,
          kind: Removed,
          before: attribute.values,
          after: [],
          transient,
        })
      Some(next) =>
        if !same_values(attribute.values, next.values) {
          changes.push({
            attribute: attribute.name,
            kind: Changed,
            before: attribute.values,
            after: next.values,
            transient,
          })
        }
    }
  }
  for attribute in after.printer.attributes {
    if seen.contains(attribute.name) {
      continue
    }
    let transient = volatile_printer_attribute(attribute.name)
    if transient && !include_volatile {
      continue
    }
    changes.push({
      attribute: attribute.name,
      kind: Added,
      before: [],
      after: attribute.values,
      transient,
    })
  }
  changes.sort_by(fn(left, right) { left.attribute.compare(right.attribute) })
  changes
}

///|
pub fn CapabilityChange::to_json(self : CapabilityChange) -> Json {
  {
    "attribute": Json::string(self.attribute),
    "kind": Json::string(
      match self.kind {
        Added => "added"
        Removed => "removed"
        Changed => "changed"
      },
    ),
    "before": Json::array(self.before.map(value => value.to_json())),
    "after": Json::array(self.after.map(value => value.to_json())),
    "transient": Json::boolean(self.transient),
  }
}