use std::collections::HashMap;

use strafesnet_common::gameplay_modes::{ModeId,StageId};
use strafesnet_common::instruction::{InstructionConsumer,InstructionEmitter,InstructionFeedback,TimedInstruction};
// session represents the non-hardware state of the client.
// Ideally it is a deterministic state which is atomically updated by instructions, same as the simulation state.
use strafesnet_common::physics::{
	ModeInstruction,MiscInstruction,
	Instruction as PhysicsInputInstruction,
	TimeInner as PhysicsTimeInner,
	Time as PhysicsTime
};
use strafesnet_common::timer::{Scaled,Timer};
use strafesnet_common::session::{TimeInner as SessionTimeInner,Time as SessionTime};
use strafesnet_settings::directories::Directories;

use crate::mouse_interpolator::{MouseInterpolator,StepInstruction,Instruction as MouseInterpolatorInstruction};
use strafesnet_physics::physics::{self,PhysicsContext,PhysicsData};
use strafesnet_settings::settings::UserSettings;

pub enum Instruction<'a>{
	Input(SessionInputInstruction),
	Control(SessionControlInstruction),
	Playback(SessionPlaybackInstruction),
	ChangeMap(&'a strafesnet_common::map::CompleteMap),
	LoadReplay(strafesnet_snf::bot::Segment),
	Idle,
}

pub enum SessionInputInstruction{
	Mouse(glam::IVec2),
	SetControl(strafesnet_common::physics::SetControlInstruction),
	Mode(ImplicitModeInstruction),
	Misc(strafesnet_common::physics::MiscInstruction),
}
/// Implicit mode instruction are fed separately to session.
/// Session generates the explicit mode instructions interlaced with a SetSensitivity instruction
#[derive(Clone,Debug)]
pub enum ImplicitModeInstruction{
	ResetAndRestart,
	ResetAndSpawn(ModeId,StageId),
}

pub enum SessionControlInstruction{
	SetPaused(bool),
	// copy the current session simulation recording into a replay and view it
	CopyRecordingIntoReplayAndSpectate,
	StopSpectate,
	SaveReplay,
	LoadIntoReplayState,
}
pub enum SessionPlaybackInstruction{
	SkipForward,
	SkipBack,
	TogglePaused,
	DecreaseTimescale,
	IncreaseTimescale,
}

pub struct FrameState{
	pub body:physics::Body,
	pub camera:physics::PhysicsCamera,
	pub time:PhysicsTime,
}

pub struct Simulation{
	timer:Timer<Scaled<SessionTimeInner,PhysicsTimeInner>>,
	physics:physics::PhysicsState,
}
impl Simulation{
	pub const fn new(
		timer:Timer<Scaled<SessionTimeInner,PhysicsTimeInner>>,
		physics:physics::PhysicsState,
	)->Self{
		Self{
			timer,
			physics,
		}
	}
	pub fn get_frame_state(&self,time:SessionTime)->FrameState{
		FrameState{
			body:self.physics.camera_body(),
			camera:self.physics.camera(),
			time:self.timer.time(time),
		}
	}
}

#[derive(Default)]
pub struct Recording{
	instructions:Vec<TimedInstruction<PhysicsInputInstruction,PhysicsTime>>,
}
impl Recording{
	pub fn new(
		instructions:Vec<TimedInstruction<PhysicsInputInstruction,PhysicsTime>>,
	)->Self{
		Self{instructions}
	}
	fn clear(&mut self){
		self.instructions.clear();
	}
}
pub struct Replay{
	next_instruction_id:usize,
	recording:Recording,
	simulation:Simulation,
}
impl Replay{
	pub const fn new(
		recording:Recording,
		simulation:Simulation,
	)->Self{
		Self{
			next_instruction_id:0,
			recording,
			simulation,
		}
	}
	pub fn advance(&mut self,physics_data:&PhysicsData,time_limit:SessionTime){
		let mut time=self.simulation.timer.time(time_limit);
		loop{
			if let Some(ins)=self.recording.instructions.get(self.next_instruction_id){
				if ins.time<time{
					PhysicsContext::run_input_instruction(&mut self.simulation.physics,physics_data,ins.clone());
					self.next_instruction_id+=1;
				}else{
					break;
				}
			}else{
				// loop playback
				self.next_instruction_id=0;
				// No need to reset physics because the very first instruction is 'Reset'
				let new_time=self.recording.instructions.first().map_or(PhysicsTime::ZERO,|ins|ins.time);
				self.simulation.timer.set_time(time_limit,new_time);
				time=new_time;
			}
		}
	}
}

#[derive(Clone,Copy,Hash,PartialEq,Eq)]
struct BotId(u32);
//#[derive(Clone,Copy,Hash,PartialEq,Eq)]
//struct PlayerId(u32);

enum ViewState{
	Play,
	//Spectate(PlayerId),
	Replay(BotId),
}

pub struct Session{
	directories:Directories,
	user_settings:UserSettings,
	mouse_interpolator:crate::mouse_interpolator::MouseInterpolator,
	view_state:ViewState,
	//gui:GuiState
	geometry_shared:physics::PhysicsData,
	simulation:Simulation,
	// below fields not included in lite session
	recording:Recording,
	//players:HashMap<PlayerId,Simulation>,
	replays:HashMap<BotId,Replay>,
}
impl Session{
	pub fn new(
		user_settings:UserSettings,
		directories:Directories,
		simulation:Simulation,
	)->Self{
		Self{
			user_settings,
			directories,
			mouse_interpolator:MouseInterpolator::new(),
			geometry_shared:Default::default(),
			simulation,
			view_state:ViewState::Play,
			recording:Default::default(),
			replays:HashMap::new(),
		}
	}
	fn clear_recording(&mut self){
		self.recording.clear();
	}
	fn change_map(&mut self,map:&strafesnet_common::map::CompleteMap){
		self.simulation.physics.clear();
		self.geometry_shared.generate_models(map);
	}
	pub fn get_frame_state(&self,time:SessionTime)->Option<FrameState>{
		match &self.view_state{
			ViewState::Play=>Some(self.simulation.get_frame_state(time)),
			ViewState::Replay(bot_id)=>self.replays.get(bot_id).map(|replay|
				replay.simulation.get_frame_state(time)
			),
		}
	}
	pub fn user_settings(&self)->&UserSettings{
		&self.user_settings
	}
}

// mouseinterpolator consumes RawInputInstruction
// mouseinterpolator emits PhysicsInputInstruction
// mouseinterpolator consumes DoStep to move on to the next emitted instruction
// Session comsumes SessionInstruction -> forwards RawInputInstruction to mouseinterpolator
// Session consumes DoStep -> forwards DoStep to mouseinterpolator
// Session emits DoStep

impl InstructionConsumer<Instruction<'_>> for Session{
	type Time=SessionTime;
	fn process_instruction(&mut self,ins:TimedInstruction<Instruction,Self::Time>){
		// repetitive procedure macro
		macro_rules! run_mouse_interpolator_instruction{
			($instruction:expr)=>{
				self.mouse_interpolator.process_instruction(TimedInstruction{
					time:ins.time,
					instruction:TimedInstruction{
						time:self.simulation.timer.time(ins.time),
						instruction:$instruction,
					},
				});
			};
		}

		// process any timeouts that occured since the last instruction
		self.process_exhaustive(ins.time);

		match ins.instruction{
			// send it down to MouseInterpolator with two timestamps, SessionTime and PhysicsTime
			Instruction::Input(SessionInputInstruction::Mouse(pos))=>{
				run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::MoveMouse(pos));
			},
			Instruction::Input(SessionInputInstruction::SetControl(set_control_instruction))=>{
				run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::SetControl(set_control_instruction));
			},
			Instruction::Input(SessionInputInstruction::Mode(ImplicitModeInstruction::ResetAndRestart))=>{
				self.clear_recording();
				let mode_id=self.simulation.physics.mode();
				run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Mode(ModeInstruction::Reset));
				run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Misc(MiscInstruction::SetSensitivity(self.user_settings().calculate_sensitivity())));
				run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Mode(ModeInstruction::Restart(mode_id)));
			},
			Instruction::Input(SessionInputInstruction::Mode(ImplicitModeInstruction::ResetAndSpawn(mode_id,spawn_id)))=>{
				self.clear_recording();
				run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Mode(ModeInstruction::Reset));
				run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Misc(MiscInstruction::SetSensitivity(self.user_settings().calculate_sensitivity())));
				run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Mode(ModeInstruction::Spawn(mode_id,spawn_id)));
			},
			Instruction::Input(SessionInputInstruction::Misc(misc_instruction))=>{
				run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Misc(misc_instruction));
			},
			Instruction::Control(SessionControlInstruction::SetPaused(paused))=>{
				// don't flush the buffered instructions in the mouse interpolator
				// until the mouse is confirmed to be not moving at a later time
				// what if they pause for 5ms lmao
				_=self.simulation.timer.set_paused(ins.time,paused);
			},
			Instruction::Control(SessionControlInstruction::CopyRecordingIntoReplayAndSpectate)=> if let ViewState::Play=self.view_state{
				// Bind: B

				// pause simulation
				_=self.simulation.timer.set_paused(ins.time,true);

				// create recording
				let mut recording=Recording::default();
				recording.instructions.extend(self.recording.instructions.iter().cloned());

				// create timer starting at first instruction (or zero if the list is empty)
				let new_time=recording.instructions.first().map_or(PhysicsTime::ZERO,|ins|ins.time);
				let timer=Timer::unpaused(ins.time,new_time);

				// create default physics state
				let simulation=Simulation::new(timer,Default::default());

				// invent a new bot id and insert the replay
				let bot_id=BotId(self.replays.len() as u32);
				self.replays.insert(bot_id,Replay::new(
					recording,
					simulation,
				));

				// begin spectate
				self.view_state=ViewState::Replay(bot_id);
			},
			Instruction::Control(SessionControlInstruction::StopSpectate)=>{
				let view_state=core::mem::replace(&mut self.view_state,ViewState::Play);
				// delete the bot, otherwise it's inaccessible and wastes CPU
				match view_state{
					ViewState::Play=>(),
					ViewState::Replay(bot_id)=>{
						self.replays.remove(&bot_id);
					},
				}
				_=self.simulation.timer.set_paused(ins.time,false);
			},
			Instruction::Control(SessionControlInstruction::SaveReplay)=>{
				// Bind: N
				let view_state=core::mem::replace(&mut self.view_state,ViewState::Play);
				match view_state{
					ViewState::Play=>(),
					ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.remove(&bot_id){
						let mut replays_path=self.directories.replays.clone();
						let file_name=format!("{}.snfb",ins.time);
						std::thread::spawn(move ||{
							std::fs::create_dir_all(replays_path.as_path()).unwrap();
							replays_path.push(file_name);
							let file=std::fs::File::create(replays_path).unwrap();
							strafesnet_snf::bot::write_bot(
								std::io::BufWriter::new(file),
								strafesnet_physics::VERSION.get(),
								replay.recording.instructions
							).unwrap();
							println!("Finished writing bot file!");
						});
					},
				}
				_=self.simulation.timer.set_paused(ins.time,false);
			},
			Instruction::Control(SessionControlInstruction::LoadIntoReplayState)=>{
				// Bind: J
				let view_state=core::mem::replace(&mut self.view_state,ViewState::Play);
				match view_state{
					ViewState::Play=>(),
					ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.remove(&bot_id){
						self.recording.instructions=replay.recording.instructions.into_iter().take(replay.next_instruction_id).collect();
						self.simulation=replay.simulation;
					},
				}
				// don't unpause -- use the replay timer state whether it is pasued or unpaused
			},
			Instruction::Playback(SessionPlaybackInstruction::IncreaseTimescale)=>{
				match &self.view_state{
					ViewState::Play=>{
						// allow simulation timescale for fun
						let scale=self.simulation.timer.get_scale();
						self.simulation.timer.set_scale(ins.time,strafesnet_common::integer::Ratio64::new(scale.num()*5,scale.den()*4).unwrap());
					},
					ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.get_mut(bot_id){
						let scale=replay.simulation.timer.get_scale();
						replay.simulation.timer.set_scale(ins.time,strafesnet_common::integer::Ratio64::new(scale.num()*5,scale.den()*4).unwrap());
					},
				}
			},
			Instruction::Playback(SessionPlaybackInstruction::DecreaseTimescale)=>{
				match &self.view_state{
					ViewState::Play=>{
						// allow simulation timescale for fun
						let scale=self.simulation.timer.get_scale();
						self.simulation.timer.set_scale(ins.time,strafesnet_common::integer::Ratio64::new(scale.num()*4,scale.den()*5).unwrap());
					},
					ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.get_mut(bot_id){
						let scale=replay.simulation.timer.get_scale();
						replay.simulation.timer.set_scale(ins.time,strafesnet_common::integer::Ratio64::new(scale.num()*4,scale.den()*5).unwrap());
					},
				}
			},
			Instruction::Playback(SessionPlaybackInstruction::SkipForward)=>{
				match &self.view_state{
					ViewState::Play=>(),
					ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.get_mut(bot_id){
						let time=replay.simulation.timer.time(ins.time+SessionTime::from_secs(5));
						replay.simulation.timer.set_time(ins.time,time);
					},
				}
			},
			Instruction::Playback(SessionPlaybackInstruction::SkipBack)=>{
				match &self.view_state{
					ViewState::Play=>(),
					ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.get_mut(bot_id){
						let time=replay.simulation.timer.time(ins.time+SessionTime::from_secs(5));
						replay.simulation.timer.set_time(ins.time,time);
						// resimulate the entire playback lol
						replay.next_instruction_id=0;
					},
				}
			},
			Instruction::Playback(SessionPlaybackInstruction::TogglePaused)=>{
				match &self.view_state{
					ViewState::Play=>(),
					ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.get_mut(bot_id){
						_=replay.simulation.timer.set_paused(ins.time,!replay.simulation.timer.is_paused());
					},
				}
			}
			Instruction::ChangeMap(complete_map)=>{
				self.clear_recording();
				self.change_map(complete_map);
			},
			Instruction::LoadReplay(bot)=>{
				// pause simulation
				_=self.simulation.timer.set_paused(ins.time,true);

				// create recording
				let recording=Recording::new(bot.instructions);

				// create timer starting at first instruction (or zero if the list is empty)
				let new_time=recording.instructions.first().map_or(PhysicsTime::ZERO,|ins|ins.time);
				let timer=Timer::unpaused(ins.time,new_time);

				// create default physics state
				let simulation=Simulation::new(timer,Default::default());

				// invent a new bot id and insert the replay
				let bot_id=BotId(self.replays.len() as u32);
				self.replays.insert(bot_id,Replay::new(
					recording,
					simulation,
				));

				// begin spectate
				self.view_state=ViewState::Replay(bot_id);
			},
			Instruction::Idle=>{
				run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Idle);
				// this just refreshes the replays
				for replay in self.replays.values_mut(){
					// TODO: filter idles from recording, inject new idles in real time
					replay.advance(&self.geometry_shared,ins.time);
				}
			}
		};

		// process all emitted output instructions
		self.process_exhaustive(ins.time);
	}
}
impl InstructionConsumer<StepInstruction> for Session{
	type Time=SessionTime;
	fn process_instruction(&mut self,ins:TimedInstruction<StepInstruction,Self::Time>){
		let time=self.simulation.timer.time(ins.time);
		if let Some(instruction)=self.mouse_interpolator.pop_buffered_instruction(ins.set_time(time)){
			//record
			self.recording.instructions.push(instruction.clone());
			PhysicsContext::run_input_instruction(&mut self.simulation.physics,&self.geometry_shared,instruction);
		}
	}
}
impl InstructionEmitter<StepInstruction> for Session{
	type Time=SessionTime;
	fn next_instruction(&self,time_limit:SessionTime)->Option<TimedInstruction<StepInstruction,Self::Time>>{
		self.mouse_interpolator.next_instruction(time_limit)
	}
}