///|
/// Direction of a J1939 transport-protocol session.
pub enum J1939TransportDirection {
  J1939TransportTransmit
  J1939TransportReceive
}

///|
pub fn j1939_transport_direction_variants() -> Array[J1939TransportDirection] {
  [J1939TransportTransmit, J1939TransportReceive]
}

///|
/// State of a J1939 BAM or TP.DT transfer.
pub enum J1939TransportState {
  J1939TransportIdle
  J1939TransportAnnounced
  J1939TransportTransferring
  J1939TransportComplete
  J1939TransportTimedOut
  J1939TransportAborted
}

///|
pub fn j1939_transport_state_variants() -> Array[J1939TransportState] {
  [
    J1939TransportIdle,
    J1939TransportAnnounced,
    J1939TransportTransferring,
    J1939TransportComplete,
    J1939TransportTimedOut,
    J1939TransportAborted,
  ]
}

///|
/// A stateful J1939 transport-protocol transfer.
pub struct J1939TransportSession {
  pgn : UInt
  direction : J1939TransportDirection
  mut total_length : Int
  mut packet_count : Int
  mut payload : Array[Byte]
  mut next_sequence : Int
  mut state : J1939TransportState
  mut last_timestamp_us : UInt64
  timeout_us : UInt64
  mut retries : Int
}

///|
pub fn new_j1939_transport_session(
  pgn : UInt,
  direction : J1939TransportDirection,
  timeout_us? : UInt64 = 750_000,
) -> J1939TransportSession {
  {
    pgn,
    direction,
    total_length: 0,
    packet_count: 0,
    payload: [],
    next_sequence: 1,
    state: J1939TransportIdle,
    last_timestamp_us: 0,
    timeout_us,
    retries: 0,
  }
}

///|
pub fn J1939TransportSession::pgn(self : J1939TransportSession) -> UInt {
  self.pgn
}

///|
pub fn J1939TransportSession::direction(
  self : J1939TransportSession,
) -> J1939TransportDirection {
  self.direction
}

///|
pub fn J1939TransportSession::total_length(self : J1939TransportSession) -> Int {
  self.total_length
}

///|
pub fn J1939TransportSession::packet_count(self : J1939TransportSession) -> Int {
  self.packet_count
}

///|
pub fn J1939TransportSession::received_length(
  self : J1939TransportSession,
) -> Int {
  self.payload.length()
}

///|
pub fn J1939TransportSession::next_sequence(
  self : J1939TransportSession,
) -> Int {
  self.next_sequence
}

///|
pub fn J1939TransportSession::state(
  self : J1939TransportSession,
) -> J1939TransportState {
  self.state
}

///|
pub fn J1939TransportSession::last_timestamp_us(
  self : J1939TransportSession,
) -> UInt64 {
  self.last_timestamp_us
}

///|
pub fn J1939TransportSession::retries(self : J1939TransportSession) -> Int {
  self.retries
}

///|
pub fn J1939TransportSession::payload(
  self : J1939TransportSession,
) -> Array[Byte] {
  self.payload.copy()
}

///|
pub fn J1939TransportSession::complete(self : J1939TransportSession) -> Bool {
  self.state is J1939TransportComplete
}

///|
/// Start a BAM transfer and generate the announcement and data packets.
pub fn J1939TransportSession::start_transmit(
  self : J1939TransportSession,
  payload : Array[Byte],
  timestamp_us : UInt64,
) -> Array[Array[Byte]] raise J1939Error {
  if self.direction is J1939TransportReceive ||
    payload.is_empty() ||
    payload.length() > 1785 {
    raise TransferTooLarge
  }
  if self.pgn > 0x3FFFF {
    raise InvalidPgn
  }
  self.total_length = payload.length()
  self.packet_count = (payload.length() + 6) / 7
  self.payload = payload.copy()
  self.next_sequence = 1
  self.state = J1939TransportAnnounced
  self.last_timestamp_us = timestamp_us
  self.next_packets()
}

///|
/// Accept a BAM announcement for a receive session.
pub fn J1939TransportSession::accept_announcement(
  self : J1939TransportSession,
  announcement : Array[Byte],
  timestamp_us : UInt64,
) -> Bool {
  match j1939_parse_bam(announcement) {
    Some((length, count, pgn)) =>
      if self.direction is J1939TransportReceive &&
        pgn == self.pgn &&
        length <= 1785 {
        self.total_length = length
        self.packet_count = count
        self.payload.clear()
        self.next_sequence = 1
        self.state = J1939TransportTransferring
        self.last_timestamp_us = timestamp_us
        true
      } else {
        false
      }
    None => false
  }
}

///|
/// Accept one TP.DT packet and return whether it advanced the transfer.
pub fn J1939TransportSession::accept_packet(
  self : J1939TransportSession,
  packet : Array[Byte],
  timestamp_us : UInt64,
) -> Bool {
  if self.state is J1939TransportComplete ||
    self.state is J1939TransportTimedOut ||
    packet.is_empty() {
    false
  } else if packet[0].to_int() != self.next_sequence {
    self.state = J1939TransportAborted
    self.retries += 1
    false
  } else {
    self.state = J1939TransportTransferring
    for byte in packet[1:] {
      if self.payload.length() < self.total_length {
        self.payload.push(byte)
      }
    }
    self.next_sequence += 1
    self.last_timestamp_us = timestamp_us
    if self.payload.length() >= self.total_length {
      self.payload = self.payload[:self.total_length].to_owned()
      self.state = J1939TransportComplete
    }
    true
  }
}

///|
pub fn J1939TransportSession::timed_out(
  self : J1939TransportSession,
  timestamp_us : UInt64,
) -> Bool {
  if self.state is J1939TransportTransferring ||
    self.state is J1939TransportAnnounced {
    if timestamp_us > self.last_timestamp_us + self.timeout_us {
      self.state = J1939TransportTimedOut
      true
    } else {
      false
    }
  } else {
    false
  }
}

///|
pub fn J1939TransportSession::take_payload(
  self : J1939TransportSession,
) -> Array[Byte]? {
  if self.complete() {
    Some(self.payload.copy())
  } else {
    None
  }
}

///|
fn J1939TransportSession::next_packets(
  self : J1939TransportSession,
) -> Array[Array[Byte]] {
  let result : Array[Array[Byte]] = []
  let announcement : Array[Byte] = [
    0x20,
    self.total_length.to_byte(),
    (self.total_length >> 8).to_byte(),
    self.packet_count.to_byte(),
    0xFF,
    self.pgn.to_byte(),
    (self.pgn >> 8).to_byte(),
    (self.pgn >> 16).to_byte(),
  ]
  result.push(announcement)
  let mut offset = 0
  for sequence in 1..<=self.packet_count {
    let count = if self.total_length - offset > 7 {
      7
    } else {
      self.total_length - offset
    }
    let packet = [sequence.to_byte()] +
      self.payload[offset:offset + count].to_owned()
    result.push(packet + Array::make(8 - packet.length(), 0xFF))
    offset += count
  }
  self.state = J1939TransportComplete
  result
}

///|
/// A J1939 address-claim record.
pub struct J1939AddressClaim {
  source_address : Byte
  name : UInt64
  manufacturer : UInt
  function : Byte
  instance : Byte
  mut last_seen_us : UInt64
  mut preferred : Bool
}

///|
pub fn j1939_address_claim(
  source_address : Byte,
  name : UInt64,
  manufacturer : UInt,
  function : Byte,
  instance : Byte,
  timestamp_us? : UInt64 = 0,
) -> J1939AddressClaim {
  {
    source_address,
    name,
    manufacturer,
    function,
    instance,
    last_seen_us: timestamp_us,
    preferred: false,
  }
}

///|
pub fn J1939AddressClaim::source_address(self : J1939AddressClaim) -> Byte {
  self.source_address
}

///|
pub fn J1939AddressClaim::name(self : J1939AddressClaim) -> UInt64 {
  self.name
}

///|
pub fn J1939AddressClaim::manufacturer(self : J1939AddressClaim) -> UInt {
  self.manufacturer
}

///|
pub fn J1939AddressClaim::function(self : J1939AddressClaim) -> Byte {
  self.function
}

///|
pub fn J1939AddressClaim::instance(self : J1939AddressClaim) -> Byte {
  self.instance
}

///|
pub fn J1939AddressClaim::last_seen_us(self : J1939AddressClaim) -> UInt64 {
  self.last_seen_us
}

///|
pub fn J1939AddressClaim::preferred(self : J1939AddressClaim) -> Bool {
  self.preferred
}

///|
pub fn J1939AddressClaim::refresh(
  self : J1939AddressClaim,
  timestamp_us : UInt64,
) -> Unit {
  self.last_seen_us = timestamp_us
}

///|
pub fn J1939AddressClaim::set_preferred(
  self : J1939AddressClaim,
  preferred : Bool,
) -> Unit {
  self.preferred = preferred
}

///|
pub fn J1939AddressClaim::wire_name(self : J1939AddressClaim) -> Array[Byte] {
  [
    self.name.to_byte(),
    (self.name >> 8).to_byte(),
    (self.name >> 16).to_byte(),
    (self.name >> 24).to_byte(),
    (self.name >> 32).to_byte(),
    (self.name >> 40).to_byte(),
    (self.name >> 48).to_byte(),
    (self.name >> 56).to_byte(),
  ]
}

///|
/// Address-claim table with deterministic conflict resolution.
pub struct J1939AddressManager {
  claims : Array[J1939AddressClaim]
  mut conflicts : Int
  mut changes : Int
  timeout_us : UInt64
}

///|
pub fn new_j1939_address_manager(
  timeout_us? : UInt64 = 2_500_000,
) -> J1939AddressManager {
  { claims: [], conflicts: 0, changes: 0, timeout_us }
}

///|
pub fn J1939AddressManager::claim(
  self : J1939AddressManager,
  claim : J1939AddressClaim,
) -> Bool {
  match self.find_index(claim.source_address()) {
    Some(index) =>
      if claim.name() < self.claims[index].name() {
        self.claims[index] = claim
        self.changes += 1
        true
      } else {
        self.conflicts += 1
        false
      }
    None => {
      self.claims.push(claim)
      self.changes += 1
      true
    }
  }
}

///|
pub fn J1939AddressManager::refresh(
  self : J1939AddressManager,
  source_address : Byte,
  timestamp_us : UInt64,
) -> Bool {
  match self.find_index(source_address) {
    Some(index) => {
      self.claims[index].refresh(timestamp_us)
      true
    }
    None => false
  }
}

///|
pub fn J1939AddressManager::expire(
  self : J1939AddressManager,
  timestamp_us : UInt64,
) -> Int {
  let mut removed = 0
  let mut index = self.claims.length() - 1
  while index >= 0 && !self.claims.is_empty() {
    if timestamp_us > self.claims[index].last_seen_us() + self.timeout_us {
      ignore(self.claims.remove(index))
      removed += 1
    }
    index -= 1
  }
  removed
}

///|
pub fn J1939AddressManager::find(
  self : J1939AddressManager,
  source_address : Byte,
) -> J1939AddressClaim? {
  match self.find_index(source_address) {
    Some(index) => Some(self.claims[index])
    None => None
  }
}

///|
pub fn J1939AddressManager::claims(
  self : J1939AddressManager,
) -> Array[J1939AddressClaim] {
  self.claims.copy()
}

///|
pub fn J1939AddressManager::conflicts(self : J1939AddressManager) -> Int {
  self.conflicts
}

///|
pub fn J1939AddressManager::changes(self : J1939AddressManager) -> Int {
  self.changes
}

///|
fn J1939AddressManager::find_index(
  self : J1939AddressManager,
  source_address : Byte,
) -> Int? {
  for index, claim in self.claims {
    if claim.source_address() == source_address {
      return Some(index)
    }
  }
  None
}

///|
/// A scale and offset definition for a J1939 SPN.
pub struct J1939SpnDefinition {
  spn : UInt
  name : String
  start_bit : Int
  length : Int
  factor : Double
  offset : Double
  minimum : Double
  maximum : Double
  unit : String
}

///|
pub fn j1939_spn(
  spn : UInt,
  name : String,
  start_bit : Int,
  length : Int,
  factor? : Double = 1.0,
  offset? : Double = 0.0,
  minimum? : Double = 0.0,
  maximum? : Double = 0.0,
  unit? : String = "",
) -> J1939SpnDefinition {
  { spn, name, start_bit, length, factor, offset, minimum, maximum, unit }
}

///|
pub fn J1939SpnDefinition::spn(self : J1939SpnDefinition) -> UInt {
  self.spn
}

///|
pub fn J1939SpnDefinition::name(self : J1939SpnDefinition) -> String {
  self.name
}

///|
pub fn J1939SpnDefinition::start_bit(self : J1939SpnDefinition) -> Int {
  self.start_bit
}

///|
pub fn J1939SpnDefinition::length(self : J1939SpnDefinition) -> Int {
  self.length
}

///|
pub fn J1939SpnDefinition::factor(self : J1939SpnDefinition) -> Double {
  self.factor
}

///|
pub fn J1939SpnDefinition::offset(self : J1939SpnDefinition) -> Double {
  self.offset
}

///|
pub fn J1939SpnDefinition::unit(self : J1939SpnDefinition) -> String {
  self.unit
}

///|
pub fn J1939SpnDefinition::decode(
  self : J1939SpnDefinition,
  data : Array[Byte],
) -> Double {
  let mut raw : UInt = 0
  for bit in 0..> bit_index) & 1) == 1 {
      raw = raw | (1 << bit)
    }
  }
  raw.to_double() * self.factor + self.offset
}

///|
pub fn J1939SpnDefinition::encode(
  self : J1939SpnDefinition,
  data : Array[Byte],
  value : Double,
) -> Array[Byte] {
  let raw = ((value - self.offset) / self.factor).to_int().reinterpret_as_uint()
  let result = data.copy()
  while result.length() < (self.start_bit + self.length + 7) / 8 {
    result.push(0)
  }
  for bit in 0..> bit) & 1) == 1 {
      result[byte_index] = result[byte_index] | mask
    } else {
      result[byte_index] = result[byte_index] & (0xFF ^ mask.to_int()).to_byte()
    }
  }
  result
}

///|
/// Decode multiple configured SPNs from a PGN payload.
pub fn j1939_decode_spns(
  definitions : Array[J1939SpnDefinition],
  data : Array[Byte],
) -> Array[(UInt, Double)] {
  let result : Array[(UInt, Double)] = []
  for item in definitions {
    result.push((item.spn(), item.decode(data)))
  }
  result
}