///|
/// None removes an object member or array element. Set replaces the addressed
/// value, or adds a final object member. Parent containers must already exist.
pub(all) struct PayloadEdit {
  path : String
  replacement : Json?
}

///|
fn payload_edit_at(
  document : Json,
  path : Array[String],
  position : Int32,
  replacement : Json?,
) -> Json raise McapError {
  if position == path.length() {
    return match replacement {
      Some(value) => value
      None => raise Invalid("cannot remove payload root")
    }
  }
  let key = path[position]
  let last = position + 1 == path.length()
  match document {
    Object(fields) => {
      let copy : Map[String, Json] = Map([])
      for name in fields.keys() {
        copy[name] = fields[name]
      }
      if last {
        match replacement {
          Some(value) => copy[key] = value
          None => {
            if !copy.contains(key) {
              raise Invalid("payload removal path absent")
            }
            copy.remove(key) |> ignore
          }
        }
      } else {
        guard fields.get(key) is Some(child) else {
          raise Invalid("payload parent path absent")
        }
        copy[key] = payload_edit_at(child, path, position + 1, replacement)
      }
      Json::object(copy)
    }
    Array(items) => {
      let index = parse_decimal(key)
      if index >= items.length().to_uint64() || index.to_string() != key {
        raise Invalid("payload array index invalid")
      }
      let copy = items.copy()
      if last && replacement is None {
        copy.remove(index.to_int()) |> ignore
      } else {
        copy[index.to_int()] = payload_edit_at(
          items[index.to_int()],
          path,
          position + 1,
          replacement,
        )
      }
      Json::array(copy)
    }
    _ => raise Invalid("payload edit traverses a scalar")
  }
}

///|
fn payload_edit_paths(
  edits : Array[PayloadEdit],
) -> Array[Array[String]] raise McapError {
  if edits.length() > 128 {
    raise Invalid("payload edit count exceeds 128")
  }
  let paths = edits.map(e => json_pointer(e.path))
  for i = 0; i < paths.length(); i = i + 1 {
    for j = 0; j < i; j = j + 1 {
      let a = paths[i]
      let b = paths[j]
      let mut common = true
      for k = 0; k < a.length() && k < b.length(); k = k + 1 {
        if a[k] != b[k] {
          common = false
          break
        }
      }
      if common {
        raise Invalid("overlapping payload edits")
      }
    }
  }
  paths
}

///|
pub fn edit_payload_json(
  document : Json,
  edits : Array[PayloadEdit],
) -> Json raise McapError {
  let paths = payload_edit_paths(edits)
  let mut result = document
  // Array removals affect subsequent indices; their order is deliberately the
  // caller's explicit edit order, with each path resolved against that result.
  for i = 0; i < edits.length(); i = i + 1 {
    result = payload_edit_at(result, paths[i], 0, edits[i].replacement)
  }
  result
}

///|
fn payload_unsigned(value : Json, bits : Int32) -> UInt64 raise McapError {
  let text = match value {
    String(text) => text
    Number(_, ..) => value.stringify()
    _ => raise Invalid("payload unsigned integer expected")
  }
  let result = parse_decimal(text)
  if bits < 64 && result >= 1UL << bits {
    raise Invalid("payload unsigned integer out of range")
  }
  result
}

///|
fn payload_signed(value : Json, bits : Int32) -> UInt64 raise McapError {
  let number = proto_integer(value)
  if bits < 64 && (number < -(1L << (bits - 1)) || number >= 1L << (bits - 1)) {
    raise Invalid("payload signed integer out of range")
  }
  number.reinterpret_as_uint64()
}

///|
fn payload_float(value : Json) -> Double raise McapError {
  match value {
    Number(number, ..) if !number.is_nan() && !number.is_inf() => number
    String("NaN") => @double.not_a_number
    String("Infinity") => @double.infinity
    String("-Infinity") => @double.neg_infinity
    _ => raise Invalid("payload floating point value expected")
  }
}

///|
#warnings("-missing_priv")
struct PayloadOutput {
  buffer : @buffer.Buffer
  maximum : Int32
  budget : Array[Int32]
  little : Bool
  aligned : Bool
}

///|
fn PayloadOutput::write(
  self : PayloadOutput,
  bytes : Bytes,
) -> Unit raise McapError {
  if bytes.length() > self.maximum - self.buffer.length() {
    raise Invalid("encoded payload exceeds byte budget")
  }
  self.buffer.write_bytes(bytes[:])
}

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

///|
fn PayloadOutput::unsigned(
  self : PayloadOutput,
  number : UInt64,
  width : Int32,
) -> Unit raise McapError {
  let padding = if self.aligned {
    (width - (self.buffer.length() - 4) % width) % width
  } else {
    0
  }
  self.write(Bytes::make(padding, 0x00))
  let bytes = Bytes::makei(width, fn(i) {
    (number >> (8 * (if self.little { i } else { width - i - 1 }))).to_byte()
  })
  self.write(bytes)
}

///|
fn CdrDecoder::encode_value(
  self : CdrDecoder,
  wire : PayloadOutput,
  field : CdrField,
  value : Json,
  depth : Int32,
) -> Unit raise McapError {
  wire.item()
  match field.base {
    "bool" =>
      match value {
        True => wire.unsigned(1UL, 1)
        False => wire.unsigned(0UL, 1)
        _ => raise Invalid("payload boolean expected")
      }
    "byte" | "char" | "uint8" => wire.unsigned(payload_unsigned(value, 8), 1)
    "int8" => wire.unsigned(payload_signed(value, 8), 1)
    "uint16" => wire.unsigned(payload_unsigned(value, 16), 2)
    "int16" => wire.unsigned(payload_signed(value, 16), 2)
    "uint32" => wire.unsigned(payload_unsigned(value, 32), 4)
    "int32" => wire.unsigned(payload_signed(value, 32), 4)
    "uint64" => wire.unsigned(payload_unsigned(value, 64), 8)
    "int64" => wire.unsigned(payload_signed(value, 64), 8)
    "float64" => wire.unsigned(payload_float(value).reinterpret_as_uint64(), 8)
    "float32" => {
      let number = payload_float(value)
      let converted = Float::from_double(number)
      if !number.is_inf() && converted.to_double().is_inf() {
        raise Invalid("payload float32 overflow")
      }
      wire.unsigned(converted.reinterpret_as_uint().to_uint64(), 4)
    }
    "string" => {
      guard value is String(text) else {
        raise Invalid("payload string expected")
      }
      let bytes = @utf8.encode(text[:])
      if bytes.length() > self.options.string_bytes ||
        (field.string_bound is Some(bound) && bytes.length() > bound) ||
        (!self.ros1 && bytes.iter().any(fn(b) { b == 0x00 })) {
        raise Invalid("payload string exceeds bound or contains CDR NUL")
      }
      wire.unsigned(
        (bytes.length() + (if self.ros1 { 0 } else { 1 })).to_uint64(),
        4,
      )
      wire.write(bytes)
      if !self.ros1 {
        wire.write(b"\x00")
      }
    }
    _ => self.encode_message(wire, field.base, value, depth + 1)
  }
}

///|
fn CdrDecoder::encode_message(
  self : CdrDecoder,
  wire : PayloadOutput,
  name : String,
  value : Json,
  depth : Int32,
) -> Unit raise McapError {
  if depth > self.options.depth {
    raise Invalid("payload encoding depth exceeded")
  }
  guard value is Object(values) else {
    raise Invalid("payload message object expected")
  }
  let fields = self.definitions[name]
  for key in values.keys() {
    if !fields.any(fn(f) { f.name == key }) {
      raise Invalid("unknown payload field: " + key)
    }
  }
  if self.ros1 {
    validate_ros1_time(name, values)
  }
  if fields.is_empty() && !self.ros1 {
    wire.item()
    wire.unsigned(0UL, 1)
  }
  for field in fields {
    guard values.get(field.name) is Some(value) else {
      raise Invalid("required payload field absent: " + field.name)
    }
    match field.array {
      None => self.encode_value(wire, field, value, depth)
      Some(size) => {
        wire.item()
        guard value is Array(items) else {
          raise Invalid("payload array expected")
        }
        if items.length() > wire.budget[0] ||
          (size >= 0 && items.length() != size) ||
          (field.bound is Some(bound) && items.length() > bound) {
          raise Invalid("payload array size or budget exceeded")
        }
        if size < 0 {
          wire.unsigned(items.length().to_uint64(), 4)
        }
        for item in items {
          self.encode_value(wire, field, item, depth)
        }
      }
    }
  }
}

///|
pub fn CdrDecoder::encode(
  self : CdrDecoder,
  value : Json,
  little_endian~ : Bool = true,
) -> Bytes raise McapError {
  let wire : PayloadOutput = {
    buffer: @buffer.Buffer::Buffer(),
    maximum: self.options.message_bytes,
    budget: [self.options.items],
    little: little_endian,
    aligned: true,
  }
  wire.write(
    if little_endian {
      b"\x00\x01\x00\x00"
    } else {
      b"\x00\x00\x00\x00"
    },
  )
  self.encode_message(wire, self.name, value, 1)
  let bytes = wire.buffer.to_bytes()
  // Apply the same payload validation as the decoder before returning bytes.
  ignore(self.decode(bytes))
  bytes
}

///|
pub fn Ros1Decoder::encode(
  self : Ros1Decoder,
  value : Json,
) -> Bytes raise McapError {
  let wire : PayloadOutput = {
    buffer: @buffer.Buffer::Buffer(),
    maximum: self.layout.options.message_bytes,
    budget: [self.layout.options.items],
    little: true,
    aligned: false,
  }
  self.layout.encode_message(wire, self.layout.name, value, 1)
  let bytes = wire.buffer.to_bytes()
  ignore(self.decode(bytes))
  bytes
}