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

Doom 3 BFG Edition GPL Source Code
Copyright (C) 1993-2012 id Software LLC, a ZeniMax Media company. 

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

Doom 3 BFG Edition 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 BFG Edition 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 BFG Edition Source Code.  If not, see <http://www.gnu.org/licenses/>.

In addition, the Doom 3 BFG Edition 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 BFG Edition 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.

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

#include "../idlib/precompiled.h"
#pragma hdrstop

#include "Game_local.h"

/*
================================================================================================
Contains the AimAssist implementation.
================================================================================================
*/

idCVar aa_targetAimAssistEnable						(	"aa_targetAimAssistEnable",						"0",		CVAR_BOOL | CVAR_ARCHIVE,	"Enables/Disables the entire Aim Assist system" );

idCVar aa_targetAdhesionEnable						(	"aa_targetAdhesionEnable",						"1",		CVAR_BOOL,	"Enables Target Adhesion" );
idCVar aa_targetFrictionEnable						(	"aa_targetFrictionEnable",						"1",		CVAR_BOOL,	"Enables Target Friction" );

// Selection
idCVar aa_targetMaxDistance							(	"aa_targetMaxDistance",							"3000",		CVAR_FLOAT, "The Maximum Distance away for a target to be considered for adhesion, friction and target lock-to" );
idCVar aa_targetSelectionRadius						(	"aa_targetSelectionRadius",						"128.0",	CVAR_FLOAT, "Radius used to select targets for auto aiming" );

// Adhesion
idCVar aa_targetAdhesionRadius						(	"aa_targetAdhesionRadius",						"96.0",		CVAR_FLOAT, "Radius used to apply adhesion amount" );
idCVar aa_targetAdhesionYawSpeedMax					(	"aa_targetAdhesionYawSpeedMax",					"0.6",		CVAR_FLOAT, "Max Yaw Adhesion Speed" );
idCVar aa_targetAdhesionPitchSpeedMax				(	"aa_targetAdhesionPitchSpeedMax",				"0.6",		CVAR_FLOAT, "Max Pitch Adhesion Speed" );
idCVar aa_targetAdhesionContributionPctMax			(	"aa_targetAdhesionContributionPctMax",			"0.25",		CVAR_FLOAT, "Max Adhesion Contribution Percentage - Range 0.0 - 1.0" );
idCVar aa_targetAdhesionPlayerSpeedThreshold		(	"aa_targetAdhesionPlayerSpeedThreshold",		"10.0",		CVAR_FLOAT, "Speed Threshold that determines how fast the player needs to move before adhesion is allowed to kick in" );

// Friction
idCVar aa_targetFrictionMaxDistance					(	"aa_targetFrictionMaxDistance",					"1024.0",	CVAR_FLOAT, "Minimum Distance Friction takes effect" );
idCVar aa_targetFrictionOptimalDistance				(	"aa_targetFrictionOptimalDistance",				"768.0",	CVAR_FLOAT, "Optimal Distance for Friction to take an effect" );
idCVar aa_targetFrictionRadius						(	"aa_targetFrictionRadius",						"96.0",		CVAR_FLOAT, "Friction Collision Sphere Radius" );
idCVar aa_targetFrictionOptimalRadius				(	"aa_targetFrictionOptimalRadius",				"192.0",	CVAR_FLOAT, "Friction Collision Sphere Radius when at Optimal Distance" );
idCVar aa_targetFrictionMultiplierMin				(	"aa_targetFrictionMultiplierMin",				"1.0",		CVAR_FLOAT, "Minimum Friction Scalar" );
idCVar aa_targetFrictionMultiplierMax				(	"aa_targetFrictionMultiplierMax",				"0.4",		CVAR_FLOAT, "Maximum Friction Scalar" );

/*
========================
idAimAssist::Init
========================
*/
void idAimAssist::Init( idPlayer *player_ ) {
	player = player_;
	angleCorrection = ang_zero;
	frictionScalar = 1.0f;
	lastTargetPos = vec3_zero;
}

/*
========================
idAimAssist::Update
========================
*/
void idAimAssist::Update() {
	angleCorrection = ang_zero;

	UpdateNewAimAssist();		
}

/*
========================
idAimAssist::UpdateNewAimAssist
========================
*/
void idAimAssist::UpdateNewAimAssist() {

	angleCorrection = ang_zero;
	frictionScalar = 1.0f;
	idEntity* lastTarget = targetEntity;
	targetEntity = NULL;

	// is aim assisting allowed?  If not then just bail out
	if ( !aa_targetAimAssistEnable.GetBool() ) {
		return;
	}

	bool forceLastTarget = false;
	idVec3 targetPos;

	idEntity * entity = NULL;
	if ( forceLastTarget ) {
		entity = lastTarget;
		targetPos = lastTargetPos;
	} else {
		entity = FindAimAssistTarget( targetPos );
	}

	if ( entity != NULL ) {

		UpdateFriction( entity, targetPos );

		// by default we don't allow adhesion when we are standing still
		const float playerMovementSpeedThreshold = Square( aa_targetAdhesionPlayerSpeedThreshold.GetFloat() );
		float playerSpeed = player->GetPhysics()->GetLinearVelocity().LengthSqr();

		// only allow adhesion on actors (ai) or players.  Disallow adhesion on any static world entities such as explosive barrels
		if ( playerSpeed > playerMovementSpeedThreshold ) {
			UpdateAdhesion( entity, targetPos );
		}

		targetEntity = entity;
	}	

	lastTargetPos = targetPos;
}

/*
========================
idAimAssist::FindAimAssistTarget
========================
*/
idEntity* idAimAssist::FindAimAssistTarget( idVec3& targetPos ) {
	if ( player == NULL ) {
		return NULL;
	}

	//TO DO: Make this faster
	//TO DO: Defer Traces
	idEntity *	optimalTarget = NULL;
	float		currentBestScore = -idMath::INFINITY;
	targetPos = vec3_zero;

	idVec3 cameraPos;
	idMat3 cameraAxis;
	player->GetViewPos( cameraPos, cameraAxis );

	float maxDistanceSquared = Square( aa_targetMaxDistance.GetFloat() );

	idVec3 dirToTarget;
	float  distanceToTargetSquared;
	idVec3 primaryTargetPos;
	idVec3 secondaryTargetPos;
	
	for ( idEntity * entity = gameLocal.aimAssistEntities.Next(); entity != NULL; entity = entity->aimAssistNode.Next() ) {
		if ( !entity->IsActive() ) {
			continue;
		}

		if ( entity->IsHidden() ) {
			continue;
		}

		if ( entity->IsType( idActor::Type ) ) {
			idActor * actor = static_cast<idActor *>( entity );
			if ( actor->team == player->team ) {
				// In DM, LMS, and Tourney, all players are on the same team
				if ( gameLocal.gameType == GAME_CTF || gameLocal.gameType == GAME_TDM || gameLocal.gameType == GAME_SP ) {
					continue;
				}
			}
		}

		if ( entity->IsType( idAI::Type ) ) {
			idAI * aiEntity = static_cast<idAI *>( entity );
			if ( aiEntity->ReactionTo( player ) == ATTACK_IGNORE ) {
				continue;
			}
		}

		// check whether we have a valid target position for this entity - skip it if we don't
		if ( !ComputeTargetPos( entity, primaryTargetPos, secondaryTargetPos ) ) {
			continue;
		}

		// is it close enough to us
		dirToTarget = primaryTargetPos-cameraPos;
		distanceToTargetSquared = dirToTarget.LengthSqr();
		if ( distanceToTargetSquared > maxDistanceSquared ) {
			continue;
		}

		// Compute a score in the range of 0..1 for how much are looking towards the target.
		idVec3 forward = cameraAxis[ 0 ];
		forward.Normalize();
		dirToTarget.Normalize();
		float ViewDirDotTargetDir = idMath::ClampFloat( -1.0f, 1.0f, forward * dirToTarget ); // compute the dot and clamp to account for floating point error

		// throw out anything thats outside of weapon's global FOV.
		if ( ViewDirDotTargetDir < 0.0f ) {
			continue;
		}

		// to be consistent we always use the primaryTargetPos to compute the score for this entity
		float computedScore = ComputeEntityAimAssistScore( primaryTargetPos, cameraPos, cameraAxis );

		// check if the current score beats our current best score and we have line of sight to it.
		if ( computedScore > currentBestScore ) {

			// determine if the current target is in our line of sight
			trace_t tr;
			gameLocal.clip.TracePoint( tr, cameraPos, primaryTargetPos, MASK_MONSTERSOLID, player );
		
			// did our trace fail?
			if ( ( ( tr.fraction < 1.0f ) && ( tr.c.entityNum != entity->entityNumber ) ) || ( tr.fraction >= 1.0f ) ) {

				// if the collision test failed for the primary position -- check the secondary position
				trace_t tr2;
				gameLocal.clip.TracePoint( tr2, cameraPos, secondaryTargetPos, MASK_MONSTERSOLID, player );

				if ( ( ( tr2.fraction < 1.0f ) && ( tr2.c.entityNum != entity->entityNumber ) ) || ( tr2.fraction >= 1.0f ) ) {
					// if the secondary position is also not visible then give up
					continue;
				}

				// we can see the secondary target position so we should consider this entity but use
				// the secondary position as the target position
				targetPos = secondaryTargetPos;
			}  else {
				targetPos = primaryTargetPos;
			}

			// if we got here then this is our new best score
			optimalTarget = entity;
			currentBestScore = computedScore;
		}
	}

	return optimalTarget;
}

/*
========================
idAimAssist::ComputeEntityAimAssistScore
========================
*/
float idAimAssist::ComputeEntityAimAssistScore( const idVec3& targetPos, const idVec3& cameraPos, const idMat3& cameraAxis ) {

	float score = 0.0f;
		
	idVec3 dirToTarget = targetPos - cameraPos;	
	float distanceToTarget = dirToTarget.Length();

	// Compute a score in the range of 0..1 for how much are looking towards the target.
	idVec3 forward = cameraAxis[0];
	forward.Normalize();	
	dirToTarget.Normalize();
	float ViewDirDotTargetDir = idMath::ClampFloat( 0.0f, 1.0f, forward * dirToTarget ); // compute the dot and clamp to account for floating point error

	// the more we look at the target the higher our score
	score = ViewDirDotTargetDir;

	// weigh the score from the view angle higher than the distance score
	static float aimWeight = 0.8f;
	score *= aimWeight;
	// Add a score of 0..1 for how close the target is to the player
	if ( distanceToTarget < aa_targetMaxDistance.GetFloat() ) {
		float distanceScore = 1.0f - ( distanceToTarget / aa_targetMaxDistance.GetFloat() );
		float distanceWeight = 1.0f - aimWeight;
		score += ( distanceScore * distanceWeight );
	}

	return score * 1000.0f;
}

/*
========================
idAimAssist::UpdateAdhesion
========================
*/
void idAimAssist::UpdateAdhesion( idEntity* pTarget, const idVec3& targetPos ) {

	if ( !aa_targetAdhesionEnable.GetBool() ) {
		return;
	}

	if ( !pTarget ) {
		return;
	}
	
	float contributionPctMax = aa_targetAdhesionContributionPctMax.GetFloat();
	
	idVec3 cameraPos;
	idMat3 cameraAxis;
	player->GetViewPos(cameraPos, cameraAxis);

	idAngles cameraViewAngles = cameraAxis.ToAngles();
	cameraViewAngles.Normalize180();

	idVec3 cameraViewPos = cameraPos;
	idVec3 dirToTarget = targetPos - cameraViewPos;
	float distanceToTarget = dirToTarget.Length();

	idAngles aimAngles = dirToTarget.ToAngles();
	aimAngles.Normalize180();

	// find the delta
	aimAngles -= cameraViewAngles;

	// clamp velocities to some max values
	aimAngles.yaw = idMath::ClampFloat( -aa_targetAdhesionYawSpeedMax.GetFloat(), aa_targetAdhesionYawSpeedMax.GetFloat(), aimAngles.yaw );
	aimAngles.pitch = idMath::ClampFloat( -aa_targetAdhesionPitchSpeedMax.GetFloat(), aa_targetAdhesionPitchSpeedMax.GetFloat(), aimAngles.pitch );
	
	idVec3 forward = cameraAxis[0];
	forward.Normalize();
	dirToTarget.Normalize();
	float ViewDirDotTargetDir = idMath::ClampFloat( 0.0f, 1.0f, forward * dirToTarget ); // compute the dot and clamp to account for floating point error
	float aimLength = ViewDirDotTargetDir * distanceToTarget;
	idVec3 aimPoint = cameraPos + ( forward * aimLength );
	float delta = idMath::Sqrt( Square( distanceToTarget ) - Square( aimLength ) );

	float contribution = idMath::ClampFloat( 0.0f, contributionPctMax, 1.0f - ( delta / aa_targetAdhesionRadius.GetFloat() ) );
	angleCorrection.yaw = aimAngles.yaw * contribution;
	angleCorrection.pitch = aimAngles.pitch * contribution;
}

/*
========================
idAimAssist::ComputeFrictionRadius
========================
*/
float idAimAssist::ComputeFrictionRadius( float distanceToTarget ) {

	if ( ( distanceToTarget <= idMath::FLT_SMALLEST_NON_DENORMAL ) || distanceToTarget > aa_targetFrictionMaxDistance.GetFloat() ) {
		return aa_targetFrictionRadius.GetFloat();
	}

	float distanceContributionScalar = ( aa_targetFrictionOptimalDistance.GetFloat() > 0.0f ) ? ( distanceToTarget / aa_targetFrictionOptimalDistance.GetFloat() ) : 0.0f;

	if ( distanceToTarget > aa_targetFrictionOptimalDistance.GetFloat() ) {
		const float range = idMath::ClampFloat( 0.0f, aa_targetFrictionMaxDistance.GetFloat(), aa_targetFrictionMaxDistance.GetFloat() - aa_targetFrictionOptimalDistance.GetFloat() );
		if ( range > idMath::FLT_SMALLEST_NON_DENORMAL ) {
			distanceContributionScalar = 1.0f - ( ( distanceToTarget - aa_targetFrictionOptimalDistance.GetFloat() ) / range );
		}
	}

	return Lerp( aa_targetFrictionRadius.GetFloat(), aa_targetFrictionOptimalRadius.GetFloat(), distanceContributionScalar );
}

/*
========================
idAimAssist::UpdateFriction
========================
*/
void idAimAssist::UpdateFriction( idEntity* pTarget, const idVec3& targetPos ) {

	if ( !aa_targetFrictionEnable.GetBool() ) {
		return;
	}

	if ( pTarget == NULL ) {
		return;
	}
	
	idVec3 cameraPos;
	idMat3 cameraAxis;
	player->GetViewPos(cameraPos, cameraAxis);
	idVec3 dirToTarget = targetPos - cameraPos;
	float  distanceToTarget = dirToTarget.Length();
	idVec3 forward = cameraAxis[0];
	forward.Normalize();
	dirToTarget.Normalize();
	float ViewDirDotTargetDir = idMath::ClampFloat( 0.0f, 1.0f, forward * dirToTarget ); // compute the dot and clamp to account for floating point error
	float aimLength = ViewDirDotTargetDir * distanceToTarget;
	idVec3 aimPoint = cameraPos + ( forward * aimLength );
	float delta = idMath::Sqrt( Square( distanceToTarget ) - Square( aimLength ) );

	const float radius = ComputeFrictionRadius( distanceToTarget );
	if ( delta < radius ) {
		float alpha = 1.0f - ( delta / radius );
		frictionScalar = Lerp( aa_targetFrictionMultiplierMin.GetFloat(), aa_targetFrictionMultiplierMax.GetFloat(), alpha );
	}
}

/*
========================
idAimAssist::ComputeTargetPos
========================
*/
bool idAimAssist::ComputeTargetPos( idEntity* entity, idVec3& primaryTargetPos, idVec3& secondaryTargetPos ) {

	primaryTargetPos = vec3_zero;
	secondaryTargetPos = vec3_zero;

	if ( entity == NULL ) {
		return false;
	}

	// The target point on actors can now be either the head or the torso
	idActor * actor = NULL;
	if ( entity->IsType( idActor::Type ) ) {
		actor = ( idActor *) entity;
	}
	if ( actor != NULL ) {
		// Actor AimPoint

		idVec3 torsoPos;
		idVec3 headPos = actor->GetEyePosition();

		if ( actor->GetHeadEntity() != NULL ) {
			torsoPos = actor->GetHeadEntity()->GetPhysics()->GetOrigin();
		} else {
			const float offsetScale = 0.9f;
			torsoPos = actor->GetPhysics()->GetOrigin() + ( actor->EyeOffset() * offsetScale );
		}

		primaryTargetPos = torsoPos;
		secondaryTargetPos = headPos;
		return true;

	} else if ( entity->GetPhysics()->GetClipModel() != NULL ) {

		const idBounds& box = entity->GetPhysics()->GetClipModel()->GetAbsBounds();
		primaryTargetPos = box.GetCenter();
		secondaryTargetPos = box.GetCenter();
		return true;
	}

	return false;
}
