///|
/// HIL (Hardware-in-the-Loop) communication closed-loop PoC.
///
/// This module wires CycleHook + CoSimAdapter + TaskSchedule into a
/// concrete, testable closed-loop configuration.
///
/// Architecture:
///   Runtime.run_cycle
///       ↓ on_pre_cycle
///   HilCycleHook
///       → CoSimAdapter.read_inputs (plant → EtherCAT inputs from prev cycle)
///       → tasks_due_at_cycle → for each task: controller logic
///       → CoSimAdapter.write_outputs (EtherCAT outputs → plant)
///       ↓ PDO exchange
///       ↓ on_post_cycle
///   HilCycleHook
///       → CoSimAdapter.step(dt)
///       → verdict / telemetry collection

///|
/// HilSession holds the state for one HIL closed-loop run.
pub(all) struct HilSession {
  /// The co-simulation adapter (plant model).
  mut sim_time : @simulation.SimTime
  /// Cycle period in nanoseconds (from MasterConfig).
  cycle_period_ns : Int64
  /// Task schedule for multi-rate execution.
  schedule : @simulation.TaskSchedule
  /// Accumulated step results for post-analysis.
  step_log : Array[@simulation.SimStepResult]
  /// Whether the session has been terminated by the plant model.
  mut terminated : Bool
  /// Termination reason, if any.
  mut termination_reason : String
}

///|
pub fn HilSession::new(
  cycle_period_ns : Int64,
  schedule : @simulation.TaskSchedule,
) -> HilSession {
  {
    sim_time: @simulation.SimTime::zero(),
    cycle_period_ns,
    schedule,
    step_log: [],
    terminated: false,
    termination_reason: "",
  }
}

///|
/// HilCycleHook — CycleHook implementation that drives a CoSimAdapter.
///
/// Generic over `A : @simulation.CoSimAdapter` so the plant model is monomorphized
/// into the hook → zero dispatch overhead in the PDO hot path.
pub(all) struct HilCycleHook[A] {
  adapter : A
  session : HilSession
}

///|
pub fn[A] HilCycleHook::new(
  adapter : A,
  session : HilSession,
) -> HilCycleHook[A] {
  { adapter, session }
}

///|
pub impl[A : @simulation.CoSimAdapter] CycleHook for HilCycleHook[A] with fn on_pre_cycle(
  self,
  snap,
  ctx,
) {
  if self.session.terminated {
    return StopGraceful
  }
  // Determine which tasks are due this cycle
  let due = @simulation.tasks_due_at_cycle(self.session.schedule, snap.cycle)
  // For each due task, write outputs from adapter into PI
  for task in due {
    A::write_outputs(self.adapter, ctx.image, task.slice)
  }
  Continue
}

///|
pub impl[A : @simulation.CoSimAdapter] CycleHook for HilCycleHook[A] with fn on_post_cycle(
  self,
  _snap,
  ctx,
) {
  if self.session.terminated {
    return StopGraceful
  }
  // Read inputs from PI into adapter
  for task in self.session.schedule.tasks {
    A::read_inputs(self.adapter, ctx.image, task.slice)
  }
  // Advance the plant model by one cycle period
  let result = A::step(
    self.adapter,
    self.session.sim_time,
    self.session.cycle_period_ns,
  )
  self.session.step_log.push(result)
  match result {
    @simulation.SimStepResult::Ok(new_time) => {
      self.session.sim_time = new_time
      Continue
    }
    @simulation.SimStepResult::Terminated(reason) => {
      self.session.terminated = true
      self.session.termination_reason = reason
      StopGraceful
    }
    @simulation.SimStepResult::Error(reason) => {
      self.session.terminated = true
      self.session.termination_reason = reason
      StopGraceful
    }
  }
}

///|
pub impl[A : @simulation.CoSimAdapter] CycleHook for HilCycleHook[A] with fn on_fault(
  self,
  _snap,
  _err,
) {
  ignore(self)
  Continue
}

///|
/// Get the current simulation time from the HIL session.
pub fn HilSession::get_sim_time(self : HilSession) -> @simulation.SimTime {
  self.sim_time
}

///|
/// Check if the HIL session has been terminated.
pub fn HilSession::is_terminated(self : HilSession) -> Bool {
  self.terminated
}

///|
/// Get the termination reason.
pub fn HilSession::get_termination_reason(self : HilSession) -> String {
  self.termination_reason
}

///|
/// Get the number of simulation steps completed.
pub fn HilSession::step_count(self : HilSession) -> Int {
  self.step_log.length()
}