//! Utilities for working with file system backed work item queue.

use anyhow::anyhow;
use codec::{Decode, Encode};
use jam_types::WorkItem;

#[derive(Encode, Decode, Debug, Default)]
struct QueueState {
	items: Vec<WorkItem>,
}

fn with_persistent<T: Encode + Decode, R>(
	name: &str,
	f: impl FnOnce(&mut Option<T>) -> R,
) -> anyhow::Result<R> {
	let dirs = directories::ProjectDirs::from("io", "parity", "jam")
		.ok_or(anyhow!("This platform has no place for persistent data"))?;
	let dir = dirs.cache_dir();
	if !dir.exists() {
		std::fs::create_dir_all(dir)?;
	}
	let path = dir.join(name);
	let mut state = match std::fs::read(&path) {
		Ok(data) => Some(T::decode(&mut &data[..])?),
		Err(ref e) if e.kind() == std::io::ErrorKind::NotFound => None,
		Err(e) => return Err(e.into()),
	};
	let r = f(&mut state);
	if let Some(s) = state {
		let new_path = format!(".{name}.new");
		let old_path = format!("{name}.old");
		std::fs::write(&new_path, s.encode())?;
		std::fs::remove_file(&old_path).ok_if_not_found()?;
		std::fs::rename(&path, &old_path).ok_if_not_found()?;
		std::fs::rename(&new_path, &path)?;
	} else {
		std::fs::remove_file(&path).ok_if_not_found()?;
	}
	Ok(r)
}

pub fn push(item: WorkItem) -> anyhow::Result<usize> {
	with_persistent::<QueueState, usize>("queue", |queue| {
		let mut q = queue.take().unwrap_or_default();
		q.items.push(item);
		let r = q.items.len();
		*queue = Some(q);
		r
	})
}

pub fn peek() -> anyhow::Result<Vec<WorkItem>> {
	with_persistent::<QueueState, _>("queue", |queue| {
		queue.as_ref().map(|q| q.items.clone()).unwrap_or_default()
	})
}

pub fn take() -> anyhow::Result<Vec<WorkItem>> {
	with_persistent::<QueueState, _>("queue", |queue| queue.take().unwrap_or_default().items)
}

/// Prints the number of work-items in the persistent queue.
pub fn queue_status() -> anyhow::Result<()> {
	match peek()?.len() {
		0 => println!("Queue is empty"),
		1 => println!("Queue has 1 item"),
		x => println!("Queue has {x} items"),
	}
	Ok(())
}

/// Ergonomically ignore [`std::io::ErrorKind::NotFound`] errors.
///
/// Mainly useful for file deletion.
trait OkIfNotFound {
	fn ok_if_not_found(self) -> Self;
}

impl OkIfNotFound for std::io::Result<()> {
	fn ok_if_not_found(self) -> Self {
		match self {
			Err(ref e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
			other => other,
		}
	}
}
