namespace BP
{	
	const float PLAYER_INITIAL_MAX_HEALTH = 200.0f;
	const float PLAYER_INITIAL_DAMAGE = 1.0f;
	const float PLAYER_INITIAL_SPEED = 0.6f;
	const float PLAYER_INITIAL_POWERTIME = 1.0f;
	const int PLAYER_INITIAL_MAX_AIR_JUMPS = 1;
	const float PLAYER_AIR_JUMP_VELOCITY = 10.0f;
	const float PLAYER_GROUND_POUND_HEIGHT = 300.0f;
	const float PLAYER_GROUND_POUND_VELOCITY = -60.0f;
	const float PLAYER_TAKE_DAMAGE_SCALE = 1.5f;

	class Player : ScriptObject
	{
		kActor @self;

		int lastWeaponID = -1;
		int teamID;
		
		BP::Ailments::AilmentMan ailments;
		
		//current stats
		float speed = PLAYER_INITIAL_SPEED; //current speed
		float damage = PLAYER_INITIAL_DAMAGE; // current damage
		float powerTime = PLAYER_INITIAL_POWERTIME; // current powerTime
		
		Dash groundDash;
		Dash airDash;
		
		// int maxAirJumps = PLAYER_INITIAL_MAX_AIR_JUMPS; //Serialize
		int airJumpsDone;
		bool wasUnderWater; //Serialize
		bool wasGrounded; //Serialize
		bool airDashEnabled; //Serialize
		bool groundPound; //Serialize
		bool lastNoWeapon; //Serialize
		bool isGrappling;
		kVec3 grappleStartPoint;
		kVec3 grappleEndPoint;
		float grappleTime;
		int grappleRegion;
		
		bool hasTicked;
		bool wasDeserialized;
		//------------------------------------------------------------------------------------------------------------------------
		Player(kActor @actor)
		{
			@self = actor;
			@BP::Trigger::scriptResult = null;
			@BP::LocalPlayer::Actor = @self;
			@BP::LocalPlayer::Script = @this;
			@ailments.onChange = BP::Ailments::AilmentChangeCallBack(SetStats);
		}
		//------------------------------------------------------------------------------------------------------------------------
		void OnSerialize(kDict& out dict)
		{
			SERIALIZE_INT(teamID);
			SERIALIZE_INT(airJumpsDone);
			SERIALIZE_BOOL(wasGrounded);
			SERIALIZE_BOOL(airDashEnabled);
			SERIALIZE_BOOL(wasUnderWater);
			SERIALIZE_BOOL(groundPound);
			SERIALIZE_BOOL(lastNoWeapon);
			groundDash.Serialize(dict, "groundDash");
			airDash.Serialize(dict, "airDash");
			ailments.OnSerialize(dict);
			BP::Game::OnSerialize(dict);
		}
		//------------------------------------------------------------------------------------------------------------------------
		void OnDeserialize(kDict& in dict)
		{
			DESERIALIZE_INT(teamID);
			DESERIALIZE_INT(airJumpsDone);
			DESERIALIZE_BOOL(wasGrounded);
			DESERIALIZE_BOOL(airDashEnabled);
			DESERIALIZE_BOOL(wasUnderWater);
			DESERIALIZE_BOOL(groundPound);
			DESERIALIZE_BOOL(lastNoWeapon);
			groundDash.Deserialize(dict, "groundDash");
			airDash.Deserialize(dict, "airDash");
			ailments.OnDeserialize(dict);
			BP::Game::OnDeserialize(dict);
		}
		//------------------------------------------------------------------------------------------------------------------------
		void OnSpawn()
		{
			if (self.Deserialized())
			{
				wasDeserialized = true;
			}
			else
			{
				//load map actor if map has one
				int mapID = Game.ActiveMapID();
				if (mapID != -1)
				{
					kIndexDefManager iDefManager;
					iDefManager.LoadFile("defs/mapInfo.txt");
					kDictMem@ dictMem = iDefManager.GetEntry(mapID);
					if (@dictMem != null)
					{
						int mapActorID;
						if (dictMem.GetInt("ActorID", mapActorID))
						{
							BP::Spawn::Actor(mapActorID, Math::vecZero);
						}
					}
				}
				
				BP::Game::OnStart();
			}
			
			SetStats();
			//if not dead and Health <= 0.0f then set health to 1.0f
			if ((self.Flags() & AF_DEAD) == 0 && self.Health() <= 0.0f)
			{
				//since only 1 life kick to title screen to prevent 255 lives cheat
				//self.Health() = 1.0f;
				CinemaPlayer.StartCinema("cinemas/bp/totitle.txt");
				return;
			}
		}
		//------------------------------------------------------------------------------------------------------------------------
		void OnPreDamage(kDamageInfo& in dmgInfo)
		{
			dmgInfo.hits = dmgInfo.hits * PLAYER_TAKE_DAMAGE_SCALE;
		}
		//------------------------------------------------------------------------------------------------------------------------
		void OnDamage(kDamageInfo& in dmgInfo)
		{
			//if hit by enemy
			kActor @src = dmgInfo.source;
			if (@src != null && src.EnemyAIComponent() !is null)
			{
				BP::LocalPlayer::PlayHurtTaunt();
			}
		}
		//------------------------------------------------------------------------------------------------------------------------
		void OnDeath(kDamageInfo& in dmgInfo)
		{
			ailments.RemoveAll();
			BP::LocalPlayer::PlayDeathTaunt();
		}
		//------------------------------------------------------------------------------------------------------------------------
		void OnAIKIll(kActor@ actor, kDamageInfo& in dmgInfo)
		{
			BP::LocalPlayer::PlayKillTaunt();
		}
		//------------------------------------------------------------------------------------------------------------------------
		BP::Ailments::Ailment@ AddAilment(int type)
		{
			BP::Ailments::Ailment @ailment = ailments.Add(type);
			return @ailment;
		}
		//------------------------------------------------------------------------------------------------------------------------
		bool OnPickup(kActor@ actorPickup)
		{
			switch (actorPickup.Type())
			{
				case kActor_Pickup_Regen:
				{
					AddAilment(BP::Ailments::TYPE_REGEN);
					// BP::LocalPlayer::ScreenFlash(blue: 255.0f, alpha: 128.0f, sustainTime: 15.0f * 14, decayTime: 15.0f);
					break;
				}
				case kActor_Pickup_Quad:
				{
					AddAilment(BP::Ailments::TYPE_QUAD);
					// BP::LocalPlayer::ScreenFlash(red: 255.0f, alpha: 128.0f, sustainTime: 15.0f * 14, decayTime: 15.0f);
					break;
				}
				case kActor_Pickup_Speed:
				{
					AddAilment(BP::Ailments::TYPE_HASTE);
					// BP::LocalPlayer::ScreenFlash(red: 128.0f, blue: 128.0f, alpha: 128.0f, sustainTime: 15.0f * 14, decayTime: 15.0f);
					break;
				}
				case kActor_BPHealth2:
				{
					if (BP::LocalPlayer::HasFullHealth())
					{
						return false;
					}
					else
					{
						BP::LocalPlayer::GiveHealth(2.0f);
					}
					break;
				}
				case kActor_BPHealth10:
				{
					if (BP::LocalPlayer::HasFullHealth())
					{
						return false;
					}
					else
					{
						BP::LocalPlayer::GiveHealth(10.0f);
					}
					break;
				}
				case kActor_BPHealthFull:
				{
					if (BP::LocalPlayer::HasFullHealth())
					{
						return false;
					}
					else
					{
						BP::LocalPlayer::GiveHealth(50.0f);
					}
					break;
				}
				case kActor_BPHealthUltra:
				{
					if (BP::LocalPlayer::HasFullHealth())
					{
						return false;
					}
					else
					{
						Game.PlayVoice(341); //ultra Health
						BP::LocalPlayer::GiveHealth(100.0f);
					}
					break;
				}
				case kActor_Inventory_AirJump:
				{
					Game.PlayVoice(333); //Planeswalker
					BP::Inventory::Give(kActor_Inventory_AirJump, 1);
					break;
				}
				case kActor_MaxHealth:
				{
					Game.PlayVoice(510); //Health Granted
					BP::Game::playerMaxHealth += 10;
					BP::LocalPlayer::GiveHealth(10.0f);
					break;
				}
				case kActor_DamageUp:
				{
					BP::Game::playerDamageBase += 0.05f;
					break;
				}
				case kActor_SpeedUp:
				{
					BP::Game::playerSpeedBase += 0.005f;
					break;
				}
				case kActor_Mag60Upgrade:
				{
					BP::Game::playerMag60Upgrade = true;
					break;
				}
				case kActor_GroundPoundUpgrade:
				{
					BP::Inventory::Give(kActor_GroundPoundUpgrade, 1);
					break;
				}
				case kActor_RazorWindUpgrade:
				{
					BP::Inventory::Give(kActor_RazorWindUpgrade, 1);
					break;
				}
			}
			
			return true;
		}
		//------------------------------------------------------------------------------------------------------------------------
		void SetStats()
		{
			damage = BP::Game::playerDamageBase + ailments.DamageBonus();
			speed = BP::Game::playerSpeedBase + ailments.SpeedBonus();
			powerTime = BP::Game::playerPowerTimeBase;
			
			self.MovementComponent().Friction() = speed;
			self.MovementComponent().AirFriction() = speed;
			
			//DebugLog("damage: " + damage + " speed: " + speed + " powerTime: " + powerTime);
		}
		//------------------------------------------------------------------------------------------------------------------------
		void TryAirJump()
		{
			//climbing, or in water, or grappling
			if ((self.WorldComponent().Flags() & (WCF_AT_CLIMBING_SURFACE | WCF_UNDER_WATER | WCF_ON_WATER)) != 0 || isGrappling)
			{
				return;
			}
			
			bool doingJump = (LocalPlayer.Actor().PlayerFlags() & PF_JUMPING) != 0;
			bool hasJumped = (LocalPlayer.Actor().PlayerFlags() & PF_HASJUMPED) != 0;
			if (!BP::Actor::IsGrounded(@self) && !doingJump && hasJumped && airJumpsDone < BP::Game::playerMaxAirJumps)
			{
				kVec3 vel = self.MovementComponent().Velocity();
				self.MovementComponent().Velocity() = kVec3(vel.x, vel.y, PLAYER_AIR_JUMP_VELOCITY);
				self.PlaySound(kSfx_TurokJump);
				airJumpsDone++;
			}
		}
		//------------------------------------------------------------------------------------------------------------------------
		void TryGroundPound()
		{
			//climbing, or in water, or grappling
			if ((self.WorldComponent().Flags() & (WCF_AT_CLIMBING_SURFACE | WCF_UNDER_WATER | WCF_ON_WATER)) != 0 || isGrappling ||
				(BP::Inventory::Has(kActor_GroundPoundUpgrade)))
			{
				return;
			}
			
			if (!groundPound && !BP::Actor::IsGrounded(self) && self.Origin().z >= self.WorldComponent().FloorHeight() + PLAYER_GROUND_POUND_HEIGHT)
			{
				groundPound = true;
				LocalPlayer.Actor().PlayerFlags() |= PF_HASJUMPED;
				airDashEnabled = false;
				self.MovementComponent().Velocity() = kVec3(0.0f, 0.0f, PLAYER_GROUND_POUND_VELOCITY);
			}
		}
		//------------------------------------------------------------------------------------------------------------------------
		void TryDash(float yawOffset)
		{
			//climbing, or in water, or grappling
			if ((self.WorldComponent().Flags() & (WCF_AT_CLIMBING_SURFACE | WCF_UNDER_WATER | WCF_ON_WATER)) != 0 || isGrappling)
			{
				return;
			}
				
			if (BP::Actor::IsGrounded(self))
			{
				if (airDash.cooldown <= 0.0f)
				{
					bool didGroundDash = groundDash.TryInput(@self, yawOffset);
					if (didGroundDash)
					{
						self.PlaySound(kSfx_TurokDash);
					}
				}
			}
			else
			{
				//airDash allowed and grounddash not on cooldown and not in water
				if (airDashEnabled && groundDash.cooldown <= 0.0f)
				{
					bool didAirDash = airDash.TryInput(@self, yawOffset);
					if (didAirDash)
					{
						self.PlaySound(kSfx_TurokAirDash);
						airDashEnabled = false;
					}
				}
			}
		}
		//------------------------------------------------------------------------------------------------------------------------
		void OnGroundedChanged(bool isGrounded)
		{
			if (isGrounded)
			{
				if (groundPound)
				{
					groundPound = false;
					BP::Spawn::Particle("particles/BP/TurokGroundPound.particle", @self, self.Origin() + kVec3(0.0f, 0.0f, 20.0f));
					SpawnShake(kVec3(0, 0, 125.0f), BP::LocalPlayer::Actor.Origin(), 10000, true);
				}
				
				airJumpsDone = 0;
				airDashEnabled = true;
			}
		}
		//------------------------------------------------------------------------------------------------------------------------
		void OnUnderWaterChanged(bool isUnderWater)
		{
			if (groundPound)
			{
				groundPound = false;
			}
		}
		//------------------------------------------------------------------------------------------------------------------------
		void OnTick(void)
		{
			if (!hasTicked)
			{
				hasTicked = true;
				if (wasDeserialized)
				{
					BP::Game::OnDeserializeStart();
				}
			}

			BP::Game::OnTick();
			ailments.OnTick();
			groundDash.OnTick(@self);
			airDash.OnTick(@self);
			
			bool isUnderWater = (self.WorldComponent().Flags() & WCF_UNDER_WATER) != 0;
			if (wasUnderWater != isUnderWater)
			{
				OnUnderWaterChanged(isUnderWater);
				wasUnderWater = isUnderWater;
			}
			
			bool isGrounded = BP::Actor::IsGrounded(@self);
			if (wasGrounded != isGrounded)
			{
				OnGroundedChanged(isGrounded);
				wasGrounded = isGrounded;
			}
			
			int currentWeaponID = LocalPlayer.CurrentWeaponID();
			if (lastWeaponID != currentWeaponID)
			{
				lastWeaponID = currentWeaponID;
				BP::Weapon::Scripts[currentWeaponID].OnEquipped();
			}
		
			//update all weapons
			for (int i = 0; i < MAX_WEAPONS; i++)
			{
				BP::Weapon::Scripts[i].OnGameTick();
			}

			if (BP::LocalPlayer::IsMovementLocked())
			{
				self.MovementComponent().Flags() |= MCF_SLIDING;
				LocalPlayer.Actor().PlayerFlags() |= PF_HASJUMPED;
			}
			
			bool isNoWeapon = BP::LocalPlayer::IsNoWeapon();
			if (lastNoWeapon != isNoWeapon)
			{
				lastNoWeapon = isNoWeapon;
				if (isNoWeapon)
				{
					LocalPlayer.Actor().PlayerFlags() |= (1 << 21); //PF_NOWEAPON
				}
				else
				{
					LocalPlayer.Actor().PlayerFlags() &= ~(1 << 21); //PF_NOWEAPON
				}
			}
			
			if (BP::Input::ForwardDown())
			{
				TryDash(0.0f);
			}
			else if (BP::Input::BackwardDown())
			{
				TryDash(Math::pi);
			}
			
			if (BP::Input::StrafeLeftDown())
			{
				TryDash(-Math::piHalf);
			}
			else if (BP::Input::StrafeRightDown())
			{
				TryDash(Math::piHalf);
			}
			
			if (BP::Input::JumpDown())
			{
				TryAirJump();
			}
			else if (BP::Input::CrouchDown())
			{
				TryGroundPound();
			}
			
		}
		//------------------------------------------------------------------------------------------------------------------------
	};
}
