///|
/// IO data object — references a slot/subslot within an IOCR.
pub(all) struct IoDataObject {
  slot_number : Int
  subslot_number : Int
  frame_offset : Int
} derive(Eq, Debug)

///|
/// API (Application Process Identifier) with its IO data objects.
pub(all) struct IocrApi {
  api : Int
  io_data_objects : Array[IoDataObject]
  iocs : Array[IoDataObject]
} derive(Eq, Debug)

///|
/// IOCR (IO Communication Relationship) block request.
pub(all) struct IOCRBlockReq {
  iocr_type : Int
  iocr_reference : Int
  lt : Int
  iocr_properties : Int
  data_length : Int
  frame_id : Int
  send_clock_factor : Int
  reduction_ratio : Int
  phase : Int
  sequence : Int
  frame_send_offset : Int
  watchdog_factor : Int
  data_hold_factor : Int
  iocr_tag_header : Int
  iocr_multicast_mac : Bytes
  apis : Array[IocrApi]
} derive(Eq, Debug)

///|
fn default_iocr_multicast_mac() -> Bytes {
  Bytes::make(6, b'\x00')
}

///|
/// Check the IOCR Phase invariant from PN-AL: 1 <= Phase <= ReductionRatio.
pub fn iocr_phase_valid(phase : Int, reduction_ratio : Int) -> Bool {
  phase > 0 && phase <= reduction_ratio
}

///|
/// Return true when Sequence uses the mandatory random-sequence mode.
pub fn iocr_sequence_is_random(sequence : Int) -> Bool {
  sequence == 0
}

///|
/// Label the IOCR Sequence mode. Non-zero values are defined optional sequences.
pub fn iocr_sequence_label(sequence : Int) -> String {
  if iocr_sequence_is_random(sequence) {
    "random"
  } else {
    "defined"
  }
}

///|
pub fn IOCRBlockReq::phase_valid(self : IOCRBlockReq) -> Bool {
  iocr_phase_valid(self.phase, self.reduction_ratio)
}

///|
pub fn IOCRBlockReq::sequence_is_random(self : IOCRBlockReq) -> Bool {
  iocr_sequence_is_random(self.sequence)
}

///|
pub fn IOCRBlockReq::default_input(frame_id : Int) -> IOCRBlockReq {
  IOCRBlockReq::{
    iocr_type: iocr_type_input,
    iocr_reference: 1,
    lt: 0x8892,
    iocr_properties: 2,
    data_length: 40,
    frame_id,
    send_clock_factor: 32,
    reduction_ratio: 2,
    phase: 1,
    sequence: 0,
    frame_send_offset: 0xFFFFFFFF,
    watchdog_factor: 3,
    data_hold_factor: 3,
    iocr_tag_header: 0xC000,
    iocr_multicast_mac: default_iocr_multicast_mac(),
    apis: [],
  }
}

///|
pub fn IOCRBlockReq::default_output(frame_id : Int) -> IOCRBlockReq {
  IOCRBlockReq::{
    iocr_type: iocr_type_output,
    iocr_reference: 2,
    lt: 0x8892,
    iocr_properties: 2,
    data_length: 40,
    frame_id,
    send_clock_factor: 32,
    reduction_ratio: 2,
    phase: 1,
    sequence: 0,
    frame_send_offset: 0xFFFFFFFF,
    watchdog_factor: 3,
    data_hold_factor: 3,
    iocr_tag_header: 0xC000,
    iocr_multicast_mac: default_iocr_multicast_mac(),
    apis: [],
  }
}

///|
fn encode_io_data_object(output : Array[Byte], obj : IoDataObject) -> Unit {
  push_u16_be(output, obj.slot_number)
  push_u16_be(output, obj.subslot_number)
  push_u16_be(output, obj.frame_offset)
}

///|
/// Encode IOCR block request body.
pub fn encode_iocr_block_req(req : IOCRBlockReq) -> Bytes {
  let output : Array[Byte] = []
  push_u16_be(output, req.iocr_type)
  push_u16_be(output, req.iocr_reference)
  push_u16_be(output, req.lt)
  push_u32_be(output, req.iocr_properties)
  push_u16_be(output, req.data_length)
  push_u16_be(output, req.frame_id)
  push_u16_be(output, req.send_clock_factor)
  push_u16_be(output, req.reduction_ratio)
  push_u16_be(output, req.phase)
  push_u16_be(output, req.sequence)
  push_u32_be(output, req.frame_send_offset)
  push_u16_be(output, req.watchdog_factor)
  push_u16_be(output, req.data_hold_factor)
  push_u16_be(output, req.iocr_tag_header)
  append_bytes(output, req.iocr_multicast_mac)
  // number of APIs
  push_u16_be(output, req.apis.length())
  for api in req.apis {
    push_u32_be(output, api.api)
    // number of IO data objects
    push_u16_be(output, api.io_data_objects.length())
    for obj in api.io_data_objects {
      encode_io_data_object(output, obj)
    }
    // number of IOCs
    push_u16_be(output, api.iocs.length())
    for obj in api.iocs {
      encode_io_data_object(output, obj)
    }
  }
  Bytes::from_array(output)
}

///|
/// IOCR block response.
pub(all) struct IOCRBlockRes {
  iocr_type : Int
  iocr_reference : Int
  frame_id : Int
} derive(Eq, Debug)

///|
/// Parse IOCR block response from body bytes.
pub fn parse_iocr_block_res(
  data : Bytes,
  offset : Int,
) -> IOCRBlockRes raise @frame.FrameError {
  guard data.length() >= offset + 6 else {
    raise @frame.FrameError::InvalidMacLength(data.length())
  }
  IOCRBlockRes::{
    iocr_type: read_u16_be(data, offset),
    iocr_reference: read_u16_be(data, offset + 2),
    frame_id: read_u16_be(data, offset + 4),
  }
}

///|
/// Format IOCR block request summary.
pub fn format_iocr_block_req(req : IOCRBlockReq) -> String {
  let lines : Array[String] = []
  lines.push("iocr_type=" + iocr_type_label(req.iocr_type))
  lines.push("iocr_reference=" + req.iocr_reference.to_string())
  lines.push("frame_id=0x" + @frame.uint16_to_hex(req.frame_id))
  lines.push("data_length=" + req.data_length.to_string())
  lines.push("send_clock_factor=" + req.send_clock_factor.to_string())
  lines.push("reduction_ratio=" + req.reduction_ratio.to_string())
  lines.push("phase=" + req.phase.to_string())
  lines.push(
    "sequence=" +
    req.sequence.to_string() +
    " (" +
    iocr_sequence_label(req.sequence) +
    ")",
  )
  lines.push("watchdog_factor=" + req.watchdog_factor.to_string())
  lines.push("api_count=" + req.apis.length().to_string())
  lines.join("\n")
}