use opengl_rust::{
	prelude::*,
	physics::{
		PhysicsBody,
		Triangle,
	},
};

use crate::{
	LoadedLevel,
	VirtualGamepad,
};

#[derive (Clone, Default)]
pub struct LogicState {
	pub player: PhysicsBody,
	player_jump_vec: Option <Vec3>,
}

#[derive (Clone, Copy, Default)]
pub struct LogicStepOutput {
	pub reset_level: bool,
}

impl LogicState {
	pub fn reset_level (&mut self, level: &LoadedLevel) {
		self.player = Default::default ();
		self.player.pos = level.player_spawn;
	}
	
	pub fn step (&mut self, phys_tris: &[Triangle], phys_params: &opengl_rust::physics::Params, p_gp: VirtualGamepad) -> LogicStepOutput 
	{
		let player_acc = 1.0;
		let player_acc_air = 0.125;
		let player_max_speed = 4.0;
		let player_max_speed_air = 8.0;
		let player_jump_speed = 12.0;
		let kill_z = -3.0;
		
		let mut output = LogicStepOutput::default ();
		let mut wanted_dir = Vec3::default ();
		
		if p_gp.d_left.any_press () {
			wanted_dir.x -= 1.0;
		}
		if p_gp.d_right.any_press () {
			wanted_dir.x += 1.0;
		}
		if p_gp.d_up.any_press () {
			wanted_dir.y += 1.0;
		}
		if p_gp.d_down.any_press () {
			wanted_dir.y -= 1.0;
		}
		
		let wanted_dir = if wanted_dir.length_squared () >= 1.0 {
			wanted_dir.normalize ()
		}
		else {
			wanted_dir
		};
		
		let wanted_dir = wanted_dir;
		
		let old_vel = self.player.vel;
		let old_vel_2 = old_vel * Vec3::new (1.0, 1.0, 0.0);
		
		let new_vel_2 = match self.player_jump_vec.as_ref () {
			Some (v) => {
				// Ground
				let acc = player_acc * v.z * v.z;
				let wanted_vel = wanted_dir * player_max_speed;
				let diff = wanted_vel - old_vel_2;
				
				if diff.length_squared () < acc * acc {
					// We're near the wanted velocity, so snap to it
					wanted_vel
				}
				else {
					// We're not near the wanted velocity, so accelerate
					old_vel_2 + diff.normalize_or_zero () * acc
				}
			},
			// Air
			None => {
				let proposed_vel = old_vel_2 + wanted_dir * player_acc_air;
				
				if old_vel_2.length_squared () < player_max_speed_air * player_max_speed_air {
					// Air control is normal below player_max_speed
					proposed_vel
				}
				else {
					// If the player's input would push them beyond player_max_speed,
					// apply drag to preserve overall energy
					proposed_vel * old_vel_2.length () / proposed_vel.length ()
				}
			},
		};
		
		self.player.vel = new_vel_2;
		self.player.vel.z = old_vel.z;
		
		// dbg! (self.player.vel);
		
		if p_gp.jump.pressed {
			if let Some (normal) = self.player_jump_vec.clone () {
				self.player.vel.z = 0.0;
				self.player.vel += normal * player_jump_speed;
			}
		}
		
		let phys_result = opengl_rust::physics::step (&phys_params, phys_tris, &[], 0.5, &self.player);
		
		self.player = phys_result.body;
		if self.player.pos.z < kill_z {
			output.reset_level = true;
		}
		
		// tracing::debug! ("player pos: {}", self.player.pos);
		
		self.player_jump_vec = None;
		for normal in &phys_result.normals_hit {
			self.player_jump_vec = Some (match self.player_jump_vec {
				None => *normal,
				Some (old) => {
					if normal.z > old.z {
						*normal
					}
					else {
						old
					}
				},
			});
		}
		
		output
	}
}
