///|
/// Endpoint Mapper lookup opnum used by JIYUAN `DceClient.LookupEndPointMapper`.
pub let endpoint_mapper_opnum_lookup : Int = 2

///|
pub(all) struct EndpointMapperObject {
  uuid : Uuid
  tower_referent_id : Int
  annotation_hex : String
  udp_port : Int?
  ip_text : String?
  rpc_version_minor : Int?
  uuid_floor_uuids : Array[Uuid]
  uuid_floors : Array[String]
  floor_count : Int
} derive(Eq, Debug)

///|
pub(all) struct EndpointMapperResponse {
  handle : Bytes
  object_count : Int
  objects : Array[EndpointMapperObject]
  return_code : Int
} derive(Eq, Debug)

///|
fn copy_bytes(
  data : Bytes,
  offset : Int,
  length : Int,
) -> Bytes raise @frame.FrameError {
  guard data.length() >= offset + length else {
    raise @frame.FrameError::InvalidMacLength(data.length())
  }
  let output : Array[Byte] = []
  for i in offset..<(offset + length) {
    output.push(data[i])
  }
  Bytes::from_array(output)
}

///|
fn align4(value : Int) -> Int {
  let rem = value % 4
  if rem == 0 {
    value
  } else {
    value + 4 - rem
  }
}

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

///|
/// Encode the EPM lookup request body (`aa.a()` in the C# reference).
pub fn encode_endpoint_mapper_lookup_body(
  handle? : Bytes = Bytes::make(20, b'\x00'),
) -> Bytes raise @frame.FrameError {
  guard handle.length() == 20 else {
    raise @frame.FrameError::InvalidMacLength(handle.length())
  }
  let output : Array[Byte] = []
  push_u32_le(output, 0) // inquiry type: x.a
  push_u32_le(output, 1)
  append_bytes(output, Uuid::empty().to_dce_bytes_le())
  push_u32_le(output, 2)
  append_bytes(output, Uuid::from_hex(uuid_pnio_device).to_dce_bytes_le())
  push_u16_le(output, 1)
  push_u16_le(output, 0)
  push_u32_le(output, 1)
  append_bytes(output, handle)
  push_u32_le(output, 1)
  Bytes::from_array(output)
}

///|
/// Assemble a DCE/RPC Endpoint Mapper lookup request packet.
pub fn encode_endpoint_mapper_lookup_request(
  activity_uuid : Uuid,
  sequence_number? : Int = 0,
  handle? : Bytes = Bytes::make(20, b'\x00'),
) -> Bytes raise @frame.FrameError {
  let body = encode_endpoint_mapper_lookup_body(handle~)
  let hdr = DceHeader::{
    ..DceHeader::default(),
    packet_type: packet_type_request,
    interface_uuid: Uuid::from_hex(uuid_endpoint_mapper),
    activity_uuid,
    opnum: endpoint_mapper_opnum_lookup,
    sequence_number,
    body_length: body.length(),
  }
  let output : Array[Byte] = []
  append_bytes(output, encode_dce_header(hdr))
  append_bytes(output, body)
  Bytes::from_array(output)
}

///|
fn parse_endpoint_mapper_floor(
  data : Bytes,
  cursor : Int,
  udp_port : Int?,
  ip_text : String?,
) -> (Int, Int?, String?, Int?, Uuid?, String?) raise @frame.FrameError {
  guard data.length() >= cursor + 4 else {
    raise @frame.FrameError::InvalidMacLength(data.length())
  }
  let lhs_len = read_u16_le(data, cursor)
  guard data.length() >= cursor + lhs_len + 4 else {
    raise @frame.FrameError::InvalidMacLength(data.length())
  }
  let floor_type = data[cursor + 2].to_int()
  let rhs_len_offset = cursor + lhs_len + 2
  guard data.length() >= rhs_len_offset + 2 else {
    raise @frame.FrameError::InvalidMacLength(data.length())
  }
  let rhs_len = read_u16_le(data, rhs_len_offset)
  let floor_end = cursor + lhs_len + rhs_len + 4
  guard data.length() >= floor_end else {
    raise @frame.FrameError::InvalidMacLength(data.length())
  }
  let floor_data_start = cursor + 3
  let floor_data_len = lhs_len + rhs_len + 1
  let mut next_udp_port = udp_port
  let mut next_ip_text = ip_text
  let mut rpc_version_minor : Int? = None
  let mut uuid_floor_uuid : Uuid? = None
  let mut uuid_floor : String? = None
  if floor_type == 8 &&
    floor_data_len >= 4 &&
    data.length() >= floor_data_start + 4 {
    next_udp_port = Some(
      (data[floor_data_start + 2].to_int() << 8) +
      data[floor_data_start + 3].to_int(),
    )
  }
  if floor_type == 9 &&
    floor_data_len >= 6 &&
    data.length() >= floor_data_start + 6 {
    let ip = @frame.IPv4Address::new(
      Bytes::from_array([
        data[floor_data_start + 2],
        data[floor_data_start + 3],
        data[floor_data_start + 4],
        data[floor_data_start + 5],
      ]),
    )
    next_ip_text = Some(ip.to_text())
  }
  if floor_type == 10 &&
    floor_data_len >= 4 &&
    data.length() >= floor_data_start + 4 {
    rpc_version_minor = Some(read_u16_be_local(data, floor_data_start + 2))
  }
  if floor_type == 13 &&
    floor_data_len >= 22 &&
    data.length() >= floor_data_start + 22 {
    let uuid = Uuid::from_dce_bytes_le(data, floor_data_start)
    uuid_floor_uuid = Some(uuid)
    uuid_floor = Some(
      uuid.to_hex() +
      "@version:" +
      data[floor_data_start + 16].to_int().to_string() +
      "." +
      data[floor_data_start + 17].to_int().to_string() +
      ",minor:" +
      read_u16_be_local(data, floor_data_start + 20).to_string(),
    )
  }
  (
    floor_end, next_udp_port, next_ip_text, rpc_version_minor, uuid_floor_uuid, uuid_floor,
  )
}

///|
/// Parse the DCE body of an Endpoint Mapper lookup response.
pub fn parse_endpoint_mapper_response_body(
  data : Bytes,
) -> EndpointMapperResponse raise @frame.FrameError {
  guard data.length() >= 40 else {
    raise @frame.FrameError::InvalidMacLength(data.length())
  }
  let handle = copy_bytes(data, 0, 20)
  let mut cursor = 24
  guard data.length() >= cursor + 12 else {
    raise @frame.FrameError::InvalidMacLength(data.length())
  }
  let object_count = read_u32_le(data, cursor + 8)
  cursor = cursor + 12
  let objects : Array[EndpointMapperObject] = []
  let mut remaining = object_count
  while remaining > 0 {
    guard data.length() >= cursor + 28 else {
      raise @frame.FrameError::InvalidMacLength(data.length())
    }
    let uuid = Uuid::from_dce_bytes_le(data, cursor)
    let tower_referent_id = read_u32_le(data, cursor + 16)
    let annotation_len = read_u32_le(data, cursor + 24)
    let annotation_start = cursor + 28
    let annotation = copy_bytes(data, annotation_start, annotation_len)
    cursor = align4(annotation_start + annotation_len)
    guard data.length() >= cursor + 10 else {
      raise @frame.FrameError::InvalidMacLength(data.length())
    }
    let mut floors = read_u16_le(data, cursor + 8)
    cursor = cursor + 10
    let mut floor_count = 0
    let mut udp_port : Int? = None
    let mut ip_text : String? = None
    let mut rpc_version_minor : Int? = None
    let uuid_floor_uuids : Array[Uuid] = []
    let uuid_floors : Array[String] = []
    while floors > 0 {
      let (
        next_cursor,
        next_udp_port,
        next_ip_text,
        next_rpc_version_minor,
        uuid_floor_uuid,
        uuid_floor,
      ) = parse_endpoint_mapper_floor(data, cursor, udp_port, ip_text)
      cursor = next_cursor
      udp_port = next_udp_port
      ip_text = next_ip_text
      match next_rpc_version_minor {
        Some(value) => rpc_version_minor = Some(value)
        None => ()
      }
      match uuid_floor_uuid {
        Some(value) => uuid_floor_uuids.push(value)
        None => ()
      }
      match uuid_floor {
        Some(value) => uuid_floors.push(value)
        None => ()
      }
      floor_count = floor_count + 1
      floors = floors - 1
    }
    objects.push(EndpointMapperObject::{
      uuid,
      tower_referent_id,
      annotation_hex: @frame.bytes_to_hex(annotation, separator=""),
      udp_port,
      ip_text,
      rpc_version_minor,
      uuid_floor_uuids,
      uuid_floors,
      floor_count,
    })
    remaining = remaining - 1
  }
  guard data.length() >= cursor + 4 else {
    raise @frame.FrameError::InvalidMacLength(data.length())
  }
  EndpointMapperResponse::{
    handle,
    object_count,
    objects,
    return_code: read_u32_le(data, cursor),
  }
}

///|
pub fn first_endpoint_mapper_object_uuid(
  response : EndpointMapperResponse,
) -> Uuid? {
  if response.objects.length() > 0 && response.objects[0].uuid != Uuid::empty() {
    Some(response.objects[0].uuid)
  } else {
    None
  }
}

///|
pub fn endpoint_mapper_handle_is_zero(handle : Bytes) -> Bool {
  for byte in handle {
    if byte != b'\x00' {
      return false
    }
  }
  true
}

///|
pub fn endpoint_mapper_response_matches_interface(
  response : EndpointMapperResponse,
  interface_uuid : Uuid,
) -> Bool {
  for object in response.objects {
    for uuid in object.uuid_floor_uuids {
      if uuid == interface_uuid {
        return true
      }
    }
  }
  false
}

///|
pub fn endpoint_mapper_object_uuid_for_interface(
  response : EndpointMapperResponse,
  interface_uuid : Uuid,
) -> Uuid? {
  for object in response.objects {
    for uuid in object.uuid_floor_uuids {
      if uuid == interface_uuid {
        return Some(object.uuid)
      }
    }
  }
  None
}

///|
pub fn endpoint_mapper_udp_port_for_interface(
  response : EndpointMapperResponse,
  interface_uuid : Uuid,
) -> Int? {
  for object in response.objects {
    for uuid in object.uuid_floor_uuids {
      if uuid == interface_uuid {
        return object.udp_port
      }
    }
  }
  None
}

///|
pub fn format_endpoint_mapper_response(
  response : EndpointMapperResponse,
) -> String {
  let lines : Array[String] = []
  lines.push("epm_return_code=0x" + @frame.uint32_to_hex(response.return_code))
  lines.push(
    "epm_handle_hex=" + @frame.bytes_to_hex(response.handle, separator=""),
  )
  lines.push("epm_object_count=" + response.object_count.to_string())
  for index, object in response.objects {
    lines.push(
      "epm_object_" +
      (index + 1).to_string() +
      "=uuid:" +
      object.uuid.to_hex() +
      ",annotation_hex:" +
      object.annotation_hex +
      ",floors:" +
      object.floor_count.to_string(),
    )
    match object.udp_port {
      Some(port) =>
        lines.push(
          "epm_object_" +
          (index + 1).to_string() +
          "_udp_port=" +
          port.to_string(),
        )
      None => ()
    }
    match object.ip_text {
      Some(ip) =>
        lines.push("epm_object_" + (index + 1).to_string() + "_ip=" + ip)
      None => ()
    }
    match object.rpc_version_minor {
      Some(value) =>
        lines.push(
          "epm_object_" +
          (index + 1).to_string() +
          "_rpc_version_minor=" +
          value.to_string(),
        )
      None => ()
    }
    for floor_index, floor in object.uuid_floors {
      lines.push(
        "epm_object_" +
        (index + 1).to_string() +
        "_uuid_floor_" +
        (floor_index + 1).to_string() +
        "=" +
        floor,
      )
    }
  }
  lines.join("\n")
}