///|
fn push_u16_be(output : Array[Byte], value : Int) -> Unit {
  output.push(((value >> 8) & 0xFF).to_byte())
  output.push((value & 0xFF).to_byte())
}

///|
fn push_u32_be(output : Array[Byte], value : Int) -> Unit {
  output.push(((value >> 24) & 0xFF).to_byte())
  output.push(((value >> 16) & 0xFF).to_byte())
  output.push(((value >> 8) & 0xFF).to_byte())
  output.push((value & 0xFF).to_byte())
}

///|
fn read_u16_be(data : Bytes, offset : Int) -> Int {
  (data[offset].to_int() << 8) + data[offset + 1].to_int()
}

///|
fn read_u32_be(data : Bytes, offset : Int) -> Int {
  (data[offset].to_int() << 24) +
  (data[offset + 1].to_int() << 16) +
  (data[offset + 2].to_int() << 8) +
  data[offset + 3].to_int()
}

///|
fn append_bytes(output : Array[Byte], data : Bytes) -> Unit {
  for byte in data {
    output.push(byte)
  }
}

///|
/// IO slot definition.
pub(all) struct IoSlot {
  slot_number : Int
  module_ident_number : Int
  subslots : Array[IoSubslot]
} derive(Eq, Debug)

///|
/// IO subslot definition.
pub(all) struct IoSubslot {
  subslot_number : Int
  submodule_ident_number : Int
} derive(Eq, Debug)

///|
/// Expected/Real identification data — block for AR connect.
pub(all) struct IdentificationData {
  api : Int
  slots : Array[IoSlot]
} derive(Eq, Debug)

///|
/// Block type for expected identification data.
pub let block_type_expected_identification : Int = 18

///|
/// Block type for real identification data.
pub let block_type_real_identification : Int = 19

///|
/// Encode expected/real identification data body.
pub fn encode_identification_data(data : IdentificationData) -> Bytes {
  let output : Array[Byte] = []
  // Number of APIs
  push_u16_be(output, 1)
  push_u32_be(output, data.api)
  // Number of slots
  push_u16_be(output, data.slots.length())
  for slot in data.slots {
    push_u16_be(output, slot.slot_number)
    push_u32_be(output, slot.module_ident_number)
    // Number of subslots
    push_u16_be(output, slot.subslots.length())
    for sub in slot.subslots {
      push_u16_be(output, sub.subslot_number)
      push_u32_be(output, sub.submodule_ident_number)
    }
  }
  Bytes::from_array(output)
}

///|
/// Parse identification data from body bytes.
pub fn parse_identification_data(
  data : Bytes,
  offset : Int,
) -> IdentificationData raise @frame.FrameError {
  guard data.length() >= offset + 8 else {
    raise @frame.FrameError::InvalidMacLength(data.length())
  }
  let api = read_u32_be(data, offset + 2)
  let num_slots = read_u16_be(data, offset + 6)
  let slots : Array[IoSlot] = []
  let mut pos = offset + 8
  for i = 0; i < num_slots; i = i + 1 {
    guard data.length() >= pos + 8 else {
      raise @frame.FrameError::InvalidMacLength(data.length())
    }
    let slot_number = read_u16_be(data, pos)
    let module_ident = read_u32_be(data, pos + 2)
    let num_subslots = read_u16_be(data, pos + 6)
    pos = pos + 8
    let subslots : Array[IoSubslot] = []
    for j = 0; j < num_subslots; j = j + 1 {
      guard data.length() >= pos + 6 else {
        raise @frame.FrameError::InvalidMacLength(data.length())
      }
      subslots.push(IoSubslot::{
        subslot_number: read_u16_be(data, pos),
        submodule_ident_number: read_u32_be(data, pos + 2),
      })
      pos = pos + 6
    }
    slots.push(IoSlot::{
      slot_number,
      module_ident_number: module_ident,
      subslots,
    })
  }
  IdentificationData::{ api, slots }
}

///|
/// IO connection state — models an active cyclic data connection.
pub(all) struct IoConnection {
  uuid : @rpc.Uuid
  rx_data_length : Int
  tx_data_length : Int
  is_tx_station_ok : Bool
  is_tx_provider_in_run : Bool
  is_tx_data_valid : Bool
  is_tx_primary : Bool
} derive(Eq, Debug)

///|
pub fn IoConnection::default(uuid : @rpc.Uuid) -> IoConnection {
  IoConnection::{
    uuid,
    rx_data_length: 40,
    tx_data_length: 40,
    is_tx_station_ok: true,
    is_tx_provider_in_run: true,
    is_tx_data_valid: true,
    is_tx_primary: true,
  }
}

///|
/// IO cyclic data frame — provider status byte + data.
pub(all) struct IoCyclicFrame {
  frame_id : Int
  cycle_counter : Int
  data_status : Byte
  transfer_status : Byte
  payload : Bytes
} derive(Eq, Debug)

///|
pub(all) struct DataStatusFields {
  state : String
  redundancy : String
  data_valid : String
  provider_state : String
  station_problem : String
  ignore : String
} derive(Eq, Debug)

///|
fn data_status_bit_set(status : Byte, mask : Int) -> Bool {
  (status.to_int() & mask) != 0
}

///|
pub fn decode_data_status(status : Byte) -> DataStatusFields {
  let primary = data_status_bit_set(status, 0x01)
  let redundancy_set = data_status_bit_set(status, 0x02)
  let state = if primary { "Primary" } else { "Backup" }
  let redundancy = if primary {
    if redundancy_set {
      "BackupAr"
    } else {
      "PrimaryAr"
    }
  } else if redundancy_set {
    "NoPrimaryArPresent"
  } else {
    "PrimaryArPresent"
  }
  let data_valid = if data_status_bit_set(status, 0x04) {
    "DataItemValid"
  } else {
    "DataItemInvalid"
  }
  let provider_state = if data_status_bit_set(status, 0x10) {
    "Run"
  } else {
    "Stop"
  }
  let station_problem = if data_status_bit_set(status, 0x20) {
    "NormalOperation"
  } else {
    "ProblemDetected"
  }
  let ignore = if data_status_bit_set(status, 0x80) {
    "Ignore"
  } else {
    "Evaluate"
  }
  { state, redundancy, data_valid, provider_state, station_problem, ignore }
}

///|
/// Encode a cyclic IO frame (for provider).
pub fn encode_cyclic_frame(frame : IoCyclicFrame) -> Bytes {
  let output : Array[Byte] = []
  push_u16_be(output, frame.frame_id)
  append_bytes(output, frame.payload)
  push_u16_be(output, frame.cycle_counter)
  output.push(frame.data_status)
  output.push(frame.transfer_status)
  Bytes::from_array(output)
}

///|
/// Parse a cyclic IO frame.
pub fn parse_cyclic_frame(
  data : Bytes,
  offset : Int,
  data_length : Int,
) -> IoCyclicFrame raise @frame.FrameError {
  guard data.length() >= offset + 2 + data_length + 4 else {
    raise @frame.FrameError::InvalidMacLength(data.length())
  }
  let frame_id = read_u16_be(data, offset)
  let payload_bytes : Array[Byte] = []
  for i = 0; i < data_length; i = i + 1 {
    payload_bytes.push(data[offset + 2 + i])
  }
  let status_offset = offset + 2 + data_length
  IoCyclicFrame::{
    frame_id,
    cycle_counter: read_u16_be(data, status_offset),
    data_status: data[status_offset + 2],
    transfer_status: data[status_offset + 3],
    payload: Bytes::from_array(payload_bytes),
  }
}

///|
/// I&M0 (Identification & Maintenance) data.
pub(all) struct IAndM0 {
  vendor_id : Int
  order_id : String
  serial_number : String
  hardware_revision : Int
  sw_revision_prefix : Byte
  sw_revision_functional_enhancement : Byte
  sw_revision_bugfix : Byte
  sw_revision_internal_change : Byte
  revision_counter : Int
  profile_id : Int
  profile_specific_type : Int
  im_version_major : Byte
  im_version_minor : Byte
  im_supported : Int
} derive(Eq, Debug)

///|
/// Block type for I&M0.
pub let block_type_iam0 : Int = 32

///|
/// Block type for I&M1.
pub let block_type_iam1 : Int = 33

///|
/// Parse I&M0 from body bytes.
pub fn parse_iam0(data : Bytes, offset : Int) -> IAndM0 raise @frame.FrameError {
  guard data.length() >= offset + 54 else {
    raise @frame.FrameError::InvalidMacLength(data.length())
  }
  let vendor_id = read_u16_be(data, offset)
  // order_id: 20 bytes ASCII
  let order_chars : Array[Byte] = []
  for i = 0; i < 20; i = i + 1 {
    order_chars.push(data[offset + 2 + i])
  }
  let order_id = ascii_bytes_to_string(order_chars)
  // serial_number: 16 bytes ASCII
  let serial_chars : Array[Byte] = []
  for i = 0; i < 16; i = i + 1 {
    serial_chars.push(data[offset + 22 + i])
  }
  let serial_number = ascii_bytes_to_string(serial_chars)
  IAndM0::{
    vendor_id,
    order_id,
    serial_number,
    hardware_revision: read_u16_be(data, offset + 38),
    sw_revision_prefix: data[offset + 40],
    sw_revision_functional_enhancement: data[offset + 41],
    sw_revision_bugfix: data[offset + 42],
    sw_revision_internal_change: data[offset + 43],
    revision_counter: read_u16_be(data, offset + 44),
    profile_id: read_u16_be(data, offset + 46),
    profile_specific_type: read_u16_be(data, offset + 48),
    im_version_major: data[offset + 50],
    im_version_minor: data[offset + 51],
    im_supported: read_u16_be(data, offset + 52),
  }
}

///|
/// I&M1 data.
pub(all) struct IAndM1 {
  function_tag : String
  location_tag : String
} derive(Eq, Debug)

///|
/// Parse I&M1 from body bytes.
pub fn parse_iam1(data : Bytes, offset : Int) -> IAndM1 raise @frame.FrameError {
  guard data.length() >= offset + 54 else {
    raise @frame.FrameError::InvalidMacLength(data.length())
  }
  let func_chars : Array[Byte] = []
  for i = 0; i < 32; i = i + 1 {
    func_chars.push(data[offset + i])
  }
  let loc_chars : Array[Byte] = []
  for i = 0; i < 22; i = i + 1 {
    loc_chars.push(data[offset + 32 + i])
  }
  IAndM1::{
    function_tag: ascii_bytes_to_trimmed_string(func_chars),
    location_tag: ascii_bytes_to_trimmed_string(loc_chars),
  }
}

///|
pub fn encode_iam1(data : IAndM1) -> Bytes {
  let output : Array[Byte] = []
  append_ascii_padded(output, data.function_tag, 32)
  append_ascii_padded(output, data.location_tag, 22)
  Bytes::from_array(output)
}

///|
pub fn encode_iam1_block(data : IAndM1) -> Bytes {
  let output : Array[Byte] = []
  push_u16_be(output, block_type_iam1)
  push_u16_be(output, 0x0038)
  output.push(b'\x01')
  output.push(b'\x00')
  append_bytes(output, encode_iam1(data))
  Bytes::from_array(output)
}

///|
fn append_ascii_padded(
  output : Array[Byte],
  text : String,
  width : Int,
) -> Unit {
  let mut count = 0
  for char in text {
    if count < width {
      output.push((char.to_int() & 0x7F).to_byte())
      count = count + 1
    }
  }
  for i = count; i < width; i = i + 1 {
    output.push(b'\x20')
  }
}

///|
fn ascii_bytes_to_string(bytes : Array[Byte]) -> String {
  let mut s = ""
  for b in bytes {
    if b.to_int() == 0 {
      break
    }
    s = s + b.to_int().unsafe_to_char().to_string()
  }
  s
}

///|
fn ascii_bytes_to_trimmed_string(bytes : Array[Byte]) -> String {
  let mut end = bytes.length()
  while end > 0 && (bytes[end - 1].to_int() == 0 || bytes[end - 1] == b'\x20') {
    end = end - 1
  }
  let mut s = ""
  for i = 0; i < end; i = i + 1 {
    if bytes[i].to_int() == 0 {
      return s
    }
    s = s + bytes[i].to_int().unsafe_to_char().to_string()
  }
  s
}

///|
/// Format I&M0 summary.
pub fn format_iam0(data : IAndM0) -> String {
  let lines : Array[String] = []
  lines.push("vendor_id=" + data.vendor_id.to_string())
  lines.push("order_id=" + data.order_id)
  lines.push("serial_number=" + data.serial_number)
  lines.push("hardware_revision=" + data.hardware_revision.to_string())
  lines.push(
    "sw_revision=" +
    data.sw_revision_prefix.to_int().unsafe_to_char().to_string() +
    data.sw_revision_functional_enhancement.to_int().to_string() +
    "." +
    data.sw_revision_bugfix.to_int().to_string() +
    "." +
    data.sw_revision_internal_change.to_int().to_string(),
  )
  lines.push("profile_id=" + data.profile_id.to_string())
  lines.push(
    "im_version=" +
    data.im_version_major.to_int().to_string() +
    "." +
    data.im_version_minor.to_int().to_string(),
  )
  lines.push("im_supported=0x" + @frame.uint16_to_hex(data.im_supported))
  lines.join("\n")
}