use strafesnet_common::mouse::MouseState;
use strafesnet_common::physics::{
	MouseInstruction,SetControlInstruction,ModeInstruction,MiscInstruction,
	Instruction as PhysicsInstruction,
	TimeInner as PhysicsTimeInner,
	Time as PhysicsTime,
};
use strafesnet_common::session::Time as SessionTime;
use strafesnet_common::instruction::{InstructionConsumer,InstructionEmitter,TimedInstruction};

type TimedSelfInstruction=TimedInstruction<Instruction,PhysicsTime>;
type DoubleTimedSelfInstruction=TimedInstruction<TimedSelfInstruction,SessionTime>;

type TimedPhysicsInstruction=TimedInstruction<PhysicsInstruction,PhysicsTime>;

const MOUSE_TIMEOUT:SessionTime=SessionTime::from_millis(10);

/// To be fed into MouseInterpolator
#[derive(Clone,Debug)]
pub(crate) enum Instruction{
	MoveMouse(glam::IVec2),
	SetControl(SetControlInstruction),
	Mode(ModeInstruction),
	Misc(MiscInstruction),
	Idle,
}
#[derive(Clone,Debug)]
enum UnbufferedInstruction{
	MoveMouse(glam::IVec2),
	NonMouse(NonMouseInstruction),
}
#[derive(Clone,Debug)]
enum BufferedInstruction{
	Mouse(MouseInstruction),
	NonMouse(NonMouseInstruction),
}
#[derive(Clone,Debug)]
pub(crate) enum NonMouseInstruction{
	SetControl(SetControlInstruction),
	Mode(ModeInstruction),
	Misc(MiscInstruction),
	Idle,
}
impl From<Instruction> for UnbufferedInstruction{
	#[inline]
	fn from(value:Instruction)->Self{
		match value{
			Instruction::MoveMouse(mouse_instruction)=>UnbufferedInstruction::MoveMouse(mouse_instruction),
			Instruction::SetControl(set_control_instruction)=>UnbufferedInstruction::NonMouse(NonMouseInstruction::SetControl(set_control_instruction)),
			Instruction::Mode(mode_instruction)=>UnbufferedInstruction::NonMouse(NonMouseInstruction::Mode(mode_instruction)),
			Instruction::Misc(misc_instruction)=>UnbufferedInstruction::NonMouse(NonMouseInstruction::Misc(misc_instruction)),
			Instruction::Idle=>UnbufferedInstruction::NonMouse(NonMouseInstruction::Idle),
		}
	}
}
impl From<BufferedInstruction> for PhysicsInstruction{
	#[inline]
	fn from(value:BufferedInstruction)->Self{
		match value{
			BufferedInstruction::Mouse(mouse_instruction)=>PhysicsInstruction::Mouse(mouse_instruction),
			BufferedInstruction::NonMouse(non_mouse_instruction)=>match non_mouse_instruction{
				NonMouseInstruction::SetControl(set_control_instruction)=>PhysicsInstruction::SetControl(set_control_instruction),
				NonMouseInstruction::Mode(mode_instruction)=>PhysicsInstruction::Mode(mode_instruction),
				NonMouseInstruction::Misc(misc_instruction)=>PhysicsInstruction::Misc(misc_instruction),
				NonMouseInstruction::Idle=>PhysicsInstruction::Idle,
			},
		}
	}
}

pub(crate) enum StepInstruction{
	Pop,
	Timeout,
}

#[derive(Clone,Debug)]
enum BufferState{
	Unbuffered,
	Initializing(SessionTime,MouseState<PhysicsTimeInner>),
	Buffered(SessionTime,MouseState<PhysicsTimeInner>),
}

pub struct MouseInterpolator{
	buffer_state:BufferState,
	// double timestamped timeline?
	buffer:std::collections::VecDeque<TimedPhysicsInstruction>,
	output:std::collections::VecDeque<TimedPhysicsInstruction>,
}
// Maybe MouseInterpolator manipulation is better expressed using impls
// and called from Instruction trait impls in session
impl InstructionConsumer<TimedSelfInstruction> for MouseInterpolator{
	type Time=SessionTime;
	fn process_instruction(&mut self,ins:DoubleTimedSelfInstruction){
		self.push_unbuffered_input(ins.time,ins.instruction.time,ins.instruction.instruction.into())
	}
}
impl InstructionEmitter<StepInstruction> for MouseInterpolator{
	type Time=SessionTime;
	fn next_instruction(&self,time_limit:SessionTime)->Option<TimedInstruction<StepInstruction,Self::Time>>{
		self.buffered_instruction_with_timeout(time_limit)
	}
}
impl MouseInterpolator{
	pub fn new()->MouseInterpolator{
		MouseInterpolator{
			buffer_state:BufferState::Unbuffered,
			buffer:std::collections::VecDeque::new(),
			output:std::collections::VecDeque::new(),
		}
	}
	fn push_mouse_and_flush_buffer(&mut self,ins:TimedInstruction<MouseInstruction,PhysicsTime>){
		self.buffer.push_front(TimedInstruction{
			time:ins.time,
			instruction:BufferedInstruction::Mouse(ins.instruction).into(),
		});
		// flush buffer to output
		if self.output.len()==0{
			// swap buffers
			core::mem::swap(&mut self.buffer,&mut self.output);
		}else{
			// append buffer contents to output
			self.output.append(&mut self.buffer);
		}
	}
	fn get_mouse_timedout_at(&self,time_limit:SessionTime)->Option<SessionTime>{
		match &self.buffer_state{
			BufferState::Unbuffered=>None,
			BufferState::Initializing(time,_mouse_state)
			|BufferState::Buffered(time,_mouse_state)=>{
				let timeout=*time+MOUSE_TIMEOUT;
				(timeout<time_limit).then_some(timeout)
			}
		}
	}
	fn timeout_mouse(&mut self,timeout_time:PhysicsTime){
		// the state always changes to unbuffered
		let buffer_state=core::mem::replace(&mut self.buffer_state,BufferState::Unbuffered);
		match buffer_state{
			BufferState::Unbuffered=>(),
			BufferState::Initializing(_time,mouse_state)=>{
				// only a single mouse move was sent in 10ms, this is very much an edge case!
				self.push_mouse_and_flush_buffer(TimedInstruction{
					time:mouse_state.time,
					instruction:MouseInstruction::ReplaceMouse{
						m1:MouseState{pos:mouse_state.pos,time:timeout_time},
						m0:mouse_state,
					},
				});
			}
			BufferState::Buffered(_time,mouse_state)=>{
				// duplicate the currently buffered mouse state but at a later (future, from the physics perspective) time
				self.push_mouse_and_flush_buffer(TimedInstruction{
					time:mouse_state.time,
					instruction:MouseInstruction::SetNextMouse(MouseState{pos:mouse_state.pos,time:timeout_time}),
				});
			},
		}
	}
	fn push_unbuffered_input(&mut self,session_time:SessionTime,physics_time:PhysicsTime,ins:UnbufferedInstruction){
		// new input
		// if there is zero instruction buffered, it means the mouse is not moving
		// case 1: unbuffered
		// no mouse event is buffered
		// - ins is mouse event? change to buffered
		// - ins other -> write to timeline
		// case 2: buffered
		// a mouse event is buffered, and exists within the last 10ms
		// case 3: stop
		// a mouse event is buffered, but no mouse events have transpired within 10ms

		// replace_with allows the enum variant to safely be replaced
		// from behind a mutable reference, but a panic in the closure means that
		// the entire program terminates rather than completing an unwind.
		let (ins_mouse,ins_other)=replace_with::replace_with_or_abort_and_return(&mut self.buffer_state,|buffer_state|{
			match ins{
				UnbufferedInstruction::MoveMouse(pos)=>{
					let next_mouse_state=MouseState{pos,time:physics_time};
					match buffer_state{
						BufferState::Unbuffered=>{
							((None,None),BufferState::Initializing(session_time,next_mouse_state))
						},
						BufferState::Initializing(_time,mouse_state)=>{
							let ins_mouse=TimedInstruction{
								time:mouse_state.time,
								instruction:MouseInstruction::ReplaceMouse{
									m0:mouse_state,
									m1:next_mouse_state.clone(),
								},
							};
							((Some(ins_mouse),None),BufferState::Buffered(session_time,next_mouse_state))
						},
						BufferState::Buffered(_time,mouse_state)=>{
							let ins_mouse=TimedInstruction{
								time:mouse_state.time,
								instruction:MouseInstruction::SetNextMouse(next_mouse_state.clone()),
							};
							((Some(ins_mouse),None),BufferState::Buffered(session_time,next_mouse_state))
						},
					}
				},
				UnbufferedInstruction::NonMouse(other_instruction)=>((None,Some(TimedInstruction{
					time:physics_time,
					instruction:other_instruction,
				})),buffer_state),
			}
		});
		if let Some(ins)=ins_mouse{
			self.push_mouse_and_flush_buffer(ins);
		}
		if let Some(ins)=ins_other{
			let instruction=TimedInstruction{
				time:ins.time,
				instruction:BufferedInstruction::NonMouse(ins.instruction).into(),
			};
			if matches!(self.buffer_state,BufferState::Unbuffered){
				self.output.push_back(instruction);
			}else{
				self.buffer.push_back(instruction);
			}
		}
	}
	fn buffered_instruction_with_timeout(&self,time_limit:SessionTime)->Option<TimedInstruction<StepInstruction,SessionTime>>{
		match self.get_mouse_timedout_at(time_limit){
			Some(timeout)=>Some(TimedInstruction{
				time:timeout,
				instruction:StepInstruction::Timeout,
			}),
			None=>(self.output.len()!=0).then_some(TimedInstruction{
				// this timestamp should not matter
				time:time_limit,
				instruction:StepInstruction::Pop,
			}),
		}
	}
	pub fn pop_buffered_instruction(&mut self,ins:TimedInstruction<StepInstruction,PhysicsTime>)->Option<TimedPhysicsInstruction>{
		match ins.instruction{
			StepInstruction::Pop=>(),
			StepInstruction::Timeout=>self.timeout_mouse(ins.time),
		}
		self.output.pop_front()
	}
}

#[cfg(test)]
mod test{
	use super::*;
	use strafesnet_common::session::TimeInner as SessionTimeInner;
	#[test]
	fn test(){
		let mut interpolator=MouseInterpolator::new();

		let timer=strafesnet_common::timer::Timer::<strafesnet_common::timer::Scaled<SessionTimeInner,PhysicsTimeInner>>::unpaused(SessionTime::ZERO,PhysicsTime::from_secs(1000));

		macro_rules! push{
			($time:expr,$ins:expr)=>{
				println!("in={:?}",$ins);
				interpolator.push_unbuffered_input(
					$time,
					timer.time($time),
					$ins,
				);
				while let Some(ins)=interpolator.buffered_instruction_with_timeout($time){
					let ins_retimed=TimedInstruction{
						time:timer.time(ins.time),
						instruction:ins.instruction,
					};
					let out=interpolator.pop_buffered_instruction(ins_retimed);
					println!("out={out:?}");
				}
			};
		}

		// test each buffer_state transition
		let mut t=SessionTime::ZERO;
		push!(t,UnbufferedInstruction::MoveMouse(glam::ivec2(0,0)));
		t+=SessionTime::from_millis(5);
		push!(t,UnbufferedInstruction::MoveMouse(glam::ivec2(0,0)));
		t+=SessionTime::from_millis(5);
		push!(t,UnbufferedInstruction::MoveMouse(glam::ivec2(0,0)));
		t+=SessionTime::from_millis(1);
	}
}
