///|
/// PROFIdrive controller application – drive states (IEC 61800-7-200 §6.3).
pub(all) enum DriveState {
  S1_SwitchingOnInhibited
  S2_ReadyToSwitchOn
  S3_SwitchedOn
  S4_Operation
  AUS2_CoastStop
  AUS3_QuickStop
} derive(Eq, Debug)

///|
pub fn drive_state_label(state : DriveState) -> String {
  match state {
    S1_SwitchingOnInhibited => "S1 Switching On Inhibited"
    S2_ReadyToSwitchOn => "S2 Ready To Switch On"
    S3_SwitchedOn => "S3 Switched On"
    S4_Operation => "S4 Operation"
    AUS2_CoastStop => "AUS2 Coast Stop"
    AUS3_QuickStop => "AUS3 Quick Stop"
  }
}

///|
/// STW1 bit masks (control word 1).
pub let stw1_on_off1 : Int = 0x0001

///|
pub let stw1_no_coast_stop : Int = 0x0002

///|
pub let stw1_no_quick_stop : Int = 0x0004

///|
pub let stw1_enable_operation : Int = 0x0008

///|
pub let stw1_enable_ramp : Int = 0x0010

///|
pub let stw1_unfreeze_ramp : Int = 0x0020

///|
pub let stw1_enable_setpoint : Int = 0x0040

///|
pub let stw1_fault_ack : Int = 0x0080

///|
/// ZSW1 bit masks (status word 1).
pub let zsw1_ready_to_switch_on : Int = 0x0001

///|
pub let zsw1_ready_to_operate : Int = 0x0002

///|
pub let zsw1_operation_enabled : Int = 0x0004

///|
pub let zsw1_fault_active : Int = 0x0008

///|
pub let zsw1_no_coast_stop : Int = 0x0010

///|
pub let zsw1_no_quick_stop : Int = 0x0020

///|
pub let zsw1_switching_on_inhibited : Int = 0x0040

///|
pub let zsw1_warning_active : Int = 0x0080

///|
pub let zsw1_speed_deviation : Int = 0x0100

///|
pub let zsw1_control_requested : Int = 0x0200

///|
pub let zsw1_target_reached : Int = 0x0400

///|
/// Derive drive state from ZSW1 status word.
pub fn drive_state_from_zsw1(zsw1 : Int) -> DriveState {
  if (zsw1 & zsw1_switching_on_inhibited) != 0 {
    S1_SwitchingOnInhibited
  } else if (zsw1 & zsw1_operation_enabled) != 0 {
    S4_Operation
  } else if (zsw1 & zsw1_ready_to_operate) != 0 {
    S3_SwitchedOn
  } else if (zsw1 & zsw1_ready_to_switch_on) != 0 {
    S2_ReadyToSwitchOn
  } else {
    S1_SwitchingOnInhibited
  }
}

///|
/// Build STW1 for a desired target state transition.
pub fn stw1_for_transition(target : DriveState) -> Int {
  match target {
    S1_SwitchingOnInhibited => 0
    S2_ReadyToSwitchOn => stw1_no_coast_stop | stw1_no_quick_stop
    S3_SwitchedOn => stw1_on_off1 | stw1_no_coast_stop | stw1_no_quick_stop
    S4_Operation =>
      stw1_on_off1 |
      stw1_no_coast_stop |
      stw1_no_quick_stop |
      stw1_enable_operation |
      stw1_enable_ramp |
      stw1_unfreeze_ramp |
      stw1_enable_setpoint
    AUS2_CoastStop => 0
    AUS3_QuickStop => stw1_no_coast_stop
  }
}

///|
/// Telegram data structure (PDC_DO_TELEGRAM_TYPE, §5.2).
pub(all) struct TelegramData {
  /// Control word 1
  stw1 : Int
  /// Control word 2
  stw2 : Int
  /// Speed setpoint 16-bit (N_SOLL_A, 0x4000 = 100%)
  n_soll_a : Int
  /// Speed setpoint 32-bit (N_SOLL_B)
  n_soll_b : Int
  /// Sensor 1 control word
  g1_stw : Int
  /// Sensor 2 control word
  g2_stw : Int
  /// System deviation
  xerr : Int
  /// Position controller gain
  kpc : Int
  /// Status word 1
  zsw1 : Int
  /// Status word 2
  zsw2 : Int
  /// Actual speed 16-bit (NIST_A)
  nist_a : Int
  /// Actual speed 32-bit (NIST_B)
  nist_b : Int
  /// Sensor 1 status word
  g1_zsw : Int
  /// Sensor 1 position 1
  g1_xist1 : Int
  /// Sensor 1 position 2
  g1_xist2 : Int
  /// Sensor 2 status word
  g2_zsw : Int
  /// Sensor 2 position 1
  g2_xist1 : Int
  /// Sensor 2 position 2
  g2_xist2 : Int
} derive(Eq, Debug)

///|
/// Speed setpoint normalisation: 0x4000 (16384) = 100%.
pub let n_soll_a_100_percent : Int = 0x4000

///|
/// Speed setpoint normalisation for 32-bit: 0x40000000 = 100%.
pub let n_soll_b_100_percent : Int = 0x40000000

///|
pub fn speed_percent_a(n_soll : Int) -> Double {
  n_soll.to_double() / n_soll_a_100_percent.to_double() * 100.0
}

///|
/// Encode setpoint portion of telegram (STW1+STW2+N_SOLL_A = 6 bytes).
pub fn encode_telegram_setpoint(tg : TelegramData) -> Bytes {
  let out : Array[Byte] = []
  push_u16_be(out, tg.stw1)
  push_u16_be(out, tg.stw2)
  push_u16_be(out, tg.n_soll_a)
  Bytes::from_array(out)
}

///|
/// Parse actual-value portion of telegram (ZSW1+ZSW2+NIST_A = 6 bytes).
pub fn parse_telegram_actual(
  data : Bytes,
  offset : Int,
) -> TelegramData raise @frame.FrameError {
  guard data.length() >= offset + 6 else {
    raise @frame.FrameError::InvalidMacLength(data.length())
  }
  TelegramData::{
    stw1: 0,
    stw2: 0,
    n_soll_a: 0,
    n_soll_b: 0,
    g1_stw: 0,
    g2_stw: 0,
    xerr: 0,
    kpc: 0,
    zsw1: read_u16_be(data, offset),
    zsw2: read_u16_be(data, offset + 2),
    nist_a: read_u16_be(data, offset + 4),
    nist_b: 0,
    g1_zsw: 0,
    g1_xist1: 0,
    g1_xist2: 0,
    g2_zsw: 0,
    g2_xist1: 0,
    g2_xist2: 0,
  }
}

///|
/// Format a telegram summary.
pub fn format_telegram(tg : TelegramData) -> String {
  let lines : Array[String] = []
  let state = drive_state_from_zsw1(tg.zsw1)
  lines.push("state=" + drive_state_label(state))
  lines.push("STW1=0x" + hex16(tg.stw1) + " ZSW1=0x" + hex16(tg.zsw1))
  lines.push(
    "N_SOLL_A=" + tg.n_soll_a.to_string() + " NIST_A=" + tg.nist_a.to_string(),
  )
  if (tg.zsw1 & zsw1_fault_active) != 0 {
    lines.push("FAULT ACTIVE")
  }
  if (tg.zsw1 & zsw1_warning_active) != 0 {
    lines.push("WARNING ACTIVE")
  }
  if (tg.zsw1 & zsw1_target_reached) != 0 {
    lines.push("TARGET REACHED")
  }
  lines.join("\n")
}

///|
fn hex16(v : Int) -> String {
  let hex = "0123456789ABCDEF"
  let c : Array[Char] = []
  c.push(hex[(v >> 12) & 0xF].to_int().unsafe_to_char())
  c.push(hex[(v >> 8) & 0xF].to_int().unsafe_to_char())
  c.push(hex[(v >> 4) & 0xF].to_int().unsafe_to_char())
  c.push(hex[v & 0xF].to_int().unsafe_to_char())
  String::from_array(c)
}

///|
/// Additional standard PROFIdrive PNUs.
pub let pnu_telegram_selection : Int = 922

///|
pub let pnu_station_name_2 : Int = 924

///|
pub let pnu_sign_of_life_tolerance : Int = 925

///|
pub let pnu_operating_mode : Int = 930

///|
pub let pnu_fault_message_counter : Int = 944

///|
pub let pnu_fault_code : Int = 945

///|
pub let pnu_fault_code_list : Int = 946

///|
pub let pnu_fault_number : Int = 947

///|
pub let pnu_fault_time : Int = 948

///|
pub let pnu_fault_value : Int = 949

///|
pub let pnu_fault_buffer_scaling : Int = 950

///|
pub let pnu_fault_number_text : Int = 951

///|
pub let pnu_fault_situation_counter : Int = 952

///|
pub let pnu_do_identification : Int = 975

///|
pub let pnu_do_id_list : Int = 978

///|
pub let pnu_sensor_format : Int = 979

///|
/// Extended PNU label lookup.
pub fn pnu_label_ext(pnu : Int) -> String {
  match pnu {
    922 => "TelegramSelection"
    925 => "SignOfLifeTolerance"
    930 => "OperatingMode"
    944 => "FaultMsgCounter"
    945 => "FaultCode"
    946 => "FaultCodeList"
    947 => "FaultNumber"
    948 => "FaultTime"
    949 => "FaultValue"
    950 => "FaultBufferScaling"
    951 => "FaultNumberText"
    952 => "FaultSituationCounter"
    975 => "DOIdentification"
    978 => "DOIdList"
    979 => "SensorFormat"
    _ => pnu_label(pnu)
  }
}

///|
/// Drive Object properties (DO Identification, PNU 975).
pub(all) struct DriveObjectProperties {
  telegram_no : Int
  hw_version : String
  sw_version : String
  speed_norm_value : Int
  max_speed_rpm : Int
  rated_current_ma : Int
  rated_torque_nm : Int
} derive(Eq, Debug)

///|
pub fn format_drive_object_properties(props : DriveObjectProperties) -> String {
  let lines : Array[String] = []
  lines.push("=== Drive Object Properties ===")
  lines.push("TelegramNo=" + props.telegram_no.to_string())
  lines.push("HW=" + props.hw_version + " SW=" + props.sw_version)
  lines.push("SpeedNormValue=" + props.speed_norm_value.to_string())
  lines.push("MaxSpeed=" + props.max_speed_rpm.to_string() + " rpm")
  lines.push("RatedCurrent=" + props.rated_current_ma.to_string() + " mA")
  lines.push("RatedTorque=" + props.rated_torque_nm.to_string() + " Nm")
  lines.join("\n")
}

///|
/// PNC address types (PN-Controller geographic addressing).
pub(all) enum PncAddressType {
  /// PNC_GEO_ADDR_SUBMOD — Geographic address of sub-module.
  GeoAddrSubmod
  /// PNC_SUBMOD_LIST — Sub-module list for a device.
  SubmodList
  /// PNC_CTRL_INFO — Controller-specific information.
  CtrlInfo
} derive(Eq, Debug)

///|
/// PNC geographic submodule address.
pub(all) struct PncGeoAddr {
  station_number : Int
  slot_number : Int
  subslot_number : Int
} derive(Eq, Debug)

///|
pub fn format_pnc_geo_addr(addr : PncGeoAddr) -> String {
  "station=" +
  addr.station_number.to_string() +
  " slot=" +
  addr.slot_number.to_string() +
  " subslot=" +
  addr.subslot_number.to_string()
}

///|
/// Speed reference calculation: convert RPM to normalized setpoint.
pub fn rpm_to_n_soll_a(rpm : Double, max_rpm : Double) -> Int {
  let ratio = rpm / max_rpm
  let raw = (ratio * n_soll_a_100_percent.to_double()).to_int()
  if raw > 0x7FFF {
    0x7FFF
  } else if raw < -0x7FFF {
    -0x7FFF
  } else {
    raw
  }
}

///|
/// Reverse: normalized setpoint → RPM.
pub fn n_soll_a_to_rpm(n_soll : Int, max_rpm : Double) -> Double {
  n_soll.to_double() / n_soll_a_100_percent.to_double() * max_rpm
}

///|
/// Position setpoint: convert encoder counts to normalized value.
pub fn counts_to_position_norm(counts : Int, resolution : Int) -> Double {
  if resolution == 0 {
    0.0
  } else {
    counts.to_double() / resolution.to_double()
  }
}

// ─── PNC Sub-module List Types ───

///|
/// Data properties for a PNC submodule.
pub(all) struct PncSubmodDataProp {
  len_in : Int
  len_out : Int
} derive(Eq, Debug)

///|
/// Communication properties for a PNC submodule.
pub(all) struct PncSubmodComProp {
  cycle_time : Int
  cacf : Int
  red_factor : Int
  phase : Int
  ti : Int
  to : Int
} derive(Eq, Debug)

///|
/// PROFINET properties for a PNC submodule.
pub(all) struct PncSubmodPnProp {
  mod_id : Int
  submod_id : Int
} derive(Eq, Debug)

///|
/// PNC_SUBMOD_LIST — Full sub-module entry in the PNC device list.
pub(all) struct PncSubmodList {
  sm_ref : Int
  data_prop : PncSubmodDataProp
  com_prop : PncSubmodComProp
  pn_prop : PncSubmodPnProp
} derive(Eq, Debug)

///|
pub fn format_pnc_submod_list(entry : PncSubmodList) -> String {
  let lines : Array[String] = []
  lines.push("=== PNC SubmodList ===")
  lines.push("SmRef=" + entry.sm_ref.to_string())
  lines.push(
    "Data: in=" +
    entry.data_prop.len_in.to_string() +
    " out=" +
    entry.data_prop.len_out.to_string(),
  )
  lines.push(
    "Com: cycle=" +
    entry.com_prop.cycle_time.to_string() +
    " cacf=" +
    entry.com_prop.cacf.to_string() +
    " red=" +
    entry.com_prop.red_factor.to_string(),
  )
  lines.push(
    "PN: modId=0x" +
    to_hex_pad(entry.pn_prop.mod_id, 8) +
    " submodId=0x" +
    to_hex_pad(entry.pn_prop.submod_id, 8),
  )
  lines.join("\n")
}

///|
fn to_hex_pad(value : Int, width : Int) -> String {
  let hex = to_hex(value)
  let pad = width - hex.length()
  if pad > 0 {
    "0".repeat(pad) + hex
  } else {
    hex
  }
}

///|
fn to_hex(value : Int) -> String {
  if value == 0 {
    return "0"
  }
  let digits = "0123456789ABCDEF"
  let buf : Array[String] = []
  let mut v = if value < 0 { -value } else { value }
  while v > 0 {
    buf.push(digits[v % 16].to_string())
    v = v / 16
  }
  buf.rev().join("")
}

// ─── PNC Controller Info ───

///|
/// PNC_CTRL_INFO — Controller-specific information.
pub(all) struct PncCtrlInfo {
  ctrl_name : String
  ctrl_ip : String
  subnet_mask : String
  gateway_ip : String
} derive(Eq, Debug)

///|
pub fn format_pnc_ctrl_info(info : PncCtrlInfo) -> String {
  let lines : Array[String] = []
  lines.push("=== PNC Controller Info ===")
  lines.push("Name: " + info.ctrl_name)
  lines.push("IP:   " + info.ctrl_ip)
  lines.push("Mask: " + info.subnet_mask)
  lines.push("GW:   " + info.gateway_ip)
  lines.join("\n")
}

// ─── PNC Alarm Types ───

///|
/// PNC_PN_ALARM_TYPE — Alarm type enumeration (corresponds to alarm module).
pub(all) enum PncAlarmType {
  Diagnostic
  Process
  Pull
  Plug
  Status
  Update
  Redundancy
  Controlled
  Released
  PlugWrong
  Return
  Diagnosis
  MaintenanceRequired
  MaintenanceDemanded
  PortDataChanged
  SyncDataChanged
  IsochDataChanged
  NetworkComponentProblem
  MultiplexerProblem
  UploadRetrievalNotification
  DevFailure
  DevReturn
} derive(Eq, Debug)

///|
pub fn pnc_alarm_type_value(alarm : PncAlarmType) -> Int {
  match alarm {
    Diagnostic => 0x0001
    Process => 0x0002
    Pull => 0x0003
    Plug => 0x0004
    Status => 0x0005
    Update => 0x0006
    Redundancy => 0x0007
    Controlled => 0x0008
    Released => 0x0009
    PlugWrong => 0x000A
    Return => 0x000B
    Diagnosis => 0x000C
    MaintenanceRequired => 0x000D
    MaintenanceDemanded => 0x000E
    PortDataChanged => 0x000F
    SyncDataChanged => 0x0010
    IsochDataChanged => 0x0011
    NetworkComponentProblem => 0x0012
    MultiplexerProblem => 0x0013
    UploadRetrievalNotification => 0x0014
    DevFailure => 0x10000
    DevReturn => 0x10001
  }
}

// ─── PDC API — State Machine Control Functions ───

///|
/// Build STW1 for PDC_Precharge: S2 → S3 transition.
pub fn pdc_precharge(stw1 : Int) -> Int {
  stw1 | stw1_on_off1 | stw1_no_coast_stop | stw1_no_quick_stop
}

///|
/// Build STW1 for PDC_PulseEnable: S3 → S4 transition.
pub fn pdc_pulse_enable(stw1 : Int) -> Int {
  stw1 | stw1_on_off1 | stw1_enable_operation | stw1_enable_ramp
}

///|
/// Build STW1 for PDC_PulseInhibit: S4 → S3 transition.
pub fn pdc_pulse_inhibit(stw1 : Int) -> Int {
  stw1 & lnot(stw1_enable_operation)
}

///|
/// Build STW1 for coast stop (AUS2): any → AUS2.
pub fn pdc_coast_stop(stw1 : Int) -> Int {
  stw1 & lnot(stw1_no_coast_stop)
}

///|
/// Build STW1 for quick stop (AUS3): any → AUS3.
pub fn pdc_quick_stop(stw1 : Int) -> Int {
  stw1 & lnot(stw1_no_quick_stop)
}

///|
/// Build STW1 for fault acknowledge.
pub fn pdc_fault_acknowledge(stw1 : Int) -> Int {
  stw1 | stw1_fault_ack
}

///|
/// Set speed setpoint in a TelegramData.
pub fn pdc_set_speed(
  td : TelegramData,
  rpm : Double,
  max_rpm : Double,
) -> TelegramData {
  let n_soll = rpm_to_n_soll_a(rpm, max_rpm)
  { ..td, n_soll_a: n_soll }
}

///|
/// Get actual speed from TelegramData.
pub fn pdc_get_speed(td : TelegramData, max_rpm : Double) -> Double {
  n_soll_a_to_rpm(td.nist_a, max_rpm)
}

///|
fn lnot(x : Int) -> Int {
  x.lxor(0xFFFF)
}