///|
pub(all) enum JobState {
  Pending
  PendingHeld
  Processing
  ProcessingStopped
  Canceled
  Aborted
  Completed
  Other(Int)
} derive(Eq, Debug)

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

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

///|
pub fn job_state_from_code(code : Int) -> JobState {
  match code {
    3 => Pending
    4 => PendingHeld
    5 => Processing
    6 => ProcessingStopped
    7 => Canceled
    8 => Aborted
    9 => Completed
    _ => Other(code)
  }
}

///|
pub fn JobState::code(self : JobState) -> Int {
  match self {
    Pending => 3
    PendingHeld => 4
    Processing => 5
    ProcessingStopped => 6
    Canceled => 7
    Aborted => 8
    Completed => 9
    Other(code) => code
  }
}

///|
pub fn JobState::name(self : JobState) -> String {
  match self {
    Pending => "pending"
    PendingHeld => "pending-held"
    Processing => "processing"
    ProcessingStopped => "processing-stopped"
    Canceled => "canceled"
    Aborted => "aborted"
    Completed => "completed"
    Other(code) => "unknown-\{code}"
  }
}

///|
pub fn JobState::terminal(self : JobState) -> Bool {
  self == Completed || self == Canceled || self == Aborted
}

///|
pub(all) struct Job {
  id : Int
  uri : String?
  name : String?
  state : JobState?
  reasons : Array[String]
  message : String?
  impressions_completed : Int?
  attributes : Group
} derive(Eq, Debug)

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

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

///|
pub fn Job::from_group(group : Group) -> Job raise IppError {
  if group.tag != 2 {
    raise Protocol("expected job attribute group")
  }
  let id = match group.find("job-id") {
    Some(attribute) =>
      match attribute.values {
        [Integer(id)] if id > 0 => id
        _ => raise Protocol("job group requires one positive integer job-id")
      }
    None => raise Protocol("job group has no positive job-id")
  }
  let reasons = match group.find("job-state-reasons") {
    Some(attribute) => attribute.string_values()
    None => []
  }
  let state = match group.get_int("job-state") {
    Some(code) => Some(job_state_from_code(code))
    None => None
  }
  {
    id,
    uri: group.get_string("job-uri"),
    name: group.get_string("job-name"),
    state,
    reasons,
    message: group.get_string("job-state-message"),
    impressions_completed: group.get_int("job-impressions-completed"),
    attributes: group,
  }
}

///|
pub fn jobs_from_response(response : Message) -> Array[Job] raise IppError {
  if !response.is_success() {
    raise Protocol(status_name(response.code))
  }
  let jobs : Array[Job] = []
  for group in response.groups {
    if group.tag == 2 {
      jobs.push(Job::from_group(group))
    }
  }
  jobs
}

///|
pub fn job_from_response(response : Message) -> Job raise IppError {
  let jobs = jobs_from_response(response)
  if jobs.length() != 1 {
    raise Protocol("expected exactly one job group")
  }
  jobs[0]
}

///|
pub(all) enum SubmissionState {
  NotSent
  Sending
  OutcomeUnknown
  Rejected(Int, String)
  Accepted(Job)
} derive(Eq, Debug)

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

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

///|
pub fn SubmissionState::may_submit_again(self : SubmissionState) -> Bool {
  match self {
    NotSent => true
    Sending | OutcomeUnknown | Accepted(_) | Rejected(_, _) => false
  }
}

///|
pub(all) struct JobObservation {
  job : Job
  elapsed_ms : Int
} derive(Eq, Debug)

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

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

///|
pub(all) struct PollPolicy {
  interval_ms : Int
  maximum_polls : Int
} derive(Eq, Debug)

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

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

///|
pub fn PollPolicy::defaults() -> PollPolicy {
  { interval_ms: 1000, maximum_polls: 60, }
}

///|
pub fn PollPolicy::validate(self : PollPolicy) -> Unit raise IppError {
  if self.interval_ms < 1 ||
    self.interval_ms > 60000 ||
    self.maximum_polls < 1 ||
    self.maximum_polls > 10000 {
    raise Invalid("invalid polling limits")
  }
  if self.interval_ms > 2147483647 / self.maximum_polls {
    raise Invalid("poll duration overflows")
  }
}

///|
pub fn validate_job_observation(
  previous : Job?,
  current : Job,
) -> Unit raise IppError {
  match previous {
    Some(previous) => {
      if previous.id != current.id {
        raise Protocol("polled response returned a different job-id")
      }
      match previous.state {
        Some(state) if state.terminal() =>
          if current.state != previous.state {
            raise Protocol("terminal job state changed")
          }
        _ => ()
      }
    }
    None => ()
  }
}