///|
/// Mailbox transport layer: SM-level send, receive, and poll operations.
/// Uses FPWR/FPRD to mailbox Sync Manager address ranges for communication
/// with slave mailboxes per ETG.1000.4.

///|
let sm_register_window_size : Int = 8

///|
/// SM status register offset within the SM config register area.
let sm_status_offset : UInt = 5U

///|
/// SM activate register offset within the SM config register area.
let sm_activate_offset : UInt = 6U

///|
/// SM PDI/status register offset within the SM config register area.
let sm_pdi_control_offset : UInt = 7U

///|
/// Mailbox status bit (bit 3) in the SM status register.
/// For SM Input: 1 = mailbox full (data available for master to read).
/// For SM Output: 0 = mailbox empty (ready for master to write).
let mb_status_bit : Byte = b'\x08'

///|
/// Maximum number of poll attempts before returning timeout.
let default_max_polls : Int = 1000

///|
/// Maximum number of polls when waiting for the output mailbox to become empty.
let mailbox_send_wait_max_polls : Int = 32

///|
let sm_control_deactivate_mask : UInt = 0x0100U

///|
let sm_control_repeat_request_mask : UInt = 0x0002U

///|
/// Compute the SM register window base address for a given SM index.
fn sm_register_address(sm_index : Int) -> UInt {
  reg_sm_base +
  sm_index.reinterpret_as_uint() * sm_entry_size.reinterpret_as_uint()
}

///|
fn mailbox_status_from_sm_window(window : Bytes) -> Bool {
  if window.length() <= sm_status_offset.reinterpret_as_int() {
    return false
  }
  (window[sm_status_offset.reinterpret_as_int()] & mb_status_bit) != b'\x00'
}

///|
fn validate_input_mailbox_sm_window(window : Bytes) -> Unit raise @hal.EcError {
  if window.length() < sm_register_window_size {
    return
  }
  let start = read_u16_le(window, 0)
  let length = read_u16_le(window, 2)
  let control_word = read_u16_le(window, 6)
  let deactivated = (control_word & sm_control_deactivate_mask) != 0U
  let repeat_requested = (control_word & sm_control_repeat_request_mask) != 0U
  if deactivated || (start == 0U && length == 0U && repeat_requested) {
    raise @hal.EcError::MailboxProtocol(
      "input mailbox SyncManager is deactivated/reset: start=0x" +
      start.to_string(radix=16) +
      " length=0x" +
      length.to_string(radix=16) +
      " control=0x" +
      control_word.to_string(radix=16),
    )
  }
}

///|
fn read_sm_window_activate_byte(window : Bytes) -> Byte raise @hal.EcError {
  if window.length() <= sm_activate_offset.reinterpret_as_int() {
    raise @hal.EcError::MailboxProtocol(
      "short SyncManager window: missing activate byte",
    )
  }
  window[sm_activate_offset.reinterpret_as_int()]
}

///|
fn read_sm_window_pdi_control_byte(window : Bytes) -> Byte raise @hal.EcError {
  if window.length() <= sm_pdi_control_offset.reinterpret_as_int() {
    raise @hal.EcError::MailboxProtocol(
      "short SyncManager window: missing PDI control byte",
    )
  }
  window[sm_pdi_control_offset.reinterpret_as_int()]
}

///|
fn[N : @hal.Nic] mailbox_repeat_read_fp(
  nic : N,
  station : UInt,
  mb_config : @mailbox.MailboxConfig,
  rmsm : @mailbox.Rmsm,
  timeout : @hal.Duration,
  max_polls : Int,
) -> Bytes raise @hal.EcError {
  let sm_addr = sm_register_address(mb_config.in_sm_index)
  let sm_window = read_register_fp(
    nic, station, sm_addr, sm_register_window_size, timeout,
  )
  let repeat_activate = rmsm.begin_read_repeat(
    read_sm_window_activate_byte(sm_window.data),
  )
  let _ = write_register_fp(
    nic,
    station,
    sm_addr + sm_activate_offset,
    Bytes::from_array([repeat_activate]),
    timeout,
  )
  let mut ack_seen = false
  for _attempt = 0; _attempt < max_polls; _attempt = _attempt + 1 {
    let poll_window = read_register_fp(
      nic, station, sm_addr, sm_register_window_size, timeout,
    )
    if !ack_seen {
      ack_seen = rmsm.observe_repeat_ack(
        read_sm_window_pdi_control_byte(poll_window.data),
      )
    }
    if ack_seen && mailbox_status_from_sm_window(poll_window.data) {
      let pdu = read_register_fp(
        nic,
        station,
        mb_config.in_address,
        mb_config.in_size.reinterpret_as_int(),
        timeout,
      )
      return pdu.data
    }
  }
  raise @hal.EcError::MailboxTimeout
}

///|
fn[N : @hal.Nic] mailbox_repeat_read_ap(
  nic : N,
  position : UInt,
  mb_config : @mailbox.MailboxConfig,
  rmsm : @mailbox.Rmsm,
  timeout : @hal.Duration,
  max_polls : Int,
) -> Bytes raise @hal.EcError {
  let sm_addr = sm_register_address(mb_config.in_sm_index)
  let sm_window = read_register_ap(
    nic, position, sm_addr, sm_register_window_size, timeout,
  )
  let repeat_activate = rmsm.begin_read_repeat(
    read_sm_window_activate_byte(sm_window.data),
  )
  let _ = write_register_ap(
    nic,
    position,
    sm_addr + sm_activate_offset,
    Bytes::from_array([repeat_activate]),
    timeout,
  )
  let mut ack_seen = false
  for _attempt = 0; _attempt < max_polls; _attempt = _attempt + 1 {
    let poll_window = read_register_ap(
      nic, position, sm_addr, sm_register_window_size, timeout,
    )
    if !ack_seen {
      ack_seen = rmsm.observe_repeat_ack(
        read_sm_window_pdi_control_byte(poll_window.data),
      )
    }
    if ack_seen && mailbox_status_from_sm_window(poll_window.data) {
      return mailbox_read_ap(nic, position, mb_config, timeout)
    }
  }
  raise @hal.EcError::MailboxTimeout
}

///|
/// Poll a slave's input mailbox (SM In) to check if data is available.
/// Returns true if the mailbox contains data ready to be read.
pub fn[N : @hal.Nic] mailbox_poll(
  nic : N,
  station : UInt,
  mb_config : @mailbox.MailboxConfig,
  timeout : @hal.Duration,
) -> Bool raise @hal.EcError {
  let status_addr = sm_register_address(mb_config.in_sm_index)
  let pdu = read_register_fp(
    nic, station, status_addr, sm_register_window_size, timeout,
  )
  validate_input_mailbox_sm_window(pdu.data)
  if pdu.data.length() <= sm_status_offset.reinterpret_as_int() {
    return false
  }
  (pdu.data[sm_status_offset.reinterpret_as_int()] & mb_status_bit) != b'\x00'
}

///|
/// Poll a slave's input mailbox using auto-increment addressing.
/// Returns true if the mailbox contains data ready to be read.
pub fn[N : @hal.Nic] mailbox_poll_ap(
  nic : N,
  position : UInt,
  mb_config : @mailbox.MailboxConfig,
  timeout : @hal.Duration,
) -> Bool raise @hal.EcError {
  let status_addr = sm_register_address(mb_config.in_sm_index)
  let pdu = read_register_ap(
    nic, position, status_addr, sm_register_window_size, timeout,
  )
  validate_input_mailbox_sm_window(pdu.data)
  if pdu.data.length() <= sm_status_offset.reinterpret_as_int() {
    return false
  }
  (pdu.data[sm_status_offset.reinterpret_as_int()] & mb_status_bit) != b'\x00'
}

///|
/// Send a mailbox frame to a slave by writing to the SM Output address range.
/// This function:
/// 1. Checks that the data fits in the slave's mailbox.
/// 2. Pads the data to the full SM Out size (required by EtherCAT spec).
/// 3. Writes to the SM Out physical address via FPWR.
pub fn[N : @hal.Nic] mailbox_send(
  nic : N,
  station : UInt,
  mb_config : @mailbox.MailboxConfig,
  data : Bytes,
  timeout : @hal.Duration,
) -> Unit raise @hal.EcError {
  let out_size = mb_config.out_size.reinterpret_as_int()
  if data.length() > out_size {
    raise @hal.EcError::MailboxProtocol(
      "mailbox data too large: " +
      data.length().to_string() +
      " > " +
      out_size.to_string(),
    )
  }
  // Pad data to full SM Out size (required by protocol)
  let padded = Bytes::makei(out_size, fn(i) {
    if i < data.length() {
      data[i]
    } else {
      b'\x00'
    }
  })
  let status_addr = sm_register_address(mb_config.out_sm_index)
  for _attempt = 0
      _attempt < mailbox_send_wait_max_polls
      _attempt = _attempt + 1 {
    let pdu = read_register_fp(
      nic, station, status_addr, sm_register_window_size, timeout,
    )
    if pdu.data.length() > sm_status_offset.reinterpret_as_int() &&
      (pdu.data[sm_status_offset.reinterpret_as_int()] & mb_status_bit) ==
      b'\x00' {
      let _ = write_register_fp(
        nic,
        station,
        mb_config.out_address,
        padded,
        timeout,
      )
      return
    }
  }
  raise @hal.EcError::MailboxTimeout
}

///|
/// Send a mailbox frame to a slave using auto-increment addressing.
pub fn[N : @hal.Nic] mailbox_send_ap(
  nic : N,
  position : UInt,
  mb_config : @mailbox.MailboxConfig,
  data : Bytes,
  timeout : @hal.Duration,
) -> Unit raise @hal.EcError {
  let out_size = mb_config.out_size.reinterpret_as_int()
  if data.length() > out_size {
    raise @hal.EcError::MailboxProtocol(
      "mailbox data too large: " +
      data.length().to_string() +
      " > " +
      out_size.to_string(),
    )
  }
  let padded = Bytes::makei(out_size, fn(i) {
    if i < data.length() {
      data[i]
    } else {
      b'\x00'
    }
  })
  let status_addr = sm_register_address(mb_config.out_sm_index)
  for _attempt = 0
      _attempt < mailbox_send_wait_max_polls
      _attempt = _attempt + 1 {
    let pdu = read_register_ap(
      nic, position, status_addr, sm_register_window_size, timeout,
    )
    if pdu.data.length() > sm_status_offset.reinterpret_as_int() &&
      (pdu.data[sm_status_offset.reinterpret_as_int()] & mb_status_bit) ==
      b'\x00' {
      let _ = write_register_ap(
        nic,
        position,
        mb_config.out_address,
        padded,
        timeout,
      )
      return
    }
  }
  raise @hal.EcError::MailboxTimeout
}

///|
/// Receive a mailbox frame from a slave by reading the SM Input address range.
/// This function:
/// 1. Polls the SM In status until data is available (up to max_polls attempts).
/// 2. Reads the full SM In address range via FPRD.
/// 3. Returns the raw mailbox frame bytes.
pub fn[N : @hal.Nic] mailbox_recv(
  nic : N,
  station : UInt,
  mb_config : @mailbox.MailboxConfig,
  timeout : @hal.Duration,
  max_polls? : Int = default_max_polls,
) -> Bytes raise @hal.EcError {
  // Poll until data is available
  for _attempt = 0; _attempt < max_polls; _attempt = _attempt + 1 {
    let ready = mailbox_poll(nic, station, mb_config, timeout)
    if ready {
      // Read the full SM In buffer
      let pdu = read_register_fp(
        nic,
        station,
        mb_config.in_address,
        mb_config.in_size.reinterpret_as_int(),
        timeout,
      )
      return pdu.data
    }
  }
  raise @hal.EcError::MailboxTimeout
}

///|
/// Receive a mailbox frame and use RMSM Repeat/RepeatAck recovery once when
/// the initial mailbox read times out.
pub fn[N : @hal.Nic] mailbox_recv_rmsm(
  nic : N,
  station : UInt,
  mb_config : @mailbox.MailboxConfig,
  rmsm : @mailbox.Rmsm,
  timeout : @hal.Duration,
  max_polls? : Int = default_max_polls,
) -> Bytes raise @hal.EcError {
  let initial = Result::Ok(
    mailbox_recv(nic, station, mb_config, timeout, max_polls~),
  ) catch {
    err => Result::Err(err)
  }
  match initial {
    Ok(data) => data
    Err(@hal.EcError::MailboxTimeout) =>
      mailbox_repeat_read_fp(nic, station, mb_config, rmsm, timeout, max_polls)
    Err(err) => raise err
  }
}

///|
/// Read the current AP mailbox input buffer once without polling.
pub fn[N : @hal.Nic] mailbox_read_ap(
  nic : N,
  position : UInt,
  mb_config : @mailbox.MailboxConfig,
  timeout : @hal.Duration,
) -> Bytes raise @hal.EcError {
  let pdu = read_register_ap(
    nic,
    position,
    mb_config.in_address,
    mb_config.in_size.reinterpret_as_int(),
    timeout,
  )
  pdu.data
}

///|
/// Receive a mailbox frame from a slave using auto-increment addressing.
pub fn[N : @hal.Nic] mailbox_recv_ap(
  nic : N,
  position : UInt,
  mb_config : @mailbox.MailboxConfig,
  timeout : @hal.Duration,
  max_polls? : Int = default_max_polls,
) -> Bytes raise @hal.EcError {
  for _attempt = 0; _attempt < max_polls; _attempt = _attempt + 1 {
    let ready = mailbox_poll_ap(nic, position, mb_config, timeout)
    if ready {
      return mailbox_read_ap(nic, position, mb_config, timeout)
    }
  }
  raise @hal.EcError::MailboxTimeout
}

///|
/// Receive a mailbox frame using AP addressing and use RMSM Repeat/RepeatAck
/// recovery once when the initial mailbox read times out.
pub fn[N : @hal.Nic] mailbox_recv_ap_rmsm(
  nic : N,
  position : UInt,
  mb_config : @mailbox.MailboxConfig,
  rmsm : @mailbox.Rmsm,
  timeout : @hal.Duration,
  max_polls? : Int = default_max_polls,
) -> Bytes raise @hal.EcError {
  let initial = Result::Ok(
    mailbox_recv_ap(nic, position, mb_config, timeout, max_polls~),
  ) catch {
    err => Result::Err(err)
  }
  match initial {
    Ok(data) => data
    Err(@hal.EcError::MailboxTimeout) =>
      mailbox_repeat_read_ap(nic, position, mb_config, rmsm, timeout, max_polls)
    Err(err) => raise err
  }
}

///|
/// Send a mailbox frame and wait for the response.
/// Combines mailbox_send + mailbox_recv into a single round-trip.
pub fn[N : @hal.Nic] mailbox_exchange(
  nic : N,
  station : UInt,
  mb_config : @mailbox.MailboxConfig,
  rmsm : @mailbox.Rmsm,
  data : Bytes,
  timeout : @hal.Duration,
) -> Bytes raise @hal.EcError {
  mailbox_send(nic, station, mb_config, data, timeout)
  mailbox_recv_rmsm(nic, station, mb_config, rmsm, timeout)
}

///|
/// Send a mailbox frame and wait for the response using auto-increment addressing.
pub fn[N : @hal.Nic] mailbox_exchange_ap(
  nic : N,
  position : UInt,
  mb_config : @mailbox.MailboxConfig,
  rmsm : @mailbox.Rmsm,
  data : Bytes,
  timeout : @hal.Duration,
) -> Bytes raise @hal.EcError {
  mailbox_send_ap(nic, position, mb_config, data, timeout)
  mailbox_recv_ap_rmsm(nic, position, mb_config, rmsm, timeout)
}