///|
/// Co-simulation adapter contract for HIL (Hardware-in-the-Loop) integration.
///
/// This module defines the trait-based boundary between MoonECAT's runtime
/// and external plant/controller models (Simulink, Modelica, custom C code).
///
/// Architecture:
///   MoonECAT Runtime ←→ CoSimAdapter ←→ External Plant/Controller
///
/// The adapter translates between:
///   - EtherCAT process image bytes (outputs/inputs)
///   - Physical quantities the plant model consumes/produces
///
/// Zero-copy principle:
///   The adapter receives direct references to ProcessImage slices;
///   no intermediate serialization unless the external model requires it.

///|
/// SimTime represents simulation time in nanoseconds since simulation start.
/// Distinct from HAL Timestamp to decouple real-time from simulated time.
pub(all) struct SimTime(Int64) derive(Eq, Debug, Compare)

///|
pub fn SimTime::zero() -> SimTime {
  SimTime(0L)
}

///|
pub fn SimTime::from_ns(ns : Int64) -> SimTime {
  SimTime(ns)
}

///|
pub fn SimTime::to_ns(self : SimTime) -> Int64 {
  self.0
}

///|
pub fn SimTime::advance(self : SimTime, delta_ns : Int64) -> SimTime {
  SimTime(self.0 + delta_ns)
}

///|
/// SimStepResult reports the outcome of one simulation step.
pub(all) enum SimStepResult {
  /// Step completed normally; simulation time advanced.
  Ok(SimTime)
  /// Plant model reports a termination condition (e.g., collision, limit hit).
  Terminated(String)
  /// Simulation error (e.g., solver divergence).
  Error(String)
} derive(Eq, Debug)

///|
/// CoSimAdapter — the trait that external plant/controller models implement.
///
/// Lifecycle:
///   1. `init(config)` — one-time initialization with cycle period and PI layout
///   2. Per cycle: `write_outputs(image, slice)` → `step(dt)` → `read_inputs(image, slice)`
///   3. `shutdown()` — cleanup
///
/// The runtime invokes these in deterministic order within each CycleHook callback.
pub(open) trait CoSimAdapter {
  /// Initialize the adapter with simulation parameters.
  fn init_(Self, CoSimConfig) -> Unit

  /// Write EtherCAT outputs into the plant model's input ports.
  /// Called before `step()`. The adapter reads from `image.outputs` at the
  /// given slice region and translates to physical quantities.
  fn write_outputs(Self, @protocol.ProcessImage, ProcessImageSlice) -> Unit

  /// Advance the plant model by one time step.
  fn step(Self, SimTime, Int64) -> SimStepResult

  /// Read plant model outputs back into EtherCAT inputs.
  /// Called after `step()`. The adapter writes to `image.inputs` at the
  /// given slice region.
  fn read_inputs(Self, @protocol.ProcessImage, ProcessImageSlice) -> Unit

  /// One-time cleanup.
  fn shutdown(Self) -> Unit
}

///|
/// CoSimConfig provides the adapter with the simulation parameters it needs.
pub(all) struct CoSimConfig {
  /// Base cycle period in nanoseconds.
  cycle_period_ns : Int64
  /// Number of slaves in the configuration.
  slave_count : Int
  /// Total output image size in bytes.
  output_size : Int
  /// Total input image size in bytes.
  input_size : Int
} derive(Eq, Debug)

///|
/// LoopbackCoSimAdapter — trivial adapter that copies outputs to inputs.
/// Useful for communication-closed-loop testing without an external model.
pub struct LoopbackCoSimAdapter {
  mut sim_time : SimTime
}

///|
pub fn LoopbackCoSimAdapter::new() -> LoopbackCoSimAdapter {
  { sim_time: SimTime::zero() }
}

///|
pub impl CoSimAdapter for LoopbackCoSimAdapter with fn init_(self, _config) {
  ignore(self)
}

///|
pub impl CoSimAdapter for LoopbackCoSimAdapter with fn write_outputs(
  self,
  _image,
  _slice,
) {
  ignore(self)
}

///|
pub impl CoSimAdapter for LoopbackCoSimAdapter with fn step(self, current, dt) {
  self.sim_time = current.advance(dt)
  Ok(self.sim_time)
}

///|
pub impl CoSimAdapter for LoopbackCoSimAdapter with fn read_inputs(
  self,
  image,
  slice,
) {
  ignore(self)
  // Copy outputs to inputs (loopback)
  let len = if slice.output_length < slice.input_length {
    slice.output_length
  } else {
    slice.input_length
  }
  let buf = Buffer(size_hint=image.input_size)
  for i = 0; i < image.input_size; i = i + 1 {
    if i >= slice.input_offset && i < slice.input_offset + len {
      buf.write_byte(
        image.outputs[slice.output_offset + (i - slice.input_offset)],
      )
    } else {
      buf.write_byte(image.inputs[i])
    }
  }
  image.inputs = buf.to_bytes()
}

///|
pub impl CoSimAdapter for LoopbackCoSimAdapter with fn shutdown(self) {
  ignore(self)
}