use std::{collections::HashMap, fmt, mem};
use indexmap::IndexMap;
use ltk_hash::{BinHash, HashWidth};
use crate::{
path::{MapKey, ValuePath},
property::{values, ValueSlot},
walk::{self, Trail, TrailSegment},
Bin, BinObject, PropertyValueEnum,
};
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct MergeReport {
pub objects_added: Vec<BinHash>,
pub objects_merged: Vec<BinHash>,
pub objects_replaced: Vec<BinObject>,
pub replaced: Vec<Replaced>,
pub inserted: usize,
pub keys_inserted: usize,
pub dependencies_added: Vec<String>,
}
impl Default for MergeReport {
fn default() -> Self {
Self {
objects_added: Vec::new(),
objects_merged: Vec::new(),
objects_replaced: Vec::new(),
replaced: Vec::new(),
inserted: 0,
keys_inserted: 0,
dependencies_added: Vec::new(),
}
}
}
impl MergeReport {
#[must_use]
pub fn is_unchanged(&self) -> bool {
self.objects_added.is_empty()
&& self.objects_replaced.is_empty()
&& self.replaced.is_empty()
&& self.inserted == 0
&& self.keys_inserted == 0
&& self.dependencies_added.is_empty()
}
}
impl fmt::Display for MergeReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} added, {} merged, {} replaced; {} values replaced ({} mismatched), {} inserted, \
{} keys inserted, {} dependencies added",
self.objects_added.len(),
self.objects_merged.len(),
self.objects_replaced.len(),
self.replaced.len(),
self.replaced.iter().filter(|r| r.mismatched).count(),
self.inserted,
self.keys_inserted,
self.dependencies_added.len(),
)
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct Replaced {
pub object_hash: BinHash,
pub at: ValuePath,
pub was: PropertyValueEnum,
pub mismatched: bool,
}
impl fmt::Display for Replaced {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:08x} {}", self.object_hash, self.at)?;
if self.mismatched {
f.write_str(" (mismatched)")?;
}
Ok(())
}
}
impl Bin {
pub fn merge(&mut self, edited: &Self) -> MergeReport {
let mut merger = Merger::new();
for (object_hash, object) in &edited.objects {
match self.objects.get_mut(object_hash) {
Some(base) => merger.object(base, object),
None => {
self.objects.insert(*object_hash, object.clone());
merger.report.objects_added.push(*object_hash);
}
}
}
for dependency in &edited.dependencies {
if !self.dependencies.contains(dependency) {
self.dependencies.push(dependency.clone());
merger.report.dependencies_added.push(dependency.clone());
}
}
merger.report
}
}
impl BinObject {
pub fn merge(&mut self, edited: &Self) -> MergeReport {
let mut merger = Merger::new();
merger.object(self, edited);
merger.report
}
}
impl values::Struct {
pub fn merge(&mut self, edited: &Self) -> MergeReport {
let mut merger = Merger::new();
if combines(self, edited) {
merger.properties(&mut self.properties, &edited.properties, edited.class_hash);
} else if self != edited {
let was = mem::replace(self, edited.clone());
let mismatched = was.class_hash != edited.class_hash;
merger.record(PropertyValueEnum::Struct(was), mismatched);
}
merger.report
}
}
impl PropertyValueEnum {
pub fn merge(&mut self, edited: &Self) -> MergeReport {
let mut merger = Merger::new();
merger.value(self, edited);
merger.report
}
}
pub(crate) fn combines(base: &values::Struct, edited: &values::Struct) -> bool {
base.class_hash == edited.class_hash && *base.class_hash != 0
}
fn shapes_differ(base: &PropertyValueEnum, edited: &PropertyValueEnum) -> bool {
let shape = |value| {
let declaration = walk::owned::declaration(value);
(
declaration.kind,
declaration.item_kind,
declaration.key_kind,
declaration.class,
)
};
shape(base) != shape(edited) || hash_widths_differ(hash_widths(base), hash_widths(edited))
}
fn hash_widths(value: &PropertyValueEnum) -> [Option<HashWidth>; 2] {
use PropertyValueEnum as V;
match value {
V::Hash(hash) => [Some(hash.width()), None],
V::Container(container) | V::UnorderedContainer(values::UnorderedContainer(container)) => {
[container.hash_width(), None]
}
V::Optional(optional) => match optional.value() {
Some(V::Hash(hash)) => [Some(hash.width()), None],
_ => [None, None],
},
V::Map(map) => map_hash_widths(map),
_ => [None, None],
}
}
fn map_hash_widths(map: &values::Map) -> [Option<HashWidth>; 2] {
[map.key_hash_width(), map.value_hash_width()]
}
fn hash_widths_differ(base: [Option<HashWidth>; 2], edited: [Option<HashWidth>; 2]) -> bool {
base.into_iter()
.zip(edited)
.any(|widths| matches!(widths, (Some(base), Some(edited)) if base != edited))
}
pub(crate) fn key_index(map: &values::Map) -> Option<HashMap<MapKey, usize>> {
let mut index = HashMap::with_capacity(map.entries().len());
for (at, key) in map_keys(map)?.into_iter().enumerate() {
index.entry(key).or_insert(at);
}
Some(index)
}
pub(crate) fn map_keys(map: &values::Map) -> Option<Vec<MapKey>> {
map.entries()
.iter()
.map(|(key, value)| {
let fits = key.kind() == map.key_kind() && value.kind() == map.value_kind();
fits.then(|| MapKey::try_from(key).ok()).flatten()
})
.collect()
}
struct Merger<'e> {
object_hash: BinHash,
trail: Trail<&'e PropertyValueEnum>,
report: MergeReport,
}
impl<'e> Merger<'e> {
fn new() -> Self {
Self {
object_hash: BinHash(0),
trail: Trail::new(),
report: MergeReport::default(),
}
}
fn object(&mut self, base: &mut BinObject, edited: &'e BinObject) {
if base.class_hash != edited.class_hash {
let was = mem::replace(base, edited.clone());
self.report.objects_replaced.push(was);
return;
}
self.object_hash = base.path_hash;
self.trail.clear();
self.report.objects_merged.push(base.path_hash);
self.properties(&mut base.properties, &edited.properties, edited.class_hash);
}
fn properties(
&mut self,
base: &mut IndexMap<BinHash, PropertyValueEnum>,
edited: &'e IndexMap<BinHash, PropertyValueEnum>,
class: BinHash,
) {
for (field, value) in edited {
match base.get_mut(field) {
Some(existing) => {
self.trail.push_field(*field, class);
self.value(existing, value);
self.trail.pop();
}
None => {
base.insert(*field, value.clone());
self.report.inserted += 1;
}
}
}
}
fn value(&mut self, base: &mut PropertyValueEnum, edited: &'e PropertyValueEnum) {
use PropertyValueEnum as V;
match (&mut *base, edited) {
(V::Struct(b), V::Struct(e))
| (V::Embedded(values::Embedded(b)), V::Embedded(values::Embedded(e)))
if combines(b, e) =>
{
return self.properties(&mut b.properties, &e.properties, e.class_hash);
}
(V::Map(b), V::Map(e))
if b.key_kind() == e.key_kind()
&& b.value_kind() == e.value_kind()
&& !hash_widths_differ(map_hash_widths(b), map_hash_widths(e)) =>
{
if self.map(b, e) {
return;
}
}
(V::Optional(b), V::Optional(e)) if b.item_kind() == e.item_kind() => {
if let (Some(b), Some(e)) = (b.slot(), e.value()) {
self.trail.push(TrailSegment::Index(0));
self.value(b.into_inner(), e);
self.trail.pop();
return;
}
}
_ => {}
}
if *base != *edited {
let mismatched = shapes_differ(base, edited);
let was = mem::replace(base, edited.clone());
self.record(was, mismatched);
}
}
fn map(&mut self, base: &mut values::Map, edited: &'e values::Map) -> bool {
let Some(mut index) = key_index(base) else {
return false;
};
let Some(keys) = map_keys(edited) else {
return false;
};
for ((key, value), map_key) in edited.entries().iter().zip(keys) {
let existing = index.get(&map_key).and_then(|at| base.slot(*at));
match existing {
Some(existing) => {
self.trail.push(TrailSegment::Key(key));
self.value(ValueSlot::into_inner(existing), value);
self.trail.pop();
}
None => {
index.insert(map_key, base.entries().len());
base.push(key.clone(), value.clone())
.expect("the kinds and the hash widths of both maps are equal");
self.report.keys_inserted += 1;
}
}
}
true
}
fn record(&mut self, was: PropertyValueEnum, mismatched: bool) {
let at = self
.trail
.to_value_path()
.expect("every key on the trail converted when its map was indexed");
self.report.replaced.push(Replaced {
object_hash: self.object_hash,
at,
was,
mismatched,
});
}
}