use crate::core::Expression;
use crate::parser::config::ParserConfig;
use crate::parser::Parser;
use dirs;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::sync::{Arc, OnceLock, RwLock};
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PersistentCacheEntry {
pub expression_hash: u64,
pub simplified_expression: String,
pub access_count: u64,
pub last_access: u64,
pub created_at: u64,
}
#[derive(Debug, Clone)]
pub struct PersistentCacheConfig {
pub cache_directory: PathBuf,
pub max_entries: usize,
pub max_age_seconds: u64,
pub min_access_count: u64,
pub save_frequency: usize,
}
impl Default for PersistentCacheConfig {
fn default() -> Self {
Self {
cache_directory: get_default_cache_directory(),
max_entries: 50000,
max_age_seconds: 7 * 24 * 60 * 60,
min_access_count: 2,
save_frequency: 100,
}
}
}
pub struct PersistentCache {
entries: Arc<RwLock<HashMap<u64, PersistentCacheEntry>>>,
config: PersistentCacheConfig,
operations_since_save: Arc<RwLock<usize>>,
cache_file_path: PathBuf,
}
impl PersistentCache {
pub fn new(config: PersistentCacheConfig) -> Self {
let cache_file_path = config.cache_directory.join("mathhook_cache.json");
let cache = Self {
entries: Arc::new(RwLock::new(HashMap::new())),
config,
operations_since_save: Arc::new(RwLock::new(0)),
cache_file_path,
};
cache.load_from_disk();
cache
}
pub fn get(&self, expression_hash: u64) -> Option<Expression> {
if let Ok(mut entries) = self.entries.write() {
if let Some(entry) = entries.get_mut(&expression_hash) {
let parser = Parser::new(&ParserConfig::default());
entry.access_count += 1;
entry.last_access = current_timestamp();
if let Ok(expr) = parser.parse(&entry.simplified_expression) {
self.increment_operations();
return Some(expr);
}
}
}
None
}
pub fn put(&self, expression_hash: u64, simplified: &Expression) {
let serialized = match self.serialize_expression(simplified) {
Ok(s) => s,
Err(_) => return,
};
let entry = PersistentCacheEntry {
expression_hash,
simplified_expression: serialized,
access_count: 1,
last_access: current_timestamp(),
created_at: current_timestamp(),
};
if let Ok(mut entries) = self.entries.write() {
entries.insert(expression_hash, entry);
if entries.len() > self.config.max_entries {
self.cleanup_old_entries(&mut entries);
}
}
self.increment_operations();
}
fn load_from_disk(&self) {
if !self.cache_file_path.exists() {
return;
}
match fs::read_to_string(&self.cache_file_path) {
Ok(content) => {
match serde_json::from_str::<HashMap<u64, PersistentCacheEntry>>(&content) {
Ok(loaded_entries) => {
if let Ok(mut entries) = self.entries.write() {
let current_time = current_timestamp();
let valid_entries: HashMap<u64, PersistentCacheEntry> = loaded_entries
.into_iter()
.filter(|(_, entry)| {
current_time - entry.created_at < self.config.max_age_seconds
})
.collect();
*entries = valid_entries;
println!("Loaded {} persistent cache entries", entries.len());
}
}
Err(e) => {
eprintln!("WARNING: Failed to parse persistent cache: {}", e);
}
}
}
Err(e) => {
eprintln!("WARNING: Failed to read persistent cache file: {}", e);
}
}
}
pub fn save_to_disk(&self) {
if let Some(parent) = self.cache_file_path.parent() {
if let Err(e) = fs::create_dir_all(parent) {
eprintln!("WARNING: Failed to create cache directory: {}", e);
return;
}
}
if let Ok(entries) = self.entries.read() {
let current_time = current_timestamp();
let persistent_entries: HashMap<u64, PersistentCacheEntry> = entries
.iter()
.filter(|(_, entry)| {
entry.access_count >= self.config.min_access_count
&& current_time - entry.created_at < self.config.max_age_seconds
})
.map(|(k, v)| (*k, v.clone()))
.collect();
match serde_json::to_string_pretty(&persistent_entries) {
Ok(content) => {
if let Err(e) = fs::write(&self.cache_file_path, content) {
eprintln!("WARNING: Failed to write persistent cache: {}", e);
} else {
println!(
"Saved {} entries to persistent cache",
persistent_entries.len()
);
}
}
Err(e) => {
eprintln!("WARNING: Failed to serialize persistent cache: {}", e);
}
}
}
if let Ok(mut ops) = self.operations_since_save.write() {
*ops = 0;
}
}
fn cleanup_old_entries(&self, entries: &mut HashMap<u64, PersistentCacheEntry>) {
let current_time = current_timestamp();
let target_size = (self.config.max_entries as f64 * 0.8) as usize;
let mut scored_entries: Vec<(u64, f64)> = entries
.iter()
.map(|(hash, entry)| {
let age_factor = 1.0 / (1.0 + (current_time - entry.last_access) as f64 / 3600.0);
let access_factor = (entry.access_count as f64).ln().max(1.0);
let score = age_factor * access_factor;
(*hash, score)
})
.collect();
scored_entries.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
let to_remove = entries.len().saturating_sub(target_size);
for (hash, _) in scored_entries.iter().take(to_remove) {
entries.remove(hash);
}
println!("Cleaned up {} old cache entries", to_remove);
}
fn increment_operations(&self) {
if let Ok(mut ops) = self.operations_since_save.write() {
*ops += 1;
if *ops >= self.config.save_frequency {
self.save_to_disk();
}
}
}
fn serialize_expression(&self, expr: &Expression) -> Result<String, String> {
Ok(format!("{:?}", expr))
}
pub fn get_statistics(&self) -> PersistentCacheStatistics {
if let Ok(entries) = self.entries.read() {
let total_entries = entries.len();
let total_access_count: u64 = entries.values().map(|e| e.access_count).sum();
let average_access_count = if total_entries > 0 {
total_access_count as f64 / total_entries as f64
} else {
0.0
};
let current_time = current_timestamp();
let recent_entries = entries
.values()
.filter(|e| current_time - e.last_access < 3600)
.count();
PersistentCacheStatistics {
total_entries,
recent_entries,
total_access_count,
average_access_count,
cache_file_size: self.get_cache_file_size(),
cache_directory: self.config.cache_directory.clone(),
}
} else {
PersistentCacheStatistics::default()
}
}
fn get_cache_file_size(&self) -> u64 {
fs::metadata(&self.cache_file_path)
.map(|m| m.len())
.unwrap_or(0)
}
pub fn force_save(&self) {
self.save_to_disk();
}
pub fn clear(&self) {
if let Ok(mut entries) = self.entries.write() {
entries.clear();
}
let _ = fs::remove_file(&self.cache_file_path);
}
}
#[derive(Debug, Clone, Default)]
pub struct PersistentCacheStatistics {
pub total_entries: usize,
pub recent_entries: usize,
pub total_access_count: u64,
pub average_access_count: f64,
pub cache_file_size: u64,
pub cache_directory: PathBuf,
}
fn get_default_cache_directory() -> PathBuf {
if let Some(cache_dir) = dirs::cache_dir() {
cache_dir.join("mathhook")
} else {
PathBuf::from(".mathhook_cache")
}
}
fn current_timestamp() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
static GLOBAL_PERSISTENT_CACHE: OnceLock<PersistentCache> = OnceLock::new();
pub fn get_global_persistent_cache() -> &'static PersistentCache {
GLOBAL_PERSISTENT_CACHE.get_or_init(|| PersistentCache::new(PersistentCacheConfig::default()))
}
pub fn get_persistent_cached_result(expression_hash: u64) -> Option<Expression> {
get_global_persistent_cache().get(expression_hash)
}
pub fn store_persistent_cached_result(expression_hash: u64, simplified: &Expression) {
get_global_persistent_cache().put(expression_hash, simplified);
}
pub fn get_persistent_cache_statistics() -> PersistentCacheStatistics {
get_global_persistent_cache().get_statistics()
}
pub fn save_persistent_cache() {
get_global_persistent_cache().force_save();
}
pub fn clear_persistent_cache() {
get_global_persistent_cache().clear();
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_persistent_cache_creation() {
let temp_dir = TempDir::new().expect("Failed to create temp directory for test");
let config = PersistentCacheConfig {
cache_directory: temp_dir.path().to_path_buf(),
..Default::default()
};
let cache = PersistentCache::new(config);
let stats = cache.get_statistics();
assert_eq!(stats.total_entries, 0);
assert_eq!(stats.total_access_count, 0);
}
#[test]
fn test_cache_file_path() {
let temp_dir = TempDir::new().expect("Failed to create temp directory for test");
let config = PersistentCacheConfig {
cache_directory: temp_dir.path().to_path_buf(),
..Default::default()
};
let cache = PersistentCache::new(config);
assert!(cache.cache_file_path.ends_with("mathhook_cache.json"));
}
#[test]
fn test_default_cache_directory() {
let default_dir = get_default_cache_directory();
assert!(default_dir.to_string_lossy().contains("mathhook"));
}
#[test]
fn test_global_persistent_cache() {
let stats = get_persistent_cache_statistics();
assert!(stats.total_entries == stats.total_entries);
save_persistent_cache();
}
}