use std::{
collections::{HashMap, HashSet},
fmt,
};
use indexmap::IndexMap;
use ltk_hash::BinHash;
use crate::{
merge::{combines, key_index, map_keys},
path::{FieldNames, MapKey, Nameless, NamelessKind, PropertyPath, ValuePath, ValueShape},
property::values,
walk::{Trail, TrailSegment},
Bin, BinObject, BinOverride, PropertyPatch, PropertyValueEnum,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub struct DiffOptions {
pub deletions: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Lift {
MapInsert {
object_hash: BinHash,
at: ValuePath,
keys: usize,
},
Nameless {
object_hash: BinHash,
at: ValuePath,
cause: Nameless,
},
Mismatch {
object_hash: BinHash,
at: ValuePath,
},
}
impl Lift {
#[must_use]
pub fn object_hash(&self) -> BinHash {
match self {
Self::MapInsert { object_hash, .. }
| Self::Nameless { object_hash, .. }
| Self::Mismatch { object_hash, .. } => *object_hash,
}
}
#[must_use]
pub fn at(&self) -> &ValuePath {
match self {
Self::MapInsert { at, .. } | Self::Nameless { at, .. } | Self::Mismatch { at, .. } => {
at
}
}
}
}
impl fmt::Display for Lift {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:08x} {}: ", self.object_hash(), self.at())?;
match self {
Self::MapInsert { keys, .. } => write!(f, "{keys} map entries inserted"),
Self::Nameless { cause, .. } => write!(f, "nameless, {cause}"),
Self::Mismatch { .. } => f.write_str("shapes differ"),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct DiffReport {
pub records: usize,
pub objects: Vec<BinHash>,
pub deleted: Vec<BinHash>,
pub dependencies: Vec<String>,
pub lifted: Vec<Lift>,
}
impl fmt::Display for DiffReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} records, {} objects, {} deleted, {} dependencies, {} lifted",
self.records,
self.objects.len(),
self.deleted.len(),
self.dependencies.len(),
self.lifted.len(),
)
}
}
impl Bin {
pub fn diff(&self, edited: &Self, names: &dyn FieldNames) -> (BinOverride, DiffReport) {
self.diff_with(edited, names, &DiffOptions::default())
}
pub fn diff_with(
&self,
edited: &Self,
names: &dyn FieldNames,
options: &DiffOptions,
) -> (BinOverride, DiffReport) {
let mut patch = BinOverride::new();
let mut differ = Differ {
names,
object_hash: BinHash(0),
trail: Trail::new(),
report: DiffReport::default(),
};
for (object_hash, object) in &edited.objects {
let Some(base) = self.objects.get(object_hash) else {
differ.take_object(&mut patch, object.clone());
continue;
};
differ.object_hash = *object_hash;
differ.trail.clear();
if base.class_hash != object.class_hash {
differ.lift(|object_hash, at| Lift::Mismatch { object_hash, at });
differ.take_object(&mut patch, object.clone());
continue;
}
let mut records = Vec::new();
match differ.properties(
&base.properties,
&object.properties,
object.class_hash,
&mut records,
) {
Ok(()) => {
differ.report.records += records.len();
patch
.patches
.extend(records.into_iter().map(|(path, value)| PropertyPatch {
object_hash: *object_hash,
path,
value,
}));
}
Err(_) => {
let mut merged = base.clone();
merged.merge(object);
differ.take_object(&mut patch, merged);
}
}
}
if options.deletions {
for object_hash in self.objects.keys() {
if !edited.objects.contains_key(object_hash) {
patch.deleted.push(*object_hash);
differ.report.deleted.push(*object_hash);
}
}
}
differ.report.dependencies = edited
.dependencies
.iter()
.filter(|dependency| !self.dependencies.contains(dependency))
.cloned()
.collect();
(patch, differ.report)
}
}
type Pending = (PropertyPath, PropertyValueEnum);
type Escalate = usize;
struct Differ<'n, 'e> {
names: &'n dyn FieldNames,
object_hash: BinHash,
trail: Trail<&'e PropertyValueEnum>,
report: DiffReport,
}
impl<'e> Differ<'_, 'e> {
fn take_object(&mut self, patch: &mut BinOverride, object: BinObject) {
self.report.objects.push(object.path_hash);
patch.objects.insert(object.path_hash, object);
}
fn at(&self) -> ValuePath {
self.trail
.to_value_path()
.expect("every key on the trail converted when its map was indexed")
}
fn lift(&mut self, lift: impl FnOnce(BinHash, ValuePath) -> Lift) {
let lift = lift(self.object_hash, self.at());
self.report.lifted.push(lift);
}
fn properties(
&mut self,
base: &IndexMap<BinHash, PropertyValueEnum>,
edited: &'e IndexMap<BinHash, PropertyValueEnum>,
class: BinHash,
records: &mut Vec<Pending>,
) -> Result<(), Escalate> {
let mut escalate = None;
for (field, value) in edited {
self.trail.push_field(*field, class);
let found = match base.get(field) {
Some(existing) => self.value(existing, value, records),
None => self.record(records, || value.clone()),
};
self.trail.pop();
escalate = shallowest(escalate, found.err());
}
escalate.map_or(Ok(()), Err)
}
fn value(
&mut self,
base: &PropertyValueEnum,
edited: &'e PropertyValueEnum,
records: &mut Vec<Pending>,
) -> Result<(), Escalate> {
use PropertyValueEnum as V;
if base == edited {
return Ok(());
}
let mark = records.len();
let escalate = match (base, edited) {
(V::Struct(b), V::Struct(e))
| (V::Embedded(values::Embedded(b)), V::Embedded(values::Embedded(e)))
if combines(b, e) =>
{
self.properties(&b.properties, &e.properties, e.class_hash, records)
.err()
}
(V::Map(b), V::Map(e))
if b.key_kind() == e.key_kind() && b.value_kind() == e.value_kind() =>
{
match self.map(b, e, records) {
Some(found) => found.err(),
None => return self.replace(base, edited, records),
}
}
(V::Optional(b), V::Optional(e)) if b.item_kind() == e.item_kind() => {
match (b.value(), e.value()) {
(Some(b), Some(e)) => {
self.trail.push(TrailSegment::Index(0));
let found = self.value(b, e, records);
self.trail.pop();
found.err()
}
_ => return self.replace(base, edited, records),
}
}
_ => return self.replace(base, edited, records),
};
let Some(to) = escalate else {
return Ok(());
};
records.truncate(mark);
if self.trail.len() > to {
return Err(to);
}
self.record(records, || merged(base, edited))
}
fn map(
&mut self,
base: &values::Map,
edited: &'e values::Map,
records: &mut Vec<Pending>,
) -> Option<Result<(), Escalate>> {
let index = key_index(base)?;
let keys = map_keys(edited)?;
let mut occurrences: HashMap<&MapKey, usize> = HashMap::new();
for key in &keys {
*occurrences.entry(key).or_default() += 1;
}
let mut escalate = None;
let mut inserted = HashSet::new();
let mut written: HashMap<usize, PropertyValueEnum> = HashMap::new();
for ((key, value), map_key) in edited.entries().iter().zip(&keys) {
let Some(at) = index.get(map_key).copied() else {
inserted.insert(map_key);
continue;
};
let existing = written.get(&at).unwrap_or(&base.entries()[at].1);
self.trail.push(TrailSegment::Key(key));
let found = if existing == value {
Ok(())
} else if shadowed(base, at) {
self.shadowed_key(map_key)
} else {
self.value(existing, value, records)
};
self.trail.pop();
escalate = shallowest(escalate, found.err());
if occurrences[map_key] > 1 {
let next = merged(existing, value);
written.insert(at, next);
}
}
if !inserted.is_empty() {
self.lift(|object_hash, at| Lift::MapInsert {
object_hash,
at,
keys: inserted.len(),
});
escalate = shallowest(escalate, Some(self.trail.len()));
}
Some(escalate.map_or(Ok(()), Err))
}
fn shadowed_key(&mut self, key: &MapKey) -> Result<(), Escalate> {
let segment = self.trail.len() - 1;
self.lift(|object_hash, at| Lift::Nameless {
object_hash,
at,
cause: Nameless {
segment,
kind: NamelessKind::Key(key.kind()),
},
});
Err(segment)
}
fn replace(
&mut self,
base: &PropertyValueEnum,
edited: &PropertyValueEnum,
records: &mut Vec<Pending>,
) -> Result<(), Escalate> {
if ValueShape::of(base).matches(&ValueShape::of(edited)) {
return self.record(records, || edited.clone());
}
self.lift(|object_hash, at| Lift::Mismatch { object_hash, at });
Err(self.trail.len() - 1)
}
fn record(
&mut self,
records: &mut Vec<Pending>,
value: impl FnOnce() -> PropertyValueEnum,
) -> Result<(), Escalate> {
let at = self.at();
match at.to_property_path(self.names) {
Ok(path) => {
records.push((path, value()));
Ok(())
}
Err(cause) => {
let to = cause.segment;
self.report.lifted.push(Lift::Nameless {
object_hash: self.object_hash,
at,
cause,
});
Err(to)
}
}
}
}
fn shadowed(map: &values::Map, at: usize) -> bool {
let PropertyValueEnum::F32(key) = &map.entries()[at].0 else {
return false;
};
key.value == 0.0
&& map.entries()[..at].iter().any(|(earlier, _)| {
matches!(earlier, PropertyValueEnum::F32(e) if e.value == 0.0 && e.value.to_bits() != key.value.to_bits())
})
}
fn merged(base: &PropertyValueEnum, edited: &PropertyValueEnum) -> PropertyValueEnum {
let mut merged = base.clone();
merged.merge(edited);
merged
}
fn shallowest(a: Option<Escalate>, b: Option<Escalate>) -> Option<Escalate> {
match (a, b) {
(Some(a), Some(b)) => Some(a.min(b)),
(a, b) => a.or(b),
}
}