///|
/// AR startup request assembly for Write, PrmEnd, and ApplicationReady.
///
/// Both blocks use the ControlBlockConnect body layout. PrmEnd is sent as
/// IODControlReq (0x0110); ApplicationReady is sent as IOXControlReq0 (0x0112).
/// The CL-RPC envelope uses RPC opnum 3 for Write and opnum 4 for Control.

///|
pub fn encode_write_blocks(
  req : @record.IODWriteReqHeader,
  record_data : Bytes,
) -> Bytes {
  encode_io_block_with_user_data(
    @record.block_type_iod_write_req_header,
    @record.encode_iod_write_req_with_data(req, record_data),
    record_data.length(),
  )
}

///|
pub fn encode_multiple_write_blocks(
  outer_req : @record.IODWriteReqHeader,
  embedded_write_blocks : Bytes,
) -> Bytes {
  encode_io_block_with_user_data(
    @record.block_type_iod_write_req_header,
    @record.encode_iod_write_req_with_data(outer_req, embedded_write_blocks),
    embedded_write_blocks.length(),
  )
}

///|
pub fn encode_read_blocks(req : @record.IODReadReqHeader) -> Bytes {
  encode_io_block(
    @record.block_type_iod_read_req_header,
    @record.encode_iod_read_req_header(req),
  )
}

///|
pub fn encode_read_request(
  blocks : Bytes,
  activity_uuid : @rpc.Uuid,
  object_uuid? : @rpc.Uuid = @rpc.Uuid::empty(),
  sequence_number? : Int = 1,
  opnum? : Int = 2,
) -> Bytes raise @frame.FrameError {
  let ndr = @rpc.IoRequest::{
    ..@rpc.IoRequest::default(),
    args_length: blocks.length(),
    actual_count: blocks.length(),
    payload: blocks,
  }
  let ndr_bytes = @rpc.encode_io_request(ndr)
  let hdr = @rpc.DceHeader::{
    ..@rpc.DceHeader::default(),
    packet_type: @rpc.packet_type_request,
    object_uuid,
    interface_uuid: @rpc.Uuid::from_hex(@rpc.uuid_pnio_device),
    activity_uuid,
    interface_version: 1,
    opnum,
    sequence_number,
    body_length: ndr_bytes.length(),
  }
  let output : Array[Byte] = []
  append_bytes(output, @rpc.encode_dce_header(hdr))
  append_bytes(output, ndr_bytes)
  Bytes::from_array(output)
}

///|
pub fn encode_write_request(
  blocks : Bytes,
  activity_uuid : @rpc.Uuid,
  object_uuid? : @rpc.Uuid = @rpc.Uuid::empty(),
  sequence_number? : Int = 1,
) -> Bytes raise @frame.FrameError {
  let ndr = @rpc.IoRequest::{
    ..@rpc.IoRequest::default(),
    args_length: blocks.length(),
    actual_count: blocks.length(),
    payload: blocks,
  }
  let ndr_bytes = @rpc.encode_io_request(ndr)
  let hdr = @rpc.DceHeader::{
    ..@rpc.DceHeader::default(),
    packet_type: @rpc.packet_type_request,
    object_uuid,
    interface_uuid: @rpc.Uuid::from_hex(@rpc.uuid_pnio_device),
    activity_uuid,
    interface_version: 1,
    opnum: 3,
    sequence_number,
    body_length: ndr_bytes.length(),
  }
  let output : Array[Byte] = []
  append_bytes(output, @rpc.encode_dce_header(hdr))
  append_bytes(output, ndr_bytes)
  Bytes::from_array(output)
}

///|
fn encode_control_body(
  ar_uuid : @rpc.Uuid,
  session_key : Int,
  control_command : Int,
  control_block_properties : Int,
) -> Bytes {
  @record.encode_iod_control_req(@record.IODControlReq::{
    ar_uuid,
    session_key,
    control_command,
    control_block_properties,
  })
}

///|
pub fn encode_prm_end_blocks(ar_uuid : @rpc.Uuid, session_key : Int) -> Bytes {
  encode_io_block(
    @record.block_type_iod_control_req,
    encode_control_body(ar_uuid, session_key, @record.control_command_prmend, 0),
  )
}

///|
pub fn encode_application_ready_blocks(
  ar_uuid : @rpc.Uuid,
  session_key : Int,
) -> Bytes {
  encode_io_block(
    block_type_iox_control_req,
    encode_control_body(
      ar_uuid, session_key, @record.control_command_application_ready, 0,
    ),
  )
}

///|
pub fn encode_application_ready_response_blocks(
  ar_uuid : @rpc.Uuid,
  session_key : Int,
) -> Bytes {
  encode_io_block(
    block_type_iox_control_res,
    encode_control_body(ar_uuid, session_key, @record.control_command_done, 0),
  )
}

///|
pub fn encode_release_blocks(ar_uuid : @rpc.Uuid, session_key : Int) -> Bytes {
  encode_io_block(
    @record.block_type_release_req,
    @record.encode_release_block_req({
      ar_uuid,
      session_key,
      control_command: @record.control_command_release,
      control_block_properties: 0,
    }),
  )
}

///|
pub fn encode_control_request(
  blocks : Bytes,
  activity_uuid : @rpc.Uuid,
  object_uuid? : @rpc.Uuid = @rpc.Uuid::empty(),
  sequence_number? : Int = 1,
) -> Bytes raise @frame.FrameError {
  let ndr = @rpc.IoRequest::{
    ..@rpc.IoRequest::default(),
    args_length: blocks.length(),
    actual_count: blocks.length(),
    payload: blocks,
  }
  let ndr_bytes = @rpc.encode_io_request(ndr)
  let hdr = @rpc.DceHeader::{
    ..@rpc.DceHeader::default(),
    packet_type: @rpc.packet_type_request,
    object_uuid,
    interface_uuid: @rpc.Uuid::from_hex(@rpc.uuid_pnio_device),
    activity_uuid,
    interface_version: 1,
    opnum: 4,
    sequence_number,
    body_length: ndr_bytes.length(),
  }
  let output : Array[Byte] = []
  append_bytes(output, @rpc.encode_dce_header(hdr))
  append_bytes(output, ndr_bytes)
  Bytes::from_array(output)
}

///|
pub fn encode_release_request(
  blocks : Bytes,
  activity_uuid : @rpc.Uuid,
  object_uuid? : @rpc.Uuid = @rpc.Uuid::empty(),
  sequence_number? : Int = 1,
) -> Bytes raise @frame.FrameError {
  let ndr = @rpc.IoRequest::{
    ..@rpc.IoRequest::default(),
    args_length: blocks.length(),
    actual_count: blocks.length(),
    payload: blocks,
  }
  let ndr_bytes = @rpc.encode_io_request(ndr)
  let hdr = @rpc.DceHeader::{
    ..@rpc.DceHeader::default(),
    packet_type: @rpc.packet_type_request,
    object_uuid,
    interface_uuid: @rpc.Uuid::from_hex(@rpc.uuid_pnio_device),
    activity_uuid,
    interface_version: 1,
    opnum: 1,
    sequence_number,
    body_length: ndr_bytes.length(),
  }
  let output : Array[Byte] = []
  append_bytes(output, @rpc.encode_dce_header(hdr))
  append_bytes(output, ndr_bytes)
  Bytes::from_array(output)
}

///|
pub fn encode_control_response(
  blocks : Bytes,
  request_header : @rpc.DceHeader,
) -> Bytes {
  let ndr = @rpc.IoResponse::{
    status: 0,
    args_length: blocks.length(),
    max_count: 0xFE4,
    offset: 0,
    actual_count: blocks.length(),
    payload: blocks,
  }
  let ndr_bytes = @rpc.encode_io_response(ndr)
  let hdr = @rpc.DceHeader::{
    ..@rpc.DceHeader::default(),
    packet_type: @rpc.packet_type_response,
    object_uuid: request_header.object_uuid,
    interface_uuid: request_header.interface_uuid,
    activity_uuid: request_header.activity_uuid,
    interface_version: request_header.interface_version,
    opnum: request_header.opnum,
    sequence_number: request_header.sequence_number,
    body_length: ndr_bytes.length(),
  }
  let output : Array[Byte] = []
  append_bytes(output, @rpc.encode_dce_header(hdr))
  append_bytes(output, ndr_bytes)
  Bytes::from_array(output)
}

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

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

///|
fn control_request_uses_little_endian(request : Bytes) -> Bool {
  request.length() >= 7 && request[4] == b'\x10'
}

///|
fn encode_control_io_response_with_endian(
  blocks : Bytes,
  little_endian : Bool,
) -> Bytes {
  let output : Array[Byte] = []
  if little_endian {
    push_u32_le_local(output, 0)
    push_u32_le_local(output, blocks.length())
    push_u32_le_local(output, 0xFE4)
    push_u32_le_local(output, 0)
    push_u32_le_local(output, blocks.length())
  } else {
    push_u32_be(output, 0)
    push_u32_be(output, blocks.length())
    push_u32_be(output, 0xFE4)
    push_u32_be(output, 0)
    push_u32_be(output, blocks.length())
  }
  append_bytes(output, blocks)
  Bytes::from_array(output)
}

///|
fn encode_control_response_header_from_request(
  request : Bytes,
  body_length : Int,
  little_endian : Bool,
) -> Bytes raise @frame.FrameError {
  guard request.length() >= 80 else {
    raise @frame.FrameError::InvalidMacLength(request.length())
  }
  let output : Array[Byte] = []
  for i = 0; i < 74; i = i + 1 {
    if i == 1 {
      output.push(@rpc.packet_type_response)
    } else {
      output.push(request[i])
    }
  }
  if little_endian {
    push_u16_le_local(output, body_length)
  } else {
    push_u16_be(output, body_length)
  }
  for i = 76; i < 80; i = i + 1 {
    output.push(request[i])
  }
  Bytes::from_array(output)
}

///|
pub fn encode_control_response_from_request(
  blocks : Bytes,
  request : Bytes,
) -> Bytes raise @frame.FrameError {
  let little_endian = control_request_uses_little_endian(request)
  let ndr_bytes = encode_control_io_response_with_endian(blocks, little_endian)
  let hdr = encode_control_response_header_from_request(
    request,
    ndr_bytes.length(),
    little_endian,
  )
  let output : Array[Byte] = []
  append_bytes(output, hdr)
  append_bytes(output, ndr_bytes)
  Bytes::from_array(output)
}