///|
pub(all) struct MessageMetadata {
  name : String
  payload_size : Int
  transmitter : String
  additional_transmitters : Array[String]
} derive(Eq, Debug)

///|
pub(all) enum DatabaseChange {
  VersionChangedChange(String?, String?)
  NodesChangedChange(Array[String], Array[String])
  CommentAddedChange(DbcComment)
  CommentRemovedChange(DbcComment)
  CommentModifiedChange(DbcCommentTarget, String, String)
  MessageAddedChange(Message)
  MessageRemovedChange(Message)
  MessageMetadataChangedChange(UInt, MessageMetadata, MessageMetadata)
  SignalAddedChange(UInt, Signal)
  SignalRemovedChange(UInt, Signal)
  SignalModifiedChange(UInt, String, Signal, Signal)
  ValueTableAddedChange(ValueTable)
  ValueTableRemovedChange(ValueTable)
  ValueTableModifiedChange(UInt, String, ValueTable, ValueTable)
  GlobalValueTableAddedChange(GlobalValueTable)
  GlobalValueTableRemovedChange(GlobalValueTable)
  GlobalValueTableModifiedChange(String, GlobalValueTable, GlobalValueTable)
  EnvironmentVariableAddedChange(EnvironmentVariable)
  EnvironmentVariableRemovedChange(EnvironmentVariable)
  EnvironmentVariableModifiedChange(
    String,
    EnvironmentVariable,
    EnvironmentVariable
  )
  SignalGroupAddedChange(SignalGroup)
  SignalGroupRemovedChange(SignalGroup)
  SignalGroupModifiedChange(UInt, String, SignalGroup, SignalGroup)
  AttributesChangedChange(DbcAttributes, DbcAttributes)
} derive(Eq, Debug)

///|
fn message_metadata(message : Message) -> MessageMetadata {
  {
    name: message.name,
    payload_size: message.payload_size,
    transmitter: message.transmitter,
    additional_transmitters: message.additional_transmitters,
  }
}

///|
fn diff_comments(
  before : Database,
  after : Database,
  changes : Array[DatabaseChange],
) -> Unit {
  for comment in before.comments {
    match after.find_comment(comment.target) {
      None => changes.push(CommentRemovedChange(comment))
      Some(after_comment) =>
        if comment.text != after_comment.text {
          changes.push(
            CommentModifiedChange(
              comment.target,
              comment.text,
              after_comment.text,
            ),
          )
        }
    }
  }
  for comment in after.comments {
    if before.find_comment(comment.target) is None {
      changes.push(CommentAddedChange(comment))
    }
  }
}

///|
fn find_signal_in(message : Message, name : StringView) -> Signal? {
  for signal in message.signals {
    if signal.name.view() == name {
      return Some(signal)
    }
  }
  None
}

///|
fn diff_message(
  before : Message,
  after : Message,
  changes : Array[DatabaseChange],
) -> Unit {
  let before_metadata = message_metadata(before)
  let after_metadata = message_metadata(after)
  if before_metadata != after_metadata {
    changes.push(
      MessageMetadataChangedChange(before.id, before_metadata, after_metadata),
    )
  }
  for signal in before.signals {
    match find_signal_in(after, signal.name) {
      None => changes.push(SignalRemovedChange(before.id, signal))
      Some(after_signal) =>
        if signal != after_signal {
          changes.push(
            SignalModifiedChange(before.id, signal.name, signal, after_signal),
          )
        }
    }
  }
  for signal in after.signals {
    if find_signal_in(before, signal.name) is None {
      changes.push(SignalAddedChange(before.id, signal))
    }
  }
}

///|
fn diff_value_tables(
  before : Database,
  after : Database,
  changes : Array[DatabaseChange],
) -> Unit {
  for table in before.value_tables {
    match after.find_value_table(table.message_id, table.signal_name) {
      None => changes.push(ValueTableRemovedChange(table))
      Some(after_table) =>
        if table != after_table {
          changes.push(
            ValueTableModifiedChange(
              table.message_id,
              table.signal_name,
              table,
              after_table,
            ),
          )
        }
    }
  }
  for table in after.value_tables {
    if before.find_value_table(table.message_id, table.signal_name) is None {
      changes.push(ValueTableAddedChange(table))
    }
  }
}

///|
fn diff_signal_groups(
  before : Database,
  after : Database,
  changes : Array[DatabaseChange],
) -> Unit {
  for group in before.signal_groups {
    match after.find_signal_group(group.message_id, group.name) {
      None => changes.push(SignalGroupRemovedChange(group))
      Some(after_group) =>
        if group != after_group {
          changes.push(
            SignalGroupModifiedChange(
              group.message_id,
              group.name,
              group,
              after_group,
            ),
          )
        }
    }
  }
  for group in after.signal_groups {
    if before.find_signal_group(group.message_id, group.name) is None {
      changes.push(SignalGroupAddedChange(group))
    }
  }
}

///|
fn diff_global_value_tables(
  before : Database,
  after : Database,
  changes : Array[DatabaseChange],
) -> Unit {
  for table in before.global_value_tables {
    match after.find_global_value_table(table.name) {
      None => changes.push(GlobalValueTableRemovedChange(table))
      Some(after_table) =>
        if table != after_table {
          changes.push(
            GlobalValueTableModifiedChange(table.name, table, after_table),
          )
        }
    }
  }
  for table in after.global_value_tables {
    if before.find_global_value_table(table.name) is None {
      changes.push(GlobalValueTableAddedChange(table))
    }
  }
}

///|
fn diff_environment_variables(
  before : Database,
  after : Database,
  changes : Array[DatabaseChange],
) -> Unit {
  for variable in before.environment_variables {
    match after.find_environment_variable(variable.name) {
      None => changes.push(EnvironmentVariableRemovedChange(variable))
      Some(after_variable) =>
        if variable != after_variable {
          changes.push(
            EnvironmentVariableModifiedChange(
              variable.name,
              variable,
              after_variable,
            ),
          )
        }
    }
  }
  for variable in after.environment_variables {
    if before.find_environment_variable(variable.name) is None {
      changes.push(EnvironmentVariableAddedChange(variable))
    }
  }
}

///|
/// Compare this database with a newer database. Changes are returned in a
/// deterministic order: global metadata, removals/modifications, then additions.
pub fn Database::diff(
  self : Database,
  newer : Database,
) -> Array[DatabaseChange] {
  let changes : Array[DatabaseChange] = []
  if self.version != newer.version {
    changes.push(VersionChangedChange(self.version, newer.version))
  }
  if self.nodes != newer.nodes {
    changes.push(NodesChangedChange(self.nodes, newer.nodes))
  }
  diff_comments(self, newer, changes)
  for message in self.messages {
    match newer.find_message(message.id) {
      None => changes.push(MessageRemovedChange(message))
      Some(newer_message) => diff_message(message, newer_message, changes)
    }
  }
  for message in newer.messages {
    if self.find_message(message.id) is None {
      changes.push(MessageAddedChange(message))
    }
  }
  diff_value_tables(self, newer, changes)
  diff_global_value_tables(self, newer, changes)
  diff_environment_variables(self, newer, changes)
  diff_signal_groups(self, newer, changes)
  if self.attributes != newer.attributes {
    changes.push(AttributesChangedChange(self.attributes, newer.attributes))
  }
  changes
}