use alloc::{
borrow::{Cow, ToOwned},
string::{String, ToString},
vec,
vec::Vec,
};
use crate::{
param::IcalParam,
prop::{IcalPropKind, IcalPropName},
tree::{
line::IcalLine,
merge::{IcalComponentPath, IcalMergeAction, IcalPropPath, Op, Slot, node::Node},
},
value::IcalValue,
version::IcalVersion,
};
pub(super) struct Diff<'n, 'c, 'a> {
pub(super) base: &'n [Node<'c, 'a>],
pub(super) side: &'n [Node<'c, 'a>],
pub(super) version: IcalVersion,
}
impl<'a> Diff<'_, '_, 'a> {
pub(super) fn run(&self) -> Vec<Op<'a>> {
let mut ops = Vec::new();
for node in self.base {
if !self.side.iter().any(|held| held.path == node.path)
&& !self.removed_above(&node.path)
{
ops.push(Op {
action: IcalMergeAction::ComponentRemoved {
at: node.path.clone(),
},
source: None,
slot: Slot::Component,
});
}
}
for node in self.side {
if !self.base.iter().any(|held| held.path == node.path) && !self.added_above(&node.path)
{
ops.push(Op {
action: IcalMergeAction::ComponentAdded {
at: node.path.clone(),
},
source: None,
slot: Slot::Component,
});
}
}
let mut taken = vec![false; self.side.len()];
for node in self.base {
let Some((at, held)) = self
.side
.iter()
.enumerate()
.find(|(at, held)| !taken[*at] && held.path == node.path)
else {
continue;
};
taken[at] = true;
self.component(node, held, &mut ops);
}
ops
}
fn removed_above(&self, path: &IcalComponentPath<'_>) -> bool {
path.ancestors().any(|above| {
self.base.iter().any(|node| node.path == above)
&& !self.side.iter().any(|node| node.path == above)
})
}
fn added_above(&self, path: &IcalComponentPath<'_>) -> bool {
path.ancestors().any(|above| {
self.side.iter().any(|node| node.path == above)
&& !self.base.iter().any(|node| node.path == above)
})
}
fn component(&self, base: &Node<'_, 'a>, side: &Node<'_, 'a>, ops: &mut Vec<Op<'a>>) {
let base_props: Vec<&IcalLine<'a>> = base.cst.prop_lines().collect();
let side_props: Vec<&IcalLine<'a>> = side.cst.prop_lines().collect();
let mut names: Vec<String> = Vec::new();
for line in base_props.iter().chain(&side_props) {
let name = line.name.get().to_ascii_uppercase();
if !names.contains(&name) {
names.push(name);
}
}
for name in names {
let of = |lines: &[&IcalLine<'a>]| -> Vec<usize> {
lines
.iter()
.enumerate()
.filter(|(_, line)| line.name.get().eq_ignore_ascii_case(&name))
.map(|(index, _)| index)
.collect()
};
let mut base_free = of(&base_props);
let mut side_free = of(&side_props);
let mut pairs = Vec::new();
let mut b = 0;
while b < base_free.len() {
let held = identity_in(&base_props, base_free[b]);
let same = held.and_then(|held| {
side_free
.iter()
.position(|&s| identity_in(&side_props, s).is_some_and(|side| side == held))
});
match same {
Some(s) => pairs.push((base_free.remove(b), side_free.remove(s))),
None => b += 1,
}
}
let mut b = 0;
while b < base_free.len() {
let same = side_free.iter().position(|&s| {
base_props[base_free[b]].decode(self.version)
== side_props[s].decode(self.version)
});
match same {
Some(s) => pairs.push((base_free.remove(b), side_free.remove(s))),
None => b += 1,
}
}
let mut b = 0;
while b < base_free.len() {
if identity_in(&base_props, base_free[b]).is_some() {
b += 1;
continue;
}
let same = side_free
.iter()
.position(|&s| identity_in(&side_props, s).is_none());
match same {
Some(s) => pairs.push((base_free.remove(b), side_free.remove(s))),
None => break,
}
}
for index in base_free {
let line = base_props[index];
let at = IcalPropPath::of(&base.path, &base_props, index);
ops.push(Op {
action: IcalMergeAction::PropRemoved {
value: line.decode(self.version).value.into_owned(),
at,
},
source: None,
slot: Slot::Prop,
});
}
for index in side_free {
let line = side_props[index];
let at = IcalPropPath::of(&side.path, &side_props, index);
ops.push(Op {
action: IcalMergeAction::PropAdded {
value: line.decode(self.version).value.into_owned(),
at: at.clone(),
},
source: Some(at),
slot: Slot::Prop,
});
}
for (b, s) in pairs {
self.prop(&base.path, &base_props, b, &side_props, s, ops);
}
}
}
fn prop(
&self,
component: &IcalComponentPath<'a>,
lines: &[&IcalLine<'a>],
at: usize,
side_lines: &[&IcalLine<'a>],
side_at: usize,
ops: &mut Vec<Op<'a>>,
) {
let base = lines[at];
let side = side_lines[side_at];
let at = IcalPropPath::of(component, lines, at);
let source = IcalPropPath::of(component, side_lines, side_at);
let base_prop = base.decode(self.version);
let side_prop = side.decode(self.version);
for (index, param) in base_prop.params.iter().enumerate() {
let name = param.merge_name();
let ordinal = ordinal_of(&base_prop.params, index, &name);
let held = nth_param(&side_prop.params, &name, ordinal);
let action = match held {
None => IcalMergeAction::ParamRemoved {
at: at.clone(),
param: param.clone().into_owned(),
},
Some(held) if !base.params[index].same_param_as(&side.params[held]) => {
IcalMergeAction::ParamChanged {
at: at.clone(),
old: param.clone().into_owned(),
new: side_prop.params[held].clone().into_owned(),
}
}
Some(_) => continue,
};
ops.push(Op {
action,
source: Some(source.clone()),
slot: Slot::Param { name, at: ordinal },
});
}
for (index, param) in side_prop.params.iter().enumerate() {
let name = param.merge_name();
let ordinal = ordinal_of(&side_prop.params, index, &name);
if nth_param(&base_prop.params, &name, ordinal).is_some() {
continue;
}
ops.push(Op {
action: IcalMergeAction::ParamAdded {
at: at.clone(),
param: param.clone().into_owned(),
},
source: Some(source.clone()),
slot: Slot::Param { name, at: ordinal },
});
}
if base.value.same_value_as(&side.value) {
return;
}
match (&base_prop.value, &side_prop.value) {
(IcalValue::TextList(old), IcalValue::TextList(new)) => {
list_ops(&at, &source, &old.0, &new.0, ops)
}
(IcalValue::DateTimeList(old), IcalValue::DateTimeList(new)) => {
list_ops(&at, &source, &old.0, &new.0, ops)
}
(old, new) => ops.push(Op {
action: IcalMergeAction::ValueChanged {
at,
old: old.clone().into_owned(),
new: new.clone().into_owned(),
},
source: Some(source),
slot: Slot::Value,
}),
}
}
}
impl<'a> IcalLine<'a> {
fn prop_identity(&self) -> Option<Cow<'a, str>> {
let name = IcalPropName::from(Cow::Owned(self.name.get().to_owned()));
let identified = matches!(
name,
IcalPropName::Kind(
IcalPropKind::Attendee
| IcalPropKind::Attach
| IcalPropKind::RelatedTo
| IcalPropKind::Conference
| IcalPropKind::Image
)
);
identified.then(|| Cow::Owned(self.value_key()))
}
}
pub(super) fn identity_in<'a>(lines: &[&IcalLine<'a>], at: usize) -> Option<Cow<'a, str>> {
let held = lines[at].prop_identity()?;
let name = lines[at].name.get();
let twice = lines.iter().enumerate().any(|(index, line)| {
index != at
&& line.name.get().eq_ignore_ascii_case(name)
&& line.prop_identity().is_some_and(|line| line == held)
});
(!twice).then_some(held)
}
fn list_ops<'a>(
at: &IcalPropPath<'a>,
source: &IcalPropPath<'a>,
old: &[Cow<'_, str>],
new: &[Cow<'_, str>],
ops: &mut Vec<Op<'a>>,
) {
let (added, removed) = list_diff(old, new);
for item in removed {
ops.push(Op {
action: IcalMergeAction::ValueItemRemoved {
at: at.clone(),
item: Cow::Owned(item.to_string()),
},
source: Some(source.clone()),
slot: Slot::Items,
});
}
for item in added {
ops.push(Op {
action: IcalMergeAction::ValueItemAdded {
at: at.clone(),
item: Cow::Owned(item.to_string()),
},
source: Some(source.clone()),
slot: Slot::Items,
});
}
}
fn list_diff<'a>(
old: &[Cow<'a, str>],
new: &[Cow<'a, str>],
) -> (Vec<Cow<'a, str>>, Vec<Cow<'a, str>>) {
let mut removed: Vec<Option<&Cow<'a, str>>> = old.iter().map(Some).collect();
let mut added = Vec::new();
for item in new {
let kept = removed
.iter()
.position(|old| old.is_some_and(|old| old.as_ref() == item.as_ref()));
match kept {
Some(i) => removed[i] = None,
None => added.push(item.clone()),
}
}
let removed = removed.into_iter().flatten().cloned().collect();
(added, removed)
}
fn ordinal_of(params: &[IcalParam<'_>], at: usize, name: &str) -> usize {
params[..at]
.iter()
.filter(|held| held.merge_name() == name)
.count()
}
fn nth_param(params: &[IcalParam<'_>], name: &str, at: usize) -> Option<usize> {
params
.iter()
.enumerate()
.filter(|(_, held)| held.merge_name() == name)
.map(|(index, _)| index)
.nth(at)
}