///|
/// One operation owns its budget, including keys and pointer traversal.
priv struct Decode {
  data : Bytes
  base : Int
  end : Int
  limits : Limits
  mut remaining : Int
  mut payload : Int
  mut next : Int
  active : Array[Int]
  work : OperationWork?
}

///|
fn decoder(
  data : Bytes,
  base : Int,
  end : Int,
  limits : Limits,
  work? : OperationWork? = None,
) -> Decode {
  {
    data,
    base,
    end,
    limits,
    remaining: limits.max_values,
    payload: limits.max_payload_bytes,
    next: base,
    active: [],
    work,
  }
}

///|
fn bounded(ctx : Decode, offset : Int, size : Int) -> Unit raise MmdbError {
  if offset < ctx.base ||
    size < 0 ||
    offset > ctx.end ||
    size > ctx.end - offset {
    raise MmdbError("out-of-bounds", offset, "Value exceeds its section")
  }
}

///|
fn uint_be(ctx : Decode, offset : Int, size : Int) -> UInt64 raise MmdbError {
  bounded(ctx, offset, size)
  let mut value : UInt64 = 0
  for i in offset..<(offset + size) {
    value = (value << 8) | ctx.data[i].to_uint64()
  }
  value
}

///|
// Encoded offsets and 16/32-bit scalars do not need a 64-bit accumulator.
fn uint32_be(ctx : Decode, offset : Int, size : Int) -> UInt raise MmdbError {
  bounded(ctx, offset, size)
  let mut value : UInt = 0
  for i in offset..<(offset + size) {
    value = (value << 8) | ctx.data[i].to_uint()
  }
  value
}

///|
/// Base-10 arithmetic avoids any floating-point conversion of UInt128.
fn decimal_bytes(data : Bytes, offset : Int, size : Int) -> String {
  let digits : Array[Int] = [0]
  for i in offset..<(offset + size) {
    let mut carry = data[i].to_int()
    for j in 0.. 0 {
      digits.push(carry % 10)
      carry = carry / 10
    }
  }
  let out = StringBuilder()
  for i = digits.length() - 1; i >= 0; i = i - 1 {
    out.write_string(digits[i].to_string())
  }
  out.to_string()
}

///|
fn decode(
  ctx : Decode,
  offset : Int,
  depth : Int,
) -> (Value, Int) raise MmdbError {
  let value = decode_into(ctx, offset, depth)
  (value, ctx.next)
}

///|
// Recursive calls share an operation-local next offset instead of allocating
// a (Value, Int) pair for every decoded value. Pointer returns restore their
// own encoded end; they never expose the referenced record's end to a parent.
fn decode_into(
  ctx : Decode,
  offset : Int,
  depth : Int,
) -> Value raise MmdbError {
  bounded(ctx, offset, 1)
  if depth > ctx.limits.max_depth {
    raise MmdbError("depth-limit", offset, "Nesting budget exhausted")
  }
  if ctx.remaining <= 0 {
    raise MmdbError("value-limit", offset, "Decode work budget exhausted")
  }
  if ctx.active.contains(offset) {
    raise MmdbError("pointer-cycle", offset, "Cyclic data reference")
  }
  ctx.remaining = ctx.remaining - 1
  if ctx.work is Some(work) {
    work.charge(1, offset)
  }
  ctx.active.push(offset)
  let result = decode_value(ctx, offset, depth)
  ignore(ctx.active.pop())
  result
}

///|
fn decode_value(
  ctx : Decode,
  offset : Int,
  depth : Int,
) -> Value raise MmdbError {
  let control = ctx.data[offset].to_int()
  let mut kind = control >> 5
  let mut pos = offset + 1
  if kind == 1 {
    let width = ((control >> 3) & 3) + 1
    let mut pointer = uint32_be(ctx, pos, width)
    if width < 4 {
      pointer = pointer | ((control & 7).reinterpret_as_uint() << (width * 8))
    }
    if width == 2 {
      pointer = pointer + 2048
    }
    if width == 3 {
      pointer = pointer + 526336
    }
    if pointer >= (ctx.end - ctx.base).reinterpret_as_uint() {
      raise MmdbError("out-of-bounds", offset, "Pointer outside data section")
    }
    let target = ctx.base + pointer.reinterpret_as_int()
    if ctx.data[target].to_int() >> 5 == 1 {
      raise MmdbError(
        "pointer-to-pointer", offset, "Pointer targets another pointer",
      )
    }
    let value = decode_into(ctx, target, depth + 1)
    ctx.next = pos + width
    return value
  }
  if kind == 0 {
    bounded(ctx, pos, 1)
    kind = ctx.data[pos].to_int() + 7
    pos = pos + 1
  }
  let mut size = control & 31
  if size >= 29 {
    let extra = size - 28
    let base = if size == 29 { 29 } else if size == 30 { 285 } else { 65821 }
    size = base + uint32_be(ctx, pos, extra).reinterpret_as_int()
    pos = pos + extra
  }
  if kind == 7 || kind == 11 {
    let children = if kind == 7 { size * 2 } else { size }
    if children > ctx.remaining {
      raise MmdbError(
        "value-limit", offset, "Declared container exceeds work budget",
      )
    }
    if kind == 11 {
      let items : Array[Value] = []
      for _ in 0.. 16 { Some(Map([])) } else { None }
    for _ in 0.. s
        _ =>
          raise MmdbError(
            "invalid-map-key", pos, "Map key must be a UTF-8 string",
          )
      }
      let duplicate = match seen {
        Some(seen) => seen.contains(name)
        None => items.any(fn(item) { item.0 == name })
      }
      if duplicate {
        raise MmdbError(
          "duplicate-key", pos, "Duplicate map key is outside reader profile",
        )
      }
      if seen is Some(seen) {
        seen[name] = true
      }
      let value = decode_into(ctx, next, depth + 1)
      items.push((name, value))
      pos = ctx.next
    }
    ctx.next = pos
    return Object(items)
  }
  if kind == 14 {
    if size > 1 {
      raise MmdbError(
        "invalid-size", offset, "Boolean size must be zero or one",
      )
    }
    ctx.next = pos
    return Boolean(size == 1)
  }
  let max_size = match kind {
    2 | 4 => size
    3 => 8
    5 => 2
    6 | 8 | 15 => 4
    9 => 8
    10 => 16
    _ =>
      raise MmdbError(
        "unsupported-type", offset, "Unsupported or reserved MMDB type",
      )
  }
  if size > max_size || ((kind == 3 || kind == 15) && size != max_size) {
    raise MmdbError("invalid-size", offset, "Invalid encoded scalar size")
  }
  bounded(ctx, pos, size)
  let end = pos + size
  let value = match kind {
    2 | 4 => {
      if size > ctx.payload {
        raise MmdbError(
          "payload-limit", offset, "Expanded bytes budget exhausted",
        )
      }
      ctx.payload = ctx.payload - size
      if ctx.work is Some(work) {
        work.charge((size + 63) / 64, offset)
      }
      if kind == 2 {
        let text = @utf8.decode(ctx.data[pos:end], ignore_bom=false) catch {
          _ => raise MmdbError("invalid-utf8", pos, "String is not valid UTF-8")
        }
        Text(text)
      } else {
        Blob(ctx.data[pos:end].to_owned())
      }
    }
    3 => Real64({ bits: uint_be(ctx, pos, size), })
    5 => Unsigned16(uint32_be(ctx, pos, size))
    6 => Unsigned32(uint32_be(ctx, pos, size))
    8 => Signed32(uint32_be(ctx, pos, size).reinterpret_as_int())
    9 => Unsigned64(uint_be(ctx, pos, size))
    10 => Unsigned128(decimal_bytes(ctx.data, pos, size))
    15 => Real32({ bits: uint32_be(ctx, pos, size), })
    _ => raise MmdbError("unsupported-type", offset, "Unsupported MMDB scalar")
  }
  ctx.next = end
  value
}