1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
24use std::fs;
25use std::path::Path;
26
27use serde_json::Value;
28
29pub struct TreeEntry {
31 pub id: String,
32 pub depth: usize,
34 pub repeat: bool,
36 pub orphan_parent: bool,
39}
40
41pub struct Lineage {
42 titles: BTreeMap<String, String>,
43 parents: BTreeMap<String, BTreeSet<String>>,
44 children: BTreeMap<String, BTreeSet<String>>,
45 claimed_parents: BTreeMap<String, BTreeSet<String>>,
46 claimed_children: BTreeMap<String, BTreeSet<String>>,
47}
48
49#[allow(dead_code)]
52impl Lineage {
53 pub fn load(fur_dir: &Path) -> Result<Lineage, String> {
56 let threads = fur_dir.join("threads");
57 let entries = fs::read_dir(&threads)
58 .map_err(|e| format!("cannot read {}: {}", threads.display(), e))?;
59
60 let mut lineage = Lineage {
61 titles: BTreeMap::new(),
62 parents: BTreeMap::new(),
63 children: BTreeMap::new(),
64 claimed_parents: BTreeMap::new(),
65 claimed_children: BTreeMap::new(),
66 };
67
68 for entry in entries.flatten() {
69 let path = entry.path();
70 if path.extension().and_then(|e| e.to_str()) != Some("json") {
71 continue;
72 }
73
74 let Some(content) = crate::security::io::read_text_file(&path) else {
75 continue;
76 };
77 let Ok(convo) = serde_json::from_str::<Value>(&content) else {
78 continue;
79 };
80 let Some(id) = convo["id"].as_str() else {
81 continue;
82 };
83
84 lineage.titles.insert(
85 id.to_string(),
86 convo["title"].as_str().unwrap_or("Untitled").to_string(),
87 );
88
89 for parent in string_list(&convo, "parents") {
90 lineage
91 .claimed_parents
92 .entry(id.to_string())
93 .or_default()
94 .insert(parent.clone());
95 lineage.add_edge(&parent, id);
96 }
97 for child in string_list(&convo, "children") {
98 lineage
99 .claimed_children
100 .entry(id.to_string())
101 .or_default()
102 .insert(child.clone());
103 lineage.add_edge(id, &child);
104 }
105 }
106
107 Ok(lineage)
108 }
109
110 fn add_edge(&mut self, parent: &str, child: &str) {
113 self.children
114 .entry(parent.to_string())
115 .or_default()
116 .insert(child.to_string());
117 self.parents
118 .entry(child.to_string())
119 .or_default()
120 .insert(parent.to_string());
121 }
122
123 pub fn is_empty(&self) -> bool {
124 self.parents.is_empty() && self.children.is_empty()
125 }
126
127 pub fn is_local(&self, id: &str) -> bool {
128 self.titles.contains_key(id)
129 }
130
131 pub fn title(&self, id: &str) -> Option<&str> {
132 self.titles.get(id).map(|s| s.as_str())
133 }
134
135 pub fn would_cycle(&self, parent: &str, child: &str) -> bool {
138 let mut seen: HashSet<&str> = HashSet::new();
139 let mut stack = vec![child];
140
141 while let Some(id) = stack.pop() {
142 if id == parent {
143 return true;
144 }
145 if !seen.insert(id) {
146 continue;
147 }
148 if let Some(kids) = self.children.get(id) {
149 stack.extend(kids.iter().map(|s| s.as_str()));
150 }
151 }
152
153 false
154 }
155
156 pub fn ancestry(&self, start: &str) -> Vec<String> {
163 let mut out = Vec::new();
164 let mut state: HashMap<String, u8> = HashMap::new();
165 self.visit_up(start, &mut state, &mut out);
166 out
167 }
168
169 fn visit_up(&self, id: &str, state: &mut HashMap<String, u8>, out: &mut Vec<String>) {
173 match state.get(id) {
174 Some(_) => return,
175 None => state.insert(id.to_string(), 1),
176 };
177
178 if let Some(parents) = self.parents.get(id) {
179 for parent in parents {
180 if self.is_local(parent) {
181 self.visit_up(parent, state, out);
182 }
183 }
184 }
185
186 state.insert(id.to_string(), 2);
187
188 if self.is_local(id) {
189 out.push(id.to_string());
190 }
191 }
192
193 pub fn descendants(&self, start: &str) -> Vec<String> {
200 let mut reachable: HashSet<String> = HashSet::new();
201 let mut stack = vec![start.to_string()];
202
203 while let Some(id) = stack.pop() {
204 if !reachable.insert(id.clone()) {
205 continue;
206 }
207 if let Some(kids) = self.children.get(&id) {
208 stack.extend(kids.iter().cloned());
209 }
210 }
211
212 let mut out = Vec::new();
213 let mut state: HashMap<String, u8> = HashMap::new();
214
215 state.insert(start.to_string(), 2);
218 if self.is_local(start) {
219 out.push(start.to_string());
220 }
221
222 let mut members: Vec<&String> = reachable.iter().filter(|id| *id != start).collect();
223 members.sort();
224
225 for id in members {
226 self.visit_up_within(id, &reachable, &mut state, &mut out);
227 }
228
229 out
230 }
231
232 fn visit_up_within(
235 &self,
236 id: &str,
237 allowed: &HashSet<String>,
238 state: &mut HashMap<String, u8>,
239 out: &mut Vec<String>,
240 ) {
241 match state.get(id) {
242 Some(_) => return,
243 None => state.insert(id.to_string(), 1),
244 };
245
246 if let Some(parents) = self.parents.get(id) {
247 for parent in parents {
248 if allowed.contains(parent) && self.is_local(parent) {
249 self.visit_up_within(parent, allowed, state, out);
250 }
251 }
252 }
253
254 state.insert(id.to_string(), 2);
255
256 if self.is_local(id) {
257 out.push(id.to_string());
258 }
259 }
260
261 pub fn absent_ancestors(&self, start: &str) -> Vec<String> {
264 let local = self.ancestry(start);
265 let mut out = BTreeSet::new();
266
267 for id in local.iter().map(|s| s.as_str()).chain(std::iter::once(start)) {
268 if let Some(parents) = self.parents.get(id) {
269 for parent in parents {
270 if !self.is_local(parent) {
271 out.insert(parent.clone());
272 }
273 }
274 }
275 }
276
277 out.into_iter().collect()
278 }
279
280 pub fn parents_of(&self, id: &str) -> Vec<String> {
283 self.parents
284 .get(id)
285 .map(|set| set.iter().filter(|p| self.is_local(p)).cloned().collect())
286 .unwrap_or_default()
287 }
288
289 pub fn ids(&self) -> Vec<String> {
291 self.titles.keys().cloned().collect()
292 }
293
294 pub fn all_parents(&self, id: &str) -> Vec<String> {
296 self.parents
297 .get(id)
298 .map(|set| set.iter().cloned().collect())
299 .unwrap_or_default()
300 }
301
302 pub fn all_children(&self, id: &str) -> Vec<String> {
304 self.children
305 .get(id)
306 .map(|set| set.iter().cloned().collect())
307 .unwrap_or_default()
308 }
309
310 pub fn asymmetric(&self) -> Vec<(String, String)> {
317 let mut out = BTreeSet::new();
318
319 for (parent, kids) in &self.claimed_children {
320 for child in kids {
321 if !self.is_local(parent) || !self.is_local(child) {
322 continue;
323 }
324 let mirrored = self
325 .claimed_parents
326 .get(child)
327 .map(|set| set.contains(parent))
328 .unwrap_or(false);
329 if !mirrored {
330 out.insert((parent.clone(), child.clone()));
331 }
332 }
333 }
334
335 for (child, parents) in &self.claimed_parents {
336 for parent in parents {
337 if !self.is_local(parent) || !self.is_local(child) {
338 continue;
339 }
340 let mirrored = self
341 .claimed_children
342 .get(parent)
343 .map(|set| set.contains(child))
344 .unwrap_or(false);
345 if !mirrored {
346 out.insert((parent.clone(), child.clone()));
347 }
348 }
349 }
350
351 out.into_iter().collect()
352 }
353
354 pub fn dangling(&self) -> Vec<String> {
356 self.parents
357 .keys()
358 .chain(self.children.keys())
359 .chain(self.parents.values().flatten())
360 .chain(self.children.values().flatten())
361 .filter(|id| !self.is_local(id))
362 .cloned()
363 .collect::<BTreeSet<_>>()
364 .into_iter()
365 .collect()
366 }
367
368 pub fn forest(&self, order: &[String]) -> Vec<TreeEntry> {
376 let mut out = Vec::new();
377 let mut visited: HashSet<String> = HashSet::new();
378
379 for id in order {
380 if !self.is_local(id) || self.has_local_parent(id) {
381 continue;
382 }
383 self.descend(id, 0, order, &mut visited, &mut out);
384 }
385
386 for id in order {
387 if self.is_local(id) && !visited.contains(id) {
388 self.descend(id, 0, order, &mut visited, &mut out);
389 }
390 }
391
392 out
393 }
394
395 fn descend(
396 &self,
397 id: &str,
398 depth: usize,
399 order: &[String],
400 visited: &mut HashSet<String>,
401 out: &mut Vec<TreeEntry>,
402 ) {
403 let repeat = !visited.insert(id.to_string());
404
405 out.push(TreeEntry {
406 id: id.to_string(),
407 depth,
408 repeat,
409 orphan_parent: depth == 0 && self.has_only_absent_parents(id),
410 });
411
412 if repeat {
413 return;
414 }
415
416 for child in self.ordered_local_children(id, order) {
417 self.descend(&child, depth + 1, order, visited, out);
418 }
419 }
420
421 fn has_local_parent(&self, id: &str) -> bool {
422 self.parents
423 .get(id)
424 .map(|set| set.iter().any(|p| self.is_local(p)))
425 .unwrap_or(false)
426 }
427
428 fn has_only_absent_parents(&self, id: &str) -> bool {
429 match self.parents.get(id) {
430 Some(set) if !set.is_empty() => set.iter().all(|p| !self.is_local(p)),
431 _ => false,
432 }
433 }
434
435 fn ordered_local_children(&self, id: &str, order: &[String]) -> Vec<String> {
436 let Some(kids) = self.children.get(id) else {
437 return Vec::new();
438 };
439
440 let mut out: Vec<String> = kids.iter().filter(|k| self.is_local(k)).cloned().collect();
441
442 out.sort_by_key(|k| order.iter().position(|o| o == k).unwrap_or(usize::MAX));
443 out
444 }
445}
446
447fn string_list(value: &Value, key: &str) -> Vec<String> {
448 value[key]
449 .as_array()
450 .map(|arr| {
451 arr.iter()
452 .filter_map(|v| v.as_str().map(|s| s.to_string()))
453 .collect()
454 })
455 .unwrap_or_default()
456}