Documentation
use std::sync::atomic::{
	AtomicU64,
	Ordering
};

use rusqlite::Result as SqlResult;

use crate::peer::NodeID;
use crate::store::{
	Action,
	Conspirator,
	Op,
	Store,
	Version
};


struct Permuter<const N: usize> {
	index: usize,
	stack: [usize; N],
	previous: Option<[usize; N]>
}
impl<const N: usize> Permuter<N> {
	fn new() -> Self {
		let index = 0;
		let stack = [0; N];
		Self {index, stack, previous: None}
	}
}
impl<const N: usize> Iterator for Permuter<N> {
	type Item = [usize; N];
	fn next(&mut self) -> Option<Self::Item> {
		// Heap's Algorithm, non-recursive format from wikipedia (adapted)
		let Some(previous) = self.previous.as_mut() else {
			let mut i = 0..N;
			self.previous = Some([0; N].map(|_| {
				i.next().unwrap()
			}));
			return self.previous;
		};
		loop {
			if self.index >= N {return None;}
			if self.stack[self.index] < self.index {
				if self.index % 2 == 0 {
					previous.swap(0, self.index);
				}
				else {
					previous.swap(self.stack[self.index], self.index);
				}
				self.stack[self.index] += 1;
				self.index = 1;
				break Some(*previous);
			}
			else {
				self.stack[self.index] = 0;
				self.index += 1;
			}
		}
	}
}

struct Node(NodeID, AtomicU64);
impl Node {
	fn create(id: NodeID, name: String) -> (Self, Op) {
		let node = Self(id, AtomicU64::new(0));
		let op = Op {
			origin: node.next_origin(),
			previous: None,
			target: id,
			action: Action::Name(name)
		};
		(node, op)
	}
	#[must_use]
	fn next_origin(&self) -> Version {
		Version {
			node: self.0,
			counter: self.1.fetch_add(1, Ordering::SeqCst)
		}
	}
}

impl Store {
	fn print_ops(&self) -> SqlResult<()> {
		let ops: Vec<Op> = self.query_all(Op::SORT_SQL)?;

		if ops.is_empty() {
			println!("no ops");
			return Ok(());
		}

		println!("  origin   ( previous ) | target:   action");
		for Op { origin, previous, target, action } in ops {
			if let Some(p) = previous {
				println!("{origin} ({p}) | {target}: {action:?}");
			}
			else {
				println!("{origin} (        / ) | {target}: {action:?}");
			}
		}
		Ok(())
	}
}

#[test]
fn converge() -> SqlResult<()> {
	let (node_a, name_a) = Node::create(NodeID::random(), "A".to_string());
	let (node_b, name_b) = Node::create(NodeID::random(), "B".to_string());
	let (node_c, name_c) = Node::create(NodeID::random(), "C".to_string());

	let active_c = Op {
		origin: node_c.next_origin(),
		previous: Some(name_c.origin),
		target: node_c.0,
		action: Action::Active(true)
	};
	let rename_b = Op {
		origin: node_b.next_origin(),
		previous: Some(active_c.origin),
		target: node_b.0,
		action: Action::Name(String::from("B2"))
	};

	let mut ops = vec![
		name_a,
		name_b,
		name_c,
	];
	ops.push(active_c);
	ops.push(rename_b);

	let store = Store::create_in_memory()?;
	//let store = Store::init("/tmp/store.db".as_ref())?;

	for op in ops.clone() {
		//println!("Absorbing: {op:?}");
		store.absorb_op(op)?;
	}
	store.print_ops()?;
	let hash = store.hash()?;
	println!("Hash: {hash:016x}");

	let list = store.get_all::<Conspirator>()?;
	println!("{list:?}");

	for node in [&node_a, &node_b, &node_c] {
		assert!(list.iter().any(|c| c.id == node.0));
	}
	let check_list = |other: &[Conspirator]| {
		assert_eq!(other.len(), 3);
		for node in [&node_a, &node_b, &node_c] {
			assert_eq!(
				list.iter().find(|c| c.id == node.0).unwrap(),
				other.iter().find(|c| c.id == node.0).unwrap()
			);
		}
	};

	let store2 = Store::create_in_memory()?;
	for op in ops.clone() {
		store2.absorb_op(op)?;
	}

	let list2 = store2.get_all::<Conspirator>()?;
	assert_eq!(list, list2);
	check_list(&list2);
	let hash2 = store2.hash()?;
	assert_eq!(hash, hash2);

	for permutation in Permuter::<5>::new() {
		let permuted_store = Store::create_in_memory()?;
		for idx in permutation {
			permuted_store.absorb_op(ops[idx].clone())?;
		}
		let permuted_list = permuted_store.get_all::<Conspirator>()?;
		let permuted_hash = permuted_store.hash()?;
		//println!("{permutation:?}: {permuted_list:?}");
		println!("{permutation:?}: {permuted_hash:016x}");
		check_list(&permuted_list);
		if hash != permuted_hash {
			permuted_store.print_ops()?;
		}
		assert_eq!(hash, permuted_hash);
	}

	let double_store = Store::create_in_memory()?;
	for op in ops.clone() {
		double_store.absorb_op(op)?;
	}
	for op in ops.clone() {
		double_store.absorb_op(op)?;
	}
	let double_hash = double_store.hash()?;
	assert_eq!(hash, double_hash);
	let double_list = double_store.get_all::<Conspirator>()?;
	check_list(&double_list);

	Ok(())
}

#[test]
fn sync() -> SqlResult<()> {
	let store = Store::create_in_memory()?;
	let id = NodeID::random();
	let id_2 = NodeID::random();

	let op = store.create_op(id, id, Action::Active(true))?;
	store.absorb_op(op.clone())?;

	assert!(store.sync(&[])?.contains(&op));
	assert!(store.sync(&[Version::new(id, 0)])?.is_empty());
	assert!(store.sync(&[Version::new(id, 1)])?.is_empty());
	assert!(store.sync(&[Version::new(id_2, 3)])?.contains(&op));

	let op_2 = store.create_op(id_2, id_2, Action::Active(true))?;
	store.absorb_op(op_2.clone())?;

	let ops = store.sync(&[])?;
	assert!(ops.contains(&op));
	assert!(ops.contains(&op_2));

	Ok(())
}

#[test]
fn hold() -> SqlResult<()> {
	let store_a = Store::create_in_memory()?;
	let store_b = Store::create_in_memory()?;
	let id_a = NodeID::random();
	let id_b = NodeID::random();

	// test that ops are held until their referenced op is available
	// A/0 : A active
	let op_a = store_a.create_op(id_a, id_a, Action::Active(true))?;
	store_a.absorb_op(op_a.clone())?;
	// B/0 → A/0 : B active
	let op_b = store_a.create_op(id_b, id_b, Action::Active(true))?;

	// should store the op but be unable to sort it (since op_a is missing)
	store_b.absorb_op(op_b.clone())?;

	assert_eq!(store_b.previous()?, None);
	assert_eq!(store_b.node_counter(id_b)?, None);
	assert_eq!(store_b.node_counter(id_a)?, None);

	// now that op_a is available, op_b should get sorted as well
	store_b.absorb_op(op_a.clone())?;
	assert_eq!(store_b.previous()?, Some(Version { node: id_b, counter: 0 }));
	assert_eq!(store_b.node_counter(id_a)?, Some(0));
	assert_eq!(store_b.node_counter(id_b)?, Some(0));

	Ok(())
}