use std::collections::HashMap;
use std::path::PathBuf;
use uuid::Uuid;
use crate::config::Config;
use crate::error::{Error, Result};
use crate::project::{BOOKS_DIR, ProjectLayout};
use crate::store::Store;
use crate::store::node::{Node, NodeKind};
pub struct Hierarchy {
by_id: HashMap<Uuid, Node>,
order: Vec<Uuid>,
}
impl Hierarchy {
pub fn load(store: &Store) -> Result<Self> {
let raw = store
.raw()
.list_metadata()
.map_err(|e| Error::Store(format!("list_metadata: {e}")))?;
let mut by_id = HashMap::with_capacity(raw.len());
for (id, value) in raw {
if let Ok(node) = Node::from_json(id, &value) {
by_id.insert(id, node);
}
}
let mut order: Vec<Uuid> = by_id.keys().copied().collect();
order.sort_by_key(|id| {
let n = &by_id[id];
(n.path.len(), n.order, n.slug.clone())
});
Ok(Self { by_id, order })
}
pub fn is_empty(&self) -> bool {
self.by_id.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &Node> {
self.order.iter().map(move |id| &self.by_id[id])
}
pub fn get(&self, id: Uuid) -> Option<&Node> {
self.by_id.get(&id)
}
pub fn children_of(&self, parent_id: Option<Uuid>) -> Vec<&Node> {
let mut out: Vec<&Node> = self
.iter()
.filter(|n| n.parent_id == parent_id)
.collect();
out.sort_by_key(|n| n.order);
out
}
pub fn flatten(&self) -> Vec<(&Node, usize)> {
let mut out: Vec<(&Node, usize)> = Vec::new();
for root in self.children_of(None) {
self.walk_into(root, 0, &mut out);
}
out
}
pub fn flatten_with_collapsed(
&self,
collapsed: &std::collections::HashSet<Uuid>,
) -> Vec<(&Node, usize)> {
let mut out: Vec<(&Node, usize)> = Vec::new();
for root in self.children_of(None) {
self.walk_into_collapsed(root, 0, collapsed, &mut out);
}
out
}
fn walk_into<'a>(&'a self, node: &'a Node, depth: usize, out: &mut Vec<(&'a Node, usize)>) {
out.push((node, depth));
for child in self.children_of(Some(node.id)) {
self.walk_into(child, depth + 1, out);
}
}
fn walk_into_collapsed<'a>(
&'a self,
node: &'a Node,
depth: usize,
collapsed: &std::collections::HashSet<Uuid>,
out: &mut Vec<(&'a Node, usize)>,
) {
out.push((node, depth));
if collapsed.contains(&node.id) {
return;
}
for child in self.children_of(Some(node.id)) {
self.walk_into_collapsed(child, depth + 1, collapsed, out);
}
}
pub fn has_children(&self, node_id: Uuid) -> bool {
!self.children_of(Some(node_id)).is_empty()
}
pub fn next_order(&self, parent_id: Option<Uuid>) -> u32 {
self.children_of(parent_id)
.iter()
.map(|n| n.order)
.max()
.map(|m| m + 1)
.unwrap_or(1)
}
pub fn pick_parent_for(
&self,
cfg: &Config,
start: Option<Uuid>,
child_kind: NodeKind,
) -> Result<Option<&Node>> {
if child_kind == NodeKind::Book {
return Ok(None);
}
let mut current = start;
while let Some(id) = current {
let node = self
.get(id)
.ok_or_else(|| Error::Store(format!("hierarchy missing node {id}")))?;
if self
.validate_placement(cfg, Some(node), child_kind)
.is_ok()
{
return Ok(Some(node));
}
current = node.parent_id;
}
Err(Error::Store(format!(
"no ancestor accepts a {} as a child",
child_kind.as_str()
)))
}
pub fn collect_subtree(&self, root: Uuid) -> Vec<Uuid> {
let mut out = Vec::new();
self.walk_ids(root, &mut out);
out
}
fn walk_ids(&self, node_id: Uuid, out: &mut Vec<Uuid>) {
if !self.by_id.contains_key(&node_id) {
return;
}
out.push(node_id);
for child in self.children_of(Some(node_id)) {
self.walk_ids(child.id, out);
}
}
pub fn find_by_path(&self, path: &str) -> Option<&Node> {
let segments: Vec<&str> = path
.split('/')
.map(str::trim)
.filter(|s| !s.is_empty())
.collect();
if segments.is_empty() {
return None;
}
let mut current_parent: Option<Uuid> = None;
let mut current: Option<&Node> = None;
for seg in segments {
let next = self
.children_of(current_parent)
.into_iter()
.find(|n| n.slug == seg)?;
current_parent = Some(next.id);
current = Some(next);
}
current
}
pub fn fs_path(&self, node: &Node, _layout: &ProjectLayout) -> PathBuf {
let mut p = PathBuf::from(BOOKS_DIR);
for ancestor in self.ancestors(node) {
p.push(ancestor.fs_name());
}
p.push(node.fs_name());
p
}
pub fn ancestors(&self, node: &Node) -> Vec<&Node> {
let mut chain: Vec<&Node> = Vec::new();
let mut cur = node.parent_id;
while let Some(id) = cur {
if let Some(parent) = self.by_id.get(&id) {
chain.push(parent);
cur = parent.parent_id;
} else {
break;
}
}
chain.reverse();
chain
}
pub fn slug_path(&self, node: &Node) -> String {
let mut parts: Vec<&str> = self
.ancestors(node)
.into_iter()
.map(|n| n.slug.as_str())
.collect();
parts.push(&node.slug);
parts.join("/")
}
pub fn validate_placement(
&self,
cfg: &Config,
parent: Option<&Node>,
child_kind: NodeKind,
) -> Result<()> {
let allowed = match (parent.map(|p| p.kind), child_kind) {
(None, NodeKind::Book) => true,
(None, _) => false,
(Some(_), NodeKind::Book) => false,
(Some(NodeKind::Book), NodeKind::Chapter) => true,
(Some(NodeKind::Book), NodeKind::Paragraph) => true,
(Some(NodeKind::Book), NodeKind::Image) => true,
(Some(NodeKind::Book), NodeKind::Script) => true,
(Some(NodeKind::Chapter), NodeKind::Subchapter) => true,
(Some(NodeKind::Chapter), NodeKind::Paragraph) => true,
(Some(NodeKind::Chapter), NodeKind::Image) => true,
(Some(NodeKind::Chapter), NodeKind::Script) => true,
(Some(NodeKind::Subchapter), NodeKind::Paragraph) => true,
(Some(NodeKind::Subchapter), NodeKind::Image) => true,
(Some(NodeKind::Subchapter), NodeKind::Script) => true,
(Some(NodeKind::Subchapter), NodeKind::Subchapter) => {
cfg.hierarchy.unbounded_subchapters
}
_ => false,
};
if allowed {
Ok(())
} else {
let parent_desc = parent
.map(|p| format!("a {}", p.kind.as_str()))
.unwrap_or_else(|| "the root".into());
Err(Error::Store(format!(
"{} cannot be placed under {}",
child_kind.as_str(),
parent_desc
)))
}
}
}