///|
/// EoE (Ethernet over EtherCAT) Virtual Switch engine.
///
/// Implements L2 frame fragmentation, reassembly, and routing between
/// the host (master) and EoE-capable slaves. Designed for integration
/// with the Runtime PDO cycle via poll_eoe(), following the same pattern
/// as poll_emergencies().
///
/// Phase 1 scope: async/network — fragment/reassemble frames, IP config.
/// Phase 2 scope: embedded relay — lightweight, no OS network stack.

///|
/// Maximum number of concurrent reassembly buffers per station.
let eoe_max_reassembly_buffers : Int = 4

///|
/// EoE fragment size alignment (32-byte boundary per SOEM convention).
let eoe_fragment_align : Int = 32

///|
/// Mailbox header (6 bytes) + EoE header (4 bytes) overhead.
let eoe_overhead : Int = 10

///|
/// Reassembly buffer for one in-progress EoE frame from a single slave.
struct EoeReassemblyBuffer {
  /// Frame number being reassembled (4-bit, 0–15).
  frame_number : UInt
  /// Collected fragment data in order; index = fragment_number.
  fragments : Array[Bytes?]
  /// Total bytes accumulated so far.
  mut total_bytes : Int
  /// Whether the last fragment has been received.
  mut last_seen : Bool
  /// Number of fragments expected (set when last fragment arrives).
  mut expected_count : UInt
} derive(Debug)

///|
fn EoeReassemblyBuffer::new(frame_number : UInt) -> EoeReassemblyBuffer {
  {
    frame_number,
    fragments: Array::make(64, None),
    total_bytes: 0,
    last_seen: false,
    expected_count: 0U,
  }
}

///|
/// Insert a fragment into the reassembly buffer.
/// Returns true if the frame is now complete.
fn EoeReassemblyBuffer::insert(
  self : EoeReassemblyBuffer,
  frag_number : UInt,
  last_fragment : Bool,
  data : Bytes,
) -> Bool {
  let idx = frag_number.reinterpret_as_int()
  if idx < self.fragments.length() && self.fragments[idx] is None {
    self.fragments[idx] = Some(data)
    self.total_bytes = self.total_bytes + data.length()
  }
  if last_fragment {
    self.last_seen = true
    self.expected_count = frag_number + 1U
  }
  self.is_complete()
}

///|
/// Check if all fragments have been received.
fn EoeReassemblyBuffer::is_complete(self : EoeReassemblyBuffer) -> Bool {
  if !self.last_seen {
    return false
  }
  let count = self.expected_count.reinterpret_as_int()
  for i = 0; i < count; i = i + 1 {
    if self.fragments[i] is None {
      return false
    }
  }
  true
}

///|
/// Assemble the complete Ethernet frame from collected fragments.
fn EoeReassemblyBuffer::assemble(self : EoeReassemblyBuffer) -> Bytes {
  let buf = Buffer(size_hint=self.total_bytes)
  let count = self.expected_count.reinterpret_as_int()
  for i = 0; i < count; i = i + 1 {
    guard self.fragments[i] is Some(frag) else { continue }
    for j = 0; j < frag.length(); j = j + 1 {
      buf.write_byte(frag[j])
    }
  }
  buf.to_bytes()
}

///|
/// Per-station EoE context: tracks reassembly state and send counter.
pub(all) struct EoeStationContext {
  /// Station address (configured address).
  station : UInt
  /// Mailbox configuration for this slave.
  mb_config : @mailbox.MailboxConfig
  /// Active reassembly buffers keyed by frame_number.
  reassembly : Array[EoeReassemblyBuffer?]
  /// Monotonic frame number counter for outgoing frames (0–15 wrap).
  mut tx_frame_number : UInt
  /// Mailbox counter for EoE frames (1–7 wrap, 0 reserved).
  mut mb_counter : Byte
} derive(Debug)

///|
fn EoeStationContext::new(
  station : UInt,
  mb_config : @mailbox.MailboxConfig,
) -> EoeStationContext {
  {
    station,
    mb_config,
    reassembly: Array::make(eoe_max_reassembly_buffers, None),
    tx_frame_number: 0U,
    mb_counter: b'\x01',
  }
}

///|
/// Advance the mailbox counter (1–7 wrap).
fn EoeStationContext::next_counter(self : EoeStationContext) -> Byte {
  let c = self.mb_counter
  let next = c.to_int() + 1
  self.mb_counter = if next > 7 { b'\x01' } else { next.to_byte() }
  c
}

///|
/// Advance the TX frame number (0–15 wrap).
fn EoeStationContext::next_frame_number(self : EoeStationContext) -> UInt {
  let n = self.tx_frame_number
  self.tx_frame_number = (n + 1U) & 0xFU
  n
}

///|
/// Find or allocate a reassembly buffer for the given frame_number.
/// Evicts the oldest buffer if all slots are full.
fn EoeStationContext::get_reassembly(
  self : EoeStationContext,
  frame_number : UInt,
) -> EoeReassemblyBuffer {
  // Look for existing buffer with matching frame_number.
  for i = 0; i < self.reassembly.length(); i = i + 1 {
    guard self.reassembly[i] is Some(buf) else { continue }
    if buf.frame_number == frame_number {
      return buf
    }
  }
  // Look for an empty slot.
  for i = 0; i < self.reassembly.length(); i = i + 1 {
    if self.reassembly[i] is None {
      let buf = EoeReassemblyBuffer::new(frame_number)
      self.reassembly[i] = Some(buf)
      return buf
    }
  }
  // Evict slot 0 (oldest).
  let buf = EoeReassemblyBuffer::new(frame_number)
  self.reassembly[0] = Some(buf)
  buf
}

///|
/// Remove the reassembly buffer for the given frame_number.
fn EoeStationContext::clear_reassembly(
  self : EoeStationContext,
  frame_number : UInt,
) -> Unit {
  for i = 0; i < self.reassembly.length(); i = i + 1 {
    guard self.reassembly[i] is Some(buf) else { continue }
    if buf.frame_number == frame_number {
      self.reassembly[i] = None
      return
    }
  }
}

///|
/// Received EoE frame delivered to the host.
pub(all) struct EoeReceivedFrame {
  /// Source station address.
  station : UInt
  /// Reassembled Ethernet frame bytes.
  data : Bytes
} derive(Eq, Debug)

///|
/// EoE Virtual Switch state.
pub(all) struct EoeSwitch {
  /// Per-station EoE contexts.
  mut stations : Array[EoeStationContext]
  /// Completed frames ready for delivery to the host.
  rx_queue : Array[EoeReceivedFrame]
  /// Telemetry: total fragments sent.
  mut tx_fragments : Int64
  /// Telemetry: total fragments received.
  mut rx_fragments : Int64
  /// Telemetry: total frames reassembled.
  mut rx_frames_complete : Int64
  /// Telemetry: reassembly errors (unexpected fragment, etc.).
  mut rx_errors : Int64
} derive(Debug)

///|
pub fn EoeSwitch::new() -> EoeSwitch {
  {
    stations: [],
    rx_queue: [],
    tx_fragments: 0L,
    rx_fragments: 0L,
    rx_frames_complete: 0L,
    rx_errors: 0L,
  }
}

///|
/// Register an EoE-capable station.
pub fn EoeSwitch::add_station(
  self : EoeSwitch,
  station : UInt,
  mb_config : @mailbox.MailboxConfig,
) -> Unit {
  // Avoid duplicate registration.
  for ctx in self.stations {
    if ctx.station == station {
      return
    }
  }
  self.stations.push(EoeStationContext::new(station, mb_config))
}

///|
/// Calculate the maximum fragment data size for a given mailbox config.
/// Follows SOEM convention: (mailbox_out_size - overhead) rounded down
/// to 32-byte boundary.
pub fn eoe_max_fragment_size(mb_config : @mailbox.MailboxConfig) -> Int {
  let raw = mb_config.out_size.reinterpret_as_int() - eoe_overhead
  if raw <= 0 {
    return 0
  }
  raw / eoe_fragment_align * eoe_fragment_align
}

///|
/// Fragment and send an Ethernet frame to a specific EoE station.
/// Returns the number of fragments sent.
pub fn[N : @hal.Nic] EoeSwitch::send_frame(
  self : EoeSwitch,
  nic : N,
  station : UInt,
  eth_frame : Bytes,
  timeout : @hal.Duration,
) -> Int raise @hal.EcError {
  let ctx = self.find_station(station)
  guard ctx is Some(ctx) else {
    raise @hal.EcError::MailboxProtocol(
      "EoE station not registered: " + station.reinterpret_as_int().to_string(),
    )
  }
  let max_frag = eoe_max_fragment_size(ctx.mb_config)
  if max_frag <= 0 {
    raise @hal.EcError::MailboxProtocol("EoE mailbox too small for fragments")
  }
  let frame_number = ctx.next_frame_number()
  let total = eth_frame.length()
  let mut offset = 0
  let mut frag_num = 0U
  while offset < total {
    let remaining = total - offset
    let chunk_size = if remaining > max_frag { max_frag } else { remaining }
    let last = offset + chunk_size >= total
    let data = Bytes::makei(chunk_size, fn(i) { eth_frame[offset + i] })
    let frame_offset = (offset / eoe_fragment_align).reinterpret_as_uint()
    let counter = ctx.next_counter()
    let mbx_frame = @mailbox.encode_eoe_fragment(
      counter, 0U, last, frag_num, frame_offset, frame_number, data,
    )
    @protocol.mailbox_send(nic, ctx.station, ctx.mb_config, mbx_frame, timeout)
    self.tx_fragments = self.tx_fragments + 1L
    offset = offset + chunk_size
    frag_num = frag_num + 1U
  }
  frag_num.reinterpret_as_int()
}

///|
/// Process a received raw mailbox frame as an EoE response.
/// If a complete Ethernet frame is reassembled, it is placed in the rx_queue.
/// Returns true if a complete frame was produced.
pub fn EoeSwitch::process_received(
  self : EoeSwitch,
  station : UInt,
  frame : Bytes,
) -> Bool {
  let response = @mailbox.decode_eoe_response(frame) catch {
    _ => {
      self.rx_errors = self.rx_errors + 1L
      return false
    }
  }
  match response {
    @mailbox.EoeFragment(
      frame_type=@mailbox.FragData,
      last_fragment~,
      frag_number~,
      frame_number~,
      data~,
      ..
    ) => {
      self.rx_fragments = self.rx_fragments + 1L
      let ctx = self.find_station(station)
      guard ctx is Some(ctx) else {
        self.rx_errors = self.rx_errors + 1L
        return false
      }
      let buf = ctx.get_reassembly(frame_number)
      let complete = buf.insert(frag_number, last_fragment, data)
      if complete {
        let assembled = buf.assemble()
        ctx.clear_reassembly(frame_number)
        self.rx_frames_complete = self.rx_frames_complete + 1L
        self.rx_queue.push(EoeReceivedFrame::{ station, data: assembled })
        true
      } else {
        false
      }
    }
    _ => false
  }
}

///|
/// Drain all completed frames from the receive queue.
pub fn EoeSwitch::drain_rx(self : EoeSwitch) -> Array[EoeReceivedFrame] {
  let result : Array[EoeReceivedFrame] = []
  while self.rx_queue.length() > 0 {
    // pop from front
    guard self.rx_queue.pop() is Some(f) else { break }
    result.push(f)
  }
  result
}

///|
/// Poll all registered EoE stations for incoming mailbox data.
/// Returns the number of complete Ethernet frames reassembled this cycle.
pub fn[N : @hal.Nic] EoeSwitch::poll(
  self : EoeSwitch,
  nic : N,
  timeout : @hal.Duration,
) -> Int {
  let mut complete = 0
  for ctx in self.stations {
    let has_data = @protocol.mailbox_poll(
      nic,
      ctx.station,
      ctx.mb_config,
      timeout,
    ) catch {
      _ => false
    }
    if has_data {
      let frame = @protocol.mailbox_recv(
        nic,
        ctx.station,
        ctx.mb_config,
        timeout,
        max_polls=1,
      ) catch {
        _ => continue
      }
      if self.process_received(ctx.station, frame) {
        complete = complete + 1
      }
    }
  }
  complete
}

///|
/// Send a Set IP parameters request to an EoE station.
pub fn[N : @hal.Nic] EoeSwitch::set_ip(
  self : EoeSwitch,
  nic : N,
  station : UInt,
  ip : Bytes,
  subnet : Bytes,
  gateway : Bytes,
  timeout : @hal.Duration,
) -> @mailbox.EoeResponse raise @hal.EcError {
  let ctx = self.find_station(station)
  guard ctx is Some(ctx) else {
    raise @hal.EcError::MailboxProtocol(
      "EoE station not registered: " + station.reinterpret_as_int().to_string(),
    )
  }
  let counter = ctx.next_counter()
  let req = @mailbox.encode_eoe_set_ip_req(counter, 0U, ip, subnet, gateway)
  let resp = @protocol.mailbox_exchange(
    nic,
    ctx.station,
    ctx.mb_config,
    @mailbox.Rmsm::new(),
    req,
    timeout,
  )
  @mailbox.decode_eoe_response(resp)
}

///|
/// Send a Get IP parameters request to an EoE station.
pub fn[N : @hal.Nic] EoeSwitch::get_ip(
  self : EoeSwitch,
  nic : N,
  station : UInt,
  timeout : @hal.Duration,
) -> @mailbox.EoeResponse raise @hal.EcError {
  let ctx = self.find_station(station)
  guard ctx is Some(ctx) else {
    raise @hal.EcError::MailboxProtocol(
      "EoE station not registered: " + station.reinterpret_as_int().to_string(),
    )
  }
  let counter = ctx.next_counter()
  let req = @mailbox.encode_eoe_get_ip_req(counter, 0U)
  let resp = @protocol.mailbox_exchange(
    nic,
    ctx.station,
    ctx.mb_config,
    @mailbox.Rmsm::new(),
    req,
    timeout,
  )
  @mailbox.decode_eoe_response(resp)
}

///|
/// Get EoE switch telemetry as JSON.
pub fn EoeSwitch::to_json(self : EoeSwitch) -> Json {
  {
    "station_count": Json::number(self.stations.length().to_double()),
    "tx_fragments": Json::number(self.tx_fragments.to_double()),
    "rx_fragments": Json::number(self.rx_fragments.to_double()),
    "rx_frames_complete": Json::number(self.rx_frames_complete.to_double()),
    "rx_errors": Json::number(self.rx_errors.to_double()),
    "rx_queue_depth": Json::number(self.rx_queue.length().to_double()),
  }
  |> Json::object
}

///|
/// Format EoE switch status as human-readable text.
pub fn EoeSwitch::format_text(self : EoeSwitch) -> String {
  let buf = StringBuilder(size_hint=256)
  buf.write_string("EoE Virtual Switch\n")
  buf.write_string("  Stations: ")
  buf.write_string(self.stations.length().to_string())
  buf.write_string("\n  TX fragments: ")
  buf.write_string(self.tx_fragments.to_string())
  buf.write_string("\n  RX fragments: ")
  buf.write_string(self.rx_fragments.to_string())
  buf.write_string("\n  RX complete frames: ")
  buf.write_string(self.rx_frames_complete.to_string())
  buf.write_string("\n  RX errors: ")
  buf.write_string(self.rx_errors.to_string())
  buf.write_string("\n  RX queue depth: ")
  buf.write_string(self.rx_queue.length().to_string())
  buf.write_string("\n")
  for ctx in self.stations {
    buf.write_string("  Station 0x")
    buf.write_string(uint_to_hex(ctx.station))
    buf.write_string(": tx_frame#=")
    buf.write_string(ctx.tx_frame_number.reinterpret_as_int().to_string())
    buf.write_string(" mb_counter=")
    buf.write_string(ctx.mb_counter.to_int().to_string())
    buf.write_string("\n")
  }
  buf.to_string()
}

///|
fn EoeSwitch::find_station(
  self : EoeSwitch,
  station : UInt,
) -> EoeStationContext? {
  for ctx in self.stations {
    if ctx.station == station {
      return Some(ctx)
    }
  }
  None
}

///|
/// Format a UInt as a hex string (no 0x prefix).
fn uint_to_hex(v : UInt) -> String {
  if v == 0U {
    return "0"
  }
  let digits = "0123456789abcdef".to_array()
  let buf = StringBuilder(size_hint=8)
  let mut val = v
  let chars : Array[Char] = []
  while val > 0U {
    let nibble = (val & 0xFU).reinterpret_as_int()
    chars.push(digits[nibble])
    val = val >> 4
  }
  // Reverse
  let len = chars.length()
  for i = len - 1; i >= 0; i = i - 1 {
    buf.write_char(chars[i])
  }
  buf.to_string()
}