///|
let max_ignored_rx_frames : Int = 32

///|
/// Perform a single EtherCAT transaction: build a frame with the given PDUs,
/// send it via the NIC, receive the response, and decode it.
/// Returns the decoded response frame.
pub fn[N : @hal.Nic] transact(
  nic : N,
  frame : EcFrame,
  timeout : @hal.Duration,
) -> EcFrame raise @hal.EcError {
  let raw = frame.encode()
  let _sent = nic.send(raw)
  let mut ignored = 0
  for ;; {
    let response = nic.recv(timeout~)
    let decoded = Result::Ok(EcFrame::decode(response)) catch {
      err => Result::Err(err)
    }
    match decoded {
      Ok(value) => return value
      Err(err) if should_ignore_rx_decode_error(err) => {
        ignored = ignored + 1
        if ignored > max_ignored_rx_frames {
          raise @hal.EcError::Internal(
            "timeout at transact.recv: ignored " +
            max_ignored_rx_frames.to_string() +
            " invalid frames; last error=" +
            @debug.to_string(err),
          )
        }
      }
      Err(err) => raise err
    }
  }
}

///|
fn should_ignore_rx_decode_error(err : @hal.EcError) -> Bool {
  match err {
    @hal.EcError::InvalidFrame(message) =>
      message.contains("frame too short") ||
      message.contains("wrong ethertype") ||
      message.contains("wrong ecat type")
    _ => false
  }
}

///|
/// Send a single PDU transaction and return the response PDU.
/// Convenience wrapper that builds a frame, sends, receives, and locates the PDU by index.
pub fn[N : @hal.Nic] transact_pdu(
  nic : N,
  command : EcCommand,
  address : UInt,
  data : Bytes,
  timeout : @hal.Duration,
) -> Pdu raise @hal.EcError {
  let builder = FrameBuilder::new()
  let idx = builder.add(command, address, data)
  let frame = builder.build()
  let response = transact(nic, frame, timeout)
  match find_pdu_by_index(response, idx) {
    Some(pdu) => pdu
    None => raise @hal.EcError::InvalidPdu("response PDU not found for index")
  }
}

///|
/// Send a single PDU and retry up to max_retries times on RecvTimeout.
pub fn[N : @hal.Nic] transact_pdu_retry(
  nic : N,
  command : EcCommand,
  address : UInt,
  data : Bytes,
  timeout : @hal.Duration,
  max_retries : Int,
) -> Pdu raise @hal.EcError {
  let mut last_err : @hal.EcError = @hal.EcError::RecvTimeout
  for i = 0; i <= max_retries; i = i + 1 {
    let result = Result::Ok(transact_pdu(nic, command, address, data, timeout)) catch {
      err => Result::Err(err)
    }
    match result {
      Ok(pdu) => return pdu
      Err(e) =>
        match e {
          @hal.EcError::RecvTimeout => last_err = e
          _ => raise e
        }
    }
  }
  raise last_err
}