///|
/// Validated transport list. Accessors return detached snapshots.
pub struct Performative {
  priv code : Int32
  priv fields : Array[Value]
}

///|
fn clone_value(value : Value) -> Value {
  match value {
    List(items) => List(items.map(clone_value))
    MapValue(items) =>
      MapValue(items.map(pair => (clone_value(pair.0), clone_value(pair.1))))
    ArrayValue(code, descriptor, items) =>
      ArrayValue(code, descriptor.map(clone_value), items.map(clone_value))
    Described(d, v) => Described(clone_value(d), clone_value(v))
    _ => value
  }
}

///|
pub fn Performative::new(
  code : Int32,
  fields : Array[Value],
  limits~ : Limits = Limits::default(),
) -> Performative raise AmqpError {
  // Validate and copy through a bounded representation, including caller-owned arrays.
  let frozen = decode_value(encode_value(List(fields), limits~), limits~)
  let items = match frozen {
    List(items) => items
    _ => abort("list invariant")
  }
  validate_performative(code, items)
  { code, fields: items, }
}

///|
pub fn Performative::descriptor(self : Performative) -> Int32 {
  self.code
}

///|
pub fn Performative::field(self : Performative, index : Int32) -> Value {
  if index < 0 || index >= self.fields.length() {
    Null
  } else {
    clone_value(self.fields[index])
  }
}

///|
pub fn Performative::value(self : Performative) -> Value {
  Described(ULong(self.code.to_uint64()), List(self.fields.map(clone_value)))
}

///|
pub fn Performative::from_value(
  value : Value,
  limits~ : Limits = Limits::default(),
) -> Performative raise AmqpError {
  match value {
    Described(descriptor, List(fields)) => {
      let code = match descriptor {
        ULong(n) if n >= 0x10UL && n <= 0x18UL => n.to_int()
        Symbol(name) =>
          match name {
            "amqp:open:list" => 0x10
            "amqp:begin:list" => 0x11
            "amqp:attach:list" => 0x12
            "amqp:flow:list" => 0x13
            "amqp:transfer:list" => 0x14
            "amqp:disposition:list" => 0x15
            "amqp:detach:list" => 0x16
            "amqp:end:list" => 0x17
            "amqp:close:list" => 0x18
            _ => raise Unsupported("unknown performative descriptor")
          }
        _ => raise Unsupported("performative descriptor")
      }
      Performative::new(code, fields, limits~)
    }
    _ => raise Invalid("performative must be described list", 0)
  }
}

///|
fn field_at(fields : Array[Value], i : Int32) -> Value {
  if i < fields.length() {
    fields[i]
  } else {
    Null
  }
}

///|
fn wire_kind(value : Value, kind : Int32) -> Bool {
  if value is Null {
    return true
  }
  match (kind, value) {
    (1, Text(_))
    | (2, UInt(_))
    | (3, UShort(_))
    | (4, Boolean(_))
    | (5, UByte(_))
    | (6, ULong(_))
    | (7, Binary(_))
    | (8, Described(_, _)) => true
    (9, ArrayValue(0xb3, None, _)) | (9, Symbol(_)) => true
    (10, MapValue(entries)) => entries.all(pair => pair.0 is Symbol(_))
    (11, MapValue(_)) => true
    _ => false
  }
}

///|
fn validate_performative(
  code : Int32,
  fields : Array[Value],
) -> Unit raise AmqpError {
  let (kinds, mandatory) : (Array[Int32], Array[Int32]) = match code {
    0x10 => ([1, 1, 2, 3, 2, 9, 9, 9, 9, 10], [0])
    0x11 => ([3, 2, 2, 2, 2, 9, 9, 10], [1, 2, 3])
    0x12 => ([1, 2, 4, 5, 5, 8, 8, 11, 4, 2, 6, 9, 9, 10], [0, 1, 2])
    0x13 => ([2, 2, 2, 2, 2, 2, 2, 2, 4, 4, 10], [1, 2, 3])
    0x14 => ([2, 2, 7, 2, 4, 4, 5, 8, 4, 4, 4], [0])
    0x15 => ([4, 2, 2, 4, 8, 4], [0, 1])
    0x16 => ([2, 4, 8], [0])
    0x17 | 0x18 => ([8], [])
    _ => raise Unsupported("transport descriptor")
  }
  if fields.length() > kinds.length() {
    raise Unsupported("extra performative fields")
  }
  for index in mandatory {
    if field_at(fields, index) is Null {
      raise Invalid("missing mandatory performative field {index}", 0)
    }
  }
  for i = 0; i < fields.length(); i = i + 1 {
    if !wire_kind(fields[i], kinds[i]) {
      raise Invalid("performative field type {i}", 0)
    }
  }
  if code == 0x10 {
    if field_at(fields, 2) is UInt(n) && n < 512U {
      raise Invalid("max-frame-size below 512", 0)
    }
  }
  if code == 0x12 {
    if field_at(fields, 3) is UByte(n) && n > 2 {
      raise Invalid("sender settle mode", 0)
    }
    if field_at(fields, 4) is UByte(n) && n > 1 {
      raise Invalid("receiver settle mode", 0)
    }
    if field_at(fields, 2) is Boolean(false) && field_at(fields, 9) is Null {
      raise Invalid("sender initial-delivery-count required", 0)
    }
  }
  if code == 0x14 {
    if field_at(fields, 6) is UByte(n) && n > 1 {
      raise Invalid("transfer settle mode", 0)
    }
    if field_at(fields, 2) is Binary(tag) && tag.length() > 32 {
      raise Invalid("delivery tag exceeds 32 bytes", 0)
    }
  }
  if code == 0x16 {
    validate_error(field_at(fields, 2))
  }
  if code == 0x17 || code == 0x18 {
    validate_error(field_at(fields, 0))
  }
}

///|
fn uint_field(perf : Performative, index : Int32, fallback : UInt32) -> UInt32 {
  match field_at(perf.fields, index) {
    UInt(n) => n
    _ => fallback
  }
}

///|
fn bool_field(perf : Performative, index : Int32, fallback : Bool) -> Bool {
  match field_at(perf.fields, index) {
    Boolean(n) => n
    _ => fallback
  }
}

///|
test "mandatory transport fields and sender delivery count are enforced" {
  assert_true(
    try {
      ignore(Performative::new(0x10, []))
      false
    } catch {
      _ => true
    },
  )
  assert_true(
    try {
      ignore(Performative::new(0x12, [Text("link"), UInt(0U), Boolean(false)]))
      false
    } catch {
      _ => true
    },
  )
  let open = Performative::new(0x10, [Text("local"), Null, UInt(512U)])
  assert_eq(Performative::from_value(open.value()).descriptor(), 0x10)
  let fields = [Text("frozen")]
  let frozen = Performative::new(0x10, fields)
  fields[0] = Text("changed")
  assert_eq(frozen.field(0).as_text(), "frozen")
}