///|
fn require_length(bytes : Bytes, expected : Int) -> Unit raise IppError {
  if bytes.length() != expected {
    raise Invalid("value length \{bytes.length()}, expected \{expected}")
  }
}

///|
fn decode_localized(bytes : Bytes) -> (String, String) raise IppError {
  let reader = Reader::new(bytes)
  let language = decode_utf8(reader.length_bytes())
  let text = decode_utf8(reader.length_bytes())
  if reader.remaining() != 0 {
    raise Invalid("trailing data in localized text")
  }
  (language, text)
}

///|
fn decode_scalar(tag : Int, bytes : Bytes) -> IppValue raise IppError {
  let reader = Reader::new(bytes)
  match tag {
    0x21 => {
      require_length(bytes, 4)
      Integer(reader.i32())
    }
    0x22 => {
      require_length(bytes, 1)
      let value = reader.byte()
      if value > 1 {
        raise Invalid("boolean value must be 0 or 1")
      }
      Boolean(value == 1)
    }
    0x23 => {
      require_length(bytes, 4)
      let value = reader.i32()
      if value < 1 {
        raise Invalid("enum value must be positive")
      }
      Enumeration(value)
    }
    0x30 => Octets(bytes)
    0x31 => {
      require_length(bytes, 11)
      let date : IppDateTime = {
        year: reader.u16(),
        month: reader.byte(),
        day: reader.byte(),
        hour: reader.byte(),
        minute: reader.byte(),
        second: reader.byte(),
        decisecond: reader.byte(),
        utc_direction: reader.byte(),
        utc_hours: reader.byte(),
        utc_minutes: reader.byte(),
      }
      date.validate()
      DateTime(date)
    }
    0x32 => {
      require_length(bytes, 9)
      let resolution : Resolution = {
        x: reader.i32(),
        y: reader.i32(),
        units: reader.byte(),
      }
      if resolution.x < 1 ||
        resolution.y < 1 ||
        (resolution.units != 3 && resolution.units != 4) {
        raise Invalid("invalid resolution")
      }
      ResolutionValue(resolution)
    }
    0x33 => {
      require_length(bytes, 8)
      let lower = reader.i32()
      let upper = reader.i32()
      if lower > upper {
        raise Invalid("reversed integer range")
      }
      Range(lower, upper)
    }
    0x35 => {
      let (language, text) = decode_localized(bytes)
      TextWithLanguage(language, text)
    }
    0x36 => {
      let (language, text) = decode_localized(bytes)
      NameWithLanguage(language, text)
    }
    0x41 => Text(decode_utf8(bytes))
    0x42 => Name(decode_utf8(bytes))
    0x44 => Keyword(decode_utf8(bytes))
    0x45 => Uri(decode_utf8(bytes))
    0x46 => UriScheme(decode_utf8(bytes))
    0x47 => Charset(decode_utf8(bytes))
    0x48 => Language(decode_utf8(bytes))
    0x49 => MimeType(decode_utf8(bytes))
    0x10 | 0x12 | 0x13 | 0x15 | 0x16 | 0x17 => {
      require_length(bytes, 0)
      OutOfBand(tag)
    }
    0x34 | 0x37 | 0x4a =>
      raise Invalid("collection structural tag outside collection")
    _ => {
      if tag < 0x10 || tag > 0x7f {
        raise Unsupported("extended value tags")
      }
      Unknown(tag, bytes)
    }
  }
}

///|
fn encode_scalar(value : IppValue) -> Bytes raise IppError {
  let output = @buffer.Buffer()
  match value {
    Integer(n) => put_i32(output, n)
    Enumeration(n) => {
      if n < 1 {
        raise Invalid("enum must be positive")
      }
      put_i32(output, n)
    }
    Boolean(b) => put_byte(output, if b { 1 } else { 0 })
    Range(lower, upper) => {
      if lower > upper {
        raise Invalid("reversed range")
      }
      put_i32(output, lower)
      put_i32(output, upper)
    }
    ResolutionValue(resolution) => {
      if resolution.x < 1 ||
        resolution.y < 1 ||
        (resolution.units != 3 && resolution.units != 4) {
        raise Invalid("invalid resolution")
      }
      put_i32(output, resolution.x)
      put_i32(output, resolution.y)
      put_byte(output, resolution.units)
    }
    DateTime(date) => {
      date.validate()
      put_u16(output, date.year)
      put_byte(output, date.month)
      put_byte(output, date.day)
      put_byte(output, date.hour)
      put_byte(output, date.minute)
      put_byte(output, date.second)
      put_byte(output, date.decisecond)
      put_byte(output, date.utc_direction)
      put_byte(output, date.utc_hours)
      put_byte(output, date.utc_minutes)
    }
    Text(s)
    | Name(s)
    | Keyword(s)
    | Uri(s)
    | UriScheme(s)
    | Charset(s)
    | Language(s)
    | MimeType(s) => output.write_bytes(encode_utf8(s))
    Octets(bytes) => output.write_bytes(bytes)
    Unknown(tag, bytes) =>
      match decode_scalar(tag, bytes) {
        Unknown(_, _) => output.write_bytes(bytes)
        _ => raise Invalid("Unknown must use an unrecognized value tag")
      }
    TextWithLanguage(language, text) | NameWithLanguage(language, text) => {
      put_length_bytes(output, encode_utf8(language))
      put_length_bytes(output, encode_utf8(text))
    }
    OutOfBand(tag) =>
      if tag != 0x10 &&
        tag != 0x12 &&
        tag != 0x13 &&
        tag != 0x15 &&
        tag != 0x16 &&
        tag != 0x17 {
        raise Invalid("invalid out-of-band tag")
      }
    Collection(_) => raise Invalid("collection must use collection encoder")
  }
  output.to_bytes()
}

///|
fn write_entry(
  output : @buffer.Buffer,
  tag : Int,
  name : String,
  bytes : Bytes,
) -> Unit raise IppError {
  if tag < 0x10 || tag > 0x7f {
    raise Unsupported("extended value tags")
  }
  put_byte(output, tag)
  put_length_bytes(output, encode_utf8(name))
  put_length_bytes(output, bytes)
}

///|
priv struct DecodeBudget {
  limits : DecodeLimits
  mut attributes : Int
  mut values : Int
}

///|
fn DecodeBudget::attribute(self : DecodeBudget) -> Unit raise IppError {
  self.attributes += 1
  if self.attributes > self.limits.max_attributes {
    raise Limit("attribute count")
  }
}

///|
fn DecodeBudget::value(self : DecodeBudget) -> Unit raise IppError {
  self.values += 1
  if self.values > self.limits.max_values {
    raise Limit("value count")
  }
}

///|
fn decode_collection(
  reader : Reader,
  budget : DecodeBudget,
  depth : Int,
) -> Array[Attribute] raise IppError {
  if depth > budget.limits.max_collection_depth {
    raise Limit("collection nesting")
  }
  let members : Array[Attribute] = []
  let seen : Map[String, Bool] = Map([])
  let mut member_name = ""
  let mut member_values : Array[IppValue] = []
  while true {
    let tag = reader.byte()
    let name = reader.length_bytes()
    let bytes = reader.length_bytes()
    if name.length() != 0 {
      raise Invalid("collection entry name must be empty")
    }
    if tag == 0x37 {
      require_length(bytes, 0)
      if member_name != "" {
        if member_values.is_empty() {
          raise Invalid("collection member has no value")
        }
        members.push({ name: member_name, values: member_values, })
      }
      return members
    }
    if tag == 0x4a {
      if member_name != "" {
        if member_values.is_empty() {
          raise Invalid("collection member has no value")
        }
        members.push({ name: member_name, values: member_values, })
      }
      member_name = decode_utf8(bytes)
      if !valid_attribute_name(member_name) {
        raise Invalid("invalid collection member name")
      }
      if seen.contains(member_name) {
        raise Invalid("duplicate collection member")
      }
      seen[member_name] = true
      budget.attribute()
      member_values = []
    } else {
      if member_name == "" {
        raise Invalid("collection value before member name")
      }
      budget.value()
      let value = if tag == 0x34 {
        require_length(bytes, 0)
        IppValue::Collection(decode_collection(reader, budget, depth + 1))
      } else {
        decode_scalar(tag, bytes)
      }
      if member_values.length() > 0 && member_values[0].tag() != value.tag() {
        raise Invalid("collection member mixes value tags")
      }
      member_values.push(value)
    }
  } nobreak {
    raise Invalid("unterminated collection")
  }
}

///|
fn encode_collection(
  output : @buffer.Buffer,
  members : Array[Attribute],
  budget : DecodeBudget,
  depth : Int,
) -> Unit raise IppError {
  if depth > budget.limits.max_collection_depth {
    raise Limit("collection nesting")
  }
  let seen : Map[String, Bool] = Map([])
  for field in members {
    budget.attribute()
    if !valid_attribute_name(field.name) || field.values.is_empty() {
      raise Invalid("invalid collection member")
    }
    if seen.contains(field.name) {
      raise Invalid("duplicate collection member")
    }
    seen[field.name] = true
    write_entry(output, 0x4a, "", encode_utf8(field.name))
    for value in field.values {
      budget.value()
      if value.tag() != field.values[0].tag() {
        raise Invalid("collection member mixes value tags")
      }
      match value {
        Collection(children) => {
          write_entry(output, 0x34, "", b"")
          encode_collection(output, children, budget, depth + 1)
        }
        _ => write_entry(output, value.tag(), "", encode_scalar(value))
      }
      if output.length() > budget.limits.max_message_bytes {
        raise Limit("message bytes")
      }
    }
  }
  write_entry(output, 0x37, "", b"")
}