///|
/// Local CANopen CiA 402 binding skeleton for W1A.

///|
pub fn package_id() -> String {
  "fieldbus_core/canopen"
}

///|
pub fn canopen_medium_id() -> String {
  "canopen"
}

///|
pub let canopen_nmt_cob_id : Int = 0x000

///|
pub let canopen_tpdo1_base_cob_id : Int = 0x180

///|
pub let canopen_rpdo1_base_cob_id : Int = 0x200

///|
pub let canopen_sdo_upload_base_cob_id : Int = 0x580

///|
pub let canopen_sdo_download_base_cob_id : Int = 0x600

///|
pub let canopen_heartbeat_base_cob_id : Int = 0x700

///|
pub fn canopen_tpdo1_cob_id(node_id~ : Int) -> Int {
  canopen_tpdo1_base_cob_id + node_id
}

///|
pub fn canopen_rpdo1_cob_id(node_id~ : Int) -> Int {
  canopen_rpdo1_base_cob_id + node_id
}

///|
pub fn canopen_sdo_upload_cob_id(node_id~ : Int) -> Int {
  canopen_sdo_upload_base_cob_id + node_id
}

///|
pub fn canopen_sdo_download_cob_id(node_id~ : Int) -> Int {
  canopen_sdo_download_base_cob_id + node_id
}

///|
pub fn canopen_heartbeat_cob_id(node_id~ : Int) -> Int {
  canopen_heartbeat_base_cob_id + node_id
}

///|
pub(all) enum CanopenNmtCommand {
  EnterOperational
  EnterStopped
  EnterPreOperational
  ResetNode
  ResetCommunication
} derive(Eq, Debug)

///|
pub fn CanopenNmtCommand::code(self : CanopenNmtCommand) -> Int {
  match self {
    EnterOperational => 1
    EnterStopped => 2
    EnterPreOperational => 128
    ResetNode => 129
    ResetCommunication => 130
  }
}

///|
pub fn CanopenNmtCommand::label(self : CanopenNmtCommand) -> String {
  match self {
    EnterOperational => "enter_operational"
    EnterStopped => "enter_stopped"
    EnterPreOperational => "enter_pre_operational"
    ResetNode => "reset_node"
    ResetCommunication => "reset_communication"
  }
}

///|
pub(all) enum CanopenNmtState {
  Bootup
  PreOperational
  Operational
  Stopped
} derive(Eq, Debug)

///|
pub fn CanopenNmtState::code(self : CanopenNmtState) -> Int {
  match self {
    Bootup => 0
    PreOperational => 127
    Operational => 5
    Stopped => 4
  }
}

///|
pub fn CanopenNmtState::label(self : CanopenNmtState) -> String {
  match self {
    Bootup => "bootup"
    PreOperational => "pre_operational"
    Operational => "operational"
    Stopped => "stopped"
  }
}

///|
pub(all) struct CanopenCia402Asset {
  object_id : String
  label : String
  index : UInt
  subindex : Byte
  channel : String
  cob_id : Int
  direction : @trace.TraceDirection
  behavior : String
} derive(Eq, Debug)

///|
pub fn canopen_cia402_assets(node_id? : Int = 1) -> Array[CanopenCia402Asset] {
  [
    {
      object_id: "axis.controlword",
      label: "cia402.controlword.write",
      index: @profile_cia402.cia402_controlword_index,
      subindex: b'\x00',
      channel: "rpdo1",
      cob_id: canopen_rpdo1_cob_id(node_id~),
      direction: @trace.Tx,
      behavior: "controller command path uses RPDO1 for 0x6040:0 controlword",
    },
    {
      object_id: "axis.statusword",
      label: "cia402.statusword.read",
      index: @profile_cia402.cia402_statusword_index,
      subindex: b'\x00',
      channel: "tpdo1",
      cob_id: canopen_tpdo1_cob_id(node_id~),
      direction: @trace.Rx,
      behavior: "drive feedback path uses TPDO1 for 0x6041:0 statusword",
    },
    {
      object_id: "axis.mode",
      label: "cia402.mode.write",
      index: @profile_cia402.cia402_mode_of_operation_index,
      subindex: b'\x00',
      channel: "sdo-download",
      cob_id: canopen_sdo_download_cob_id(node_id~),
      direction: @trace.Tx,
      behavior: "controller writes 0x6060:0 mode of operation through SDO download",
    },
    {
      object_id: "axis.target_position",
      label: "cia402.target_position.write",
      index: @profile_cia402.cia402_target_position_index,
      subindex: b'\x00',
      channel: "rpdo1",
      cob_id: canopen_rpdo1_cob_id(node_id~),
      direction: @trace.Tx,
      behavior: "controller cyclic setpoint path uses RPDO1 for 0x607A:0 target position",
    },
    {
      object_id: "axis.actual_position",
      label: "cia402.actual_position.read",
      index: @profile_cia402.cia402_actual_position_index,
      subindex: b'\x00',
      channel: "tpdo1",
      cob_id: canopen_tpdo1_cob_id(node_id~),
      direction: @trace.Rx,
      behavior: "drive cyclic feedback path uses TPDO1 for 0x6064:0 actual position",
    },
    {
      object_id: "axis.fault",
      label: "cia402.fault.detected",
      index: @profile_cia402.cia402_fault_index,
      subindex: b'\x00',
      channel: "sdo-upload",
      cob_id: canopen_sdo_upload_cob_id(node_id~),
      direction: @trace.Rx,
      behavior: "controller reads 0x603F:0 error code through SDO upload as a profile fault event",
    },
  ]
}

///|
pub fn canopen_cia402_bindings(
  node_id? : Int = 1,
) -> Array[@isocontract.ObjectBinding] {
  [
    canopen_binding(
      object_id="axis.controlword",
      binding_id="canopen.node" + node_id.to_string() + ".rpdo1.6040.write",
      label="cia402.controlword.write",
      direction=@trace.Tx,
      kind=@isocontract.CanopenPdo,
      index=@profile_cia402.cia402_controlword_index,
      channel="rpdo1",
    ),
    canopen_binding(
      object_id="axis.statusword",
      binding_id="canopen.node" + node_id.to_string() + ".tpdo1.6041.read",
      label="cia402.statusword.read",
      direction=@trace.Rx,
      kind=@isocontract.CanopenPdo,
      index=@profile_cia402.cia402_statusword_index,
      channel="tpdo1",
    ),
    canopen_binding(
      object_id="axis.mode",
      binding_id="canopen.node" + node_id.to_string() + ".sdo.6060.write",
      label="cia402.mode.write",
      direction=@trace.Tx,
      kind=@isocontract.CanopenSdo,
      index=@profile_cia402.cia402_mode_of_operation_index,
      channel="sdo-download",
    ),
    canopen_binding(
      object_id="axis.target_position",
      binding_id="canopen.node" + node_id.to_string() + ".rpdo1.607a.write",
      label="cia402.target_position.write",
      direction=@trace.Tx,
      kind=@isocontract.CanopenPdo,
      index=@profile_cia402.cia402_target_position_index,
      channel="rpdo1",
    ),
    canopen_binding(
      object_id="axis.actual_position",
      binding_id="canopen.node" + node_id.to_string() + ".tpdo1.6064.read",
      label="cia402.actual_position.read",
      direction=@trace.Rx,
      kind=@isocontract.CanopenPdo,
      index=@profile_cia402.cia402_actual_position_index,
      channel="tpdo1",
    ),
    canopen_binding(
      object_id="axis.fault",
      binding_id="canopen.node" + node_id.to_string() + ".sdo.603f.detected",
      label="cia402.fault.detected",
      direction=@trace.Rx,
      kind=@isocontract.CanopenSdo,
      index=@profile_cia402.cia402_fault_index,
      channel="sdo-upload",
    ),
  ]
}

///|
pub fn canopen_cia402_contracts(
  node_id? : Int = 1,
) -> @isocontract.ContractRegistry {
  @profile_cia402.cia402_contracts_for_bindings(
    canopen_cia402_bindings(node_id~),
  )
}

///|
pub fn canopen_cia402_protocol() -> @protocol_ast.GlobalProtocol {
  @profile_cia402.cia402_enable_fault_reset_protocol()
}

///|
pub fn canopen_cia402_observer_backend(
  node_id? : Int = 1,
) -> @protocol_ast.ObserverBackend {
  @protocol_ast.ObserverBackend::new(
    registry=canopen_cia402_contracts(node_id~),
    protocol=canopen_cia402_protocol(),
    roles=[@protocol_ast.role(name="controller")],
  )
}

///|
pub fn canopen_cia402_trace_fixture(node_id? : Int = 1) -> @trace.TraceLog {
  let log = @trace.TraceLog::new()
  log.append(
    canopen_event(
      1,
      "canopen.nmt." + EnterOperational.label(),
      @trace.Meta,
      0L,
      Some(EnterOperational.code()),
      node_id~,
    ),
  )
  log.append(
    canopen_event(
      2,
      "cia402.controlword.write",
      @trace.Tx,
      10L,
      Some(0x0006),
      node_id~,
    ),
  )
  log.append(
    canopen_event(
      3,
      "cia402.statusword.read",
      @trace.Rx,
      20L,
      Some(0x0021),
      node_id~,
    ),
  )
  log.append(
    canopen_event(4, "cia402.mode.write", @trace.Tx, 30L, Some(0x08), node_id~),
  )
  log.append(
    canopen_event(
      5,
      "cia402.target_position.write",
      @trace.Tx,
      40L,
      Some(607_000),
      node_id~,
    ),
  )
  log.append(
    canopen_event(
      6,
      "cia402.actual_position.read",
      @trace.Rx,
      50L,
      Some(606_400),
      node_id~,
    ),
  )
  log.append(
    canopen_event(7, "cia402.fault.detected", @trace.Rx, 60L, Some(1), node_id~),
  )
  log.append(
    canopen_event(
      8,
      "cia402.controlword.write",
      @trace.Tx,
      70L,
      Some(0x0080),
      node_id~,
    ),
  )
  log.append(
    canopen_event(
      9,
      "cia402.statusword.read",
      @trace.Rx,
      80L,
      Some(0x0040),
      node_id~,
    ),
  )
  log.append(
    canopen_event(
      10,
      "canopen.heartbeat." + Operational.label(),
      @trace.Meta,
      90L,
      Some(Operational.code()),
      node_id~,
    ),
  )
  log
}

///|
pub fn canopen_cia402_observer_report(
  node_id? : Int = 1,
) -> @protocol_ast.ConformanceReport {
  canopen_cia402_observer_backend(node_id~).check_trace(
    canopen_cia402_trace_fixture(node_id~),
  )
}

///|
fn canopen_binding(
  object_id~ : String,
  binding_id~ : String,
  label~ : String,
  direction~ : @trace.TraceDirection,
  kind~ : @isocontract.BindingKind,
  index~ : UInt,
  channel~ : String,
) -> @isocontract.ObjectBinding {
  @isocontract.ObjectBinding::make(
    object_id~,
    binding_id~,
    medium_id=canopen_medium_id(),
    label~,
    direction~,
    kind~,
    index~,
    subindex=b'\x00',
    channel~,
    authority=@isocontract.Role("controller"),
  )
}

///|
fn canopen_event(
  event_id : Int,
  label : String,
  direction : @trace.TraceDirection,
  time_ns : Int64,
  payload_digest : Int?,
  node_id~ : Int,
) -> @trace.TraceEvent {
  @trace.TraceEvent::make(
    event_id~,
    parent_id=None,
    vtime=@core.VTime::from_ns(time_ns),
    clock_domain="sim",
    raw_ns=time_ns,
    node_id="controller",
    medium_id=canopen_medium_id(),
    channel_id=None,
    direction~,
    payload_digest~,
    rng_step=event_id,
    seed=301 + node_id,
    backend=@core.SimNative,
    label~,
  )
}