use super::{OptimizedLookupTable, Profiler};
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use std::thread;
use std::time::{Duration, SystemTime};
pub struct HotReloadManager {
watch_dir: PathBuf,
profiler: Arc<Profiler>,
last_check: Arc<RwLock<SystemTime>>,
enabled: Arc<RwLock<bool>>,
}
impl HotReloadManager {
pub fn new(watch_dir: PathBuf, profiler: Arc<Profiler>) -> Self {
Self {
watch_dir,
profiler,
last_check: Arc::new(RwLock::new(SystemTime::now())),
enabled: Arc::new(RwLock::new(true)),
}
}
pub fn start_watching(self: Arc<Self>) {
let manager = self.clone();
thread::spawn(move || {
loop {
thread::sleep(Duration::from_secs(10));
if !*manager.enabled.read().unwrap() {
continue;
}
manager.check_for_updates();
}
});
}
pub fn check_for_updates(&self) {
let last_check = *self.last_check.read().unwrap();
if let Ok(entries) = fs::read_dir(&self.watch_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) != Some("json") {
continue;
}
if let Ok(metadata) = entry.metadata() {
if let Ok(modified) = metadata.modified() {
let modified_time = modified;
if modified_time > last_check {
self.try_load_optimization(&path);
}
}
}
}
}
*self.last_check.write().unwrap() = SystemTime::now();
}
fn try_load_optimization(&self, path: &Path) {
match fs::read_to_string(path) {
Ok(content) => {
match serde_json::from_str::<OptimizedLookupTable>(&content) {
Ok(optimization) => {
if self.validate_optimization(&optimization) {
self.profiler.load_optimization(optimization);
eprintln!("Hot-reloaded optimization from: {path:?}");
} else {
eprintln!("Invalid optimization file: {path:?}");
}
}
Err(e) => {
eprintln!("Failed to parse optimization file {path:?}: {e}");
}
}
}
Err(e) => {
eprintln!("Failed to read optimization file {path:?}: {e}");
}
}
}
fn validate_optimization(&self, optimization: &OptimizedLookupTable) -> bool {
if optimization.from_script.is_empty() || optimization.to_script.is_empty() {
return false;
}
if optimization.sequence_mappings.is_empty() && optimization.word_mappings.is_empty() {
return false;
}
for (key, value) in &optimization.sequence_mappings {
if key.is_empty() || value.is_empty() {
return false;
}
}
for (key, value) in &optimization.word_mappings {
if key.is_empty() || value.is_empty() {
return false;
}
}
true
}
pub fn set_enabled(&self, enabled: bool) {
*self.enabled.write().unwrap() = enabled;
}
pub fn reload_now(&self) {
self.check_for_updates();
}
pub fn watch_dir(&self) -> &Path {
&self.watch_dir
}
}
pub struct OptimizationCache {
cache: Arc<RwLock<FxHashMap<(String, String), OptimizedLookupTable>>>,
}
use rustc_hash::FxHashMap;
impl Default for OptimizationCache {
fn default() -> Self {
Self::new()
}
}
impl OptimizationCache {
pub fn new() -> Self {
Self {
cache: Arc::new(RwLock::new(FxHashMap::default())),
}
}
pub fn get(&self, from_script: &str, to_script: &str) -> Option<OptimizedLookupTable> {
let cache = self.cache.read().unwrap();
cache
.get(&(from_script.to_string(), to_script.to_string()))
.cloned()
}
pub fn load(&self, optimization: OptimizedLookupTable) {
let mut cache = self.cache.write().unwrap();
let key = (
optimization.from_script.clone(),
optimization.to_script.clone(),
);
cache.insert(key, optimization);
}
pub fn clear(&self) {
let mut cache = self.cache.write().unwrap();
cache.clear();
}
pub fn size(&self) -> usize {
let cache = self.cache.read().unwrap();
cache.len()
}
pub fn apply_optimization<F>(
&self,
text: &str,
from_script: &str,
to_script: &str,
fallback: F,
) -> Result<String, Box<dyn std::error::Error>>
where
F: Fn(&str) -> Result<String, Box<dyn std::error::Error>>,
{
if let Some(optimization) = self.get(from_script, to_script) {
let mut result = String::new();
let chars = text.chars();
let mut buffer = String::new();
for ch in chars {
buffer.push(ch);
let mut matched = false;
if let Some(mapped) = optimization.word_mappings.get(&buffer) {
result.push_str(mapped);
buffer.clear();
matched = true;
} else {
let chars: Vec<char> = buffer.chars().collect();
for len in (1..=chars.len()).rev() {
let seq = &chars[chars.len() - len..];
let seq_str: String = seq.iter().collect();
if let Some(mapped) = optimization.sequence_mappings.get(&seq_str) {
if chars.len() > len {
let prefix_chars = &chars[..chars.len() - len];
let prefix: String = prefix_chars.iter().collect();
result.push_str(&fallback(&prefix)?);
}
result.push_str(mapped);
buffer.clear();
matched = true;
break;
}
}
}
if !matched && buffer.len() > 10 {
result.push_str(&fallback(&buffer)?);
buffer.clear();
}
}
if !buffer.is_empty() {
result.push_str(&fallback(&buffer)?);
}
Ok(result)
} else {
fallback(text)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::modules::profiler::{OptimizationMetadata, ProfileStats};
use tempfile::tempdir;
#[test]
fn test_hot_reload_manager_creation() {
let temp_dir = tempdir().unwrap();
let profiler = Arc::new(Profiler::new());
let manager = HotReloadManager::new(temp_dir.path().to_path_buf(), profiler);
assert_eq!(manager.watch_dir, temp_dir.path());
}
#[test]
fn test_optimization_validation() {
let temp_dir = tempdir().unwrap();
let profiler = Arc::new(Profiler::new());
let manager = HotReloadManager::new(temp_dir.path().to_path_buf(), profiler);
let mut valid_opt = OptimizedLookupTable {
from_script: "devanagari".to_string(),
to_script: "iast".to_string(),
sequence_mappings: FxHashMap::default(),
word_mappings: FxHashMap::default(),
metadata: OptimizationMetadata {
generated_at: SystemTime::now(),
sequence_count: 1,
min_frequency: 10,
profile_stats: ProfileStats {
total_sequences_profiled: 100,
unique_sequences: 10,
top_sequences: vec![],
},
},
};
valid_opt
.sequence_mappings
.insert("धर्म".to_string(), "dharma".to_string());
assert!(manager.validate_optimization(&valid_opt));
let mut invalid_opt = valid_opt.clone();
invalid_opt.from_script = String::new();
assert!(!manager.validate_optimization(&invalid_opt));
let mut invalid_opt = valid_opt.clone();
invalid_opt.sequence_mappings.clear();
invalid_opt.word_mappings.clear();
assert!(!manager.validate_optimization(&invalid_opt));
}
#[test]
fn test_optimization_cache() {
let cache = OptimizationCache::new();
let mut optimization = OptimizedLookupTable {
from_script: "devanagari".to_string(),
to_script: "iast".to_string(),
sequence_mappings: FxHashMap::default(),
word_mappings: FxHashMap::default(),
metadata: OptimizationMetadata {
generated_at: SystemTime::now(),
sequence_count: 1,
min_frequency: 10,
profile_stats: ProfileStats {
total_sequences_profiled: 100,
unique_sequences: 10,
top_sequences: vec![],
},
},
};
optimization
.sequence_mappings
.insert("धर्म".to_string(), "dharma".to_string());
cache.load(optimization.clone());
assert_eq!(cache.size(), 1);
let retrieved = cache.get("devanagari", "iast").unwrap();
assert_eq!(retrieved.from_script, "devanagari");
assert_eq!(retrieved.sequence_mappings["धर्म"], "dharma");
}
}