//
// Copyright(C) 2014-2015 Samuel Villarreal
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// DESCRIPTION:
//      Weapon Actions
//

#include "scripts/animations.txt"

#include "scripts/weapons/TurokKnife.txt"
#include "scripts/weapons/TurokBow.txt"
#include "scripts/weapons/TurokPistol.txt"
#include "scripts/weapons/TurokShotgun.txt"
#include "scripts/weapons/TurokAutoShotgun.txt"
#include "scripts/weapons/TurokRifle.txt"
#include "scripts/weapons/TurokPulseRifle.txt"
#include "scripts/weapons/TurokMinigun.txt"
#include "scripts/weapons/TurokAlienRifle.txt"
#include "scripts/weapons/TurokRocketLauncher.txt"
#include "scripts/weapons/TurokFusionCannon.txt"

#include "scripts/Unreal/Unreal_Init.cs"
#include "scripts/Unreal/Unreal_Pickups.cs"
#include "scripts/Unreal/Unreal_Amp.cs"
#include "scripts/Unreal/Unreal_Proj.cs"
#include "scripts/Unreal/uDebug.cs"

enum eUTWeapons
{
	UT_Wep_Hammer   = TW_WEAPON_KNIFE,
	UT_Wep_Ripper   = TW_WEAPON_BOW,
	UT_Wep_Enforcer = TW_WEAPON_PISTOL,
	UT_Wep_Minigun  = TW_WEAPON_MINIGUN,
	UT_Wep_Rifle    = TW_WEAPON_RIFLE,
	UT_Wep_ASMD     = TW_WEAPON_SHOTGUN,
	UT_Wep_Pulse    = TW_WEAPON_PULSERIFLE,
	UT_Wep_Bio      = TW_WEAPON_ALIENGUN,
	UT_Wep_Flak     = TW_WEAPON_ASHOTGUN,
	UT_Wep_Rocket   = TW_WEAPON_MISSILE,
	UT_Wep_Redeemer = TW_WEAPON_CANNON
}

float EnforcerSpread1          = 0.01f;  // UT 0.2
float EnforcerSpread1_Akimbo   = 0.04f;  // UT 0.75
float EnforcerSpread2_Min      = 0.03f;  // UT 0.4
float EnforcerSpread2_Akimbo   = 0.06f;  // UT 1.2
float EnforcerSpread2_Max      = 0.12f;  // UT 3
float EnforcerSpreadInc        = 0.015f; // UT 0.5
float EnforcerSpreadInc_Akimbo = 0.03f;  // UT 1.5
float MinigunSpread1 = 0.01f; // UT 0.2
float MinigunSpread2 = 0.05f; // UT 0.95
float FlakSpread     = 0.06f;

kVec3 EyePos()
{
	kPuppet @src = Player.Actor();
	kVec3 v = src.Origin();
	v.z += src.ViewHeight() + src.LandingViewOffset();
	return v;
}

// kVec3::ToQuat() tends to stick when close to parallel to y-axis
kQuat ToQuat( kVec3&in v )
{
	return kQuat( v.ToYaw(), 0,0,1 ) * kQuat( -v.ToPitch(), 1,0,0 );
}

bool bFire1 = false, bFire2 = false, bFire = false;
float WeaponBobAmp = 0, WeaponBobTime = 0;
float WeaponBobEnv = 0; // 0 = ground, 1 = water, between = transitioning between

//==============================================================================

class TurokWeapon : ScriptObjectWeapon
{
	float BobDamping = 0.96f;

	kWeapon@ self;
	int IdleTick = -1;

	TurokWeapon( kWeapon@ actor )
	{
		@self = actor;
	}

	~TurokWeapon()
	{
	}

	//==========================================================================

	void AnimProperties( int&out first, int&out last, float&out rate, bool&out bLoop )
	{
		first = 0; last = 0; rate = 60; bLoop = false;
	}
	int ModFrame( int v )
	{
		return v;
	}
	bool Refire()
	{
		return false;
	}

	//==========================================================================

	kPuppet@ OwnerP()
	{
		return self.Owner().Actor();
	}
	kActor@ OwnerA()
	{
		return self.Owner().Actor().CastToActor();
	}

	int GetAmmo()
	{
		return self.Owner().GetAmmo( self.Owner().CurrentWeapon() );
	}
/*	bool HasAmmo( int amt=1 )
	{
		return GetAmmo() >= amt;
	}
	bool UseAmmo( int amt )
	{
		if ( !HasAmmo(amt) )
			return false;
		self.Owner().ConsumeAmmo( amt );
		return true;
	}
*/	bool HasAmmo()
	{
		return self.Owner().HasAmmo( self.Owner().CurrentWeapon() );
	}
	void UseAmmo( int amt )
	{
		self.Owner().ConsumeAmmo( amt );
	}

	void BlockFireState()
	{
		// idle anim prevents entering fire state
		if ( !IsPlaying(anim_weaponIdle) )
			self.AnimState().Set( anim_weaponIdle, 4, ANF_LOOP );
	}

	kVec3 FOVOffset( const array<kVec3>&in offsets )
	{
		float FOV = 74;
		kStr str;
		if ( Sys.GetCvarValue( "r_fov", str ) )
			FOV = str.Atof();
		if      ( FOV <=  74 ) return offsets[0].Lerp( offsets[1], (FOV- 47.5f)/( 74.0f- 47.5f) );
		else if ( FOV <=  90 ) return offsets[1].Lerp( offsets[2], (FOV- 74.0f)/( 90.0f- 74.0f) );
		else if ( FOV <= 110 ) return offsets[2].Lerp( offsets[3], (FOV- 90.0f)/(110.0f- 90.0f) );
		else    /*FOV >  110*/ return offsets[3].Lerp( offsets[4], (FOV-110.0f)/(120.0f-110.0f) );
	}
	float FOVOffset( const array<float>&in offsets )
	{
		float FOV = 74;
		kStr str;
		if ( Sys.GetCvarValue( "r_fov", str ) )
			FOV = str.Atof();
		if      ( FOV <=  74 ) return Math::Lerp( offsets[0], offsets[1], (FOV- 47.5f)/( 74.0f- 47.5f) );
		else if ( FOV <=  90 ) return Math::Lerp( offsets[1], offsets[2], (FOV- 74.0f)/( 90.0f- 74.0f) );
		else if ( FOV <= 110 ) return Math::Lerp( offsets[2], offsets[3], (FOV- 90.0f)/(110.0f- 90.0f) );
		else    /*FOV >  110*/ return Math::Lerp( offsets[3], offsets[4], (FOV-110.0f)/(120.0f-110.0f) );
	}

	// check position from eye, rather than from foot
	kVec3 CheckPosition( kVec3 v )
	{
		float r = OwnerP().Radius();
		float h = OwnerP().Height();
		OwnerP().Radius() = OwnerP().Height() = 0;
		kVec3 origin = OwnerP().Origin();
		OwnerP().Origin().z = v.z;
		if ( !OwnerP().CheckPosition(v) )
			v = CModel.InterceptVector();
		OwnerP().Origin() = origin;
		OwnerP().Radius() = r;
		OwnerP().Height() = h;
		return v;
	}

	// offset can't be const because kVec3 * kQuat isn't const
	void SpawnProj( const kStr&in type, kVec3&in offset, const kQuat&in q )
	{
		kVec3 v = CheckPosition( EyePos() + offset * OwnerP().Rotation() );
		Game.SpawnFx( type, OwnerP(), v, q );
	}
	void SpawnProj( const kStr&in type, float x, float y, float z, const kQuat&in q )
	{
		SpawnProj( type, kVec3(x,y,z), q );
	}
	void SpawnProj( const kStr&in type, kVec3&in offset, const kVec3&in dir )
	{
		SpawnProj( type, offset, ToQuat(dir) );
	}
	// velocity versions
	void SpawnProj( const kStr&in type, kVec3&in offset, const kQuat&in q, const kVec3&in vel )
	{
		kVec3 v = CheckPosition( EyePos() + offset * OwnerP().Rotation() );
		Game.SpawnFx( type, OwnerP(), vel, v, q );
	}
	void SpawnProj( const kStr&in type, kVec3&in offset, const kVec3&in dir, const kVec3&in vel )
	{
		SpawnProj( type, offset, ToQuat(dir), vel );
	}
/*
	float UnrealSpread( float spread )
	{
//		return spread / 20;
		return Math::Pow( spread, 0.8f ) / 15;
	}
*/
	//==========================================================================

	void PlayAnim( int ID )
	{
		self.AnimState().Set( ID, 4, 0 );
	}

	int PlayingID()
	{
		return self.AnimState().PlayingID();
	}
	bool IsPlaying( int animID )
	{
		return self.AnimState().PlayingID() == animID;
		// this one returns false if anim ID matches but is paused or stopped
//		return self.AnimState().IsPlaying( animID );
	}
	float PlayTime()
	{
		return self.AnimState().PlayTime();
	}

	void Animate()
	{
		self.AnimState().flags |= ANF_LOOP;
		self.AnimState().flags &= ~ANF_CYCLECOMPLETED;
		int first, last, v;
		float rate;
		bool bLoop;

		AnimProperties( first, last, rate, bLoop );

		switch ( PlayingID() )
		{
			// prevent idle anim from restarting when moving/stopping with generic weapon bob turned off
			case anim_weaponWalk: case anim_weaponRun: case anim_weaponIdle:
				if ( IdleTick == -1 ) IdleTick = self.GameTicks();
				v = first + int( (self.GameTicks() - IdleTick) * rate/60 + 0.5f );
				break;
			default:
				IdleTick = -1;
				v = first + int( PlayTime() * rate + 0.5f );
		}
		v = ModFrame( v );
		if ( bLoop )
			v = first + ( (v-first) % (last-first+1) );
		else if ( v >= last )
		{
			v = last;
			AnimEnd();
		}
		self.ModelVariation() = v;
		RunAnim();
	}
	void AnimEnd()
	{
		// since game can lose track of BC_ATTACK, it won't re-enter fire state on its own in such a case
		// so stay in the fire state and start the fire sequence over ourselves
		// if we know a fire button's pressed and Refire() says we're on our way out of the fire state
		if ( bFire && Refire() && HasAmmo() )
			OnBeginFire();
		else
			self.AnimState().flags |= ANF_CYCLECOMPLETED | ANF_STOPPED;
	}

	void RunAnim()
	{
		kStr generic;
		if ( Sys.GetCvarValue( "g_weaponbobbing", generic ) && generic.Atoi() == 1 )
		{
			self.RenderModel().Offset() = Math::vecZero;
			return;
		}

		float amp = 0;
		bool bSwimming = false;
		if ( !self.Owner().Locked() )
		{
			float max, fric;
			if ( OwnerP().InWater() || (OwnerP().GetWaterLevel() == WLT_BETWEEN && !OwnerP().OnGround()) )
			{
				bSwimming = true;
				amp = OwnerP().Movement().Unit();
				OwnerP().Definition().GetFloat( "player.swimForwardSpeed", max, 8.192f );
				// impulse is 100% of swimForwardSpeed
				// normal is friction * swimForwardSpeed
				// waterFriction does nothing?
				amp = amp > max ? 1.0f : amp / max;
			}
			else if ( OwnerP().OnGround() )
			{
				amp = ( OwnerP().Movement() * kVec3(1,1,0) ).Unit();
				OwnerP().Definition().GetFloat( "player.groundForwardSpeed", max, 12.8f );
				OwnerP().Definition().GetFloat( "friction", fric, 0.5f );
				max *= fric;
				amp = amp > max ? 1.0f : amp / max;
				WeaponBobTime += GAME_DELTA_TIME * (0.3f + 0.7f * amp) / 2;
			}
		}

		// OnTick() gets called twice per tick, unless player is locked, so double blending in that case
		float lerp = self.Owner().Locked() ? 0.2f : 0.1f;
		WeaponBobAmp = Math::Lerp( WeaponBobAmp, amp, lerp );
		WeaponBobEnv = Math::Lerp( WeaponBobEnv, bSwimming ? 1 : 0, lerp / 2 );
		amp = -WeaponBobAmp * 640;

		kVec3 walkBob, swimBob;
		// U1
//		walkBob.x = amp * Math::Sin(  6 * WeaponBobTime ) * 0.65f * (1-BobDamping);
//		walkBob.z = amp * Math::Sin( 12 * WeaponBobTime );
		// UT
		walkBob.x = 0.4f * amp * Math::Sin(  8 * WeaponBobTime ) * (1-BobDamping);
		walkBob.z = 0.3f * amp * Math::Sin( 16 * WeaponBobTime );

//		amp *= 200.0f / 320.0f; // U1
//		amp /= 2; // UT
		amp /= 3;
		float t = self.GameTicks() * GAME_DELTA_TIME;
		swimBob.x = 0.5f * amp * Math::Sin( 4 * t ) * (1-BobDamping);
		swimBob.z = 1.5f * amp * Math::Sin( 8 * t );

		// both U1/UT
		self.RenderModel().Offset() = walkBob.Lerp( swimBob, WeaponBobEnv );
		self.RenderModel().Offset().z *= 1 - (0.45f + 0.55f * BobDamping);
	}
/*
	void AmmoLED( int firstSurf=1 )
	{
		int ammo = GetAmmo();
		for ( int pos=0; pos<3; ++pos )
		{
			int digit = ammo % 10;
			for ( int i=0; i<10; ++i )
				self.RenderModel().HideSection( 0, firstSurf + 10*pos + i, i != digit );
			ammo /= 10;
		}
	}
*/
	void AmmoLED()
	{
		int ammo = GetAmmo();
		for ( int pos=1; pos<=3; ++pos )
		{
			int digit = ammo % 10;
			for ( int i=0; i<10; ++i )
				self.RenderModel().HideSection( pos, i, i != digit );
			ammo /= 10;
		}
	}

	void OnTick()
	{
		if ( Player.Buttons() & BC_ATTACK != 0 || bFire )
		{
			bFire2 = Player.Buttons() & BC_MAPZOOMOUT != 0;
			// also check BC_ATTACK as a fallback for basic functionality if custom binds aren't set up
			bFire1 = Player.Buttons() & BC_MAPZOOMIN != 0 || (Player.Buttons() & BC_ATTACK != 0 && !bFire2);
			// convenience bool for checking if any fire button is pressed
			bFire = bFire1 || bFire2;
		}

		Animate();
		UpdateUDam( self );
		UpdateWeaponSound();
	}

	void OnBeginFire() {}
	void OnFire() {}
	void OnEndFire() {}

	void OnRaise() {}
	void OnLower() {}
	void OnHoldster() {}
}

//==============================================================================
//
//    Turok+ compatibility
//

// as long as this is being included for compatibility, it's actually being used by minigun
// in case Turok+ isn't installed, it's initialized/cleared in Unreal_Init.cs, and updated in TurokWeapon::OnTick()
kActor@ WeaponSound;
void PlayWeaponSound( kStr&in sound )
{
	if ( WeaponSound !is null && !WeaponSound.IsStale() ) WeaponSound.PlaySound( sound );
}
void StopWeaponSound()
{
	if ( WeaponSound !is null && !WeaponSound.IsStale() ) WeaponSound.StopSound();
}
void UpdateWeaponSound()
{
	if ( WeaponSound !is null )
	{
		if ( !WeaponSound.IsStale() ) WeaponSound.Origin() = EyePos();
		else                          @WeaponSound = null;
	}
}

// actually being used by sniper rifle
bool PlayerScoped()
{
	return Camera.Active() && !Player.Locked();
}
void CancelScope()
{
	Camera.StartCinematic( CMF_NO_LETTERBOX | CMF_SHOW_HIDDEN_OBJECTS | CMF_INITIAL_FADEIN );
}

void RechargeFlareGun( float scale=1.0f )
{
}

int MeleeWeaponHand;
enum EHandedness {
	HAND_Left,
	HAND_Right
}

//==============================================================================
//
//    Turok Leftovers
//

final class TurokGrenadeLauncher : TurokWeapon
{
	TurokGrenadeLauncher( kWeapon@ actor )
	{
		super( actor );
	}
	void OnBeginFire()
	{
		self.AnimState().Set( anim_weaponFire, 4.0f, 0 );
		self.PlaySound( "sounds/shaders/grenade_launch.ksnd" );
		self.FireProjectile( "fx/grenade.kfx", 18.432f, 25.696f, -5.12f );
		self.FireProjectile( "fx/weffect_grenadelauncher.kfx", 10.3424f, 25.696f, -0.512f );
		self.Owner().Actor().LoudNoiseAlert();
		self.Owner().Actor().RecoilPitch() = -0.03490658476948738f;
		self.Owner().ConsumeAmmo( 1 );
	}
}
final class TurokAccelerator : TurokWeapon
{
	float acceleratorCycleSpeed;
	float acceleratorSpinAngle;
	int chargeStep;
	TurokAccelerator( kWeapon@ actor )
	{
		acceleratorCycleSpeed = 2.0f;
		acceleratorSpinAngle = 0;
		chargeStep = 0;
		super( actor );
	}
	void OnBeginFire()
	{
		self.AnimState().Blend( anim_weaponFire, 4.0f, 40.0f, ANF_LOOP );
		chargeStep = 0;
	}
	void OnEndFire()
	{
		float t = self.AnimState().PlayTime();
		if ( t >= 4.0f )
		{
			self.PlaySound( "sounds/shaders/generic_194.ksnd" );
			self.FireProjectile( "fx/shockwave_pulse_5_strongest.kfx", 4.096f, 25.696f, -14.336f );
			self.Owner().ConsumeAmmo( 10 );
		}
		else if ( t >= 3.0f )
		{
			self.PlaySound( "sounds/shaders/generic_193.ksnd" );
			self.FireProjectile( "fx/shockwave_pulse_4.kfx", 4.096f, 25.696f, -14.336f );
			self.Owner().ConsumeAmmo( 8 );
		}
		else if ( t >= 2.0f )
		{
			self.PlaySound( "sounds/shaders/generic_192.ksnd" );
			self.FireProjectile( "fx/shockwave_pulse_3.kfx", 4.096f, 25.696f, -14.336f );
			self.Owner().ConsumeAmmo( 6 );
		}
		else if ( t >= 1.0f )
		{
			self.PlaySound( "sounds/shaders/generic_191.ksnd" );
			self.FireProjectile( "fx/shockwave_pulse_2.kfx", 4.096f, 25.696f, -14.336f );
			self.Owner().ConsumeAmmo( 4 );
		}
		else
		{
			self.PlaySound( "sounds/shaders/generic_190.ksnd" );
			self.FireProjectile( "fx/shockwave_pulse_1_weakest.kfx", 4.096f, 25.696f, -14.336f );
			self.Owner().ConsumeAmmo( 2 );
		}
		chargeStep = 0;
		self.StopLoopingSounds();
		self.Owner().Actor().LoudNoiseAlert();
		self.AnimState().Blend( anim_weaponFireCharged, 4.0f, 4.0f, ANF_NOINTERRUPT );
	}
	void OnTick()
	{
		if ( self.AnimState().PlayingID() == anim_weaponFire )
		{
			switch ( chargeStep )
			{
				case 0:
					if ( self.AnimState().PlayTime() >= 0.0f )
					{
						self.PlaySound( "sounds/shaders/generic_160.ksnd" );
						chargeStep++;
					}
					break;
				case 1:
					if ( self.AnimState().PlayTime() >= 1.0f)
					{
						self.PlaySound( "sounds/shaders/generic_161.ksnd" );
						chargeStep++;
					}
					break;
				case 2:
					if ( self.AnimState().PlayTime() >= 2.0f)
					{
						self.PlaySound( "sounds/shaders/generic_162.ksnd" );
						chargeStep++;
					}
					break;
				case 3:
					if ( self.AnimState().PlayTime() >= 3.0f )
					{
						self.PlaySound( "sounds/shaders/generic_163.ksnd" );
						chargeStep++;
					}
					break;
				case 4:
					if ( self.AnimState().PlayTime() >= 4.0f )
					{
						self.PlaySound( "sounds/shaders/generic_164.ksnd" );
						chargeStep++;
					}
			}
		}
		switch( self.AnimState().PlayingID() )
		{
			case anim_weaponSwapOut:
				acceleratorCycleSpeed = 2;
				self.RenderModel().SetRotationOffset( 1, 0, 0,1,0 );
				break;
			case anim_weaponIdle: case anim_weaponWalk: case anim_weaponRun: case anim_weaponFireCharged:
				acceleratorCycleSpeed = (2 - acceleratorCycleSpeed) * 0.025f + acceleratorCycleSpeed;
				acceleratorSpinAngle += acceleratorCycleSpeed * (1.0f / 120.0f);
				self.RenderModel().SetRotationOffset( 1, acceleratorSpinAngle, 0,1,0 );
				break;
			case anim_weaponFire:
				acceleratorCycleSpeed = (20 - acceleratorCycleSpeed) * (0.01f*GAME_FRAME_TIME) + acceleratorCycleSpeed;
				acceleratorSpinAngle += acceleratorCycleSpeed * (1.0f / 120.0f);
				self.RenderModel().SetRotationOffset( 1, acceleratorSpinAngle, 0,1,0 );
		}
	}
	void OnLower()
	{
		acceleratorCycleSpeed = 2;
		acceleratorSpinAngle = 0;
		self.RenderModel().SetRotationOffset( 1, 0, 0, 1, 0 );
	}
	void OnHoldster()
	{
		acceleratorCycleSpeed = 2;
		acceleratorSpinAngle = 0;
		self.RenderModel().SetRotationOffset( 1, 0, 0, 1, 0 );
		self.StopSound();
	}
}
final class TurokChrono : TurokWeapon
{
	float chronoChargeTime;
	TurokChrono( kWeapon@ actor )
	{
		chronoChargeTime = 0.0f;
		super( actor );
	}
	void OnBeginFire()
	{
		self.AnimState().Blend( anim_weaponFire, 4.0f, 60.0f, ANF_LOOP );
		chronoChargeTime = 0;
	}
	void OnEndFire()
	{
		float t = self.AnimState().PlayTime() + chronoChargeTime;
		if ( t >= 1.0f )
		{
			self.FireProjectile( "fx/chronscepter.kfx", 4.096f, 25.6f, -14.336f );
			self.PlaySound( "sounds/shaders/shockwave_weapon_fire.ksnd" );
			self.AnimState().Blend(anim_weaponFireCharged, 4.0f, 4.0f, 0 );
			self.Owner().Actor().LoudNoiseAlert();
			self.Owner().ConsumeAmmo( 1 );
			chronoChargeTime = 0;
		}
		else
			self.AnimState().Blend( anim_weaponIdle, 4.0f, 8.0f, 0 );
	}
	void OnTick(void)
	{
		switch ( self.AnimState().PlayingID() )
		{
			case anim_weaponFireCharged:
				if ( (self.AnimState().flags & ANF_STOPPED) != 0 )
					self.AnimState().Blend( anim_weaponIdle, 4.0f, 8.0f, 0 );
		}
	}
}
