use std::collections::{HashMap, HashSet};
use std::sync::{Arc, RwLock};
use crate::ids::{ContentId, ContentIdPrefix, GovernanceLogId, PostId};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Shown {
pub read: String,
pub bytes: u64,
pub raw: u64,
pub post: Option<PostId>,
pub turn: u64,
}
#[derive(Debug, Default)]
struct Inner {
shown: HashMap<ContentId, Shown>,
pending: Vec<(ContentId, Shown)>,
read: String,
legends: HashSet<&'static str>,
pending_legends: Vec<&'static str>,
summaries: HashMap<(GovernanceLogId, u64), Shown>,
pending_summaries: Vec<((GovernanceLogId, u64), Shown)>,
summaries_at: HashMap<(GovernanceLogId, &'static str), Shown>,
pending_summaries_at: Vec<((GovernanceLogId, &'static str), Shown)>,
parts: HashMap<String, Shown>,
pending_parts: Vec<(String, Shown)>,
cues: Cues,
turn: u64,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Cues {
pub links: bool,
pub full_reads_left: Option<u64>,
pub summary_asked: bool,
}
pub fn text_key(text: &str) -> u64 {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
text.trim().hash(&mut h);
h.finish()
}
pub fn summary_text(text: &str) -> String {
const OPEN: &str = "Read this first: attachment=\"";
let Some(at) = text.find(OPEN) else {
return text.trim().to_string();
};
let Some(end) = text[at + OPEN.len()..].find("\".") else {
return text.trim().to_string();
};
let end = at + OPEN.len() + end + 2;
format!("{}{}", text[..at].trim_end(), &text[end..])
.trim()
.to_string()
}
#[derive(Debug, Clone, Default)]
pub struct ShownOnce(Arc<RwLock<Inner>>);
impl ShownOnce {
pub fn begin(&self, read: impl Into<String>) {
let mut inner = self.0.write().expect("shown-once lock");
inner.pending.clear();
inner.pending_legends.clear();
inner.pending_summaries.clear();
inner.pending_summaries_at.clear();
inner.pending_parts.clear();
inner.read = read.into();
}
pub fn earlier_part(&self, link: &str) -> Option<Shown> {
let inner = self.0.read().expect("shown-once lock");
inner.parts.get(link).cloned().or_else(|| {
inner
.pending_parts
.iter()
.find(|(p, _)| p == link)
.map(|(_, s)| s.clone())
})
}
pub fn mark_part(&self, link: String, bytes: usize) {
let mut inner = self.0.write().expect("shown-once lock");
let read = inner.read.clone();
let turn = inner.turn;
inner.pending_parts.push((
link,
Shown {
read,
bytes: bytes as u64,
raw: bytes as u64,
post: None,
turn,
},
));
}
pub fn next_turn(&self) {
self.0.write().expect("shown-once lock").turn += 1;
}
pub fn turn(&self) -> u64 {
self.0.read().expect("shown-once lock").turn
}
pub fn set_cues(&self, cues: Cues) {
self.0.write().expect("shown-once lock").cues = cues;
}
pub fn cues(&self) -> Cues {
self.0.read().expect("shown-once lock").cues
}
pub fn earlier_summary(
&self,
id: &GovernanceLogId,
text: Option<&str>,
) -> Option<Shown> {
let key = text.map(|t| text_key(&summary_text(t)));
let wanted = |k: &(GovernanceLogId, u64)| {
k.0 == *id && key.is_none_or(|key| k.1 == key)
};
let inner = self.0.read().expect("shown-once lock");
inner
.pending_summaries
.iter()
.find(|(k, _)| wanted(k))
.map(|(_, s)| s.clone())
.or_else(|| {
inner
.summaries
.iter()
.find(|(k, _)| wanted(k))
.map(|(_, s)| s.clone())
})
}
pub fn summary_seen(
&self,
id: &GovernanceLogId,
text: &str,
) -> Option<(Shown, usize)> {
let t = summary_text(text);
let mut ends: Vec<usize> = t
.match_indices("\n\n")
.map(|(at, _)| at)
.chain(std::iter::once(t.len()))
.collect();
ends.reverse();
ends.into_iter().find_map(|end| {
self.earlier_summary(id, Some(&t[..end])).map(|s| (s, end))
})
}
pub fn earlier_summary_at(
&self,
id: &GovernanceLogId,
version: &'static str,
) -> Option<Shown> {
let key = (id.clone(), version);
let inner = self.0.read().expect("shown-once lock");
inner
.pending_summaries_at
.iter()
.find(|(k, _)| *k == key)
.map(|(_, s)| s.clone())
.or_else(|| inner.summaries_at.get(&key).cloned())
}
pub fn mark_summary(
&self,
id: GovernanceLogId,
version: Option<&'static str>,
text: &str,
) {
self.mark_summary_from(id, version, text, 0)
}
pub fn mark_summary_from(
&self,
id: GovernanceLogId,
version: Option<&'static str>,
text: &str,
from: usize,
) {
let text = &summary_text(text);
let mut inner = self.0.write().expect("shown-once lock");
let read = inner.read.clone();
let turn = inner.turn;
let shown = Shown {
read,
bytes: text.len() as u64,
raw: text.len() as u64,
post: None,
turn,
};
inner
.pending_summaries_at
.push(((id.clone(), version.unwrap_or("latest")), shown.clone()));
let ends = text
.match_indices("\n\n")
.map(|(at, _)| at)
.chain(std::iter::once(text.len()));
for end in ends.filter(|&end| end > from) {
inner
.pending_summaries
.push(((id.clone(), text_key(&text[..end])), shown.clone()));
}
}
pub fn legend_shown(&self, legend: &'static str) -> bool {
let inner = self.0.read().expect("shown-once lock");
inner.legends.contains(legend)
|| inner.pending_legends.contains(&legend)
}
pub fn legend(&self, legend: &'static str) -> bool {
let mut inner = self.0.write().expect("shown-once lock");
if inner.legends.contains(legend)
|| inner.pending_legends.contains(&legend)
{
return false;
}
inner.pending_legends.push(legend);
true
}
pub fn read_of(id: impl std::fmt::Display) -> String {
format!("your read of {id}")
}
pub fn read_of_entry(
entry: &crate::responses::GovernanceEntryResponse,
) -> String {
let mut read = Self::read_of(&entry.id);
if let Some(n) = entry.round {
read.push_str(&format!(" round {n}"));
} else if let Some(a) = &entry.attachment {
read.push_str(&format!(" {a:?}"));
}
if let Some(version) = entry.version_label() {
read.push_str(&format!(" ({version})"));
}
read
}
pub fn read_of_content(id: ContentId) -> String {
Self::read_of(ContentIdPrefix::from(id))
}
pub fn earlier(&self, id: impl Into<ContentId>) -> Option<Shown> {
let id = id.into();
let inner = self.0.read().expect("shown-once lock");
inner.shown.get(&id).cloned().or_else(|| {
inner
.pending
.iter()
.find(|(p, _)| *p == id)
.map(|(_, s)| s.clone())
})
}
pub fn contains(&self, id: impl Into<ContentId>) -> bool {
self.earlier(id).is_some()
}
pub fn mark(
&self,
id: impl Into<ContentId>,
bytes: usize,
raw: usize,
post: Option<PostId>,
) {
let mut inner = self.0.write().expect("shown-once lock");
let read = inner.read.clone();
let turn = inner.turn;
inner.pending.push((
id.into(),
Shown {
read,
bytes: bytes as u64,
raw: raw as u64,
post,
turn,
},
));
}
pub fn commit(&self) {
let mut inner = self.0.write().expect("shown-once lock");
let pending = std::mem::take(&mut inner.pending);
for (id, shown) in pending {
inner.shown.entry(id).or_insert(shown);
}
let legends = std::mem::take(&mut inner.pending_legends);
inner.legends.extend(legends);
let summaries = std::mem::take(&mut inner.pending_summaries);
for (id, shown) in summaries {
inner.summaries.insert(id, shown);
}
let summaries = std::mem::take(&mut inner.pending_summaries_at);
for (id, shown) in summaries {
inner.summaries_at.insert(id, shown);
}
let parts = std::mem::take(&mut inner.pending_parts);
for (link, shown) in parts {
inner.parts.entry(link).or_insert(shown);
}
}
pub fn discard(&self) {
let mut inner = self.0.write().expect("shown-once lock");
inner.pending.clear();
inner.pending_legends.clear();
inner.pending_summaries.clear();
inner.pending_summaries_at.clear();
inner.pending_parts.clear();
}
pub fn comments_of(&self, post: PostId) -> (u64, u64) {
let inner = self.0.read().expect("shown-once lock");
inner
.shown
.values()
.filter(|s| s.post == Some(post))
.fold((0, 0), |(n, b), s| (n + 1, b + s.raw))
}
}
#[cfg(test)]
mod tests {
use super::*;
use uuid::Uuid;
fn id(n: u128) -> ContentId {
ContentId::from(Uuid::from_u128(n))
}
#[test]
fn marked_items_are_shown_once_committed() {
let once = ShownOnce::default();
once.begin("your read of 00000000");
once.mark(id(1), 100, 100, None);
assert!(once.contains(id(1)));
once.commit();
let shown = once.earlier(id(1)).unwrap();
assert_eq!(shown.read, "your read of 00000000");
assert_eq!(shown.bytes, 100);
}
#[test]
fn a_discarded_result_marks_nothing() {
let once = ShownOnce::default();
once.begin("a");
once.mark(id(1), 100, 100, None);
once.discard();
assert!(!once.contains(id(1)));
once.begin("b");
once.mark(id(2), 1, 1, None);
once.begin("c");
once.commit();
assert!(!once.contains(id(2)));
}
#[test]
fn a_legend_is_shown_once() {
let once = ShownOnce::default();
once.begin("a");
assert!(once.legend("x"));
assert!(!once.legend("x"), "not twice in one result");
once.discard();
once.begin("b");
assert!(once.legend("x"));
once.commit();
once.begin("c");
assert!(!once.legend("x"));
}
#[test]
fn the_first_showing_is_kept() {
let once = ShownOnce::default();
once.begin("first");
once.mark(id(1), 10, 10, None);
once.commit();
once.begin("second");
once.mark(id(1), 20, 20, None);
once.commit();
assert_eq!(once.earlier(id(1)).unwrap().read, "first");
}
#[test]
fn a_posts_comments_are_counted() {
let once = ShownOnce::default();
let post = PostId::from(Uuid::from_u128(9));
once.begin("r");
once.mark(id(1), 10, 10, Some(post));
once.mark(id(2), 30, 30, Some(post));
once.mark(id(3), 50, 50, None);
once.commit();
assert_eq!(once.comments_of(post), (2, 40));
}
}