///|
pub(all) enum EncodeError {
  EncodeInvalidLayout(Int, Int)
  EncodeFrameTooShort(Int, Int)
  ValueKindMismatch(ValueKind, RawValue)
  SignalValueTypeMismatch(SignalValueType, RawValue)
  RawValueOutOfRange(RawValue, Int)
  InvalidFactor(Double)
  NonFinitePhysicalValue(Double)
  PhysicalValueOutOfRange(Double, Double, Double)
  PhysicalRawOutOfRange(Double, Int)
} derive(Eq, Debug)

///|
fn unsigned_max(bit_length : Int) -> UInt64 {
  if bit_length == 64 {
    0UL.lnot()
  } else {
    (1UL << bit_length) - 1UL
  }
}

///|
fn raw_bits_for_signal(
  signal : Signal,
  value : RawValue,
) -> Result[UInt64, EncodeError] {
  match signal.value_type {
    Float32 =>
      match value {
        FloatRaw(raw) =>
          Ok(Float::from_double(raw).reinterpret_as_uint().to_uint64())
        _ => Err(SignalValueTypeMismatch(Float32, value))
      }
    Float64 =>
      match value {
        FloatRaw(raw) => Ok(raw.reinterpret_as_uint64())
        _ => Err(SignalValueTypeMismatch(Float64, value))
      }
    Integer =>
      match (signal.value_kind, value) {
        (Unsigned, UnsignedRaw(raw)) =>
          if raw > unsigned_max(signal.bit_length) {
            Err(RawValueOutOfRange(value, signal.bit_length))
          } else {
            Ok(raw)
          }
        (Signed, SignedRaw(raw)) => {
          if signal.bit_length < 64 {
            let limit = 1L << (signal.bit_length - 1)
            if raw < -limit || raw >= limit {
              return Err(RawValueOutOfRange(value, signal.bit_length))
            }
          }
          Ok(raw.reinterpret_as_uint64() & unsigned_max(signal.bit_length))
        }
        _ => Err(ValueKindMismatch(signal.value_kind, value))
      }
  }
}

///|
fn set_payload_bit(
  payload : Array[Byte],
  absolute_bit : Int,
  bit : UInt64,
) -> Unit {
  let byte_index = absolute_bit / 8
  let bit_mask = 1UL << (absolute_bit % 8)
  let byte = payload[byte_index].to_uint64()
  let updated = if bit == 0UL {
    byte & bit_mask.lnot()
  } else {
    byte | bit_mask
  }
  payload[byte_index] = updated.to_byte()
}

///|
fn validate_encode_layout(
  signal : Signal,
  payload : BytesView,
) -> Result[Unit, EncodeError] {
  if signal.start_bit < 0 || signal.bit_length < 1 || signal.bit_length > 64 {
    return Err(EncodeInvalidLayout(signal.start_bit, signal.bit_length))
  }
  match signal.value_type {
    Float32 if signal.bit_length != 32 =>
      return Err(EncodeInvalidLayout(signal.start_bit, signal.bit_length))
    Float64 if signal.bit_length != 64 =>
      return Err(EncodeInvalidLayout(signal.start_bit, signal.bit_length))
    _ => ()
  }
  let available_bits = payload.length() * 8
  match signal.byte_order {
    Intel =>
      if signal.bit_length > available_bits ||
        signal.start_bit > available_bits - signal.bit_length {
        Err(
          EncodeFrameTooShort(
            signal.start_bit + signal.bit_length,
            available_bits,
          ),
        )
      } else {
        Ok(())
      }
    Motorola => {
      let mut absolute_bit = signal.start_bit
      for offset = 0; offset < signal.bit_length; offset = offset + 1 {
        if absolute_bit >= available_bits {
          return Err(EncodeFrameTooShort(absolute_bit + 1, available_bits))
        }
        if offset + 1 < signal.bit_length {
          absolute_bit = if absolute_bit % 8 == 0 {
            absolute_bit + 15
          } else {
            absolute_bit - 1
          }
        }
      }
      Ok(())
    }
  }
}

///|
/// Insert a raw signal value into a copy of a CAN payload.
pub fn Signal::encode_raw(
  self : Signal,
  payload : BytesView,
  value : RawValue,
) -> Result[Bytes, EncodeError] {
  match validate_encode_layout(self, payload) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  let raw = match raw_bits_for_signal(self, value) {
    Err(error) => return Err(error)
    Ok(raw) => raw
  }
  let output = payload.to_array()
  match self.byte_order {
    Intel =>
      for offset = 0; offset < self.bit_length; offset = offset + 1 {
        set_payload_bit(output, self.start_bit + offset, (raw >> offset) & 1UL)
      }
    Motorola => {
      let mut absolute_bit = self.start_bit
      for offset = 0; offset < self.bit_length; offset = offset + 1 {
        let raw_offset = self.bit_length - 1 - offset
        set_payload_bit(output, absolute_bit, (raw >> raw_offset) & 1UL)
        if offset + 1 < self.bit_length {
          absolute_bit = if absolute_bit % 8 == 0 {
            absolute_bit + 15
          } else {
            absolute_bit - 1
          }
        }
      }
    }
  }
  Ok(Bytes::from_array(output))
}

///|
/// Convert a physical value to the nearest raw integer and insert it into a
/// copy of a CAN payload.
pub fn Signal::encode(
  self : Signal,
  payload : BytesView,
  physical : Double,
) -> Result[Bytes, EncodeError] {
  match validate_encode_layout(self, payload) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  if self.factor == 0.0 || self.factor.is_nan() || self.factor.is_inf() {
    return Err(InvalidFactor(self.factor))
  }
  if physical.is_nan() || physical.is_inf() {
    return Err(NonFinitePhysicalValue(physical))
  }
  if physical < self.minimum || physical > self.maximum {
    return Err(PhysicalValueOutOfRange(physical, self.minimum, self.maximum))
  }
  let unscaled = (physical - self.offset) / self.factor
  match self.value_type {
    Float32 => {
      let raw = Float::from_double(unscaled).to_double()
      if raw.is_nan() || raw.is_inf() {
        return Err(PhysicalRawOutOfRange(unscaled, self.bit_length))
      }
      self.encode_raw(payload, FloatRaw(raw))
    }
    Float64 => {
      if unscaled.is_nan() || unscaled.is_inf() {
        return Err(PhysicalRawOutOfRange(unscaled, self.bit_length))
      }
      self.encode_raw(payload, FloatRaw(unscaled))
    }
    Integer => {
      let raw = unscaled.round()
      match self.value_kind {
        Unsigned => {
          let maximum = unsigned_max(self.bit_length).to_double()
          let too_large = if self.bit_length == 64 {
            raw >= maximum
          } else {
            raw > maximum
          }
          if raw < 0.0 || too_large {
            return Err(PhysicalRawOutOfRange(raw, self.bit_length))
          }
          self.encode_raw(payload, UnsignedRaw(raw.to_uint64()))
        }
        Signed => {
          let limit = if self.bit_length == 64 {
            9223372036854775808.0
          } else {
            let limit = (1L << (self.bit_length - 1)).to_double()
            limit
          }
          if raw < -limit || raw >= limit {
            return Err(PhysicalRawOutOfRange(raw, self.bit_length))
          }
          self.encode_raw(payload, SignedRaw(raw.to_int64()))
        }
      }
    }
  }
}