use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use similar::{ChangeTag, TextDiff};
use time::Date;
use crate::uslm::{ElementData, TextContentField, USLMElement};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct FieldChangeEvent {
pub field_name: TextContentField,
pub from_date: Date,
pub to_date: Date,
pub old_value: String,
pub new_value: String,
pub changes: Vec<TextChange>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct TextChange {
pub value: String,
pub old_index: Option<i32>,
pub new_index: Option<i32>,
pub tag: TextChangeType,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TextChangeType {
Insert,
Delete,
Equal,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct TreeDiff {
pub root_path: String,
pub changes: Vec<FieldChangeEvent>,
pub from_element: ElementData,
pub to_element: ElementData,
pub added: Vec<ElementData>,
pub removed: Vec<ElementData>,
pub child_diffs: Vec<TreeDiff>,
}
impl TreeDiff {
pub fn from_elements(from_element: &USLMElement, to_element: &USLMElement) -> TreeDiff {
assert!(from_element.data.path == to_element.data.path);
let root_path = from_element.data.path.clone();
let changes = diff_elements(from_element, to_element);
let children_a: HashMap<String, &USLMElement> = from_element
.children
.iter()
.map(|child| (child.data.path.clone(), child))
.collect();
let children_b: HashMap<String, &USLMElement> = to_element
.children
.iter()
.map(|child| (child.data.path.clone(), child))
.collect();
let mut added = vec![];
let mut removed = vec![];
let mut child_diffs = vec![];
for (path, child_a) in &children_a {
match children_b.get(path) {
Some(child_b) => {
let child_diff = TreeDiff::from_elements(child_a, child_b);
if !child_diff.child_diffs.is_empty() || !child_diff.changes.is_empty() {
child_diffs.push(child_diff);
}
}
None => {
removed.push(child_a.data.clone()); }
}
}
for (path, child_b) in &children_b {
if !children_a.contains_key(path) {
added.push(child_b.data.clone()); }
}
TreeDiff {
changes,
root_path,
from_element: from_element.data.clone(),
to_element: to_element.data.clone(),
added,
removed,
child_diffs,
}
}
}
pub fn diff_elements(element_a: &USLMElement, element_b: &USLMElement) -> Vec<FieldChangeEvent> {
assert!(element_a.data.path == element_b.data.path);
assert!(element_a.data.element_type == element_b.data.element_type);
let mut changes: Vec<FieldChangeEvent> = Vec::new();
for field_name in [
TextContentField::Heading,
TextContentField::Chapeau,
TextContentField::Proviso,
TextContentField::Content,
TextContentField::Continuation,
]
.into_iter()
{
let field_changes = diff_field(element_a, element_b, field_name);
if !field_changes.changes.is_empty() {
changes.push(field_changes);
}
}
changes
}
fn rewrap_usize(s: Option<usize>) -> Option<i32> {
s.map(|val| val as i32)
}
fn diff_field(
element_a: &USLMElement,
element_b: &USLMElement,
field_name: TextContentField,
) -> FieldChangeEvent {
let a = element_a
.data
.get_text_content(field_name)
.unwrap_or_default();
let b = element_b
.data
.get_text_content(field_name)
.unwrap_or_default();
let diff = TextDiff::from_words(a.as_str(), b.as_str());
let changes: Vec<TextChange> = diff
.iter_all_changes()
.filter(|c| c.tag() != ChangeTag::Equal)
.map(|c| {
let tag = match c.tag() {
ChangeTag::Delete => TextChangeType::Delete,
ChangeTag::Insert => TextChangeType::Insert,
ChangeTag::Equal => TextChangeType::Equal,
};
TextChange {
value: String::from(c.value()),
old_index: rewrap_usize(c.old_index()),
new_index: rewrap_usize(c.new_index()),
tag,
}
})
.collect();
FieldChangeEvent {
field_name,
from_date: element_a.data.date,
to_date: element_b.data.date,
old_value: a,
new_value: b,
changes,
}
}