///|
/// Typed comparison ignores map entry order but preserves array order and float bits.
fn same_value(left : Value, right : Value) -> Bool {
  match (left, right) {
    (Object(a), Object(b)) => {
      if a.length() != b.length() {
        return false
      }
      let indexed : Map[String, Value] = Map(b)
      for item in a {
        let (key, value) = item
        match indexed.get(key) {
          Some(other) => if !same_value(value, other) { return false }
          None => return false
        }
      }
      true
    }
    (List(a), List(b)) => {
      if a.length() != b.length() {
        return false
      }
      for i in 0.. left == right
  }
}

///|
/// Difference for one IP and selected paths, not an exhaustive database diff.
pub(all) struct RecordDiff {
  before : Projection
  after : Projection
  record_changed : Bool
  prefix_changed : Bool
  changed_fields : Array[String]
} derive(Eq, @debug.Debug)

///|
pub fn RecordDiff::changed(self : RecordDiff) -> Bool {
  self.record_changed || self.prefix_changed || !self.changed_fields.is_empty()
}

///|
fn diff_projections(before : Projection, after : Projection) -> RecordDiff {
  let changed_fields = []
  for i in 0.. true
      (Some(a), Some(b)) => same_value(a, b)
      _ => false
    }
    if !same {
      changed_fields.push(before.fields[i].0)
    }
  }
  {
    before,
    after,
    record_changed: before.record_found != after.record_found,
    prefix_changed: before.prefix_length != after.prefix_length,
    changed_fields,
  }
}

///|
pub fn Reader::compare(
  self : Reader,
  other : Reader,
  ip : String,
  paths : Array[String],
) -> RecordDiff raise MmdbError {
  self.compare_prepared(other, ip, prepare_fields(paths))
}

///|
/// Both readers apply their own budgets. Failed queries never become a clean diff.
pub fn Reader::compare_prepared(
  self : Reader,
  other : Reader,
  ip : String,
  selector : FieldSelector,
) -> RecordDiff raise MmdbError {
  let before = self.project_prepared(ip, selector) catch {
    e =>
      raise MmdbError(e.code(), e.offset(), "before database: " + e.message())
  }
  let after = other.project_prepared(ip, selector) catch {
    e => raise MmdbError(e.code(), e.offset(), "after database: " + e.message())
  }
  diff_projections(before, after)
}

///|
pub fn RecordDiff::to_json(self : RecordDiff) -> Json {
  {
    "status": (if self.changed() { "changed" } else { "unchanged" }).to_json(),
    "record_changed": self.record_changed.to_json(),
    "prefix_changed": self.prefix_changed.to_json(),
    "changed_fields": self.changed_fields.to_json(),
    "before": self.before.to_json(),
    "after": self.after.to_json(),
  }
}