foch 0.0.2

EU4 mod analysis and merge toolkit
Documentation
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{Duration, Instant};

use super::error::CwtLoadError;
use super::query::{CompiledRulePack, CwtQuery};
use super::source::{CwtSchemaId, SchemaPack, cwt_schema_id_from_dir};

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum CwtLoadStatus {
	Embedded,
	CacheHit,
	CompiledFromSource,
}

pub(crate) struct CwtLoad {
	pub(crate) facts: Arc<CwtQuery>,
	pub(crate) status: CwtLoadStatus,
	pub(crate) source_id: CwtSchemaId,
	pub(crate) cache_path: Option<PathBuf>,
	pub(crate) timings: CwtLoadTimings,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct CwtLoadTimings {
	pub(crate) source_hash: Option<Duration>,
	pub(crate) cache_read: Option<Duration>,
	pub(crate) source_compile: Option<Duration>,
	pub(crate) total: Duration,
}

pub(crate) fn load_cwt_from_dir(
	root: &Path,
	cache_dir: Option<&Path>,
) -> Result<CwtLoad, CwtLoadError> {
	let total_started = Instant::now();
	let hash_started = Instant::now();
	let source_id = cwt_schema_id_from_dir(root)?;
	let source_hash = Some(hash_started.elapsed());
	let source_id_hex = source_id.to_hex();
	let cache_path = cache_dir.map(|dir| {
		let generation = open_compiled_rule_cache_generation(dir);
		compiled_rule_cache_path(&generation, &source_id_hex)
	});
	let mut cache_read = None;
	if let Some(path) = cache_path.as_ref() {
		let cache_started = Instant::now();
		let cached_pack = read_cached_compiled_pack(path, &source_id_hex);
		cache_read = Some(cache_started.elapsed());
		if let Some(pack) = cached_pack {
			return Ok(CwtLoad {
				facts: Arc::new(CwtQuery::new(pack)),
				status: CwtLoadStatus::CacheHit,
				source_id,
				cache_path,
				timings: CwtLoadTimings {
					source_hash,
					cache_read,
					source_compile: None,
					total: total_started.elapsed(),
				},
			});
		}
	}

	let compile_started = Instant::now();
	let schema_pack = SchemaPack::load_from_dir_with_id(root, source_id.clone())?;
	let compiled_pack = CompiledRulePack::from_schema_pack(&schema_pack);
	let source_compile = compile_started.elapsed();
	if let Some(path) = cache_path.as_ref() {
		write_cached_compiled_pack(path, &compiled_pack);
	}
	Ok(CwtLoad {
		facts: Arc::new(CwtQuery::new(compiled_pack)),
		status: CwtLoadStatus::CompiledFromSource,
		source_id,
		cache_path,
		timings: CwtLoadTimings {
			source_hash,
			cache_read,
			source_compile: Some(source_compile),
			total: total_started.elapsed(),
		},
	})
}

fn open_compiled_rule_cache_generation(cache_dir: &Path) -> PathBuf {
	let namespace = crate::platform::cache_store::cache_version_namespace(crate::DATA_VERSION)
		.expect("data version is valid SemVer");
	let generation = cache_dir.join(namespace);
	let _ = fs::create_dir_all(&generation);
	generation
}

fn compiled_rule_cache_path(generation_dir: &Path, source_id: &str) -> PathBuf {
	generation_dir.join(format!("rules-src-{source_id}.bin"))
}

fn read_cached_compiled_pack(path: &Path, source_id: &str) -> Option<CompiledRulePack> {
	let bytes = fs::read(path).ok()?;
	let pack = CompiledRulePack::from_bytes(&bytes).ok()?;
	(pack.source_id.as_deref() == Some(source_id)).then_some(pack)
}

fn write_cached_compiled_pack(path: &Path, pack: &CompiledRulePack) {
	let Some(parent) = path.parent() else {
		return;
	};
	if fs::create_dir_all(parent).is_err() {
		return;
	}
	let Ok(bytes) = pack.to_bytes() else {
		return;
	};
	let _ = fs::write(path, bytes);
}

#[cfg(test)]
mod tests {
	use std::fs;

	use super::open_compiled_rule_cache_generation;

	#[test]
	fn opening_a_generation_does_not_delete_other_generations_or_flat_entries() {
		let cache = tempfile::tempdir().expect("create cache fixture");
		let prior = cache.path().join("v0.10.0");
		let flat = cache.path().join("rules-src-old.bin");
		fs::create_dir_all(&prior).expect("create prior generation");
		fs::write(prior.join("rules.bin"), b"prior").expect("write prior cache entry");
		fs::write(&flat, b"flat").expect("write legacy flat cache entry");

		let current = open_compiled_rule_cache_generation(cache.path());

		assert!(current.is_dir());
		assert!(prior.join("rules.bin").is_file());
		assert!(flat.is_file());
	}
}