const VALVE_SCALE:Planar64=Planar64::raw(1<<28);// 1/16

use crate::integer::{int,vec3::int as int3,AbsoluteTime,Ratio64,Planar64,Planar64Vec3};
use crate::controls_bitflag::Controls;
use crate::physics::Time as PhysicsTime;

#[derive(Clone,Debug)]
pub struct StyleModifiers{
	//controls which are allowed to pass into gameplay (usually all)
	pub controls_mask:Controls,
	//controls which are masked from control state (e.g. !jump in scroll style)
	pub controls_mask_state:Controls,
	//strafing
	pub strafe:Option<StrafeSettings>,
	//player gets a controllable rocket force
	pub rocket:Option<PropulsionSettings>,
	//flying
	//pub move_type:MoveType::Fly(FlySettings)
	//MoveType::Physics(PhysicsSettings) -> PhysicsSettings (strafe,rocket,jump,walk,ladder,swim,gravity)
	//jumping is allowed
	pub jump:Option<JumpSettings>,
	//standing & walking is allowed
	pub walk:Option<WalkSettings>,
	//laddering is allowed
	pub ladder:Option<LadderSettings>,
	//water propulsion
	pub swim:Option<PropulsionSettings>,
	//maximum slope before sloped surfaces become frictionless
	pub gravity:Planar64Vec3,
	//hitbox
	pub hitbox:Hitbox,
	//camera location relative to the center (0,0,0) of the hitbox
	pub camera_offset:Planar64Vec3,
	//unused
	pub mass:Planar64,
}
impl Default for StyleModifiers{
	fn default()->Self{
		Self::roblox_bhop()
	}
}

#[derive(Clone,Debug)]
pub enum JumpCalculation{
	Max,//Roblox: jumped_speed=max(velocity.boost(),velocity.jump())
	BoostThenJump,//jumped_speed=velocity.boost().jump()
	JumpThenBoost,//jumped_speed=velocity.jump().boost()
}

#[derive(Clone,Debug)]
pub enum JumpImpulse{
	Time(AbsoluteTime),//jump time is invariant across mass and gravity changes
	Height(Planar64),//jump height is invariant across mass and gravity changes
	Linear(Planar64),//jump velocity is invariant across mass and gravity changes
	Energy(Planar64),// :)
}
//Jumping acts on dot(walks_state.normal,body.velocity)
//Energy means it adds energy
//Linear means it linearly adds on
impl JumpImpulse{
	pub fn jump(
		&self,
		velocity:Planar64Vec3,
		jump_dir:Planar64Vec3,
		gravity:&Planar64Vec3,
		_mass:Planar64,
	)->Planar64Vec3{
		match self{
			&JumpImpulse::Time(time)=>velocity-(*gravity*time).map(|t|t.divide().clamp_1()),
			&JumpImpulse::Height(height)=>{
				//height==-v.y*v.y/(2*g.y);
				//use energy to determine max height
				let gg=gravity.length_squared();
				let g=gg.sqrt();
				let v_g=gravity.dot(velocity);
				//do it backwards
				let radicand=v_g*v_g+(g*height*2).widen_4();
				velocity-(*gravity*(radicand.sqrt().wrap_2()+v_g)/gg).divide().clamp_1()
			},
			&JumpImpulse::Linear(jump_speed)=>velocity+(jump_dir*jump_speed/jump_dir.length()).divide().clamp_1(),
			&JumpImpulse::Energy(_energy)=>{
				//calculate energy
				//let e=gravity.dot(velocity);
				//add
				//you get the idea
				todo!()
			},
		}
	}
	//TODO: remove this and implement JumpCalculation properly
	pub fn get_jump_deltav(&self,gravity:&Planar64Vec3,mass:Planar64)->Planar64{
		//gravity.length() is actually the proper calculation because the jump is always opposite the gravity direction
		match self{
			&JumpImpulse::Time(time)=>(gravity.length().wrap_1()*time/2).divide().clamp_1(),
			&JumpImpulse::Height(height)=>(gravity.length()*height*2).sqrt().wrap_1(),
			&JumpImpulse::Linear(deltav)=>deltav,
			&JumpImpulse::Energy(energy)=>(energy.sqrt()*2/mass.sqrt()).divide().clamp_1(),
		}
	}
}

#[derive(Clone,Debug)]
pub struct JumpSettings{
	//information used to calculate jump power
	pub impulse:JumpImpulse,
	//information used to calculate jump behaviour
	pub calculation:JumpCalculation,
	//limit the minimum jump power when combined with downwards momentum
	//This is true in both roblox and source
	pub limit_minimum:bool,
}
impl JumpSettings{
	pub fn jumped_velocity(
		&self,
		style:&StyleModifiers,
		jump_dir:Planar64Vec3,
		rel_velocity:Planar64Vec3,
		booster:Option<&crate::gameplay_attributes::Booster>,
	)->Planar64Vec3{
		let jump_speed=self.impulse.get_jump_deltav(&style.gravity,style.mass);
		match (self.limit_minimum,&self.calculation){
			(true,JumpCalculation::Max)=>{
				//the roblox calculation
				let boost_vel=match booster{
					Some(booster)=>booster.boost(rel_velocity),
					None=>rel_velocity,
				};
				let j=boost_vel.dot(jump_dir);
				let js=jump_speed.widen_2();
				if j<js{
					//weak booster: just do a regular jump
					boost_vel+jump_dir.with_length(js-j).divide().wrap_1()
				}else{
					//activate booster normally, jump does nothing
					boost_vel
				}
			},
			(true,_)=>{
				//the source calculation (?)
				let boost_vel=match booster{
					Some(booster)=>booster.boost(rel_velocity),
					None=>rel_velocity,
				};
				let j=boost_vel.dot(jump_dir);
				let js=jump_speed.widen_2();
				if j<js{
					//speed in direction of jump cannot be lower than amount
					boost_vel+jump_dir.with_length(js-j).divide().wrap_1()
				}else{
					//boost and jump add together
					boost_vel+jump_dir.with_length(js).divide().wrap_1()
				}
			}
			(false,JumpCalculation::Max)=>{
				//??? calculation
				//max(boost_vel,jump_vel)
				let boost_vel=match booster{
					Some(booster)=>booster.boost(rel_velocity),
					None=>rel_velocity,
				};
				let boost_dot=boost_vel.dot(jump_dir);
				let js=jump_speed.widen_2();
				if boost_dot<js{
					//weak boost is extended to jump speed
					boost_vel+jump_dir.with_length(js-boost_dot).divide().wrap_1()
				}else{
					//activate booster normally, jump does nothing
					boost_vel
				}
			},
			//the strafe client calculation
			(false,_)=>{
				let boost_vel=match booster{
					Some(booster)=>booster.boost(rel_velocity),
					None=>rel_velocity,
				};
				boost_vel+jump_dir.with_length(jump_speed).divide().wrap_1()
			},
		}
	}
}

#[derive(Clone,Debug)]
pub struct ControlsActivation{
	//allowed keys
	pub controls_mask:Controls,
	//allow strafing only if any of the masked controls are held, eg W|S for shsw
	pub controls_intersects:Controls,
	//allow strafing only if all of the masked controls are held, eg W for hsw, w-only
	pub controls_contains:Controls,
	//Function(Box<dyn Fn(u32)->bool>),
}
impl ControlsActivation{
	pub const fn mask(&self,controls:Controls)->Controls{
		controls.intersection(self.controls_mask)
	}
	pub const fn activates(&self,controls:Controls)->bool{
		(self.controls_intersects.is_empty()||controls.intersects(self.controls_intersects))
		&&controls.contains(self.controls_contains)
	}
	pub const fn full_3d()->Self{
		Self{
			controls_mask:Controls::WASDQE,
			controls_intersects:Controls::WASDQE,
			controls_contains:Controls::empty(),
		}
	}
	//classical styles
	//Normal
	pub const fn full_2d()->Self{
		Self{
			controls_mask:Controls::WASD,
			controls_intersects:Controls::WASD,
			controls_contains:Controls::empty(),
		}
	}
	//Sideways
	pub const fn sideways()->Self{
		Self{
			controls_mask:Controls::MoveForward.union(Controls::MoveBackward),
			controls_intersects:Controls::MoveForward.union(Controls::MoveBackward),
			controls_contains:Controls::empty(),
		}
	}
	//Half-Sideways
	pub const fn half_sideways()->Self{
		Self{
			controls_mask:Controls::MoveForward.union(Controls::MoveLeft).union(Controls::MoveRight),
			controls_intersects:Controls::MoveLeft.union(Controls::MoveRight),
			controls_contains:Controls::MoveForward,
		}
	}
	//Surf Half-Sideways
	pub const fn surf_half_sideways()->Self{
		Self{
			controls_mask:Controls::MoveForward.union(Controls::MoveBackward).union(Controls::MoveLeft).union(Controls::MoveRight),
			controls_intersects:Controls::MoveForward.union(Controls::MoveBackward),
			controls_contains:Controls::empty(),
		}
	}
	//W-Only
	pub const fn w_only()->Self{
		Self{
			controls_mask:Controls::MoveForward,
			controls_intersects:Controls::empty(),
			controls_contains:Controls::MoveForward,
		}
	}
	//A-Only
	pub const fn a_only()->Self{
		Self{
			controls_mask:Controls::MoveLeft,
			controls_intersects:Controls::empty(),
			controls_contains:Controls::MoveLeft,
		}
	}
	//Backwards
}

#[derive(Clone,Debug)]
pub struct StrafeSettings{
	pub enable:ControlsActivation,
	pub mv:Planar64,
	pub air_accel_limit:Option<Planar64>,
	pub tick_rate:Ratio64,
}
impl StrafeSettings{
	pub fn tick_velocity(&self,velocity:Planar64Vec3,control_dir:Planar64Vec3)->Option<Planar64Vec3>{
		let d=velocity.dot(control_dir);
		let mv=self.mv.widen_2();
		match d<mv{
			true=>Some(velocity+(control_dir*self.air_accel_limit.map_or(mv-d,|limit|limit.widen_2().min(mv-d))).wrap_1()),
			false=>None,
		}
	}
	pub fn next_tick(&self,time:PhysicsTime)->PhysicsTime{
		PhysicsTime::from_nanos(self.tick_rate.rhs_div_int(self.tick_rate.mul_int(time.nanos())+1))
	}
	pub const fn activates(&self,controls:Controls)->bool{
		self.enable.activates(controls)
	}
	pub const fn mask(&self,controls:Controls)->Controls{
		self.enable.mask(controls)
	}
}

#[derive(Clone,Debug)]
pub struct PropulsionSettings{
	pub magnitude:Planar64,
}
impl PropulsionSettings{
	pub fn acceleration(&self,control_dir:Planar64Vec3)->Planar64Vec3{
		(control_dir*self.magnitude).clamp_1()
	}
}

#[derive(Clone,Debug)]
pub struct AccelerateSettings{
	pub accel:Planar64,
	pub topspeed:Planar64,
}
#[derive(Clone,Debug)]
pub struct WalkSettings{
	pub accelerate:AccelerateSettings,
	pub static_friction:Planar64,
	pub kinetic_friction:Planar64,
	//if a surf slope angle does not exist, then everything is slippery and walking is impossible
	pub surf_dot:Planar64,//surf_dot<n.dot(up)/n.length()
}
impl WalkSettings{
	pub fn accel(&self,target_diff:Planar64Vec3,gravity:Planar64Vec3)->Planar64{
		//TODO: fallible walk accel
		let diff_len=target_diff.length().wrap_1();
		let friction=if diff_len<self.accelerate.topspeed{
			self.static_friction
		}else{
			self.kinetic_friction
		};
		self.accelerate.accel.min((-gravity.y*friction).clamp_1())
	}
	pub fn get_walk_target_velocity(&self,control_dir:Planar64Vec3,normal:Planar64Vec3)->Planar64Vec3{
		if control_dir==crate::integer::vec3::zero(){
			return control_dir;
		}
		let nn=normal.length_squared();
		let mm=control_dir.length_squared();
		let nnmm=nn*mm;
		let d=normal.dot(control_dir);
		let dd=d*d;
		if dd<nnmm{
			let cr=normal.cross(control_dir);
			if cr==crate::integer::vec3::zero(){
				crate::integer::vec3::zero()
			}else{
				(cr.cross(normal)*self.accelerate.topspeed/((nn*(nnmm-dd)).sqrt())).divide().clamp_1()
			}
		}else{
			crate::integer::vec3::zero()
		}
	}
	pub fn is_slope_walkable(&self,normal:Planar64Vec3,up:Planar64Vec3)->bool{
		//normal is not guaranteed to be unit length
		let ny=normal.dot(up);
		let h=normal.length().wrap_1();
		//remember this is a normal vector
		ny.is_positive()&&h*self.surf_dot<ny
	}
}

#[derive(Clone,Debug)]
pub struct LadderSettings{
	pub accelerate:AccelerateSettings,
	//how close to pushing directly into/out of the ladder normal
	//does your input need to be to redirect straight up/down the ladder
	pub dot:Planar64,
}
impl LadderSettings{
	pub const fn accel(&self,_target_diff:Planar64Vec3,_gravity:Planar64Vec3)->Planar64{
		//TODO: fallible ladder accel
		self.accelerate.accel
	}
	pub fn get_ladder_target_velocity(&self,mut control_dir:Planar64Vec3,normal:Planar64Vec3)->Planar64Vec3{
		if control_dir==crate::integer::vec3::zero(){
			return control_dir;
		}
		let nn=normal.length_squared();
		let mm=control_dir.length_squared();
		let nnmm=nn*mm;
		let d=normal.dot(control_dir);
		let mut dd=d*d;
		if (self.dot*self.dot*nnmm).clamp_4()<dd{
			if d.is_negative(){
				control_dir=Planar64Vec3::new([Planar64::ZERO,mm.clamp_1(),Planar64::ZERO]);
			}else{
				control_dir=Planar64Vec3::new([Planar64::ZERO,-mm.clamp_1(),Planar64::ZERO]);
			}
			dd=(normal.y*normal.y).widen_4();
		}
		//n=d if you are standing on top of a ladder and press E.
		//two fixes:
		//- ladder movement is not allowed on walkable surfaces
		//- fix the underlying issue
		if dd<nnmm{
			let cr=normal.cross(control_dir);
			if cr==crate::integer::vec3::zero(){
				crate::integer::vec3::zero()
			}else{
				(cr.cross(normal)*self.accelerate.topspeed/((nn*(nnmm-dd)).sqrt())).divide().clamp_1()
			}
		}else{
			crate::integer::vec3::zero()
		}
	}
}

#[derive(Clone,Debug)]
pub enum HitboxMesh{
	Box,//source
	Cylinder,//roblox
	//Sphere,//roblox old physics
	//Point,
	//Line,
	//DualCone,
}

#[derive(Clone,Debug)]
pub struct Hitbox{
	pub halfsize:Planar64Vec3,
	pub mesh:HitboxMesh,
}
impl Hitbox{
	pub fn roblox()->Self{
		Self{
			halfsize:int3(2,5,2)>>1,
			mesh:HitboxMesh::Cylinder,
		}
	}
	pub fn source()->Self{
		Self{
			halfsize:((int3(33,73,33)>>1)*VALVE_SCALE).narrow_1().unwrap(),
			mesh:HitboxMesh::Box,
		}
	}
}

impl StyleModifiers{
	pub const RIGHT_DIR:Planar64Vec3=crate::integer::vec3::X;
	pub const UP_DIR:Planar64Vec3=crate::integer::vec3::Y;
	pub const FORWARD_DIR:Planar64Vec3=crate::integer::vec3::NEG_Z;

	pub fn neo()->Self{
		Self{
			controls_mask:Controls::all(),
			controls_mask_state:Controls::all(),
			strafe:Some(StrafeSettings{
				enable:ControlsActivation::full_2d(),
				air_accel_limit:None,
				mv:int(3),
				tick_rate:Ratio64::new(64,AbsoluteTime::ONE_SECOND.get() as u64).unwrap(),
			}),
			jump:Some(JumpSettings{
				impulse:JumpImpulse::Energy(int(512)),
				calculation:JumpCalculation::JumpThenBoost,
				limit_minimum:false,
			}),
			gravity:int3(0,-80,0),
			mass:int(1),
			rocket:None,
			walk:Some(WalkSettings{
				accelerate:AccelerateSettings{
					topspeed:int(16),
					accel:int(80),
				},
				static_friction:int(2),
				kinetic_friction:int(3),//unrealistic: kinetic friction is typically lower than static
				surf_dot:int(3)/4,
			}),
			ladder:Some(LadderSettings{
				accelerate:AccelerateSettings{
					topspeed:int(16),
					accel:int(160),
				},
				dot:(int(1)/2).sqrt(),
			}),
			swim:Some(PropulsionSettings{
				magnitude:int(12),
			}),
			hitbox:Hitbox::roblox(),
			camera_offset:int3(0,2,0),//4.5-2.5=2
		}
	}

	pub fn roblox_bhop()->Self{
		Self{
			controls_mask:Controls::all(),
			controls_mask_state:Controls::all(),
			strafe:Some(StrafeSettings{
				enable:ControlsActivation::full_2d(),
				air_accel_limit:None,
				mv:int(27)/10,
				tick_rate:Ratio64::new(100,AbsoluteTime::ONE_SECOND.get() as u64).unwrap(),
			}),
			jump:Some(JumpSettings{
				impulse:JumpImpulse::Time(AbsoluteTime::from_micros(715_588)),
				calculation:JumpCalculation::Max,
				limit_minimum:true,
			}),
			gravity:int3(0,-100,0),
			mass:int(1),
			rocket:None,
			walk:Some(WalkSettings{
				accelerate:AccelerateSettings{
					topspeed:int(18),
					accel:int(90),
				},
				static_friction:int(2),
				kinetic_friction:int(3),//unrealistic: kinetic friction is typically lower than static
				surf_dot:int(3)/4,// normal.y=0.75
			}),
			ladder:Some(LadderSettings{
				accelerate:AccelerateSettings{
					topspeed:int(18),
					accel:int(180),
				},
				dot:(int(1)/2).sqrt(),
			}),
			swim:Some(PropulsionSettings{
				magnitude:int(12),
			}),
			hitbox:Hitbox::roblox(),
			camera_offset:int3(0,2,0),//4.5-2.5=2
		}
	}
	pub fn roblox_surf()->Self{
		Self{
			gravity:int3(0,-50,0),
			..Self::roblox_bhop()
		}
	}
	pub fn roblox_rocket()->Self{
		Self{
			strafe:None,
			rocket:Some(PropulsionSettings{
				magnitude:int(200),
			}),
			..Self::roblox_bhop()
		}
	}

	pub fn source_bhop()->Self{
		Self{
			controls_mask:Controls::all(),
			controls_mask_state:Controls::all(),
			strafe:Some(StrafeSettings{
				enable:ControlsActivation::full_2d(),
				air_accel_limit:Some(Planar64::raw((150<<28)*100)),
				mv:Planar64::raw(30<<28),
				tick_rate:Ratio64::new(100,AbsoluteTime::ONE_SECOND.get() as u64).unwrap(),
			}),
			jump:Some(JumpSettings{
				impulse:JumpImpulse::Height((int(52)*VALVE_SCALE).narrow_1().unwrap()),
				calculation:JumpCalculation::JumpThenBoost,
				limit_minimum:true,
			}),
			gravity:(int3(0,-800,0)*VALVE_SCALE).narrow_1().unwrap(),
			mass:int(1),
			rocket:None,
			walk:Some(WalkSettings{
				accelerate:AccelerateSettings{
					topspeed:int(18),//?
					accel:int(90),//?
				},
				static_friction:int(2),//?
				kinetic_friction:int(3),//?
				surf_dot:int(3)/4,// normal.y=0.75
			}),
			ladder:Some(LadderSettings{
				accelerate:AccelerateSettings{
					topspeed:int(18),//?
					accel:int(180),//?
				},
				dot:(int(1)/2).sqrt(),//?
			}),
			swim:Some(PropulsionSettings{
				magnitude:int(12),//?
			}),
			hitbox:Hitbox::source(),
			camera_offset:((int3(0,64,0)-(int3(0,73,0)>>1))*VALVE_SCALE).narrow_1().unwrap(),
		}
	}
	pub fn source_surf()->Self{
		Self{
			controls_mask:Controls::all(),
			controls_mask_state:Controls::all(),
			strafe:Some(StrafeSettings{
				enable:ControlsActivation::full_2d(),
				air_accel_limit:Some((int(150)*66*VALVE_SCALE).narrow_1().unwrap()),
				mv:Planar64::raw(30<<28),
				tick_rate:Ratio64::new(66,AbsoluteTime::ONE_SECOND.get() as u64).unwrap(),
			}),
			jump:Some(JumpSettings{
				impulse:JumpImpulse::Height((int(52)*VALVE_SCALE).narrow_1().unwrap()),
				calculation:JumpCalculation::JumpThenBoost,
				limit_minimum:true,
			}),
			gravity:(int3(0,-800,0)*VALVE_SCALE).narrow_1().unwrap(),
			mass:int(1),
			rocket:None,
			walk:Some(WalkSettings{
				accelerate:AccelerateSettings{
					topspeed:int(18),//?
					accel:int(90),//?
				},
				static_friction:int(2),//?
				kinetic_friction:int(3),//?
				surf_dot:int(3)/4,// normal.y=0.75
			}),
			ladder:Some(LadderSettings{
				accelerate:AccelerateSettings{
					topspeed:int(18),//?
					accel:int(180),//?
				},
				dot:(int(1)/2).sqrt(),//?
			}),
			swim:Some(PropulsionSettings{
				magnitude:int(12),//?
			}),
			hitbox:Hitbox::source(),
			camera_offset:((int3(0,64,0)-(int3(0,73,0)>>1))*VALVE_SCALE).narrow_1().unwrap(),
		}
	}
}
