mod error;
mod util;

#[cfg(test)]
mod tests;
#[cfg(test)]
mod test_scenes;

use std::time::Instant;

use error::ReplayError;
use util::read_entire_file;
use strafesnet_physics::physics::{PhysicsData,PhysicsState,PhysicsContext};

fn main(){
	let arg=std::env::args().skip(1).next();
	match arg.as_deref(){
		Some("determinism")|None=>test_determinism().unwrap(),
		Some("replay")=>run_replay().unwrap(),
		_=>println!("invalid argument"),
	}
}

fn run_replay()->Result<(),ReplayError>{
	println!("loading map file..");
	let data=read_entire_file("../tools/bhop_maps/5692113331.snfm")?;
	let map=strafesnet_snf::read_map(data)?.into_complete_map()?;

	println!("loading bot file..");
	let data=read_entire_file("../tools/replays/535s+159764769ns.snfb")?;
	let bot=strafesnet_snf::read_bot(data)?.read_all()?;

	// create recording
	println!("generating models..");
	let physics_data=PhysicsData::new(&map);
	println!("simulating...");
	let mut physics=PhysicsState::default();
	for ins in bot.instructions{
		PhysicsContext::run_input_instruction(&mut physics,&physics_data,ins);
	}
	match physics.get_finish_time(){
		Some(time)=>println!("finish time:{}",time),
		None=>println!("simulation did not end in finished state"),
	}

	Ok(())
}
enum DeterminismResult{
	Deterministic,
	NonDeterministic,
}
struct SegmentResult{
	determinism:DeterminismResult,
	ticks:u64,
	nanoseconds:u64,
}
fn segment_determinism(bot:strafesnet_snf::bot::Segment,physics_data:&PhysicsData)->SegmentResult{
	// create default physics state
	let mut physics_deterministic=PhysicsState::default();
	// create a second physics state
	let mut physics_filtered=PhysicsState::default();

	// invent a new bot id and insert the replay
	println!("simulating...");

	let mut non_idle_count=0;
	let instruction_count=bot.instructions.len();

	let start=Instant::now();

	for (i,ins) in bot.instructions.into_iter().enumerate(){
		let state_deterministic=physics_deterministic.clone();
		let state_filtered=physics_filtered.clone();
		PhysicsContext::run_input_instruction(&mut physics_deterministic,physics_data,ins.clone());
		match ins{
			strafesnet_common::instruction::TimedInstruction{instruction:strafesnet_common::physics::Instruction::Idle,..}=>(),
			other=>{
				non_idle_count+=1;
				// run
				PhysicsContext::run_input_instruction(&mut physics_filtered,physics_data,other.clone());
				// check if position matches
				let b0=physics_deterministic.camera_trajectory(physics_data);
				let b1=physics_filtered.camera_trajectory(physics_data);
				if b0.position!=b1.position{
					let nanoseconds=start.elapsed().as_nanos() as u64;
					println!("desync at instruction #{}",i);
					println!("non idle instructions completed={non_idle_count}");
					println!("instruction #{i}={:?}",other);
					println!("deterministic state0:\n{state_deterministic:?}");
					println!("filtered state0:\n{state_filtered:?}");
					println!("deterministic state1:\n{:?}",physics_deterministic);
					println!("filtered state1:\n{:?}",physics_filtered);
					return SegmentResult{
						determinism:DeterminismResult::NonDeterministic,
						ticks:1+i as u64+non_idle_count,
						nanoseconds,
					};
				}
			},
		}
	}
	let nanoseconds=start.elapsed().as_nanos() as u64;
	match physics_deterministic.get_finish_time(){
		Some(time)=>println!("[with idle] finish time:{}",time),
		None=>println!("[with idle] simulation did not end in finished state"),
	}
	match physics_filtered.get_finish_time(){
		Some(time)=>println!("[filtered] finish time:{}",time),
		None=>println!("[filtered] simulation did not end in finished state"),
	}
	SegmentResult{
		determinism:DeterminismResult::Deterministic,
		ticks:instruction_count as u64+non_idle_count,
		nanoseconds,
	}

}
type ThreadResult=Result<Option<SegmentResult>,ReplayError>;
fn read_and_run(file_path:std::path::PathBuf,physics_data:&PhysicsData)->ThreadResult{
	let data=read_entire_file(file_path.as_path())?;
	let bot=strafesnet_snf::read_bot(data)?.read_all()?;
	println!("Running {:?}",file_path.file_stem());
	Ok(Some(segment_determinism(bot,physics_data)))
}
fn do_thread<'a>(s:&'a std::thread::Scope<'a,'_>,file_path:std::path::PathBuf,send:std::sync::mpsc::Sender<ThreadResult>,physics_data:&'a PhysicsData){
	s.spawn(move ||{
		let result=read_and_run(file_path,physics_data);
		// send when thread is complete
		send.send(result).unwrap();
	});
}
fn get_file_path(dir_entry:std::fs::DirEntry)->Result<Option<std::path::PathBuf>,std::io::Error>{
	Ok(dir_entry.file_type()?.is_file().then_some(
		dir_entry.path()
	))
}
fn test_determinism()->Result<(),ReplayError>{
	let thread_limit=std::thread::available_parallelism()?.get();
	println!("loading map file..");
	let data=read_entire_file("../tools/bhop_maps/5692113331.snfm")?;
	let map=strafesnet_snf::read_map(data)?.into_complete_map()?;

	println!("generating models..");
	let physics_data=PhysicsData::new(&map);

	let (send,recv)=std::sync::mpsc::channel();

	let mut read_dir=std::fs::read_dir("../tools/replays")?;

	// promise that &physics_data will outlive the spawned threads
	let thread_results=std::thread::scope(|s|{
		let mut thread_results=Vec::new();

		// spawn threads
		println!("spawning up to {thread_limit} threads...");
		let mut active_thread_count=0;
		while active_thread_count<thread_limit{
			if let Some(dir_entry_result)=read_dir.next(){
				if let Some(file_path)=get_file_path(dir_entry_result?)?{
					active_thread_count+=1;
					do_thread(s,file_path,send.clone(),&physics_data);
				}
			}else{
				break;
			}
		}

		// spawn another thread every time a message is received from the channel
		println!("riding parallelism wave...");
		while let Some(dir_entry_result)=read_dir.next(){
			if let Some(file_path)=get_file_path(dir_entry_result?)?{
				// wait for a thread to complete
				thread_results.push(recv.recv().unwrap());
				do_thread(s,file_path,send.clone(),&physics_data);
			}
		}

		// wait for remaining threads to complete
		println!("waiting for all threads to complete...");
		for _ in 0..active_thread_count{
			thread_results.push(recv.recv().unwrap());
		}

		println!("done.");
		Ok::<_,ReplayError>(thread_results)
	})?;

	// tally results
	#[derive(Default)]
	struct Totals{
		deterministic:u32,
		nondeterministic:u32,
		invalid:u32,
		error:u32,
	}
	let mut nanoseconds=0;
	let mut ticks=0;
	let Totals{deterministic,nondeterministic,invalid,error}=thread_results.into_iter().fold(Totals::default(),|mut totals,result|{
		match result{
			Ok(Some(segment_result))=>{
				ticks+=segment_result.ticks;
				nanoseconds+=segment_result.nanoseconds;
				match segment_result.determinism{
					DeterminismResult::Deterministic=>totals.deterministic+=1,
					DeterminismResult::NonDeterministic=>totals.nondeterministic+=1,
				}
			},
			Ok(None)=>totals.invalid+=1,
			Err(_)=>totals.error+=1,
		}
		totals
	});

	println!("deterministic={deterministic}");
	println!("nondeterministic={nondeterministic}");
	println!("invalid={invalid}");
	println!("error={error}");
	println!("average ticks/s per core: {}",ticks*1_000_000_000/nanoseconds);

	assert!(nondeterministic==0);
	assert!(invalid==0);
	assert!(error==0);

	Ok(())
}
