///|
/// Values carried by standard IEC 104 information objects.
pub enum ApplicationValue {
  SinglePointValue(SinglePointValue)
  DoublePointValue(DoublePointValue)
  StepPositionValue(StepPositionValue)
  BitStringValue(UInt)
  NormalizedMeasurement(NormalizedValue)
  ScaledMeasurement(ScaledValue)
  ShortFloatMeasurement(ShortFloatValue)
  BinaryCounterMeasurement(BinaryCounterValue)
  SingleCommand(Bool, Int)
  DoubleCommand(Int, Int)
  RegulatingStepCommand(Int, Int)
  NormalizedSetPoint(Int, Int)
  ScaledSetPoint(Int, Int)
  ShortFloatSetPoint(Float, Int)
  BitStringCommand(UInt)
  InterrogationCommand(Int)
  CounterInterrogationCommand(Int)
  ReadCommand
  ClockSyncCommand(Cp56Time)
  TestCommand(Int)
  ResetCommand(Int)
  DelayCommand(Int)
  EndOfInitialization(Int)
  RawValue(Bytes)
} derive(Eq, Debug)

///|
/// An address-qualified application object with an optional time tag.
pub struct ApplicationObject {
  address : InformationAddress
  type_id : ApplicationType
  value : ApplicationValue
  time_tag : TimeTag?
} derive(Eq, Debug)

///|
fn make_application_object(
  address : InformationAddress,
  value : ApplicationValue,
  time_tag : TimeTag?,
) -> Result[ApplicationObject, String] {
  let base_type = application_value_type(value)
  let type_id = match time_tag {
    Some(Short(_)) => timed_application_type(base_type, Cp24TimeTag)
    Some(Long(_)) => timed_application_type(base_type, Cp56TimeTag)
    None => base_type
  }
  let expected = time_tag_kind_for_type(type_id)
  match (expected, time_tag) {
    (NoTimeTag, Some(_)) => Err("application type does not accept a time tag")
    (Cp24TimeTag, Some(Short(_))) => Ok({ address, type_id, value, time_tag })
    (Cp56TimeTag, Some(Long(_))) => Ok({ address, type_id, value, time_tag })
    (NoTimeTag, None) => Ok({ address, type_id, value, time_tag })
    (_, None) => Err("time-tagged application type requires a time tag")
    _ => Err("time tag precision does not match application type")
  }
}

///|
/// Construct an application object and optionally attach its standard time tag.
pub fn ApplicationObject::new(
  address : InformationAddress,
  value : ApplicationValue,
  time_tag? : TimeTag,
) -> Result[ApplicationObject, String] {
  make_application_object(address, value, time_tag)
}

///|
pub fn ApplicationObject::address(
  self : ApplicationObject,
) -> InformationAddress {
  self.address
}

///|
pub fn ApplicationObject::type_id(self : ApplicationObject) -> ApplicationType {
  self.type_id
}

///|
pub fn ApplicationObject::value(self : ApplicationObject) -> ApplicationValue {
  self.value
}

///|
pub fn ApplicationObject::time_tag(self : ApplicationObject) -> TimeTag? {
  self.time_tag
}

///|
pub fn ApplicationObject::is_command(self : ApplicationObject) -> Bool {
  self.type_id.is_control()
}

///|
pub fn ApplicationObject::is_measurement(self : ApplicationObject) -> Bool {
  self.type_id.is_monitoring()
}

///|
pub fn application_value_type(value : ApplicationValue) -> ApplicationType {
  match value {
    SinglePointValue(_) => MSpNa
    DoublePointValue(_) => MDpNa
    StepPositionValue(_) => MStNa
    BitStringValue(_) => MBoNa
    NormalizedMeasurement(_) => MMeNa
    ScaledMeasurement(_) => MMeNb
    ShortFloatMeasurement(_) => MMeNc
    BinaryCounterMeasurement(_) => MItNa
    SingleCommand(_, _) => CScNa
    DoubleCommand(_, _) => CDcNa
    RegulatingStepCommand(_, _) => CRcNa
    NormalizedSetPoint(_, _) => CSeNa
    ScaledSetPoint(_, _) => CSeNb
    ShortFloatSetPoint(_, _) => CSeNc
    BitStringCommand(_) => CBoNa
    InterrogationCommand(_) => CIcNa
    CounterInterrogationCommand(_) => CCiNa
    ReadCommand => CRdNa
    ClockSyncCommand(_) => CCsNa
    TestCommand(_) => CTsNa
    ResetCommand(_) => CRpNa
    DelayCommand(_) => CRpNa
    EndOfInitialization(_) => MEiNa
    RawValue(_) => UnknownType(0)
  }
}

///|
pub fn application_value_width(value : ApplicationValue) -> Int {
  match value {
    SinglePointValue(_) => 1
    DoublePointValue(_) => 1
    StepPositionValue(_) => 2
    BitStringValue(_) => 5
    NormalizedMeasurement(_) => 3
    ScaledMeasurement(_) => 3
    ShortFloatMeasurement(_) => 5
    BinaryCounterMeasurement(_) => 5
    SingleCommand(_, _) => 1
    DoubleCommand(_, _) => 1
    RegulatingStepCommand(_, _) => 1
    NormalizedSetPoint(_, _) => 2
    ScaledSetPoint(_, _) => 2
    ShortFloatSetPoint(_, _) => 4
    BitStringCommand(_) => 4
    InterrogationCommand(_) => 1
    CounterInterrogationCommand(_) => 1
    ReadCommand => 0
    ClockSyncCommand(_) => 7
    TestCommand(_) => 2
    ResetCommand(_) => 1
    DelayCommand(_) => 2
    EndOfInitialization(_) => 1
    RawValue(value) => value.length()
  }
}

///|
pub fn single_point_object(
  address : InformationAddress,
  state : Bool,
  quality? : StatusQuality = StatusQuality::clear(),
  time_tag? : TimeTag,
) -> Result[ApplicationObject, String] {
  make_application_object(
    address,
    SinglePointValue(SinglePointValue::new(state, quality~)),
    time_tag,
  )
}

///|
pub fn double_point_object(
  address : InformationAddress,
  state : Int,
  quality? : StatusQuality = StatusQuality::clear(),
  time_tag? : TimeTag,
) -> Result[ApplicationObject, String] {
  match DoublePointValue::new(state, quality~) {
    Ok(value) =>
      make_application_object(address, DoublePointValue(value), time_tag)
    Err(error) => Err(error)
  }
}

///|
pub fn step_position_object(
  address : InformationAddress,
  position : Int,
  transient? : Bool = false,
  quality? : StatusQuality = StatusQuality::clear(),
  time_tag? : TimeTag,
) -> Result[ApplicationObject, String] {
  match StepPositionValue::new(position, transient~, quality~) {
    Ok(value) =>
      make_application_object(address, StepPositionValue(value), time_tag)
    Err(error) => Err(error)
  }
}

///|
pub fn bit_string_object(
  address : InformationAddress,
  value : UInt,
  time_tag? : TimeTag,
) -> Result[ApplicationObject, String] {
  make_application_object(address, BitStringValue(value), time_tag)
}

///|
pub fn normalized_object(
  address : InformationAddress,
  value : Int,
  quality? : QualityDescriptor = QualityDescriptor::clear(),
  time_tag? : TimeTag,
) -> Result[ApplicationObject, String] {
  match NormalizedValue::new(value, quality~) {
    Ok(measurement) =>
      make_application_object(
        address,
        NormalizedMeasurement(measurement),
        time_tag,
      )
    Err(error) => Err(error)
  }
}

///|
pub fn scaled_object(
  address : InformationAddress,
  value : Int,
  quality? : QualityDescriptor = QualityDescriptor::clear(),
  time_tag? : TimeTag,
) -> Result[ApplicationObject, String] {
  match ScaledValue::new(value, quality~) {
    Ok(measurement) =>
      make_application_object(address, ScaledMeasurement(measurement), time_tag)
    Err(error) => Err(error)
  }
}

///|
pub fn short_float_object(
  address : InformationAddress,
  value : Float,
  quality? : QualityDescriptor = QualityDescriptor::clear(),
  time_tag? : TimeTag,
) -> Result[ApplicationObject, String] {
  make_application_object(
    address,
    ShortFloatMeasurement(ShortFloatValue::new(value, quality~)),
    time_tag,
  )
}

///|
pub fn binary_counter_object(
  address : InformationAddress,
  value : UInt,
  sequence? : Int = 0,
  time_tag? : TimeTag,
) -> Result[ApplicationObject, String] {
  match BinaryCounterValue::new(value, sequence~) {
    Ok(measurement) =>
      make_application_object(
        address,
        BinaryCounterMeasurement(measurement),
        time_tag,
      )
    Err(error) => Err(error)
  }
}

///|
pub fn single_command_object(
  address : InformationAddress,
  state : Bool,
  qualifier : Int,
) -> Result[ApplicationObject, String] {
  if qualifier < 0 || qualifier > 255 {
    Err("single command qualifier must fit one byte")
  } else {
    ApplicationObject::new(address, SingleCommand(state, qualifier))
  }
}

///|
pub fn double_command_object(
  address : InformationAddress,
  state : Int,
  qualifier : Int,
) -> Result[ApplicationObject, String] {
  if state < 0 || state > 3 {
    Err("double command state must fit two bits")
  } else if qualifier < 0 || qualifier > 255 {
    Err("double command qualifier must fit one byte")
  } else {
    ApplicationObject::new(address, DoubleCommand(state, qualifier))
  }
}

///|
pub fn regulating_step_command_object(
  address : InformationAddress,
  step : Int,
  qualifier : Int,
) -> Result[ApplicationObject, String] {
  if step < -64 || step > 63 {
    Err("regulating step must fit signed seven bits")
  } else if qualifier < 0 || qualifier > 255 {
    Err("regulating step qualifier must fit one byte")
  } else {
    ApplicationObject::new(address, RegulatingStepCommand(step, qualifier))
  }
}

///|
pub fn normalized_set_point_object(
  address : InformationAddress,
  value : Int,
  qualifier : Int,
) -> Result[ApplicationObject, String] {
  if value < -32768 || value > 32767 {
    Err("normalized set point must fit signed 16 bits")
  } else if qualifier < 0 || qualifier > 255 {
    Err("set point qualifier must fit one byte")
  } else {
    ApplicationObject::new(address, NormalizedSetPoint(value, qualifier))
  }
}

///|
pub fn scaled_set_point_object(
  address : InformationAddress,
  value : Int,
  qualifier : Int,
) -> Result[ApplicationObject, String] {
  if value < -32768 || value > 32767 {
    Err("scaled set point must fit signed 16 bits")
  } else if qualifier < 0 || qualifier > 255 {
    Err("set point qualifier must fit one byte")
  } else {
    ApplicationObject::new(address, ScaledSetPoint(value, qualifier))
  }
}

///|
pub fn short_float_set_point_object(
  address : InformationAddress,
  value : Float,
  qualifier : Int,
) -> Result[ApplicationObject, String] {
  if qualifier < 0 || qualifier > 255 {
    Err("set point qualifier must fit one byte")
  } else {
    ApplicationObject::new(address, ShortFloatSetPoint(value, qualifier))
  }
}

///|
pub fn interrogation_object(
  address : InformationAddress,
  qualifier : Int,
) -> Result[ApplicationObject, String] {
  if qualifier < 0 || qualifier > 255 {
    Err("interrogation qualifier must fit one byte")
  } else {
    ApplicationObject::new(address, InterrogationCommand(qualifier))
  }
}

///|
pub fn counter_interrogation_object(
  address : InformationAddress,
  qualifier : Int,
) -> Result[ApplicationObject, String] {
  if qualifier < 0 || qualifier > 255 {
    Err("counter interrogation qualifier must fit one byte")
  } else {
    ApplicationObject::new(address, CounterInterrogationCommand(qualifier))
  }
}

///|
pub fn read_object(
  address : InformationAddress,
) -> Result[ApplicationObject, String] {
  ApplicationObject::new(address, ReadCommand)
}

///|
pub fn clock_sync_object(
  address : InformationAddress,
  value : Cp56Time,
) -> Result[ApplicationObject, String] {
  ApplicationObject::new(address, ClockSyncCommand(value))
}

///|
pub fn test_command_object(
  address : InformationAddress,
  value : Int,
) -> Result[ApplicationObject, String] {
  if value < 0 || value > 65535 {
    Err("test command value must fit 16 bits")
  } else {
    ApplicationObject::new(address, TestCommand(value))
  }
}

///|
pub fn reset_command_object(
  address : InformationAddress,
  value : Int,
) -> Result[ApplicationObject, String] {
  if value < 0 || value > 255 {
    Err("reset command value must fit one byte")
  } else {
    ApplicationObject::new(address, ResetCommand(value))
  }
}

///|
pub fn end_of_initialization_object(
  address : InformationAddress,
  value : Int,
) -> Result[ApplicationObject, String] {
  if value < 0 || value > 255 {
    Err("end-of-initialization value must fit one byte")
  } else {
    ApplicationObject::new(address, EndOfInitialization(value))
  }
}

///|
fn push_object_u16(out : Array[Byte], value : Int) -> Unit {
  out.push((value & 0xff).to_byte())
  out.push(((value >> 8) & 0xff).to_byte())
}

///|
fn push_object_u32(out : Array[Byte], value : UInt) -> Unit {
  out.push((value & 0xffU).to_byte())
  out.push(((value >> 8) & 0xffU).to_byte())
  out.push(((value >> 16) & 0xffU).to_byte())
  out.push(((value >> 24) & 0xffU).to_byte())
}

///|
fn push_object_address(out : Array[Byte], address : InformationAddress) -> Unit {
  out.push(address.low().to_byte())
  out.push(address.middle().to_byte())
  out.push(address.high().to_byte())
}

///|
/// Encode an IOA-qualified object without the ASDU header.
pub fn encode_application_object(object : ApplicationObject) -> Bytes {
  let out : Array[Byte] = []
  push_object_address(out, object.address)
  match object.value {
    SinglePointValue(value) => out.push(value.to_byte().to_byte())
    DoublePointValue(value) => out.push(value.to_byte().to_byte())
    StepPositionValue(value) =>
      for byte in value.to_array() {
        out.push(byte)
      }
    BitStringValue(value) => push_object_u32(out, value)
    NormalizedMeasurement(value) =>
      for byte in value.to_array() {
        out.push(byte)
      }
    ScaledMeasurement(value) =>
      for byte in value.to_array() {
        out.push(byte)
      }
    ShortFloatMeasurement(value) => {
      push_object_u32(out, value.value().reinterpret_as_uint())
      out.push(value.quality().to_byte().to_byte())
    }
    BinaryCounterMeasurement(value) => {
      push_object_u32(out, value.value())
      out.push(value.flags().to_byte())
    }
    SingleCommand(state, qualifier) =>
      out.push(((if state { 1 } else { 0 }) | (qualifier & 0xf0)).to_byte())
    DoubleCommand(state, qualifier) =>
      out.push(((state & 3) | (qualifier & 0xf0)).to_byte())
    RegulatingStepCommand(step, qualifier) => {
      let encoded = if step < 0 { step + 128 } else { step }
      out.push(((encoded & 0x7f) | (qualifier & 0xf0)).to_byte())
    }
    NormalizedSetPoint(value, qualifier) => {
      let raw = if value < 0 { value + 65536 } else { value }
      push_object_u16(out, raw)
      out.push(qualifier.to_byte())
    }
    ScaledSetPoint(value, qualifier) => {
      let raw = if value < 0 { value + 65536 } else { value }
      push_object_u16(out, raw)
      out.push(qualifier.to_byte())
    }
    ShortFloatSetPoint(value, qualifier) => {
      push_object_u32(out, value.reinterpret_as_uint())
      out.push(qualifier.to_byte())
    }
    BitStringCommand(value) => push_object_u32(out, value)
    InterrogationCommand(qualifier) => out.push(qualifier.to_byte())
    CounterInterrogationCommand(qualifier) => out.push(qualifier.to_byte())
    ReadCommand => ()
    ClockSyncCommand(value) =>
      for byte in value.to_array() {
        out.push(byte)
      }
    TestCommand(value) => push_object_u16(out, value)
    ResetCommand(value) => out.push(value.to_byte())
    DelayCommand(value) => push_object_u16(out, value)
    EndOfInitialization(value) => out.push(value.to_byte())
    RawValue(value) =>
      for byte in value {
        out.push(byte)
      }
  }
  match object.time_tag {
    Some(tag) =>
      for byte in tag.to_array() {
        out.push(byte)
      }
    None => ()
  }
  Bytes::from_array(out)
}

///|
/// Return a type-tagged object as a stable compact summary tuple.
pub fn ApplicationObject::summary(
  self : ApplicationObject,
) -> (Int, Int, Int, Bool) {
  (
    self.type_id.number(),
    self.address.number(),
    application_value_width(self.value),
    self.time_tag is Some(_),
  )
}

///|
/// Validate an application object before placing it in an ASDU.
pub fn ApplicationObject::validate(
  self : ApplicationObject,
) -> Result[Unit, Diagnostic] {
  if self.address.number() < 0 || self.address.number() > 0xffffff {
    Err(Diagnostic::new(InvalidAddress, "object address is outside 24 bits"))
  } else if self.type_id is UnknownType(_) {
    Err(
      Diagnostic::new(InvalidType, "unknown application type cannot be encoded"),
    )
  } else {
    Ok(())
  }
}

///|
pub fn application_value_examples() -> Array[ApplicationValue] {
  [
    SinglePointValue(SinglePointValue::new(true)),
    DoublePointValue(DoublePointValue::new(1).unwrap()),
    StepPositionValue(StepPositionValue::new(-3).unwrap()),
    BitStringValue(0x1234U),
    NormalizedMeasurement(NormalizedValue::new(120).unwrap()),
    ScaledMeasurement(ScaledValue::new(-20).unwrap()),
    ShortFloatMeasurement(ShortFloatValue::new(2.5)),
    BinaryCounterMeasurement(BinaryCounterValue::new(42U).unwrap()),
    SingleCommand(true, 1),
    DoubleCommand(2, 1),
    RegulatingStepCommand(-1, 1),
    NormalizedSetPoint(2, 1),
    ScaledSetPoint(3, 1),
    ShortFloatSetPoint(4.0, 1),
    BitStringCommand(0U),
    InterrogationCommand(20),
    CounterInterrogationCommand(5),
    ReadCommand,
    ClockSyncCommand(Cp56Time::new(26, 1, 1, 0, 0, 0).unwrap()),
    TestCommand(0),
    ResetCommand(1),
    DelayCommand(10),
    EndOfInitialization(0),
    RawValue(b"raw"),
  ]
}