///|
struct CdrWire {
  bytes : Bytes
  little : Bool
  mut position : Int
  budget : Array[Int]
  aligned : Bool
}

///|
fn CdrWire::take(self : CdrWire, count : Int) -> Bytes raise McapError {
  if count < 0 || count > self.bytes.length() - self.position {
    raise Invalid("truncated XCDR1 payload")
  }
  let out = self.bytes[self.position:self.position + count].to_owned()
  self.position += count
  out
}

///|
fn CdrWire::unsigned(self : CdrWire, width : Int) -> UInt64 raise McapError {
  let padding = if self.aligned {
    (width - (self.position - 4) % width) % width
  } else {
    0
  }
  ignore(self.take(padding))
  let data = self.take(width)
  let mut result = 0UL
  for i = 0; i < width; i = i + 1 {
    result = result |
      (data[if self.little { i } else { width - i - 1 }].to_uint64() << (i * 8))
  }
  result
}

///|
fn CdrWire::item(self : CdrWire) -> Unit raise McapError {
  if self.budget[0] <= 0 {
    raise Invalid("CDR item budget exceeded")
  }
  self.budget[0] -= 1
}

///|
fn CdrDecoder::value(
  self : CdrDecoder,
  wire : CdrWire,
  field : CdrField,
  depth : Int,
) -> Json raise McapError {
  wire.item()
  match field.base {
    "bool" => {
      let value = wire.unsigned(1)
      if value > 1UL {
        raise Invalid("invalid CDR boolean")
      }
      Json::boolean(value == 1UL)
    }
    "byte" | "char" | "uint8" => Json::number(wire.unsigned(1).to_double())
    "int8" => {
      let value = wire.unsigned(1).to_int()
      Json::number((if value >= 128 { value - 256 } else { value }).to_double())
    }
    "uint16" => Json::number(wire.unsigned(2).to_double())
    "int16" => {
      let value = wire.unsigned(2).to_int()
      Json::number(
        (if value >= 32768 { value - 65536 } else { value }).to_double(),
      )
    }
    "uint32" => Json::number(wire.unsigned(4).to_double())
    "int32" => Json::number(wire.unsigned(4).to_uint().to_int().to_double())
    "uint64" | "int64" => {
      let value = wire.unsigned(8)
      let text = if field.base == "int64" {
        value.to_int64().to_string()
      } else {
        value.to_string()
      }
      if self.options.int64_as_string {
        Json::string(text)
      } else {
        Json::number(
          if field.base == "int64" {
            value.to_int64().to_double()
          } else {
            value.to_double()
          },
          repr=text,
        )
      }
    }
    "float64" => protobuf_float(wire.unsigned(8).reinterpret_as_double())
    "float32" => {
      let value = Float::reinterpret_from_uint(wire.unsigned(4).to_uint())
      if value.to_double().is_nan() || value.to_double().is_inf() {
        protobuf_float(value.to_double())
      } else {
        Json::number(value.to_double(), repr=value.to_string())
      }
    }
    "string" => {
      let length = wire.unsigned(4)
      if self.ros1 {
        if length > self.options.string_bytes.to_uint64() {
          raise Invalid("ROS1 string exceeds byte budget")
        }
        return Json::string(
          @utf8.decode(wire.take(length.to_int())[:]) catch {
            _ => raise Invalid("invalid ROS1 string UTF-8")
          },
        )
      }
      if length == 0UL || length - 1UL > self.options.string_bytes.to_uint64() {
        raise Invalid("CDR string length exceeds budget or lacks NUL")
      }
      let bytes = wire.take(length.to_int())
      if bytes[bytes.length() - 1] != 0x00 ||
        bytes[0:bytes.length() - 1].iter().any(fn(b) { b == 0x00 }) {
        raise Invalid("CDR string must have one terminal NUL")
      }
      if field.string_bound is Some(bound) && length - 1UL > bound.to_uint64() {
        raise Invalid("CDR string exceeds declared byte bound")
      }
      Json::string(
        @utf8.decode(bytes[0:bytes.length() - 1]) catch {
          _ => raise Invalid("invalid CDR string UTF-8")
        },
      )
    }
    _ => self.message(wire, field.base, depth + 1)
  }
}

///|
fn CdrDecoder::message(
  self : CdrDecoder,
  wire : CdrWire,
  name : String,
  depth : Int,
) -> Json raise McapError {
  if depth > self.options.depth {
    raise Invalid("CDR message depth budget exceeded")
  }
  let fields = self.definitions[name]
  let result : Map[String, Json] = Map([])
  if fields.is_empty() && !self.ros1 {
    wire.item()
    if wire.unsigned(1) != 0UL {
      raise Invalid("invalid ROS 2 empty message sentinel")
    }
  }
  for field in fields {
    let value = match field.array {
      None => self.value(wire, field, depth)
      Some(array) => {
        wire.item()
        let count = if array < 0 {
          let size = wire.unsigned(4)
          if size > wire.budget[0].to_uint64() {
            raise Invalid("CDR sequence exceeds item budget")
          }
          size.to_int()
        } else {
          array
        }
        if count > wire.budget[0] ||
          (field.bound is Some(bound) && count > bound) {
          raise Invalid("CDR array exceeds item or declared bound")
        }
        let list = []
        for i = 0; i < count; i = i + 1 {
          list.push(self.value(wire, field, depth))
        }
        Json::array(list)
      }
    }
    result[field.name] = value
  }
  if self.ros1 {
    validate_ros1_time(name, result)
  }
  Json::object(result)
}

///|
pub fn CdrDecoder::decode(
  self : CdrDecoder,
  bytes : Bytes,
) -> Json raise McapError {
  if bytes.length() < 4 || bytes.length() > self.options.message_bytes {
    raise Invalid("CDR payload byte budget or short encapsulation")
  }
  if bytes[0] != 0x00 ||
    (bytes[1] != 0x00 && bytes[1] != 0x01) ||
    bytes[2] != 0x00 ||
    bytes[3] != 0x00 {
    raise Invalid(
      "only plain XCDR1 with zero encapsulation options is supported",
    )
  }
  let wire : CdrWire = {
    bytes,
    little: bytes[1] == 0x01,
    position: 4,
    budget: [self.options.items],
    aligned: true,
  }
  let result = self.message(wire, self.name, 1)
  let remaining = bytes.length() - wire.position
  if remaining > 3 || bytes[wire.position:].iter().any(fn(b) { b != 0x00 }) {
    raise Invalid("trailing CDR payload bytes")
  }
  result
}