///|
priv struct Budget {
  mut nodes : Int32
  limits : Limits
}

///|
fn Budget::visit(self : Budget, depth : Int32) -> Unit raise BacnetError {
  if depth > self.limits.max_depth || self.nodes >= self.limits.max_nodes {
    raise Limit("tag nesting or node count")
  }
  self.nodes += 1
}

///|
fn tag_header(
  w : Writer,
  number : Int32,
  context : Bool,
  lvt : Int32,
) -> Unit raise BacnetError {
  require_range(number, 0, 254, "tag number")
  let nibble = if number < 15 { number } else { 15 }
  w.u8((nibble << 4) | (if context { 8 } else { 0 }) | lvt)
  if number >= 15 {
    w.u8(number)
  }
}

///|
fn primitive_header(
  w : Writer,
  number : Int32,
  context : Bool,
  length : Int32,
) -> Unit raise BacnetError {
  if length < 0 {
    raise Invalid("negative length", 0)
  }
  tag_header(w, number, context, if length < 5 { length } else { 5 })
  if length >= 5 {
    if length <= 253 {
      w.u8(length)
    } else if length <= 65535 {
      w.u8(254)
      w.uint(length.reinterpret_as_uint(), 2)
    } else {
      w.u8(255)
      w.uint(length.reinterpret_as_uint(), 4)
    }
  }
}

///|
fn encode_tag(
  w : Writer,
  tag : Tag,
  b : Budget,
  depth : Int32,
) -> Unit raise BacnetError {
  b.visit(depth)
  match tag {
    Application(Boolean(value)) =>
      tag_header(w, 1, false, if value { 1 } else { 0 })
    Application(value) => {
      let (kind, body) = application_body(value, b.limits)
      primitive_header(w, kind, false, body.length())
      w.put(body)
    }
    Context(number, bytes) => {
      primitive_header(w, number, true, bytes.length())
      w.put(bytes)
    }
    Constructed(number, children) => {
      tag_header(w, number, true, 6)
      for child in children {
        encode_tag(w, child, b, depth + 1)
      }
      tag_header(w, number, true, 7)
    }
  }
}

///|
pub fn encode_tags(
  tags : Array[Tag],
  limits~ : Limits = Limits::default(),
) -> Bytes raise BacnetError {
  limits.validate()
  let w = Writer::new(limits.max_bytes)
  let b : Budget = { nodes: 0, limits, }
  for tag in tags {
    encode_tag(w, tag, b, 0)
  }
  w.finish()
}

///|
fn decode_sequence(
  r : Reader,
  b : Budget,
  depth : Int32,
  closing : Int32?,
) -> Array[Tag] raise BacnetError {
  let result : Array[Tag] = []
  while r.remaining() > 0 {
    let start = r.pos
    let first = r.u8()
    let context = (first & 8) != 0
    let mut number = first >> 4
    let lvt = first & 7
    if number == 15 {
      number = r.u8()
      if number == 255 {
        raise Invalid("reserved extended tag", start)
      }
    }
    if context && lvt == 7 {
      if closing == Some(number) {
        return result
      }
      raise Invalid("mismatched closing tag", start)
    }
    b.visit(depth)
    if context && lvt == 6 {
      result.push(
        Constructed(number, decode_sequence(r, b, depth + 1, Some(number))),
      )
      continue
    }
    if !context && number == 1 {
      if lvt > 1 {
        raise Invalid("application boolean LVT", start)
      }
      result.push(Application(Boolean(lvt == 1)))
      continue
    }
    if lvt >= 6 {
      raise Invalid("application opening or closing tag", start)
    }
    let mut length = lvt
    if lvt == 5 {
      length = r.u8()
      if length == 254 {
        length = r.uint(2).reinterpret_as_int()
      } else if length == 255 {
        let wide = r.uint(4)
        if wide > b.limits.max_bytes.reinterpret_as_uint() {
          raise Limit("tag payload")
        }
        length = wide.reinterpret_as_int()
      }
    }
    let body = r.take(length)
    result.push(
      if context {
        Context(number, body)
      } else {
        Application(application_value(number, body, b.limits))
      },
    )
  }
  if closing != None {
    raise Invalid("missing closing tag", r.pos)
  }
  result
}

///|
pub fn decode_tags(
  data : Bytes,
  limits~ : Limits = Limits::default(),
) -> Array[Tag] raise BacnetError {
  let r = Reader::new(data, limits)
  decode_sequence(r, { nodes: 0, limits, }, 0, None)
}

///|
fn clone_tags(
  tags : Array[Tag],
  limits : Limits,
) -> Array[Tag] raise BacnetError {
  decode_tags(encode_tags(tags, limits~), limits~)
}