///|
/// A bit field in a decoded packet or bus payload.
pub struct PacketField {
  name : String
  lsb : Int
  width : Int
  signed : Bool
  display : String
} derive(Debug)

///|
/// A packet layout used by protocol and register analysis.
pub struct PacketLayout {
  name : String
  total_bits : Int
  fields : Array[PacketField]
  byte_order : String
} derive(Debug)

///|
/// A decoded packet field value.
pub struct PacketFieldValue {
  name : String
  raw : Int64?
  text : String
  valid : Bool
} derive(Debug)

///|
/// One packet captured at a protocol timestamp.
pub struct PacketRecord {
  index : Int
  time : Int64
  payload : Value?
  fields : Array[PacketFieldValue]
  valid : Bool
  error : String?
} derive(Debug)

///|
/// List supported packet byte orders.
pub fn packet_byte_order_catalog() -> Array[String] {
  ["msb0", "lsb0", "little-endian", "big-endian"]
}

///|
/// Create an empty packet layout.
pub fn packet_layout(name : String, total_bits : Int) -> PacketLayout {
  { name, total_bits: Int::max(1, total_bits), fields: [], byte_order: "msb0" }
}

///|
/// Set byte order.
pub fn PacketLayout::with_byte_order(
  self : PacketLayout,
  byte_order : String,
) -> PacketLayout {
  { ..self, byte_order, }
}

///|
/// Add a field, clamped to the layout width.
pub fn PacketLayout::with_field(
  self : PacketLayout,
  name : String,
  lsb : Int,
  width : Int,
  signed : Bool,
) -> PacketLayout {
  let safe_lsb = Int::max(0, lsb)
  let safe_width = Int::max(1, Int::min(width, self.total_bits - safe_lsb))
  let fields = self.fields.copy()
  fields.push({
    name,
    lsb: safe_lsb,
    width: safe_width,
    signed,
    display: "decimal",
  })
  { ..self, fields, }
}

///|
/// Decode one payload value according to a packet layout.
pub fn decode_packet(
  layout : PacketLayout,
  time : Int64,
  payload : Value?,
) -> PacketRecord {
  let fields : Array[PacketFieldValue] = []
  for field in layout.fields {
    let raw = match payload {
      Some(value) =>
        packet_extract_bits(value, field.lsb, field.width, layout.byte_order)
      None => None
    }
    fields.push({
      name: field.name,
      raw,
      text: packet_optional_int(raw),
      valid: raw is Some(_),
    })
  }
  let valid = payload is Some(_) && fields.all(fn(item) { item.valid })
  {
    index: 0,
    time,
    payload,
    fields,
    valid,
    error: if valid {
      None
    } else {
      Some("payload is absent or has unknown bits")
    },
  }
}

///|
/// Decode a sequence of packet payloads.
pub fn decode_packets(
  layout : PacketLayout,
  payloads : Array[(Int64, Value?)],
) -> Array[PacketRecord] {
  let result : Array[PacketRecord] = []
  for index, item in payloads {
    let record = decode_packet(layout, item.0, item.1)
    result.push({ ..record, index, })
  }
  result
}

///|
/// Return the first field with a named identifier.
pub fn PacketRecord::field(
  self : PacketRecord,
  name : String,
) -> PacketFieldValue? {
  for item in self.fields {
    if item.name == name {
      return Some(item)
    }
  }
  None
}

///|
/// Return all valid records.
pub fn valid_packets(records : Array[PacketRecord]) -> Array[PacketRecord] {
  records.filter(fn(item) { item.valid })
}

///|
/// Export packet records as CSV.
pub fn packet_records_csv(records : Array[PacketRecord]) -> String {
  let out = StringBuilder()
  out.write_string("index,time,valid,payload,fields,error\n")
  for record in records {
    out.write_string(
      record.index.to_string() +
      "," +
      record.time.to_string() +
      "," +
      record.valid.to_string() +
      ",",
    )
    out.write_string(
      match record.payload {
        Some(value) => escape_text(value.display())
        None => ""
      },
    )
    out.write_char(',')
    out.write_string(
      record.fields.map(fn(item) { item.name + "=" + item.text }).join(";"),
    )
    out.write_char(',')
    out.write_string(
      match record.error {
        Some(item) => escape_text(item)
        None => ""
      },
    )
    out.write_char('\n')
  }
  out.to_string()
}

///|
/// Render a layout as JSON.
pub fn PacketLayout::to_json(self : PacketLayout) -> String {
  let fields = self.fields.map(packet_field_json).join(",")
  "{\"name\":\"" +
  escape_text(self.name) +
  "\",\"total_bits\":" +
  self.total_bits.to_string() +
  ",\"byte_order\":\"" +
  self.byte_order +
  "\",\"fields\":[" +
  fields +
  "]}"
}

///|
/// Render a packet record as JSON.
pub fn PacketRecord::to_json(self : PacketRecord) -> String {
  let fields = self.fields.map(packet_field_value_json).join(",")
  "{\"index\":" +
  self.index.to_string() +
  ",\"time\":" +
  self.time.to_string() +
  ",\"valid\":" +
  self.valid.to_string() +
  ",\"payload\":" +
  packet_value_json(self.payload) +
  ",\"fields\":[" +
  fields +
  "]}"
}

///|
/// Extract a bounded unsigned field from scalar or vector data.
fn packet_extract_bits(
  value : Value,
  lsb : Int,
  width : Int,
  byte_order : String,
) -> Int64? {
  let bits = value.bits()
  if bits.length() == 0 {
    return None
  }
  let mut result = 0L
  for offset in 0..= bits.length() {
      return None
    }
    match bits[index] {
      One => result += 1L << offset
      Zero => ()
      _ => return None
    }
  }
  Some(result)
}

///|
/// Format an optional packet field value.
fn packet_optional_int(value : Int64?) -> String {
  match value {
    Some(item) => item.to_string()
    None => "?"
  }
}

///|
/// Encode an optional packet payload.
fn packet_value_json(value : Value?) -> String {
  match value {
    Some(item) => value_json(item)
    None => "null"
  }
}

///|
/// Encode one layout field.
fn packet_field_json(item : PacketField) -> String {
  "{\"name\":\"" +
  escape_text(item.name) +
  "\",\"lsb\":" +
  item.lsb.to_string() +
  ",\"width\":" +
  item.width.to_string() +
  ",\"signed\":" +
  item.signed.to_string() +
  "}"
}

///|
/// Encode one decoded field value.
fn packet_field_value_json(item : PacketFieldValue) -> String {
  let raw = match item.raw {
    Some(value) => value.to_string()
    None => "null"
  }
  "{\"name\":\"" +
  escape_text(item.name) +
  "\",\"raw\":" +
  raw +
  ",\"text\":\"" +
  escape_text(item.text) +
  "\",\"valid\":" +
  item.valid.to_string() +
  "}"
}