///|
pub(all) enum RecordChangeKind {
  RecordAdded
  RecordRemoved
  RecordTtlChanged
  RecordDataChanged
} derive(Eq, Debug)

///|
pub struct RecordChange {
  kind_value : RecordChangeKind
  left_value : ZoneRecord?
  right_value : ZoneRecord?
} derive(Eq, Debug)

///|
pub struct ZoneComparison {
  change_values : Array[RecordChange]
  added_count_value : Int
  removed_count_value : Int
  ttl_changed_count_value : Int
  data_changed_count_value : Int
} derive(Eq, Debug)

///|
fn records_exactly_equal(left : ZoneRecord, right : ZoneRecord) -> Bool {
  left.owner() == right.owner() &&
  left.ttl() == right.ttl() &&
  left.record_class() == right.record_class() &&
  left.record_type() == right.record_type() &&
  left.data() == right.data()
}

///|
fn records_share_identity(left : ZoneRecord, right : ZoneRecord) -> Bool {
  left.owner() == right.owner() &&
  left.record_class() == right.record_class() &&
  left.record_type() == right.record_type()
}

///|
fn comparison_error(message : String) -> ZoneError {
  ZoneError::new(IntegrityViolation, message, SourceSpan::point(0, 0))
}

///|
fn check_change_limit(
  changes : Array[RecordChange],
  maximum : Int,
) -> Result[Unit, ZoneError] {
  if changes.length() > maximum {
    Err(
      comparison_error(
        "zone comparison exceeds the configured difference evidence limit",
      ),
    )
  } else {
    Ok(())
  }
}

///|
pub fn compare_zones(
  left : ZoneDocument,
  right : ZoneDocument,
  max_changes? : Int = 10000,
) -> Result[ZoneComparison, ZoneError] {
  if max_changes < 0 {
    return Err(comparison_error("maximum zone changes cannot be negative"))
  }
  let left_records = left.records()
  let right_records = right.records()
  let left_used = Array::make(left_records.length(), false)
  let right_used = Array::make(right_records.length(), false)
  let changes : Array[RecordChange] = []
  let mut added = 0
  let mut removed = 0
  let mut ttl_changed = 0
  let mut data_changed = 0
  for left_index, left_record in left_records {
    for right_index, right_record in right_records {
      if !right_used[right_index] &&
        records_exactly_equal(left_record, right_record) {
        left_used[left_index] = true
        right_used[right_index] = true
        break
      }
    }
  }
  for left_index, left_record in left_records {
    if left_used[left_index] {
      continue
    }
    let mut matched = -1
    for right_index, right_record in right_records {
      if !right_used[right_index] &&
        records_share_identity(left_record, right_record) {
        matched = right_index
        break
      }
    }
    if matched >= 0 {
      let right_record = right_records[matched]
      left_used[left_index] = true
      right_used[matched] = true
      let kind = if left_record.data() == right_record.data() {
        ttl_changed = ttl_changed + 1
        RecordTtlChanged
      } else {
        data_changed = data_changed + 1
        RecordDataChanged
      }
      changes.push({
        kind_value: kind,
        left_value: Some(left_record),
        right_value: Some(right_record),
      })
      match check_change_limit(changes, max_changes) {
        Ok(_) => ()
        Err(error) => return Err(error)
      }
    }
  }
  for index, record in left_records {
    if !left_used[index] {
      removed = removed + 1
      changes.push({
        kind_value: RecordRemoved,
        left_value: Some(record),
        right_value: None,
      })
      match check_change_limit(changes, max_changes) {
        Ok(_) => ()
        Err(error) => return Err(error)
      }
    }
  }
  for index, record in right_records {
    if !right_used[index] {
      added = added + 1
      changes.push({
        kind_value: RecordAdded,
        left_value: None,
        right_value: Some(record),
      })
      match check_change_limit(changes, max_changes) {
        Ok(_) => ()
        Err(error) => return Err(error)
      }
    }
  }
  Ok({
    change_values: changes,
    added_count_value: added,
    removed_count_value: removed,
    ttl_changed_count_value: ttl_changed,
    data_changed_count_value: data_changed,
  })
}

///|
pub fn RecordChange::kind(self : RecordChange) -> RecordChangeKind {
  self.kind_value
}

///|
pub fn RecordChange::left(self : RecordChange) -> ZoneRecord? {
  self.left_value
}

///|
pub fn RecordChange::right(self : RecordChange) -> ZoneRecord? {
  self.right_value
}

///|
pub fn ZoneComparison::changes(self : ZoneComparison) -> Array[RecordChange] {
  self.change_values.copy()
}

///|
pub fn ZoneComparison::added_count(self : ZoneComparison) -> Int {
  self.added_count_value
}

///|
pub fn ZoneComparison::removed_count(self : ZoneComparison) -> Int {
  self.removed_count_value
}

///|
pub fn ZoneComparison::ttl_changed_count(self : ZoneComparison) -> Int {
  self.ttl_changed_count_value
}

///|
pub fn ZoneComparison::data_changed_count(self : ZoneComparison) -> Int {
  self.data_changed_count_value
}

///|
pub fn ZoneComparison::identical(self : ZoneComparison) -> Bool {
  self.change_values.length() == 0
}