///|
pub(all) suberror DcpParseError {
  PacketTooShort(Int)
  UnexpectedEtherType(Int)
  TruncatedPayload(expected~ : Int, actual~ : Int)
  TruncatedBlock(at~ : Int, len~ : Int)
} derive(Eq, Debug, ToJson)

///|
pub(all) struct DeviceVendorBlock {
  block_info : Int
  value : String
} derive(Eq)

///|
pub(all) struct ResponseNameOfStationBlock {
  block_info : Int
  value : String
} derive(Eq)

///|
pub(all) struct AliasNameBlock {
  block_info : Int
  value : String
} derive(Eq)

///|
pub(all) struct DeviceIDBlock {
  block_info : Int
  vendor_id : Int
  device_id : Int
} derive(Eq)

///|
pub(all) struct DeviceInstanceBlock {
  block_info : Int
  value : Int
} derive(Eq)

///|
pub(all) struct OEMDeviceIDBlock {
  block_info : Int
  vendor_id : Int
  device_id : Int
} derive(Eq)

///|
pub(all) struct DeviceRoleBlock {
  block_info : Int
  is_device : Bool
  is_controller : Bool
  is_multidevice : Bool
  is_supervisor : Bool
} derive(Eq)

///|
pub(all) struct DeviceOptionPair {
  option : Byte
  suboption : Byte
} derive(Eq)

///|
pub(all) struct DeviceOptionsBlock {
  block_info : Int
  options : Array[DeviceOptionPair]
} derive(Eq)

///|
pub(all) struct ResponseIPParameterBlock {
  block_info : Int
  address : @frame.IPv4Address
  mask : @frame.IPv4Address
  gateway : @frame.IPv4Address
} derive(Eq)

///|
pub(all) struct DeviceInitiativeBlock {
  block_info : Int
  device_initiative : Int
} derive(Eq)

///|
pub(all) struct ControlResponseBlock {
  failed_option : Byte
  failed_suboption : Byte
  block_error : Byte
} derive(Eq)

///|
pub(all) enum ResponseBlock {
  NameOfStation(ResponseNameOfStationBlock)
  AliasName(AliasNameBlock)
  DeviceVendor(DeviceVendorBlock)
  DeviceID(DeviceIDBlock)
  DeviceInstance(DeviceInstanceBlock)
  OEMDeviceID(OEMDeviceIDBlock)
  DeviceRole(DeviceRoleBlock)
  DeviceOptions(DeviceOptionsBlock)
  IPParameter(ResponseIPParameterBlock)
  DeviceInitiative(DeviceInitiativeBlock)
  ControlResponse(ControlResponseBlock)
  Unknown(option~ : Byte, suboption~ : Byte, payload~ : Bytes)
} derive(Eq)

///|
pub(all) struct DcpResponse {
  frame_id : Int
  service_id : Byte
  service_type : Byte
  xid : Int
  source : @frame.MacAddress
  blocks : Array[ResponseBlock]
} derive(Eq)

///|
pub(all) struct DeviceSummary {
  source : @frame.MacAddress
  station_name : String?
  vendor_name : String?
  alias_name : String?
  vendor_id : Int?
  device_id : Int?
  device_instance : Int?
  oem_vendor_id : Int?
  oem_device_id : Int?
  ip_address : @frame.IPv4Address?
  subnet_mask : @frame.IPv4Address?
  gateway : @frame.IPv4Address?
  is_device : Bool
  is_controller : Bool
  is_multidevice : Bool
  is_supervisor : Bool
  options : Array[DeviceOptionPair]
  device_initiative : Int?
  control_response : ControlResponseBlock?
} derive(Eq)

///|
fn parse_name_of_station_block(payload : Bytes) -> ResponseNameOfStationBlock {
  ResponseNameOfStationBlock::{
    block_info: read_u16_be(payload, 0),
    value: @ascii.decode_lossy(payload.view(start=2)),
  }
}

///|
fn parse_device_vendor_block(payload : Bytes) -> DeviceVendorBlock {
  DeviceVendorBlock::{
    block_info: read_u16_be(payload, 0),
    value: @ascii.decode_lossy(payload.view(start=2)),
  }
}

///|
fn parse_alias_name_block(payload : Bytes) -> AliasNameBlock {
  AliasNameBlock::{
    block_info: read_u16_be(payload, 0),
    value: @ascii.decode_lossy(payload.view(start=2)),
  }
}

///|
fn parse_device_id_block(payload : Bytes) -> DeviceIDBlock {
  DeviceIDBlock::{
    block_info: read_u16_be(payload, 0),
    vendor_id: read_u16_be(payload, 2),
    device_id: read_u16_be(payload, 4),
  }
}

///|
fn parse_device_instance_block(payload : Bytes) -> DeviceInstanceBlock {
  DeviceInstanceBlock::{
    block_info: read_u16_be(payload, 0),
    value: read_u16_be(payload, 2),
  }
}

///|
fn parse_oem_device_id_block(payload : Bytes) -> OEMDeviceIDBlock {
  OEMDeviceIDBlock::{
    block_info: read_u16_be(payload, 0),
    vendor_id: read_u16_be(payload, 2),
    device_id: read_u16_be(payload, 4),
  }
}

///|
fn parse_device_role_block(payload : Bytes) -> DeviceRoleBlock {
  let flags = payload[2]
  DeviceRoleBlock::{
    block_info: read_u16_be(payload, 0),
    is_device: flags.land(1) != 0,
    is_controller: flags.land(2) != 0,
    is_multidevice: flags.land(4) != 0,
    is_supervisor: flags.land(8) != 0,
  }
}

///|
fn parse_device_options_block(payload : Bytes) -> DeviceOptionsBlock {
  let options : Array[DeviceOptionPair] = []
  for index = 2; index < payload.length(); {
    options.push(DeviceOptionPair::{
      option: payload[index],
      suboption: payload[index + 1],
    })
    continue index + 2
  }
  DeviceOptionsBlock::{ block_info: read_u16_be(payload, 0), options }
}

///|
fn parse_ip_parameter_block(payload : Bytes) -> ResponseIPParameterBlock raise {
  ResponseIPParameterBlock::{
    block_info: read_u16_be(payload, 0),
    address: @frame.IPv4Address::new(bytes_slice(payload, 2, 4)),
    mask: @frame.IPv4Address::new(bytes_slice(payload, 6, 4)),
    gateway: @frame.IPv4Address::new(bytes_slice(payload, 10, 4)),
  }
}

///|
fn parse_device_initiative_block(payload : Bytes) -> DeviceInitiativeBlock {
  DeviceInitiativeBlock::{
    block_info: read_u16_be(payload, 0),
    device_initiative: read_u16_be(payload, 2),
  }
}

///|
fn parse_control_response_block(payload : Bytes) -> ControlResponseBlock {
  ControlResponseBlock::{
    failed_option: payload[0],
    failed_suboption: payload[1],
    block_error: payload[2],
  }
}

///|
fn parse_response_block(
  option : Byte,
  suboption : Byte,
  payload : Bytes,
) -> ResponseBlock raise {
  if option == dcp_option_device && suboption == 1 {
    DeviceVendor(parse_device_vendor_block(payload))
  } else if option == dcp_option_device &&
    suboption == dcp_suboption_name_of_station {
    NameOfStation(parse_name_of_station_block(payload))
  } else if option == dcp_option_device &&
    suboption == dcp_suboption_device_alias {
    AliasName(parse_alias_name_block(payload))
  } else if option == dcp_option_device && suboption == 3 {
    DeviceID(parse_device_id_block(payload))
  } else if option == dcp_option_device &&
    suboption == dcp_suboption_device_instance {
    DeviceInstance(parse_device_instance_block(payload))
  } else if option == dcp_option_device &&
    suboption == dcp_suboption_device_oem_id {
    OEMDeviceID(parse_oem_device_id_block(payload))
  } else if option == dcp_option_device && suboption == 4 {
    DeviceRole(parse_device_role_block(payload))
  } else if option == dcp_option_device && suboption == 5 {
    DeviceOptions(parse_device_options_block(payload))
  } else if option == dcp_option_ip && suboption == dcp_suboption_ip_parameter {
    IPParameter(parse_ip_parameter_block(payload))
  } else if option == dcp_option_initiative &&
    suboption == dcp_suboption_device_initiative {
    DeviceInitiative(parse_device_initiative_block(payload))
  } else if option == dcp_option_control &&
    suboption == dcp_suboption_control_response {
    ControlResponse(parse_control_response_block(payload))
  } else {
    Unknown(option~, suboption~, payload~)
  }
}

///|
pub fn parse_response(packet : Bytes) -> DcpResponse raise {
  guard packet.length() >= 26 else {
    raise DcpParseError::PacketTooShort(packet.length())
  }
  let ether_type = read_u16_be(packet, 12)
  guard ether_type == ether_type_profinet else {
    raise DcpParseError::UnexpectedEtherType(ether_type)
  }
  let payload_len = read_u16_be(packet, 24)
  let actual_payload_len = packet.length() - 26
  guard actual_payload_len >= payload_len else {
    raise DcpParseError::TruncatedPayload(
      expected=payload_len,
      actual=actual_payload_len,
    )
  }
  let blocks : Array[ResponseBlock] = []
  let payload_end = 26 + payload_len
  for cursor = 26; cursor < payload_end; {
    guard cursor + 4 <= payload_end else {
      raise DcpParseError::TruncatedBlock(at=cursor, len=payload_end - cursor)
    }
    let option = packet[cursor]
    let suboption = packet[cursor + 1]
    let block_len = read_u16_be(packet, cursor + 2)
    let padding = if block_len % 2 == 1 { 1 } else { 0 }
    let padded_len = block_len + 4 + padding
    guard cursor + padded_len <= payload_end else {
      raise DcpParseError::TruncatedBlock(at=cursor, len=block_len)
    }
    let payload = bytes_slice(packet, cursor + 4, block_len)
    blocks.push(parse_response_block(option, suboption, payload))
    continue cursor + padded_len
  }
  DcpResponse::{
    frame_id: read_u16_be(packet, 14),
    service_id: packet[16],
    service_type: packet[17],
    xid: read_u32_be(packet, 18),
    source: @frame.MacAddress::new(bytes_slice(packet, 6, 6)),
    blocks,
  }
}

///|
pub fn response_service_type_label(service_type : Byte) -> String {
  match service_type.to_int() {
    0 => "request"
    1 => "success"
    other => "response-" + other.to_string()
  }
}

///|
pub fn response_block_info_label(block_info : Int) -> String {
  if block_info == 0 {
    "ok"
  } else {
    "status-" + block_info.to_string()
  }
}

///|
pub fn control_response_error_label(block_error : Byte) -> String {
  match block_error.to_int() {
    0 => "no_error"
    1 => "option_not_supported"
    2 => "suboption_or_block_qualifier_not_supported"
    3 => "suboption_not_set"
    4 => "resource_error"
    5 => "set_not_possible_local"
    6 => "set_not_possible_in_operation"
    other => "unknown_error_" + other.to_string()
  }
}

///|
pub fn response_block_name(block : ResponseBlock) -> String {
  match block {
    NameOfStation(_) => "name_of_station"
    AliasName(_) => "alias_name"
    DeviceVendor(_) => "device_vendor"
    DeviceID(_) => "device_id"
    DeviceInstance(_) => "device_instance"
    OEMDeviceID(_) => "oem_device_id"
    DeviceRole(_) => "device_role"
    DeviceOptions(_) => "device_options"
    IPParameter(_) => "ip_parameter"
    DeviceInitiative(_) => "device_initiative"
    ControlResponse(_) => "control_response"
    Unknown(option~, suboption~, ..) =>
      "unknown(" +
      option.to_int().to_string() +
      ":" +
      suboption.to_int().to_string() +
      ")"
  }
}

///|
pub fn response_block_info(block : ResponseBlock) -> Int {
  match block {
    NameOfStation(data) => data.block_info
    AliasName(data) => data.block_info
    DeviceVendor(data) => data.block_info
    DeviceID(data) => data.block_info
    DeviceInstance(data) => data.block_info
    OEMDeviceID(data) => data.block_info
    DeviceRole(data) => data.block_info
    DeviceOptions(data) => data.block_info
    IPParameter(data) => data.block_info
    DeviceInitiative(data) => data.block_info
    ControlResponse(_) => 0
    Unknown(payload~, ..) =>
      if payload.length() >= 2 {
        read_u16_be(payload, 0)
      } else {
        0
      }
  }
}

///|
pub fn summarize_response(response : DcpResponse) -> DeviceSummary {
  let mut station_name : String? = None
  let mut vendor_name : String? = None
  let mut alias_name : String? = None
  let mut vendor_id : Int? = None
  let mut device_id : Int? = None
  let mut device_instance : Int? = None
  let mut oem_vendor_id : Int? = None
  let mut oem_device_id : Int? = None
  let mut ip_address : @frame.IPv4Address? = None
  let mut subnet_mask : @frame.IPv4Address? = None
  let mut gateway : @frame.IPv4Address? = None
  let mut is_device = false
  let mut is_controller = false
  let mut is_multidevice = false
  let mut is_supervisor = false
  let options : Array[DeviceOptionPair] = []
  let mut device_initiative : Int? = None
  let mut control_response : ControlResponseBlock? = None
  for block in response.blocks {
    match block {
      NameOfStation(data) => station_name = Some(data.value)
      AliasName(data) => alias_name = Some(data.value)
      DeviceVendor(data) => vendor_name = Some(data.value)
      DeviceID(data) => {
        vendor_id = Some(data.vendor_id)
        device_id = Some(data.device_id)
      }
      DeviceInstance(data) => device_instance = Some(data.value)
      OEMDeviceID(data) => {
        oem_vendor_id = Some(data.vendor_id)
        oem_device_id = Some(data.device_id)
      }
      DeviceRole(data) => {
        is_device = data.is_device
        is_controller = data.is_controller
        is_multidevice = data.is_multidevice
        is_supervisor = data.is_supervisor
      }
      DeviceOptions(data) =>
        for pair in data.options {
          options.push(pair)
        }
      IPParameter(data) => {
        ip_address = Some(data.address)
        subnet_mask = Some(data.mask)
        gateway = Some(data.gateway)
      }
      DeviceInitiative(data) => device_initiative = Some(data.device_initiative)
      ControlResponse(data) => control_response = Some(data)
      _ => ()
    }
  }
  DeviceSummary::{
    source: response.source,
    station_name,
    vendor_name,
    alias_name,
    vendor_id,
    device_id,
    device_instance,
    oem_vendor_id,
    oem_device_id,
    ip_address,
    subnet_mask,
    gateway,
    is_device,
    is_controller,
    is_multidevice,
    is_supervisor,
    options,
    device_initiative,
    control_response,
  }
}