///|
/// Application payload profile used by a safety or control message.
pub(all) enum PayloadCodecProfile {
  PayloadCodecPlain
  PayloadCodecCounterCrc
  PayloadCodecCounterChecksum
  PayloadCodecAliveAndCrc
}

///|
pub fn payload_codec_profile_variants() -> Array[PayloadCodecProfile] {
  [
    PayloadCodecPlain,
    PayloadCodecCounterCrc,
    PayloadCodecCounterChecksum,
    PayloadCodecAliveAndCrc,
  ]
}

///|
/// Result classification from a payload codec operation.
pub(all) enum PayloadCodecStatus {
  PayloadCodecAccepted
  PayloadCodecBadLength
  PayloadCodecBadCounter
  PayloadCodecBadCrc
  PayloadCodecBadChecksum
}

///|
pub fn payload_codec_status_variants() -> Array[PayloadCodecStatus] {
  [
    PayloadCodecAccepted,
    PayloadCodecBadLength,
    PayloadCodecBadCounter,
    PayloadCodecBadCrc,
    PayloadCodecBadChecksum,
  ]
}

///|
pub fn payload_codec_status_text(status : PayloadCodecStatus) -> String {
  match status {
    PayloadCodecAccepted => "accepted"
    PayloadCodecBadLength => "bad-length"
    PayloadCodecBadCounter => "bad-counter"
    PayloadCodecBadCrc => "bad-crc"
    PayloadCodecBadChecksum => "bad-checksum"
  }
}

///|
/// Codec layout options for a cyclic payload.
pub struct PayloadCodecOptions {
  profile : PayloadCodecProfile
  counter_offset : Int
  counter_high_nibble : Bool
  crc_offset : Int
  checksum_offset : Int
  expected_length : Int
  counter_modulus : Int
}

///|
pub fn payload_codec_options(
  profile? : PayloadCodecProfile = PayloadCodecPlain,
  counter_offset? : Int = 0,
  counter_high_nibble? : Bool = true,
  crc_offset? : Int = -1,
  checksum_offset? : Int = -1,
  expected_length? : Int = 0,
  counter_modulus? : Int = 16,
) -> PayloadCodecOptions {
  {
    profile,
    counter_offset,
    counter_high_nibble,
    crc_offset,
    checksum_offset,
    expected_length,
    counter_modulus,
  }
}

///|
pub fn PayloadCodecOptions::profile(
  self : PayloadCodecOptions,
) -> PayloadCodecProfile {
  self.profile
}

///|
pub fn PayloadCodecOptions::counter_offset(self : PayloadCodecOptions) -> Int {
  self.counter_offset
}

///|
pub fn PayloadCodecOptions::counter_high_nibble(
  self : PayloadCodecOptions,
) -> Bool {
  self.counter_high_nibble
}

///|
pub fn PayloadCodecOptions::crc_offset(self : PayloadCodecOptions) -> Int {
  self.crc_offset
}

///|
pub fn PayloadCodecOptions::checksum_offset(self : PayloadCodecOptions) -> Int {
  self.checksum_offset
}

///|
pub fn PayloadCodecOptions::expected_length(self : PayloadCodecOptions) -> Int {
  self.expected_length
}

///|
pub fn PayloadCodecOptions::counter_modulus(self : PayloadCodecOptions) -> Int {
  self.counter_modulus
}

///|
/// A codec result includes transformed data and validation state.
pub struct PayloadCodecResult {
  status : PayloadCodecStatus
  data : Array[Byte]
  counter : Byte?
  expected_counter : Byte?
  checksum : Byte?
}

///|
pub fn PayloadCodecResult::status(
  self : PayloadCodecResult,
) -> PayloadCodecStatus {
  self.status
}

///|
pub fn PayloadCodecResult::data(self : PayloadCodecResult) -> Array[Byte] {
  self.data.copy()
}

///|
pub fn PayloadCodecResult::counter(self : PayloadCodecResult) -> Byte? {
  self.counter
}

///|
pub fn PayloadCodecResult::expected_counter(self : PayloadCodecResult) -> Byte? {
  self.expected_counter
}

///|
pub fn PayloadCodecResult::checksum(self : PayloadCodecResult) -> Byte? {
  self.checksum
}

///|
pub fn PayloadCodecResult::accepted(self : PayloadCodecResult) -> Bool {
  self.status is PayloadCodecAccepted
}

///|
pub fn PayloadCodecResult::to_text(self : PayloadCodecResult) -> String {
  "status=" +
  payload_codec_status_text(self.status) +
  " len=" +
  self.data.length().to_string()
}

///|
/// A reusable cyclic payload codec.
pub struct PayloadCodec {
  options : PayloadCodecOptions
  mut encoded : Int
  mut decoded : Int
  mut failures : Int
  mut next_counter : Byte
}

///|
pub fn new_payload_codec(
  options? : PayloadCodecOptions = payload_codec_options(),
) -> PayloadCodec {
  { options, encoded: 0, decoded: 0, failures: 0, next_counter: 0 }
}

///|
pub fn PayloadCodec::options(self : PayloadCodec) -> PayloadCodecOptions {
  self.options
}

///|
pub fn PayloadCodec::encoded(self : PayloadCodec) -> Int {
  self.encoded
}

///|
pub fn PayloadCodec::decoded(self : PayloadCodec) -> Int {
  self.decoded
}

///|
pub fn PayloadCodec::failures(self : PayloadCodec) -> Int {
  self.failures
}

///|
pub fn PayloadCodec::next_counter(self : PayloadCodec) -> Byte {
  self.next_counter
}

///|
pub fn PayloadCodec::reset(self : PayloadCodec) -> Unit {
  self.encoded = 0
  self.decoded = 0
  self.failures = 0
  self.next_counter = 0
}

///|
/// Encode a payload, setting the configured counter and integrity byte.
pub fn PayloadCodec::encode(
  self : PayloadCodec,
  data : Array[Byte],
) -> PayloadCodecResult {
  if self.options.expected_length() > 0 &&
    data.length() != self.options.expected_length() {
    self.failures += 1
    {
      status: PayloadCodecBadLength,
      data: data.copy(),
      counter: None,
      expected_counter: None,
      checksum: None,
    }
  } else {
    let result = data.copy()
    let counter = self.next_counter
    let profile = self.options.profile()
    if profile is PayloadCodecCounterCrc ||
      profile is PayloadCodecCounterChecksum ||
      profile is PayloadCodecAliveAndCrc {
      if self.options.counter_offset() >= 0 &&
        self.options.counter_offset() < result.length() {
        let position = self.options.counter_offset()
        let previous = result[position].to_int()
        let value = if self.options.counter_high_nibble() {
          (previous & 0x0F) | ((counter.to_int() & 0x0F) << 4)
        } else {
          (previous & 0xF0) | (counter.to_int() & 0x0F)
        }
        result[position] = value.to_byte()
      }
    }
    if profile is PayloadCodecCounterCrc || profile is PayloadCodecAliveAndCrc {
      self.write_crc(result)
    }
    if profile is PayloadCodecCounterChecksum {
      self.write_checksum(result)
    }
    self.next_counter = ((counter.to_int() + 1) % self.options.counter_modulus()).to_byte()
    self.encoded += 1
    {
      status: PayloadCodecAccepted,
      data: result,
      counter: Some(counter),
      expected_counter: Some(counter),
      checksum: self.read_integrity(result),
    }
  }
}

///|
/// Decode and validate a received payload against the rolling counter.
pub fn PayloadCodec::decode(
  self : PayloadCodec,
  data : Array[Byte],
  expected_counter : Byte?,
) -> PayloadCodecResult {
  if self.options.expected_length() > 0 &&
    data.length() != self.options.expected_length() {
    self.failures += 1
    {
      status: PayloadCodecBadLength,
      data: data.copy(),
      counter: None,
      expected_counter,
      checksum: None,
    }
  } else {
    let actual_counter = self.read_counter(data)
    let counter_ok = match expected_counter {
      Some(expected) =>
        match actual_counter {
          Some(actual) => counter_is_next(expected, actual)
          None => false
        }
      None =>
        match actual_counter {
          Some(_) => true
          None => false
        }
    }
    let profile = self.options.profile()
    let integrity_ok = if profile is PayloadCodecCounterCrc ||
      profile is PayloadCodecAliveAndCrc {
      self.check_crc(data)
    } else if profile is PayloadCodecCounterChecksum {
      self.check_checksum(data)
    } else {
      true
    }
    let status = if !counter_ok && actual_counter is Some(_) {
      PayloadCodecBadCounter
    } else if !integrity_ok && profile is PayloadCodecCounterChecksum {
      PayloadCodecBadChecksum
    } else if !integrity_ok {
      PayloadCodecBadCrc
    } else {
      PayloadCodecAccepted
    }
    if status is PayloadCodecAccepted {
      self.decoded += 1
    } else {
      self.failures += 1
    }
    {
      status,
      data: data.copy(),
      counter: actual_counter,
      expected_counter,
      checksum: self.read_integrity(data),
    }
  }
}

///|
fn PayloadCodec::read_counter(self : PayloadCodec, data : Array[Byte]) -> Byte? {
  if self.options.counter_offset() < 0 ||
    self.options.counter_offset() >= data.length() {
    None
  } else {
    let value = data[self.options.counter_offset()]
    Some(
      if self.options.counter_high_nibble() {
        (value.to_int() >> 4).to_byte()
      } else {
        (value.to_int() & 0x0F).to_byte()
      },
    )
  }
}

///|
fn PayloadCodec::write_crc(self : PayloadCodec, data : Array[Byte]) -> Unit {
  if self.options.crc_offset() >= 0 && self.options.crc_offset() < data.length() {
    let offset = self.options.crc_offset()
    let prefix = data[:offset].to_owned()
    data[offset] = crc8_sae_j1850(prefix)
  }
}

///|
fn PayloadCodec::write_checksum(
  self : PayloadCodec,
  data : Array[Byte],
) -> Unit {
  if self.options.checksum_offset() >= 0 &&
    self.options.checksum_offset() < data.length() {
    let offset = self.options.checksum_offset()
    let mut sum = 0
    for index, byte in data {
      if index != offset {
        sum += byte.to_int()
      }
    }
    data[offset] = (sum & 0xFF).to_byte()
  }
}

///|
fn PayloadCodec::check_crc(self : PayloadCodec, data : Array[Byte]) -> Bool {
  if self.options.crc_offset() < 0 || self.options.crc_offset() >= data.length() {
    false
  } else {
    let offset = self.options.crc_offset()
    crc8_sae_j1850(data[:offset].to_owned()) == data[offset]
  }
}

///|
fn PayloadCodec::check_checksum(
  self : PayloadCodec,
  data : Array[Byte],
) -> Bool {
  if self.options.checksum_offset() < 0 ||
    self.options.checksum_offset() >= data.length() {
    false
  } else {
    let offset = self.options.checksum_offset()
    let mut sum = 0
    for index, byte in data {
      if index != offset {
        sum += byte.to_int()
      }
    }
    (sum & 0xFF).to_byte() == data[offset]
  }
}

///|
fn PayloadCodec::read_integrity(
  self : PayloadCodec,
  data : Array[Byte],
) -> Byte? {
  if self.options.crc_offset() >= 0 && self.options.crc_offset() < data.length() {
    Some(data[self.options.crc_offset()])
  } else if self.options.checksum_offset() >= 0 &&
    self.options.checksum_offset() < data.length() {
    Some(data[self.options.checksum_offset()])
  } else {
    None
  }
}

///|
/// Pack a sequence counter and payload into a fixed-length frame payload.
pub fn payload_codec_pack_counter(
  payload : Array[Byte],
  counter : Byte,
  offset : Int,
  high_nibble : Bool,
) -> Array[Byte] raise PayloadError {
  set_counter_nibble(payload, offset, counter, high_nibble)
}

///|
/// Validate a counter sequence against a modulus.
pub fn payload_codec_counter_valid(
  previous : Byte,
  current : Byte,
  modulus : Int,
) -> Bool {
  if modulus <= 1 {
    false
  } else {
    (previous.to_int() + 1) % modulus == current.to_int()
  }
}

///|
/// Compute an additive checksum excluding one byte position.
pub fn payload_codec_checksum(data : Array[Byte], excluded : Int) -> Byte {
  let mut sum = 0
  for index, byte in data {
    if index != excluded {
      sum += byte.to_int()
    }
  }
  (sum & 0xFF).to_byte()
}

///|
/// Compute a CRC over a payload prefix.
pub fn payload_codec_crc(data : Array[Byte]) -> Byte {
  crc8_sae_j1850(data)
}

///|
/// Return whether a payload layout is internally consistent.
pub fn payload_codec_layout_valid(options : PayloadCodecOptions) -> Bool {
  options.counter_modulus() > 1 &&
  options.expected_length() >= 0 &&
  options.counter_offset() >= 0 &&
  (options.crc_offset() < 0 || options.crc_offset() < options.expected_length()) &&
  (
    options.checksum_offset() < 0 ||
    options.checksum_offset() < options.expected_length()
  )
}

///|
/// Produce a stable codec health report.
pub fn payload_codec_report(codec : PayloadCodec) -> String {
  "profile=" +
  payload_codec_profile_text(codec.options().profile()) +
  " encoded=" +
  codec.encoded().to_string() +
  " decoded=" +
  codec.decoded().to_string() +
  " failures=" +
  codec.failures().to_string()
}

///|
pub fn payload_codec_profile_text(profile : PayloadCodecProfile) -> String {
  match profile {
    PayloadCodecPlain => "plain"
    PayloadCodecCounterCrc => "counter-crc"
    PayloadCodecCounterChecksum => "counter-checksum"
    PayloadCodecAliveAndCrc => "alive-crc"
  }
}