/*
===========================================================================

Doom 3 GPL Source Code
Copyright (C) 1999-2011 id Software LLC, a ZeniMax Media company.

This file is part of the Doom 3 GPL Source Code ("Doom 3 Source Code").

Doom 3 Source Code 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 3 of the License, or
(at your option) any later version.

Doom 3 Source Code 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.

You should have received a copy of the GNU General Public License
along with Doom 3 Source Code.  If not, see <http://www.gnu.org/licenses/>.

In addition, the Doom 3 Source Code is also subject to certain additional terms. You should have received a copy of these additional terms immediately following the terms and conditions of the GNU General Public License which accompanied the Doom 3 Source Code.  If not, please request a copy in writing from id Software at the address below.

If you have questions concerning this license or the applicable additional terms, you may contact in writing id Software LLC, c/o ZeniMax Media Inc., Suite 120, Rockville, Maryland 20850 USA.

===========================================================================
*/

#ifdef _D3XP

#include "sys/platform.h"

#include "gamesys/SysCvar.h"
#include "ai/AI.h"
#include "Player.h"
#include "Moveable.h"
#include "Misc.h"

#include "Grabber.h"

#define MAX_DRAG_TRACE_DISTANCE			384.0f
#define TRACE_BOUNDS_SIZE				3.f
#define HOLD_DISTANCE					72.f
#define FIRING_DELAY					1000.0f
#define DRAG_FAIL_LEN					64.f
#define	THROW_SCALE						1000
#define MAX_PICKUP_VELOCITY				1500 * 1500
#define MAX_PICKUP_SIZE					96

/*
===============================================================================

	Allows entities to be dragged through the world with physics.

===============================================================================
*/

CLASS_DECLARATION( idEntity, idGrabber )
END_CLASS

/*
==============
idGrabber::idGrabber
==============
*/
idGrabber::idGrabber( void ) {
	dragEnt = NULL;
	owner = NULL;
	beam = NULL;
	beamTarget = NULL;
	oldUcmdFlags = 0;
	shakeForceFlip = false;
	holdingAF = false;
	endTime = 0;
	lastFiredTime = -FIRING_DELAY;
	dragFailTime = 0;
	startDragTime = 0;
	warpId = -1;
	dragTraceDist = MAX_DRAG_TRACE_DISTANCE;
}

/*
==============
idGrabber::~idGrabber
==============
*/
idGrabber::~idGrabber( void ) {
	StopDrag( true );
	if ( beam ) {
		delete beam;
	}
	if ( beamTarget ) {
		delete beamTarget;
	}
}

/*
==============
idGrabber::Save
==============
*/
void idGrabber::Save( idSaveGame *savefile ) const {

	dragEnt.Save( savefile );
	savefile->WriteStaticObject( drag );

	savefile->WriteVec3( saveGravity );
	savefile->WriteInt( id );

	savefile->WriteVec3( localPlayerPoint );

	owner.Save( savefile );

	savefile->WriteBool( holdingAF );
	savefile->WriteBool( shakeForceFlip );

	savefile->WriteInt( endTime );
	savefile->WriteInt( lastFiredTime );
	savefile->WriteInt( dragFailTime );
	savefile->WriteInt( startDragTime );
	savefile->WriteFloat( dragTraceDist );
	savefile->WriteInt( savedContents );
	savefile->WriteInt( savedClipmask );

	savefile->WriteObject( beam );
	savefile->WriteObject( beamTarget );

	savefile->WriteInt( warpId );
}

/*
==============
idGrabber::Restore
==============
*/
void idGrabber::Restore( idRestoreGame *savefile ) {
	//Spawn the beams
	Initialize();

	dragEnt.Restore( savefile );
	savefile->ReadStaticObject( drag );

	savefile->ReadVec3( saveGravity );
	savefile->ReadInt( id );

	// Restore the drag force's physics object
	if ( dragEnt.IsValid() ) {
		drag.SetPhysics( dragEnt.GetEntity()->GetPhysics(), id, dragEnt.GetEntity()->GetPhysics()->GetOrigin() );
	}

	savefile->ReadVec3( localPlayerPoint );

	owner.Restore( savefile );

	savefile->ReadBool( holdingAF );
	savefile->ReadBool( shakeForceFlip );

	savefile->ReadInt( endTime );
	savefile->ReadInt( lastFiredTime );
	savefile->ReadInt( dragFailTime );
	savefile->ReadInt( startDragTime );
	savefile->ReadFloat( dragTraceDist );
	savefile->ReadInt( savedContents );
	savefile->ReadInt( savedClipmask );

	savefile->ReadObject( reinterpret_cast<idClass *&>(beam) );
	savefile->ReadObject( reinterpret_cast<idClass *&>(beamTarget) );

	savefile->ReadInt( warpId );
}

/*
==============
idGrabber::Initialize
==============
*/
void idGrabber::Initialize( void ) {
	if ( !gameLocal.isMultiplayer ) {
		idDict args;

		if ( !beamTarget ) {
			args.SetVector( "origin", vec3_origin );
			args.SetBool( "start_off", true );
			beamTarget = ( idBeam * )gameLocal.SpawnEntityType( idBeam::Type, &args );
		}

		if ( !beam ) {
			args.Clear();
			args.Set( "target", beamTarget->name.c_str() );
			args.SetVector( "origin", vec3_origin );
			args.SetBool( "start_off", true );
			args.Set( "width", "6" );
			args.Set( "skin", "textures/smf/flareSizeable" );
			args.Set( "_color", "0.0235 0.843 0.969 0.2" );
			beam = ( idBeam * )gameLocal.SpawnEntityType( idBeam::Type, &args );
			beam->SetShaderParm( 6, 1.0f );
		}

		endTime = 0;
		dragTraceDist = MAX_DRAG_TRACE_DISTANCE;
	}
	else {
		beam = NULL;
		beamTarget = NULL;
		endTime = 0;
		dragTraceDist = MAX_DRAG_TRACE_DISTANCE;
	};
}

/*
==============
idGrabber::SetDragDistance
==============
*/
void idGrabber::SetDragDistance( float dist ) {
	dragTraceDist = dist;
}

/*
==============
idGrabber::StartDrag
==============
*/
void idGrabber::StartDrag( idEntity *grabEnt, int id ) {
	int clipModelId = id;
	idPlayer *thePlayer = owner.GetEntity();

	holdingAF = false;
	dragFailTime = gameLocal.slow.time;
	startDragTime = gameLocal.slow.time;

	oldUcmdFlags = thePlayer->usercmd.flags;

	// set grabbed state for networking
	grabEnt->SetGrabbedState( true );

	// This is the new object to drag around
	dragEnt = grabEnt;

	// Show the beams!
	UpdateBeams();
	if ( beam ) {
		beam->Show();
	}
	if ( beamTarget ) {
		beamTarget->Show();
	}

	// Move the object to the fast group (helltime)
	grabEnt->timeGroup = TIME_GROUP2;

	// Handle specific class types
	if ( grabEnt->IsType( idProjectile::Type ) ) {
		idProjectile* p = (idProjectile*)grabEnt;

		p->CatchProjectile( thePlayer, "_catch" );

		// Make the projectile non-solid to other projectiles/enemies (special hack for helltime hunter)
		if ( !idStr::Cmp( grabEnt->GetEntityDefName(), "projectile_helltime_killer" ) ) {
			savedContents = CONTENTS_PROJECTILE;
			savedClipmask = MASK_SHOT_RENDERMODEL|CONTENTS_PROJECTILE;
		} else {
			savedContents = grabEnt->GetPhysics()->GetContents();
			savedClipmask = grabEnt->GetPhysics()->GetClipMask();
		}
		grabEnt->GetPhysics()->SetContents( 0 );
		grabEnt->GetPhysics()->SetClipMask( CONTENTS_SOLID|CONTENTS_BODY );

	} else if ( grabEnt->IsType( idExplodingBarrel::Type ) ) {
		idExplodingBarrel *ebarrel = static_cast<idExplodingBarrel*>(grabEnt);

		ebarrel->StartBurning();

	} else if ( grabEnt->IsType( idAFEntity_Gibbable::Type ) ) {
		holdingAF = true;
		clipModelId = 0;

		if ( grabbableAI( grabEnt->spawnArgs.GetString( "classname" ) ) ) {
			idAI *aiEnt = static_cast<idAI*>(grabEnt);

			aiEnt->StartRagdoll();
		}
	} else if ( grabEnt->IsType( idMoveableItem::Type ) ) {
		grabEnt->PostEventMS( &EV_Touch, 250, thePlayer, 0 );
	}

	// Get the current physics object to manipulate
	idPhysics *phys = grabEnt->GetPhysics();

	// Turn off gravity on object
	saveGravity = phys->GetGravity();
	phys->SetGravity( vec3_origin );

	// hold it directly in front of player
	localPlayerPoint = ( thePlayer->firstPersonViewAxis[0] * HOLD_DISTANCE ) * thePlayer->firstPersonViewAxis.Transpose();

	// Set the ending time for the hold
	endTime = gameLocal.time + g_grabberHoldSeconds.GetFloat() * 1000;

	// Start up the Force_Drag to bring it in
	drag.Init( g_grabberDamping.GetFloat() );
	drag.SetPhysics( phys, clipModelId, thePlayer->firstPersonViewOrigin + localPlayerPoint * thePlayer->firstPersonViewAxis);

	// start the screen warp
	warpId = thePlayer->playerView.AddWarp( phys->GetOrigin(), SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, 160, 2000 );
}

/*
==============
idGrabber::StopDrag
==============
*/
void idGrabber::StopDrag( bool dropOnly ) {
	idPlayer *thePlayer = owner.GetEntity();

	if ( beam ) {
		beam->Hide();
	}
	if ( beamTarget ) {
		beamTarget->Hide();
	}

	if ( dragEnt.IsValid() ) {
		idEntity *ent = dragEnt.GetEntity();

		// set grabbed state for networking
		ent->SetGrabbedState( false );

		// If a cinematic has started, allow dropped object to think in cinematics
		if ( gameLocal.inCinematic ) {
			ent->cinematic = true;
		}

		// Restore Gravity
		ent->GetPhysics()->SetGravity( saveGravity );

		// Move the object back to the slow group (helltime)
		ent->timeGroup = TIME_GROUP1;

		if ( holdingAF ) {
			idAFEntity_Gibbable *af = static_cast<idAFEntity_Gibbable *>(ent);
			idPhysics_AF	*af_Phys = static_cast<idPhysics_AF*>(af->GetPhysics());

			if ( grabbableAI( ent->spawnArgs.GetString( "classname" ) ) ) {
				idAI *aiEnt = static_cast<idAI*>(ent);

				aiEnt->Damage( thePlayer, thePlayer, vec3_origin, "damage_suicide", 1.0f, INVALID_JOINT );
			}

			af->SetThrown( !dropOnly );

			// Reset timers so that it isn't forcibly put to rest in mid-air
			af_Phys->PutToRest();
			af_Phys->Activate();

			af_Phys->SetTimeScaleRamp( MS2SEC(gameLocal.slow.time) - 1.5f, MS2SEC(gameLocal.slow.time) + 1.0f );
		}

		// If the object isn't near its goal, just drop it in place.
		if ( !ent->IsType( idProjectile::Type ) && ( dropOnly || drag.GetDistanceToGoal() > DRAG_FAIL_LEN ) ) {
			ent->GetPhysics()->SetLinearVelocity( vec3_origin );
			thePlayer->StartSoundShader( declManager->FindSound( "grabber_maindrop" ), SND_CHANNEL_WEAPON, 0, false, NULL );

			if ( ent->IsType( idExplodingBarrel::Type ) ) {
				idExplodingBarrel *ebarrel = static_cast<idExplodingBarrel*>(ent);

				ebarrel->SetStability( true );
				ebarrel->StopBurning();
			}
		} else {
			// Shoot the object forward
			ent->ApplyImpulse( thePlayer, 0, ent->GetPhysics()->GetOrigin(), thePlayer->firstPersonViewAxis[0] * THROW_SCALE * ent->GetPhysics()->GetMass() );
			thePlayer->StartSoundShader( declManager->FindSound( "grabber_release" ), SND_CHANNEL_WEAPON, 0, false, NULL );

			// Orient projectiles away from the player
			if ( ent->IsType( idProjectile::Type ) ) {
				idPlayer *player = owner.GetEntity();
				idAngles ang = player->firstPersonViewAxis[0].ToAngles();

				ang.pitch += 90.f;
				ent->GetPhysics()->SetAxis( ang.ToMat3() );
				ent->GetPhysics()->SetAngularVelocity( vec3_origin );

				// Restore projectile contents
				ent->GetPhysics()->SetContents( savedContents );
				ent->GetPhysics()->SetClipMask( savedClipmask );

			} else if ( ent->IsType( idMoveable::Type ) ) {
				// Turn on damage for this object
				idMoveable *obj = static_cast<idMoveable*>(ent);
				obj->EnableDamage( true, 2.5f );
				obj->SetAttacker( thePlayer );

				if ( ent->IsType( idExplodingBarrel::Type ) ) {
					idExplodingBarrel *ebarrel = static_cast<idExplodingBarrel*>(ent);
					ebarrel->SetStability( false );
				}

			} else if ( ent->IsType( idMoveableItem::Type ) ) {
				ent->GetPhysics()->SetClipMask( MASK_MONSTERSOLID );
			}
		}

		// Remove the Force_Drag's control of the entity
		drag.RemovePhysics( ent->GetPhysics() );
	}

	if ( warpId != -1 ) {
		thePlayer->playerView.FreeWarp( warpId );
		warpId = -1;
	}

	lastFiredTime = gameLocal.time;
	dragEnt = NULL;
	endTime = 0;
}

/*
==============
idGrabber::Update
==============
*/
int idGrabber::Update( idPlayer *player, bool hide ) {
	trace_t trace;
	idEntity *newEnt;

	// pause before allowing refire
	if ( lastFiredTime + FIRING_DELAY > gameLocal.time ) {
		return 3;
	}

	// Dead players release the trigger
	if ( hide || player->health <= 0 ) {
		StopDrag( true );
		if ( hide ) {
			lastFiredTime = gameLocal.time - FIRING_DELAY + 250;
		}
		return 3;
	}

	// Check if object being held has been removed (dead demon, projectile, etc.)
	if ( endTime > gameLocal.time ) {
		bool abort = !dragEnt.IsValid();

		if ( !abort && dragEnt.GetEntity()->IsType( idProjectile::Type ) ) {
			idProjectile *proj = (idProjectile *)dragEnt.GetEntity();

			if ( proj->GetProjectileState() >= 3 ) {
				abort = true;
			}
		}
		if ( !abort && dragEnt.GetEntity()->IsHidden() ) {
			abort = true;
		}
		// Not in multiplayer :: Pressing "reload" lets you carefully drop an item
		if ( !gameLocal.isMultiplayer && !abort && (( player->usercmd.flags & UCF_IMPULSE_SEQUENCE ) != ( oldUcmdFlags & UCF_IMPULSE_SEQUENCE )) && (player->usercmd.impulse == IMPULSE_13) ) {
			abort = true;
		}

		if ( abort ) {
			StopDrag( true );
			return 3;
		}
	}

	owner = player;

	// if no entity selected for dragging
	if ( !dragEnt.GetEntity() ) {
		idBounds bounds;
		idVec3 end = player->firstPersonViewOrigin + player->firstPersonViewAxis[0] * dragTraceDist;

		bounds.Zero();
		bounds.ExpandSelf( TRACE_BOUNDS_SIZE );

		gameLocal.clip.TraceBounds( trace, player->firstPersonViewOrigin, end, bounds, MASK_SHOT_RENDERMODEL|CONTENTS_PROJECTILE|CONTENTS_MOVEABLECLIP, player );
		// If the trace hit something
		if ( trace.fraction < 1.0f ) {
			newEnt = gameLocal.entities[ trace.c.entityNum ];

			// if entity is already being grabbed then bypass
			if ( gameLocal.isMultiplayer && newEnt->IsGrabbed() ) {
				return 0;
			}

			// Check if this is a valid entity to hold
			if ( newEnt && ( newEnt->IsType( idMoveable::Type ) ||
					newEnt->IsType( idMoveableItem::Type ) ||
					newEnt->IsType( idProjectile::Type ) ||
					newEnt->IsType( idAFEntity_Gibbable::Type ) ) &&
					newEnt->noGrab == false &&
					newEnt->GetPhysics()->GetBounds().GetRadius() < MAX_PICKUP_SIZE &&
					newEnt->GetPhysics()->GetLinearVelocity().LengthSqr() < MAX_PICKUP_VELOCITY ) {

				bool validAF = true;

				if ( newEnt->IsType( idAFEntity_Gibbable::Type ) ) {
					idAFEntity_Gibbable *afEnt = static_cast<idAFEntity_Gibbable*>(newEnt);

					if ( grabbableAI( newEnt->spawnArgs.GetString( "classname" ) ) ) {
						// Make sure it's also active
						if ( !afEnt->IsActive() ) {
							validAF = false;
						}
					} else if ( !afEnt->IsActiveAF() ) {
						validAF = false;
					}
				}

				if ( validAF && player->usercmd.buttons & BUTTON_ATTACK ) {
					// Grab this entity and start dragging it around
					StartDrag( newEnt, trace.c.id );
				} else if ( validAF ) {
					// A holdable object is ready to be grabbed
					return 1;
				}
			}
		}
	}

	// check backwards server time in multiplayer
	bool allow = true;

	if ( gameLocal.isMultiplayer ) {

		// if we've marched backwards
		if ( gameLocal.slow.time < startDragTime ) {
			allow = false;
		}
	}


	// if there is an entity selected for dragging
	if ( dragEnt.GetEntity() && allow ) {
		idPhysics *entPhys = dragEnt.GetEntity()->GetPhysics();
		idVec3 goalPos;

		// If the player lets go of attack, or time is up
		if ( !( player->usercmd.buttons & BUTTON_ATTACK ) ) {
			StopDrag( false );
			return 3;
		}
		if ( gameLocal.time > endTime ) {
			StopDrag( true );
			return 3;
		}

		// Check if the player is standing on the object
		if ( !holdingAF ) {
			idBounds	playerBounds;
			idBounds	objectBounds = entPhys->GetAbsBounds();
			idVec3		newPoint = player->GetPhysics()->GetOrigin();

			// create a bounds at the players feet
			playerBounds.Clear();
			playerBounds.AddPoint( newPoint );
			newPoint.z -= 1.f;
			playerBounds.AddPoint( newPoint );
			playerBounds.ExpandSelf( 8.f );

			// If it intersects the object bounds, then drop it
			if ( playerBounds.IntersectsBounds( objectBounds ) ) {
				StopDrag( true );
				return 3;
			}
		}

		// Shake the object at the end of the hold
		if ( g_grabberEnableShake.GetBool() && !gameLocal.isMultiplayer ) {
			ApplyShake();
		}

		// Set and evaluate drag force
		goalPos = player->firstPersonViewOrigin + localPlayerPoint * player->firstPersonViewAxis;

		drag.SetGoalPosition( goalPos );
		drag.Evaluate( gameLocal.time );

		// If an object is flying too fast toward the player, stop it hard
		if ( g_grabberHardStop.GetBool() ) {
			idPlane theWall;
			idVec3 toPlayerVelocity, objectCenter;
			float toPlayerSpeed;

			toPlayerVelocity = -player->firstPersonViewAxis[0];
			toPlayerSpeed = entPhys->GetLinearVelocity() * toPlayerVelocity;

			if ( toPlayerSpeed > 64.f ) {
				objectCenter = entPhys->GetAbsBounds().GetCenter();

				theWall.SetNormal( player->firstPersonViewAxis[0] );
				theWall.FitThroughPoint( goalPos );

				if ( theWall.Side( objectCenter, 0.1f ) == PLANESIDE_BACK ) {
					int i, num;

					num = entPhys->GetNumClipModels();
					for ( i=0; i<num; i++ ) {
						entPhys->SetLinearVelocity( vec3_origin, i );
					}
				}
			}

			// Make sure the object isn't spinning too fast
			const float MAX_ROTATION_SPEED = 12.f;

			idVec3	angVel = entPhys->GetAngularVelocity();
			float	rotationSpeed = angVel.LengthFast();

			if ( rotationSpeed > MAX_ROTATION_SPEED ) {
				angVel.NormalizeFast();
				angVel *= MAX_ROTATION_SPEED;
				entPhys->SetAngularVelocity( angVel );
			}
		}

		// Orient projectiles away from the player
		if ( dragEnt.GetEntity()->IsType( idProjectile::Type ) ) {
			idAngles ang = player->firstPersonViewAxis[0].ToAngles();
			ang.pitch += 90.f;
			entPhys->SetAxis( ang.ToMat3() );
		}

		// Some kind of effect from gun to object?
		UpdateBeams();

		// If the object is stuck away from its intended position for more than 500ms, let it go.
		if ( drag.GetDistanceToGoal() > DRAG_FAIL_LEN ) {
			if ( dragFailTime < (gameLocal.slow.time - 500) ) {
				StopDrag( true );
				return 3;
			}
		} else {
			dragFailTime = gameLocal.slow.time;
		}

		// Currently holding an object
		return 2;
	}

	// Not holding, nothing to hold
	return 0;
}

/*
======================
idGrabber::UpdateBeams
======================
*/
void idGrabber::UpdateBeams( void ) {
	jointHandle_t	muzzle_joint;
	idVec3	muzzle_origin;
	idMat3	muzzle_axis;
	renderEntity_t *re;

	if ( !beam ) {
		return;
	}

	if ( dragEnt.IsValid() ) {
		idPlayer *thePlayer = owner.GetEntity();

		if ( beamTarget ) {
			beamTarget->SetOrigin( dragEnt.GetEntity()->GetPhysics()->GetAbsBounds().GetCenter() );
		}

		muzzle_joint = thePlayer->weapon.GetEntity()->GetAnimator()->GetJointHandle( "particle_upper" );
		if ( muzzle_joint != INVALID_JOINT ) {
			thePlayer->weapon.GetEntity()->GetJointWorldTransform( muzzle_joint, gameLocal.time, muzzle_origin, muzzle_axis );
		} else {
			muzzle_origin = thePlayer->GetPhysics()->GetOrigin();
		}

		beam->SetOrigin( muzzle_origin );
		re = beam->GetRenderEntity();
		re->origin = muzzle_origin;

		beam->UpdateVisuals();
		beam->Present();
	}
}

/*
==============
idGrabber::ApplyShake
==============
*/
void idGrabber::ApplyShake( void ) {
	float u = 1 - (float)( endTime - gameLocal.time ) / ( g_grabberHoldSeconds.GetFloat() * 1000 );

	if ( u >= 0.8f ) {
		idVec3 point, impulse;
		float shakeForceMagnitude = 450.f;
		float mass = dragEnt.GetEntity()->GetPhysics()->GetMass();

		shakeForceFlip = !shakeForceFlip;

		// get point to rotate around
		point = dragEnt.GetEntity()->GetPhysics()->GetOrigin();
		point.y += 1;

		// Articulated figures get less violent shake
		if ( holdingAF ) {
			shakeForceMagnitude = 120.f;
		}

		// calc impulse
		if ( shakeForceFlip ) {
			impulse.Set( 0, 0, shakeForceMagnitude * u * mass );
		}
		else {
			impulse.Set( 0, 0, -shakeForceMagnitude * u * mass );
		}

		dragEnt.GetEntity()->ApplyImpulse( NULL, 0, point, impulse );
	}
}

/*
==============
idGrabber::grabbableAI
==============
*/
bool idGrabber::grabbableAI( const char *aiName ) {
	// skip "monster_"
	aiName += 8;

	if (!idStr::Cmpn( aiName, "flying_lostsoul", 15 ) ||
		!idStr::Cmpn( aiName, "demon_trite", 11 ) ||
		!idStr::Cmp( aiName, "flying_forgotten" ) ||
		!idStr::Cmp( aiName, "demon_cherub" ) ||
		!idStr::Cmp( aiName, "demon_tick" )) {

		return true;
	}

	return false;
}

#endif
