///|
pub(all) enum ChangeKind {
  Added
  Removed
  Modified
} derive(Eq, Debug)

///|
pub struct SignalChange {
  kind_value : ChangeKind
  message_id_value : Int
  signal_name_value : String
  detail_value : String
  breaking_value : Bool
} derive(Eq, Debug)

///|
pub struct MessageChange {
  kind_value : ChangeKind
  message_id_value : Int
  message_name_value : String
  detail_value : String
  breaking_value : Bool
} derive(Eq, Debug)

///|
pub struct DbcComparison {
  message_changes_value : Array[MessageChange]
  signal_changes_value : Array[SignalChange]
  breaking_change_count_value : Int
} derive(Eq, Debug)

///|
pub fn SignalChange::kind(self : SignalChange) -> ChangeKind {
  self.kind_value
}

///|
pub fn SignalChange::message_id(self : SignalChange) -> Int {
  self.message_id_value
}

///|
pub fn SignalChange::signal_name(self : SignalChange) -> String {
  self.signal_name_value
}

///|
pub fn SignalChange::detail(self : SignalChange) -> String {
  self.detail_value
}

///|
pub fn SignalChange::is_breaking(self : SignalChange) -> Bool {
  self.breaking_value
}

///|
pub fn MessageChange::kind(self : MessageChange) -> ChangeKind {
  self.kind_value
}

///|
pub fn MessageChange::message_id(self : MessageChange) -> Int {
  self.message_id_value
}

///|
pub fn MessageChange::message_name(self : MessageChange) -> String {
  self.message_name_value
}

///|
pub fn MessageChange::detail(self : MessageChange) -> String {
  self.detail_value
}

///|
pub fn MessageChange::is_breaking(self : MessageChange) -> Bool {
  self.breaking_value
}

///|
pub fn DbcComparison::message_changes(
  self : DbcComparison,
) -> Array[MessageChange] {
  self.message_changes_value.copy()
}

///|
pub fn DbcComparison::signal_changes(
  self : DbcComparison,
) -> Array[SignalChange] {
  self.signal_changes_value.copy()
}

///|
pub fn DbcComparison::breaking_change_count(self : DbcComparison) -> Int {
  self.breaking_change_count_value
}

///|
pub fn DbcComparison::is_compatible(self : DbcComparison) -> Bool {
  self.breaking_change_count_value == 0
}

///|
fn signal_layout_equal(left : DbcSignal, right : DbcSignal) -> Bool {
  left.start_bit() == right.start_bit() &&
  left.bit_length() == right.bit_length() &&
  left.byte_order() == right.byte_order() &&
  left.value_type() == right.value_type() &&
  left.factor() == right.factor() &&
  left.offset() == right.offset()
}

///|
fn signal_metadata_equal(left : DbcSignal, right : DbcSignal) -> Bool {
  left.minimum() == right.minimum() &&
  left.maximum() == right.maximum() &&
  left.unit() == right.unit() &&
  left.receivers() == right.receivers() &&
  left.values() == right.values() &&
  left.comment() == right.comment()
}

///|
fn compare_signals(
  before : DbcMessage,
  after : DbcMessage,
  changes : Array[SignalChange],
) -> Unit {
  for signal in before.signals() {
    match after.find_signal(signal.name()) {
      None =>
        changes.push({
          kind_value: Removed,
          message_id_value: before.id(),
          signal_name_value: signal.name(),
          detail_value: "signal was removed",
          breaking_value: true,
        })
      Some(next) =>
        if !signal_layout_equal(signal, next) {
          changes.push({
            kind_value: Modified,
            message_id_value: before.id(),
            signal_name_value: signal.name(),
            detail_value: "signal bit layout or scaling changed",
            breaking_value: true,
          })
        } else if !signal_metadata_equal(signal, next) {
          changes.push({
            kind_value: Modified,
            message_id_value: before.id(),
            signal_name_value: signal.name(),
            detail_value: "signal metadata changed",
            breaking_value: false,
          })
        }
    }
  }
  for signal in after.signals() {
    if before.find_signal(signal.name()) is None {
      changes.push({
        kind_value: Added,
        message_id_value: after.id(),
        signal_name_value: signal.name(),
        detail_value: "signal was added",
        breaking_value: false,
      })
    }
  }
}

///|
pub fn compare_dbc(before : DbcDatabase, after : DbcDatabase) -> DbcComparison {
  let message_changes : Array[MessageChange] = []
  let signal_changes : Array[SignalChange] = []
  for message in before.messages() {
    match after.find_message_by_id(message.id()) {
      None =>
        message_changes.push({
          kind_value: Removed,
          message_id_value: message.id(),
          message_name_value: message.name(),
          detail_value: "message was removed",
          breaking_value: true,
        })
      Some(next) => {
        if message.name() != next.name() ||
          message.size() != next.size() ||
          message.transmitter() != next.transmitter() {
          message_changes.push({
            kind_value: Modified,
            message_id_value: message.id(),
            message_name_value: message.name(),
            detail_value: "message name, size, or transmitter changed",
            breaking_value: true,
          })
        } else if message.comment() != next.comment() {
          message_changes.push({
            kind_value: Modified,
            message_id_value: message.id(),
            message_name_value: message.name(),
            detail_value: "message metadata changed",
            breaking_value: false,
          })
        }
        compare_signals(message, next, signal_changes)
      }
    }
  }
  for message in after.messages() {
    if before.find_message_by_id(message.id()) is None {
      message_changes.push({
        kind_value: Added,
        message_id_value: message.id(),
        message_name_value: message.name(),
        detail_value: "message was added",
        breaking_value: false,
      })
    }
  }
  let mut breaking = 0
  for change in message_changes {
    if change.is_breaking() {
      breaking = breaking + 1
    }
  }
  for change in signal_changes {
    if change.is_breaking() {
      breaking = breaking + 1
    }
  }
  {
    message_changes_value: message_changes,
    signal_changes_value: signal_changes,
    breaking_change_count_value: breaking,
  }
}