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

///|
fn EncodeBudget::visit(
  self : EncodeBudget,
  depth : Int32,
) -> Unit raise AmqpError {
  if depth > self.limits.max_depth || self.nodes >= self.limits.max_items {
    raise Limit("encoded value nesting or nodes")
  }
  self.nodes += 1
}

///|
/// Canonical full-width forms simplify deterministic output and homogeneous arrays.
pub fn encode_value(
  value : Value,
  limits~ : Limits = Limits::default(),
) -> Bytes raise AmqpError {
  limits.validate()
  let writer = wire_writer(limits.max_value_bytes)
  write_value(writer, value, { limits, nodes: 0, }, 0)
  writer.finish()
}

///|
fn write_value(
  writer : WireWriter,
  value : Value,
  budget : EncodeBudget,
  depth : Int32,
) -> Unit raise AmqpError {
  budget.visit(depth)
  match value {
    Described(descriptor, body) => {
      if !descriptor_valid(descriptor) {
        raise Invalid("descriptor must be ulong or symbol", 0)
      }
      writer.u8(0)
      write_value(writer, descriptor, budget, depth + 1)
      write_value(writer, body, budget, depth + 1)
    }
    _ => {
      let code = value_code(value)
      writer.u8(code)
      write_body(writer, code, value, budget, depth)
    }
  }
}

///|
fn require_range(
  value : Int32,
  low : Int32,
  high : Int32,
) -> Unit raise AmqpError {
  if value < low || value > high {
    raise Invalid("integer outside wire type", 0)
  }
}

///|
fn fixed_bytes(
  writer : WireWriter,
  data : Bytes,
  size : Int32,
) -> Unit raise AmqpError {
  if data.length() != size {
    raise Invalid("fixed binary width", 0)
  }
  writer.put(data)
}

///|
fn write_body(
  writer : WireWriter,
  code : Int32,
  value : Value,
  budget : EncodeBudget,
  depth : Int32,
) -> Unit raise AmqpError {
  if code != value_code(value) {
    raise Invalid("heterogeneous array element", 0)
  }
  match value {
    Null => ()
    Boolean(flag) => writer.u8(if flag { 1 } else { 0 })
    UByte(n) => {
      require_range(n, 0, 255)
      writer.u8(n)
    }
    UShort(n) => {
      require_range(n, 0, 65535)
      writer.number(n.to_uint64(), 2)
    }
    ByteValue(n) => {
      require_range(n, -128, 127)
      writer.u8(n & 255)
    }
    Short(n) => {
      require_range(n, -32768, 32767)
      writer.number((n & 65535).to_uint64(), 2)
    }
    UInt(n) => writer.number(n.to_uint64(), 4)
    ULong(n) => writer.number(n, 8)
    IntValue(n) => writer.number(n.reinterpret_as_uint().to_uint64(), 4)
    Long(n) | Timestamp(n) => writer.number(n.reinterpret_as_uint64(), 8)
    FloatValue(n) => writer.number(n.reinterpret_as_uint().to_uint64(), 4)
    DoubleValue(n) => writer.number(n.reinterpret_as_uint64(), 8)
    Decimal32(b) => fixed_bytes(writer, b, 4)
    Decimal64(b) => fixed_bytes(writer, b, 8)
    Decimal128(b) | Uuid(b) => fixed_bytes(writer, b, 16)
    Character(n) => {
      if n > 0x10ffffU || (n >= 0xd800U && n <= 0xdfffU) {
        raise Invalid("Unicode scalar", 0)
      }
      writer.number(n.to_uint64(), 4)
    }
    Binary(b) => {
      writer.number(b.length().to_uint64(), 4)
      writer.put(b)
    }
    Text(s) | Symbol(s) => {
      let b = text_bytes(s, maximum=writer.maximum - writer.bytes.length() - 4)
      if code == 0xb3 {
        for byte in b {
          if byte.to_int() > 127 {
            raise Invalid("non-ASCII symbol", 0)
          }
        }
      }
      writer.number(b.length().to_uint64(), 4)
      writer.put(b)
    }
    List(items) => write_sequence(writer, items, budget, depth)
    MapValue(entries) => {
      if entries.length() > budget.limits.max_items / 2 {
        raise Limit("map entries")
      }
      let body = wire_writer(budget.limits.max_value_bytes)
      body.number((entries.length() * 2).to_uint64(), 4)
      let seen : Map[Bytes, Bool] = Map([])
      for (key, item) in entries {
        let key_writer = wire_writer(budget.limits.max_value_bytes)
        write_value(key_writer, key, budget, depth + 1)
        let encoded_key = key_writer.finish()
        if seen.contains(encoded_key) {
          raise Invalid("duplicate map key value", 0)
        }
        seen[encoded_key] = true
        body.put(encoded_key)
        write_value(body, item, budget, depth + 1)
      }
      writer.number(body.bytes.length().to_uint64(), 4)
      writer.put(body.finish())
    }
    ArrayValue(element_code, descriptor, items) => {
      if items.length() > budget.limits.max_items {
        raise Limit("array entries")
      }
      let body = wire_writer(budget.limits.max_value_bytes)
      body.number(items.length().to_uint64(), 4)
      if element_code == 0 || element_code == 0x40 {
        raise Invalid("invalid array constructor", 0)
      }
      // Empty arrays still carry and validate the element constructor.
      if !canonical_array_code(element_code) {
        raise Unsupported("array constructor; use full-width code")
      }
      match descriptor {
        Some(d) => {
          if !descriptor_valid(d) {
            raise Invalid("array descriptor", 0)
          }
          body.u8(0)
          write_value(body, d, budget, depth + 1)
        }
        None => ()
      }
      body.u8(element_code)
      for item in items {
        budget.visit(depth + 1)
        write_body(body, element_code, item, budget, depth + 1)
      }
      writer.number(body.bytes.length().to_uint64(), 4)
      writer.put(body.finish())
    }
    Described(_, _) => raise Invalid("nested described array body", 0)
  }
}

///|
fn canonical_array_code(code : Int32) -> Bool {
  match code {
    0x50
    | 0x60
    | 0x70
    | 0x80
    | 0x51
    | 0x61
    | 0x71
    | 0x81
    | 0x56
    | 0x72
    | 0x82
    | 0x74
    | 0x84
    | 0x94
    | 0x73
    | 0x83
    | 0x98
    | 0xb0
    | 0xb1
    | 0xb3
    | 0xd0
    | 0xd1
    | 0xf0 => true
    _ => false
  }
}

///|
fn write_sequence(
  writer : WireWriter,
  items : Array[Value],
  budget : EncodeBudget,
  depth : Int32,
) -> Unit raise AmqpError {
  if items.length() > budget.limits.max_items {
    raise Limit("container entries")
  }
  let body = wire_writer(budget.limits.max_value_bytes)
  body.number(items.length().to_uint64(), 4)
  for item in items {
    write_value(body, item, budget, depth + 1)
  }
  writer.number(body.bytes.length().to_uint64(), 4)
  writer.put(body.finish())
}

///|
test "scalar encoder retains exact 64-bit unsigned and signed domains" {
  assert_eq(
    encode_value(ULong(18446744073709551615UL)),
    b"\x80\xff\xff\xff\xff\xff\xff\xff\xff",
  )
  assert_eq(encode_value(Long(-1L)), b"\x81\xff\xff\xff\xff\xff\xff\xff\xff")
  assert_eq(encode_value(Short(-2)), b"\x61\xff\xfe")
  assert_true(
    try {
      ignore(encode_value(UByte(256)))
      false
    } catch {
      _ => true
    },
  )
  assert_true(
    try {
      ignore(encode_value(Symbol("非ASCII")))
      false
    } catch {
      _ => true
    },
  )
}