///|
pub fn valid_attribute_name(name : String) -> Bool {
  if name.length() < 1 || name.length() > 255 {
    return false
  }
  for char in name.to_array() {
    let n = char.to_int()
    if !((n >= 97 && n <= 122) ||
      (n >= 48 && n <= 57) ||
      n == 45 ||
      n == 95 ||
      n == 46) {
      return false
    }
  }
  true
}

///|
fn valid_group_tag(tag : Int) -> Bool {
  tag == 0x01 || tag == 0x02 || (tag >= 0x04 && tag <= 0x0a)
}

///|
pub fn decode(
  bytes : Bytes,
  limits? : DecodeLimits = DecodeLimits::defaults(),
) -> Message raise IppError {
  limits.validate()
  if bytes.length() < 9 {
    raise Truncated(0, 9)
  }
  let reader = Reader::new(bytes)
  reader.limit = limits.max_message_bytes
  let version = Version::new(reader.byte(), reader.byte())
  let code = reader.u16()
  let request_id = reader.i32()
  if request_id < 1 {
    raise Invalid("request-id must be positive")
  }
  let budget : DecodeBudget = { limits, attributes: 0, values: 0, }
  let groups : Array[Group] = []
  let mut group_tag = 0
  let mut attributes : Array[Attribute] = []
  while true {
    if reader.offset > limits.max_message_bytes {
      raise Limit("message bytes")
    }
    let tag = reader.byte()
    if tag == 0x03 {
      if group_tag != 0 {
        groups.push({ tag: group_tag, attributes, })
      }
      if reader.offset > limits.max_message_bytes {
        raise Limit("message bytes")
      }
      if reader.remaining() > limits.max_document_bytes {
        raise Limit("document bytes")
      }
      reader.limit = bytes.length()
      let document = reader.bytes(reader.remaining())
      return { version, code, request_id, groups, document, }
    }
    if tag < 0x10 {
      if !valid_group_tag(tag) {
        raise Invalid("invalid attribute group tag \{tag}")
      }
      if group_tag != 0 {
        groups.push({ tag: group_tag, attributes, })
      }
      group_tag = tag
      attributes = []
      continue
    }
    if group_tag == 0 {
      raise Invalid("attribute before group delimiter")
    }
    let name = decode_utf8(reader.length_bytes())
    let payload = reader.length_bytes()
    budget.value()
    let value = if tag == 0x34 {
      require_length(payload, 0)
      IppValue::Collection(decode_collection(reader, budget, 1))
    } else {
      decode_scalar(tag, payload)
    }
    if name == "" {
      if attributes.is_empty() {
        raise Invalid("additional value without an attribute")
      }
      let last = attributes[attributes.length() - 1]
      if last.values[0].tag() != value.tag() {
        raise Invalid("attribute mixes value tags")
      }
      last.values.push(value)
    } else {
      budget.attribute()
      if !valid_attribute_name(name) {
        raise Invalid("invalid attribute name")
      }
      if attributes.any(fn(attribute) { attribute.name == name }) {
        raise Invalid("duplicate attribute within group")
      }
      attributes.push({ name, values: [value], })
    }
  } nobreak {
    raise Invalid("unterminated message")
  }
}

///|
pub fn encode(
  message : Message,
  limits? : DecodeLimits = DecodeLimits::defaults(),
) -> Bytes raise IppError {
  limits.validate()
  let _ = Version::new(message.version.major, message.version.minor)
  if message.request_id < 1 {
    raise Invalid("request-id must be positive")
  }
  if message.document.length() > limits.max_document_bytes {
    raise Limit("document bytes")
  }
  let output = @buffer.Buffer()
  let budget : DecodeBudget = { limits, attributes: 0, values: 0, }
  put_byte(output, message.version.major)
  put_byte(output, message.version.minor)
  put_u16(output, message.code)
  put_i32(output, message.request_id)
  for group in message.groups {
    if !valid_group_tag(group.tag) {
      raise Invalid("invalid group tag")
    }
    put_byte(output, group.tag)
    let seen : Map[String, Bool] = Map([])
    for attribute in group.attributes {
      budget.attribute()
      if !valid_attribute_name(attribute.name) || attribute.values.is_empty() {
        raise Invalid("invalid attribute")
      }
      if seen.contains(attribute.name) {
        raise Invalid("duplicate attribute within group")
      }
      seen[attribute.name] = true
      for index, value in attribute.values {
        budget.value()
        if value.tag() != attribute.values[0].tag() {
          raise Invalid("attribute mixes value tags")
        }
        let name = if index == 0 { attribute.name } else { "" }
        match value {
          Collection(members) => {
            write_entry(output, 0x34, name, b"")
            encode_collection(output, members, budget, 1)
          }
          _ => write_entry(output, value.tag(), name, encode_scalar(value))
        }
        if output.length() > limits.max_message_bytes {
          raise Limit("message bytes")
        }
      }
      if output.length() > limits.max_message_bytes {
        raise Limit("message bytes")
      }
    }
  }
  put_byte(output, 0x03)
  if output.length() > limits.max_message_bytes {
    raise Limit("message bytes")
  }
  output.write_bytes(message.document)
  output.to_bytes()
}