use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Location {
#[serde(rename = "s")]
pub spine_idx: u32,
#[serde(rename = "o")]
pub char_offset: u64,
#[serde(rename = "h")]
pub anchor_hash: String,
}
pub fn anchor_hash(text: &str, char_offset: usize) -> String {
let chars: Vec<char> = text.chars().collect();
let half = 25usize;
let bucket = (char_offset / 16) * 16;
let start = bucket.saturating_sub(half);
let end = (bucket + half).min(chars.len());
let window: String = chars[start..end].iter().collect();
let digest = Sha256::digest(window.as_bytes());
hex::encode(&digest[..8])
}
pub fn reanchor(
new_plaintext: &str,
text: &str,
original_offset: usize,
ctx_before: &str,
ctx_after: &str,
) -> Option<usize> {
let needle = format!("{ctx_before}{text}{ctx_after}");
if let Some(i) = new_plaintext.find(&needle) {
let char_i = new_plaintext[..i].chars().count();
return Some(char_i + ctx_before.chars().count());
}
let matches: Vec<usize> = new_plaintext.match_indices(text).map(|(i, _)| i).collect();
if matches.len() == 1 {
return Some(new_plaintext[..matches[0]].chars().count());
}
if !matches.is_empty() {
let best = matches
.into_iter()
.map(|b| new_plaintext[..b].chars().count())
.min_by_key(|c| (c.wrapping_sub(original_offset) as i64).abs())?;
return Some(best);
}
None
}