use anyhow::{Context, Result};
use std::collections::{HashMap, HashSet};
use std::fs::{File, OpenOptions};
use std::io::{BufRead, BufReader, BufWriter, Write};
use std::path::Path;
use crate::error::LinkError;
use crate::link::Link;
pub struct LinkStorage {
links: HashMap<u32, Link>,
names: HashMap<u32, String>,
name_to_id: HashMap<String, u32>,
next_id: u32,
db_path: String,
trace: bool,
}
impl LinkStorage {
pub fn new(db_path: &str, trace: bool) -> Result<Self> {
let mut storage = Self {
links: HashMap::new(),
names: HashMap::new(),
name_to_id: HashMap::new(),
next_id: 1,
db_path: db_path.to_string(),
trace,
};
if Path::new(db_path).exists() {
storage.load()?;
}
Ok(storage)
}
fn load(&mut self) -> Result<()> {
let file = File::open(&self.db_path)
.with_context(|| format!("Failed to open database: {}", self.db_path))?;
let reader = BufReader::new(file);
for line in reader.lines() {
let line = line?;
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((link, name)) = self.parse_link_line(line) {
self.links.insert(link.index, link);
if link.index >= self.next_id {
self.next_id = link.index + 1;
}
if let Some(name) = name {
self.names.insert(link.index, name.clone());
self.name_to_id.insert(name, link.index);
}
}
}
if self.trace {
eprintln!(
"[TRACE] Loaded {} links from {}",
self.links.len(),
self.db_path
);
}
Ok(())
}
fn parse_link_line(&self, line: &str) -> Option<(Link, Option<String>)> {
let line = line.trim_matches(|c| c == '(' || c == ')');
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 3 {
let index = parts[0].parse().ok()?;
let source = parts[1].parse().ok()?;
let target = parts[2].parse().ok()?;
let name = if parts.len() > 3 {
Some(parts[3].trim_matches('"').to_string())
} else {
None
};
return Some((Link::new(index, source, target), name));
}
None
}
pub fn save(&self) -> Result<()> {
let file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&self.db_path)
.with_context(|| format!("Failed to create database: {}", self.db_path))?;
let mut writer = BufWriter::new(file);
let mut links: Vec<_> = self.links.values().collect();
links.sort_by_key(|l| l.index);
for link in links {
if let Some(name) = self.names.get(&link.index) {
writeln!(
writer,
"({} {} {} \"{}\")",
link.index, link.source, link.target, name
)?;
} else {
writeln!(writer, "({} {} {})", link.index, link.source, link.target)?;
}
}
writer.flush()?;
if self.trace {
eprintln!(
"[TRACE] Saved {} links to {}",
self.links.len(),
self.db_path
);
}
Ok(())
}
pub fn create(&mut self, source: u32, target: u32) -> u32 {
let id = self.next_id;
self.next_id += 1;
let link = Link::new(id, source, target);
self.links.insert(id, link);
if self.trace {
eprintln!("[TRACE] Created link: ({} {} {})", id, source, target);
}
id
}
pub fn ensure_created(&mut self, id: u32) -> u32 {
if self.links.contains_key(&id) {
return id;
}
if self.next_id > id {
let link = Link::new(id, 0, 0);
self.links.insert(id, link);
if self.trace {
eprintln!("[TRACE] Ensured link: ({} 0 0)", id);
}
return id;
}
while self.next_id <= id {
let placeholder_id = self.next_id;
self.next_id += 1;
if placeholder_id == id {
let link = Link::new(id, 0, 0);
self.links.insert(id, link);
if self.trace {
eprintln!("[TRACE] Ensured link: ({} 0 0)", id);
}
return id;
}
}
id
}
pub fn get(&self, id: u32) -> Option<&Link> {
self.links.get(&id)
}
pub fn exists(&self, id: u32) -> bool {
self.links.contains_key(&id)
}
pub fn update(&mut self, id: u32, source: u32, target: u32) -> Result<Link> {
if let Some(link) = self.links.get_mut(&id) {
let before = *link;
if self.trace {
eprintln!(
"[TRACE] Updating link {} from ({} {}) to ({} {})",
id, link.source, link.target, source, target
);
}
link.source = source;
link.target = target;
Ok(before)
} else {
Err(LinkError::NotFound(id).into())
}
}
pub fn delete(&mut self, id: u32) -> Result<Link> {
if let Some(name) = self.names.remove(&id) {
self.name_to_id.remove(&name);
}
if let Some(link) = self.links.remove(&id) {
if self.trace {
eprintln!(
"[TRACE] Deleted link: ({} {} {})",
link.index, link.source, link.target
);
}
Ok(link)
} else {
Err(LinkError::NotFound(id).into())
}
}
pub fn all(&self) -> Vec<&Link> {
self.links.values().collect()
}
pub fn query(
&self,
index: Option<u32>,
source: Option<u32>,
target: Option<u32>,
) -> Vec<&Link> {
self.links
.values()
.filter(|link| {
(index.is_none() || index == Some(link.index))
&& (source.is_none() || source == Some(link.source))
&& (target.is_none() || target == Some(link.target))
})
.collect()
}
pub fn search(&self, source: u32, target: u32) -> Option<u32> {
for link in self.links.values() {
if link.source == source && link.target == target {
return Some(link.index);
}
}
None
}
pub fn get_or_create(&mut self, source: u32, target: u32) -> u32 {
if let Some(id) = self.search(source, target) {
id
} else {
self.create(source, target)
}
}
pub fn format(&self, link: &Link) -> String {
let index_str = self
.names
.get(&link.index)
.cloned()
.unwrap_or_else(|| link.index.to_string());
let source_str = self
.names
.get(&link.source)
.cloned()
.unwrap_or_else(|| link.source.to_string());
let target_str = self
.names
.get(&link.target)
.cloned()
.unwrap_or_else(|| link.target.to_string());
format!("({} {} {})", index_str, source_str, target_str)
}
pub fn format_lino(&self, link: &Link) -> String {
format!(
"({}: {} {})",
self.format_lino_reference(link.index),
self.format_lino_reference(link.source),
self.format_lino_reference(link.target)
)
}
pub fn lino_lines(&self) -> Vec<String> {
let mut links: Vec<_> = self.all();
links.sort_by_key(|l| l.index);
links
.into_iter()
.map(|link| self.format_lino(link))
.collect()
}
pub fn write_lino_output<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let path = path.as_ref();
let file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)
.with_context(|| format!("Failed to create LiNo output: {}", path.display()))?;
let mut writer = BufWriter::new(file);
for line in self.lino_lines() {
writeln!(writer, "{line}")?;
}
writer.flush()?;
Ok(())
}
pub fn format_structure(&self, id: u32) -> Result<String> {
let mut visited = HashSet::new();
self.format_structure_recursive(id, &mut visited)
}
fn format_structure_recursive(&self, id: u32, visited: &mut HashSet<u32>) -> Result<String> {
let link = self.get(id).ok_or(LinkError::NotFound(id))?;
if !visited.insert(id) {
return Ok(self.format_lino_reference(id));
}
let source = if self.exists(link.source) && !visited.contains(&link.source) {
self.format_structure_recursive(link.source, visited)?
} else {
self.format_lino_reference(link.source)
};
let target = self.format_lino_reference(link.target);
let index = self.format_lino_reference(link.index);
visited.remove(&id);
Ok(format!("({index}: {source} {target})"))
}
pub fn print_all_links(&self) {
let mut links: Vec<_> = self.all();
links.sort_by_key(|l| l.index);
for link in links {
println!("{}", self.format(link));
}
}
pub fn print_change(&self, before: &Option<Link>, after: &Option<Link>) {
let before_text = before.map(|l| self.format(&l)).unwrap_or_default();
let after_text = after.map(|l| self.format(&l)).unwrap_or_default();
println!("({}) ({})", before_text, after_text);
}
pub fn get_or_create_named(&mut self, name: &str) -> u32 {
if let Some(&id) = self.name_to_id.get(name) {
id
} else {
let id = self.create(0, 0);
self.update(id, id, id).ok();
self.names.insert(id, name.to_string());
self.name_to_id.insert(name.to_string(), id);
if self.trace {
eprintln!("[TRACE] Created named link: {} => {}", name, id);
}
id
}
}
pub fn set_name(&mut self, id: u32, name: &str) {
if let Some(old_name) = self.names.remove(&id) {
self.name_to_id.remove(&old_name);
}
self.names.insert(id, name.to_string());
self.name_to_id.insert(name.to_string(), id);
if self.trace {
eprintln!("[TRACE] Set name: {} => {}", id, name);
}
}
pub fn get_name(&self, id: u32) -> Option<&String> {
self.names.get(&id)
}
pub fn get_by_name(&self, name: &str) -> Option<u32> {
self.name_to_id.get(name).copied()
}
pub fn remove_name(&mut self, id: u32) {
if let Some(name) = self.names.remove(&id) {
self.name_to_id.remove(&name);
if self.trace {
eprintln!("[TRACE] Removed name: {} => {}", id, name);
}
}
}
pub fn is_trace_enabled(&self) -> bool {
self.trace
}
fn format_lino_reference(&self, id: u32) -> String {
self.names
.get(&id)
.map(|name| escape_lino_reference(name))
.unwrap_or_else(|| id.to_string())
}
}
fn escape_lino_reference(reference: &str) -> String {
if reference.is_empty() || reference.trim().is_empty() {
return String::new();
}
let has_single_quote = reference.contains('\'');
let has_double_quote = reference.contains('"');
let needs_quoting = reference.contains(':')
|| reference.contains('(')
|| reference.contains(')')
|| reference.contains(' ')
|| reference.contains('\t')
|| reference.contains('\n')
|| reference.contains('\r')
|| has_single_quote
|| has_double_quote;
if has_single_quote && has_double_quote {
return format!("'{}'", reference.replace('\'', "\\'"));
}
if has_double_quote {
return format!("'{reference}'");
}
if has_single_quote {
return format!("\"{reference}\"");
}
if needs_quoting {
return format!("'{reference}'");
}
reference.to_string()
}