///|
/// Local Modbus register-backed object profile for W2A.

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

///|
pub fn modbus_medium_id() -> String {
  "modbus"
}

///|
pub(all) enum ModbusRegisterArea {
  HoldingRegister
  InputRegister
} derive(Eq, Debug)

///|
pub fn ModbusRegisterArea::channel(self : ModbusRegisterArea) -> String {
  match self {
    HoldingRegister => "holding_register"
    InputRegister => "input_register"
  }
}

///|
pub fn ModbusRegisterArea::read_function_code(self : ModbusRegisterArea) -> Int {
  match self {
    HoldingRegister => 3
    InputRegister => 4
  }
}

///|
pub fn ModbusRegisterArea::write_function_code(
  self : ModbusRegisterArea,
) -> Int? {
  match self {
    HoldingRegister => Some(6)
    InputRegister => None
  }
}

///|
pub(all) struct ModbusRegisterProfile {
  object_id : String
  label : String
  area : ModbusRegisterArea
  address : UInt
  word_count : Int
  direction : @trace.TraceDirection
  object_kind : @isocontract.ObjectKind
  behavior : String
} derive(Eq, Debug)

///|
pub(all) struct ModbusRegisterProfileReport {
  object_count : Int
  binding_count : Int
  trace_event_count : Int
  checked_events : Int
  matched_steps : Int
  eventless_lifecycle : Bool
  live_device_evidence : Bool
  conformance : @protocol_ast.ConformanceReport
} derive(Debug)

///|
pub fn ModbusRegisterProfileReport::passes(
  self : ModbusRegisterProfileReport,
) -> Bool {
  self.conformance.passes() &&
  self.object_count == 6 &&
  self.binding_count == 6 &&
  self.trace_event_count == 6 &&
  self.checked_events == 5 &&
  self.matched_steps == 5 &&
  self.eventless_lifecycle &&
  !self.live_device_evidence
}

///|
pub fn modbus_register_profiles(
  unit_id? : Int = 1,
) -> Array[ModbusRegisterProfile] {
  [
    register_profile(
      object_id="modbus.lifecycle",
      label="modbus.holding.lifecycle.write",
      area=HoldingRegister,
      address=0x0000U,
      word_count=1,
      direction=@trace.Tx,
      object_kind=@isocontract.Lifecycle,
      behavior="event-less lifecycle command encoded as a holding register write",
      unit_id~,
    ),
    register_profile(
      object_id="modbus.command",
      label="modbus.holding.command.write",
      area=HoldingRegister,
      address=0x0001U,
      word_count=1,
      direction=@trace.Tx,
      object_kind=@isocontract.Command,
      behavior="single-register command word written by the controller",
      unit_id~,
    ),
    register_profile(
      object_id="modbus.setpoint",
      label="modbus.holding.setpoint.write",
      area=HoldingRegister,
      address=0x0002U,
      word_count=2,
      direction=@trace.Tx,
      object_kind=@isocontract.Command,
      behavior="two-register setpoint write; framing is deferred to W2B",
      unit_id~,
    ),
    register_profile(
      object_id="modbus.status",
      label="modbus.input.status.read",
      area=InputRegister,
      address=0x0000U,
      word_count=1,
      direction=@trace.Rx,
      object_kind=@isocontract.Signal,
      behavior="status signal read from input register space",
      unit_id~,
    ),
    register_profile(
      object_id="modbus.actual_value",
      label="modbus.input.actual.read",
      area=InputRegister,
      address=0x0001U,
      word_count=2,
      direction=@trace.Rx,
      object_kind=@isocontract.Signal,
      behavior="actual value signal read from input registers",
      unit_id~,
    ),
    register_profile(
      object_id="modbus.parameter",
      label="modbus.holding.parameter.query",
      area=HoldingRegister,
      address=0x0010U,
      word_count=1,
      direction=@trace.Rx,
      object_kind=@isocontract.Query,
      behavior="register-backed parameter query response; request framing is deferred to W2B",
      unit_id~,
    ),
  ]
}

///|
pub fn modbus_register_bindings(
  unit_id? : Int = 1,
) -> Array[@isocontract.ObjectBinding] {
  let bindings : Array[@isocontract.ObjectBinding] = []
  for profile in modbus_register_profiles(unit_id~) {
    bindings.push(modbus_binding(profile, unit_id~))
  }
  bindings
}

///|
pub fn modbus_register_contracts(
  unit_id? : Int = 1,
) -> @isocontract.ContractRegistry {
  let registry = @isocontract.ContractRegistry::new()
  for profile in modbus_register_profiles(unit_id~) {
    registry.register(contract_for_profile(profile, unit_id~))
  }
  registry
}

///|
pub fn modbus_register_protocol() -> @protocol_ast.GlobalProtocol {
  let controller = @protocol_ast.role(name="controller")
  let device = @protocol_ast.role(name="modbus-device")
  @protocol_ast.GlobalProtocol::new(protocol_id="ModbusRegisterProfile")
  .send(
    from=controller,
    to=device,
    label="modbus.holding.lifecycle.write",
    object_id="modbus.lifecycle",
  )
  .send(
    from=controller,
    to=device,
    label="modbus.holding.command.write",
    object_id="modbus.command",
  )
  .send(
    from=controller,
    to=device,
    label="modbus.holding.setpoint.write",
    object_id="modbus.setpoint",
  )
  .send(
    from=device,
    to=controller,
    label="modbus.input.status.read",
    object_id="modbus.status",
  )
  .send(
    from=device,
    to=controller,
    label="modbus.input.actual.read",
    object_id="modbus.actual_value",
  )
}

///|
pub fn modbus_register_observer_backend(
  unit_id? : Int = 1,
) -> @protocol_ast.ObserverBackend {
  @protocol_ast.ObserverBackend::new(
    registry=modbus_register_contracts(unit_id~),
    protocol=modbus_register_protocol(),
    roles=[@protocol_ast.role(name="controller")],
  )
}

///|
pub fn modbus_register_trace_fixture(unit_id? : Int = 1) -> @trace.TraceLog {
  let log = @trace.TraceLog::new()
  log.append(
    modbus_event(
      1,
      "modbus.lifecycle.eventless.register-model",
      @trace.Meta,
      0L,
      None,
      unit_id~,
    ),
  )
  log.append(
    modbus_event(
      2,
      "modbus.holding.lifecycle.write",
      @trace.Tx,
      10L,
      Some(1),
      unit_id~,
    ),
  )
  log.append(
    modbus_event(
      3,
      "modbus.holding.command.write",
      @trace.Tx,
      20L,
      Some(0x0006),
      unit_id~,
    ),
  )
  log.append(
    modbus_event(
      4,
      "modbus.holding.setpoint.write",
      @trace.Tx,
      30L,
      Some(12_345),
      unit_id~,
    ),
  )
  log.append(
    modbus_event(
      5,
      "modbus.input.status.read",
      @trace.Rx,
      40L,
      Some(0x0021),
      unit_id~,
    ),
  )
  log.append(
    modbus_event(
      6,
      "modbus.input.actual.read",
      @trace.Rx,
      50L,
      Some(12_300),
      unit_id~,
    ),
  )
  log
}

///|
pub fn modbus_query_trace_fixture(unit_id? : Int = 1) -> @trace.TraceLog {
  let log = @trace.TraceLog::new()
  log.append(
    modbus_event(
      1,
      "modbus.holding.parameter.query",
      @trace.Rx,
      0L,
      Some(0x0042),
      unit_id~,
    ),
  )
  log
}

///|
pub fn modbus_register_observer_report(
  unit_id? : Int = 1,
) -> @protocol_ast.ConformanceReport {
  modbus_register_observer_backend(unit_id~).check_trace(
    modbus_register_trace_fixture(unit_id~),
  )
}

///|
pub fn modbus_register_profile_report(
  unit_id? : Int = 1,
) -> ModbusRegisterProfileReport {
  let profiles = modbus_register_profiles(unit_id~)
  let bindings = modbus_register_bindings(unit_id~)
  let trace = modbus_register_trace_fixture(unit_id~)
  let conformance = modbus_register_observer_backend(unit_id~).check_trace(
    trace,
  )
  {
    object_count: profiles.length(),
    binding_count: bindings.length(),
    trace_event_count: trace.len(),
    checked_events: conformance.checked_events,
    matched_steps: conformance.matched_steps,
    eventless_lifecycle: true,
    live_device_evidence: false,
    conformance,
  }
}

///|
fn register_profile(
  object_id~ : String,
  label~ : String,
  area~ : ModbusRegisterArea,
  address~ : UInt,
  word_count~ : Int,
  direction~ : @trace.TraceDirection,
  object_kind~ : @isocontract.ObjectKind,
  behavior~ : String,
  unit_id~ : Int,
) -> ModbusRegisterProfile {
  ignore(unit_id)
  {
    object_id,
    label,
    area,
    address,
    word_count,
    direction,
    object_kind,
    behavior,
  }
}

///|
fn contract_for_profile(
  profile : ModbusRegisterProfile,
  unit_id~ : Int,
) -> @isocontract.ObjectContract {
  let binding = modbus_binding(profile, unit_id~)
  match profile.object_kind {
    @isocontract.Command =>
      @isocontract.ObjectContract::make(
        id=profile.object_id,
        kind=@isocontract.Command,
        schema=if profile.word_count == 1 {
          @isocontract.U16
        } else {
          @isocontract.I32
        },
        timing=@isocontract.TimingContract::make(
          max_age_ns=1_000_000L,
          valid_for_ns=1_000_000L,
        ),
        safety=@isocontract.SafetyContract::make(
          authority=@isocontract.Role("controller"),
          timeout_action="hold_last",
        ),
      ).with_binding(binding)
    @isocontract.Signal =>
      @isocontract.ObjectContract::make(
        id=profile.object_id,
        kind=@isocontract.Signal,
        schema=if profile.word_count == 1 {
          @isocontract.U16
        } else {
          @isocontract.I32
        },
        timing=@isocontract.TimingContract::make(
          period_ns=10_000_000L,
          max_age_ns=20_000_000L,
        ),
        safety=@isocontract.SafetyContract::make(
          authority=@isocontract.ReadOnly,
        ),
      ).with_binding(binding)
    @isocontract.Query =>
      @isocontract.ObjectContract::make(
        id=profile.object_id,
        kind=@isocontract.Query,
        schema=@isocontract.U16,
        timing=@isocontract.TimingContract::empty(),
        safety=@isocontract.SafetyContract::make(
          authority=@isocontract.ReadOnly,
        ),
      ).with_binding(binding)
    @isocontract.Lifecycle =>
      @isocontract.ObjectContract::make(
        id=profile.object_id,
        kind=@isocontract.Lifecycle,
        schema=@isocontract.State,
        timing=@isocontract.TimingContract::make(max_age_ns=20_000_000L),
        safety=@isocontract.SafetyContract::make(
          authority=@isocontract.Role("controller"),
        ),
      ).with_binding(binding)
    @isocontract.Event =>
      @isocontract.ObjectContract::make(
        id=profile.object_id,
        kind=@isocontract.Event,
        schema=@isocontract.Bool,
        timing=@isocontract.TimingContract::make(max_age_ns=20_000_000L),
        safety=@isocontract.SafetyContract::make(
          authority=@isocontract.ReadOnly,
        ),
      ).with_binding(binding)
  }
}

///|
fn modbus_binding(
  profile : ModbusRegisterProfile,
  unit_id~ : Int,
) -> @isocontract.ObjectBinding {
  @isocontract.ObjectBinding::make(
    object_id=profile.object_id,
    binding_id="modbus.unit" +
      unit_id.to_string() +
      "." +
      profile.area.channel() +
      "." +
      profile.address.to_string(),
    medium_id=modbus_medium_id(),
    label=profile.label,
    direction=profile.direction,
    kind=@isocontract.ModbusRegister,
    index=profile.address,
    channel=profile.area.channel(),
    authority=@isocontract.Role("controller"),
  )
}

///|
fn modbus_event(
  event_id : Int,
  label : String,
  direction : @trace.TraceDirection,
  time_ns : Int64,
  payload_digest : Int?,
  unit_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=modbus_medium_id(),
    channel_id=None,
    direction~,
    payload_digest~,
    rng_step=event_id,
    seed=520 + unit_id,
    backend=@core.SimNative,
    label~,
  )
}