///|
pub(all) enum Sides {
  OneSided
  TwoSidedLongEdge
  TwoSidedShortEdge
} derive(Eq, Debug)

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

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

///|
pub fn Sides::keyword(self : Sides) -> String {
  match self {
    OneSided => "one-sided"
    TwoSidedLongEdge => "two-sided-long-edge"
    TwoSidedShortEdge => "two-sided-short-edge"
  }
}

///|
pub fn sides_from_string(text : String) -> Sides raise IppError {
  match text {
    "one-sided" => OneSided
    "two-sided-long-edge" => TwoSidedLongEdge
    "two-sided-short-edge" => TwoSidedShortEdge
    _ => raise Invalid("unknown sides value")
  }
}

///|
pub(all) struct PageRange {
  first : Int
  last : Int
} derive(Eq, Debug)

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

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

///|
pub fn PageRange::new(first : Int, last : Int) -> PageRange raise IppError {
  if first < 1 || last < first {
    raise Invalid("invalid page range")
  }
  { first, last, }
}

///|
pub(all) struct JobOptions {
  name : String
  document_format : String
  copies : Int
  media : String?
  sides : Sides?
  color_mode : String?
  resolution : Resolution?
  print_quality : Int?
  orientation : Int?
  number_up : Int?
  page_ranges : Array[PageRange]
} derive(Eq, Debug)

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

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

///|
pub fn JobOptions::new(name : String, document_format : String) -> JobOptions {
  {
    name,
    document_format,
    copies: 1,
    media: None,
    sides: None,
    color_mode: None,
    resolution: None,
    print_quality: None,
    orientation: None,
    number_up: None,
    page_ranges: [],
  }
}

///|
pub fn valid_mime_type(text : String) -> Bool {
  if text.length() < 3 || text.length() > 255 {
    return false
  }
  let parts = text.split("/").to_array()
  if parts.length() != 2 || parts[0].is_empty() || parts[1].is_empty() {
    return false
  }
  for char in text.to_array() {
    let n = char.to_int()
    if !((n >= 97 && n <= 122) ||
      (n >= 48 && n <= 57) ||
      n == 33 ||
      n == 35 ||
      n == 36 ||
      n == 38 ||
      n == 45 ||
      n == 46 ||
      n == 43 ||
      n == 47 ||
      n == 94 ||
      n == 95) {
      return false
    }
  }
  true
}

///|
pub fn JobOptions::validate(self : JobOptions) -> Unit raise IppError {
  let name_length = encode_utf8(self.name).length()
  if name_length < 1 || name_length > 255 {
    raise Invalid("job name must be 1 to 255 UTF-8 bytes")
  }
  if !valid_mime_type(self.document_format) {
    raise Invalid("invalid document MIME type")
  }
  if self.copies < 1 {
    raise Invalid("copies must be positive")
  }
  match self.media {
    Some(media) =>
      if !valid_attribute_name(media) {
        raise Invalid("invalid media keyword")
      }
    None => ()
  }
  match self.color_mode {
    Some(mode) =>
      if mode != "auto" &&
        mode != "color" &&
        mode != "monochrome" &&
        mode != "bi-level" {
        raise Invalid("invalid print color mode")
      }
    None => ()
  }
  match self.resolution {
    Some(resolution) =>
      if resolution.x < 1 ||
        resolution.y < 1 ||
        (resolution.units != 3 && resolution.units != 4) {
        raise Invalid("invalid printer resolution")
      }
    None => ()
  }
  match self.print_quality {
    Some(quality) =>
      if quality < 3 || quality > 5 {
        raise Invalid("print quality must be 3, 4 or 5")
      }
    None => ()
  }
  match self.orientation {
    Some(orientation) =>
      if orientation < 3 || orientation > 6 {
        raise Invalid("orientation must be between 3 and 6")
      }
    None => ()
  }
  match self.number_up {
    Some(count) => if count < 1 { raise Invalid("number-up must be positive") }
    None => ()
  }
  let mut previous = 0
  for range in self.page_ranges {
    if range.first < 1 || range.last < range.first || range.first <= previous {
      raise Invalid("page ranges must be positive, ordered and disjoint")
    }
    previous = range.last
  }
}

///|
pub fn JobOptions::attributes(self : JobOptions) -> Array[Attribute] {
  let attributes = [Attribute::new("copies", Integer(self.copies))]
  match self.media {
    Some(media) => attributes.push(Attribute::new("media", Keyword(media)))
    None => ()
  }
  match self.sides {
    Some(sides) =>
      attributes.push(Attribute::new("sides", Keyword(sides.keyword())))
    None => ()
  }
  match self.color_mode {
    Some(mode) =>
      attributes.push(Attribute::new("print-color-mode", Keyword(mode)))
    None => ()
  }
  match self.resolution {
    Some(resolution) =>
      attributes.push(
        Attribute::new("printer-resolution", ResolutionValue(resolution)),
      )
    None => ()
  }
  match self.print_quality {
    Some(quality) =>
      attributes.push(Attribute::new("print-quality", Enumeration(quality)))
    None => ()
  }
  match self.orientation {
    Some(orientation) =>
      attributes.push(
        Attribute::new("orientation-requested", Enumeration(orientation)),
      )
    None => ()
  }
  match self.number_up {
    Some(count) => attributes.push(Attribute::new("number-up", Integer(count)))
    None => ()
  }
  if !self.page_ranges.is_empty() {
    attributes.push({
      name: "page-ranges",
      values: self.page_ranges.map(fn(range) {
        IppValue::Range(range.first, range.last)
      }),
    })
  }
  attributes
}

///|
pub fn parse_page_ranges(text : String) -> Array[PageRange] raise IppError {
  if text.is_empty() {
    raise Invalid("page range text is empty")
  }
  let ranges : Array[PageRange] = []
  let mut previous = 0
  for part in text.split(",") {
    let value = part.to_owned()
    let range = match value.split_once("-") {
      Some((start, end)) =>
        PageRange::new(
          parse_decimal(start.to_owned(), 2147483647),
          parse_decimal(end.to_owned(), 2147483647),
        )
      None => {
        let page = parse_decimal(value, 2147483647)
        PageRange::new(page, page)
      }
    }
    if range.first <= previous {
      raise Invalid("page ranges overlap or are not sorted")
    }
    previous = range.last
    ranges.push(range)
  }
  ranges
}

///|
pub fn infer_document_format(filename : String) -> String? {
  let lower = filename.to_lower()
  if lower.has_suffix(".pdf") {
    Some("application/pdf")
  } else if lower.has_suffix(".jpg") || lower.has_suffix(".jpeg") {
    Some("image/jpeg")
  } else if lower.has_suffix(".png") {
    Some("image/png")
  } else if lower.has_suffix(".ps") {
    Some("application/postscript")
  } else if lower.has_suffix(".txt") {
    Some("text/plain")
  } else {
    None
  }
}