/*****************************************************************************
 * $Id: game_base_pilot.cc,v 1.40 2006/08/01 23:14:03 devel Exp $
 * @file
 * @author	Jim E. Brooks  http://www.jimbrooks.org
 * @brief	Auto-pilot class and AI logic.
 * @remarks	- Be aware of any side-effects from Dyna/Craft methods.
 *            One side-effect is that Craft::Translate() might initiate
 *            the animated explosion state which isn't desired by the auto-pilot
 *            when it is experimenting with maneuvers.
 *          - A Craft can be noticably ascending or descending even though
 *            its Y axis is very close to 1.0.  Even m[Yy] = 0.999.
 *          - Rationale for a state-machine (virtual processor):
 *            The AI implemented by a state-matchine can be programmed by
 *            changing adata-structure (virtual instructions) that drive its,
 *            as opposed to writing specialized and less-flexible C++ code.
 *            The ability of a state-machine to suspend its execution solves
 *            the problem of continuing a manuever that spans multiple frames.
 *//*
 * LEGAL:	COPYRIGHT (C) JIM E. BROOKS 2005
 *          THIS SOURCE CODE IS RELEASED UNDER THE TERMS
 *          OF THE GNU GENERAL PUBLIC LICENSE (GPL).
 *****************************************************************************/

#define GAME_BASE_PILOT_CC 1
#include "game_base_common.hh"
#include "game_base_pilot.hh"
#include "game_base_combat.hh"
#include "gfx_machine.hh"
#include "game_base_machine_pursue.hh"

////////////////////////////////////////////////////////////////////////////////
//////////////////////////////  Pilot class  ///////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

/*****************************************************************************
 * Animation is driven by a timer.
 * See eng_dyna.hh about relationship of Animate() methods.
 *****************************************************************************/
int
PilotGame::Animate( eAnimate animate )
{
	int redisplay = 0;
	switch ( animate )
	{
		case eAnimate_PILOT_PURSUE:
		{
			// TODO calling the machine should be for other modes, not just pursue mode.
			// TODO Need a patrol mode which avoids the case
			// when pursuer hasn't yet acquired a target.
			mProbing = true; // although the pursue AI doesn't probe currently, the effect is to disable crashing
			if ( EX(mPursueAI.IfActive()) )
				mPursueAI.Tick();
			else
				mPursueAI.Step(); // TODO FIXME in lieue of patrol mode
			mProbing = false;
			redisplay = true;
		}
		break;
#if DEBUG
		case eAnimate_PILOT_TERRAIN_FOLLOW:
		{
			// TODO reinstate terrain-following algorithm using state-machine
			//redisplay = TerrainFollow();
			ASSERT(0);
		}
		break;
#endif
		default:
		{
			// Defer to base class.
			redisplay = Parent::Animate( animate );
		}
		break;
	}
	return redisplay;
}

////////////////////////////////////////////////////////////////////////////////
///////////////////////////////  PURSUE AI  ////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

const fp PursueAI::mDegAligned[3] = { 0.80, 0.80, 0.80 };
Vector3 PursueAI::mTargetPos;	// temporary that lives in the current frame

/*****************************************************************************
 * Constructor.
 *****************************************************************************/
PursueAI::PursueAI( void )
{
	mMachine = new Machine( gSmprocPursue, this );
	Init( NULL, NULL );
}
PursueAI::~PursueAI()
{
	DELETE_NULL( mMachine );
}

/*****************************************************************************
 * This algorithm aligns the Craft toward its target by alternately yawing and pitching.
 * It is implement as a state-machine (Machine class).
 *****************************************************************************/
void
PursueAI::Init( Craft* pursuer, Dyna* target )
{
	mPursuer = pursuer;
	mTarget  = target;
	mTickTransformTarget = -1;
}

/*****************************************************************************
 * Transform target into the pursuer's local space.
 *****************************************************************************/
void
PursueAI::TransformTarget( void )
{
	// Translate target into pursuer's local space.
	const fp x = mTarget->mCm[Ox] - mPursuer->mCm[Ox];
	const fp y = mTarget->mCm[Oy] - mPursuer->mCm[Oy];
	const fp z = mTarget->mCm[Oz] - mPursuer->mCm[Oz];

	// Rotate target into pursuer's local space.
	// Effectively a multiplication thru the pursuer's transpose matrix.
	const Matrix& m = mPursuer->mCm;
	mTargetPos.x = x * m[Xx] + y * m[Yx] + z * m[Zx];	// Xx+Yx+Xz rather than Xx+Xy+Xz
	mTargetPos.y = x * m[Xy] + y * m[Yy] + z * m[Zy];
	mTargetPos.z = x * m[Xz] + y * m[Yz] + z * m[Zz];
}
// Transform target once per frame.
// For speed, avoids unnecessary retransformations by PursueIf*().
void
PursueAI::TransformTargetOnce( void )
{
	const int tick = mMachine->GetTick();
	if ( mTickTransformTarget != tick ) TransformTarget();
	mTickTransformTarget = tick;
}

/*****************************************************************************
 * Get maneuverability as a rate of degree/second (rate of axis rotation).
 *****************************************************************************/
fp
PursueAI::GetPursuerManeuverability( uint axis )
{
	return mPursuer->GetManeuverability(axis) * gEng.GetMilsecFrame();
}

/*****************************************************************************
 * Perform a maneuver.
 *****************************************************************************/
void
PursueAI::DoManeuver( uint axis )
{
ASSERT( CHECK_AXIS(axis) );

	// Decide whether to rotate by a coarse or fine degree
	// depending on the angle between the pursuer and target.
	const fp deg = GetPursuerManeuverability( axis );

	// mCalcAngle.mResult holds result from the last CalcAngle()
	// which was recently invoked by the state-machine code.
	if ( IF_DEG_LT( mCalcAngle.mResult, deg ) )
	{
		// Fine degree.
		DoManeuver( axis, mCalcAngle.mResult, mCalcAngle.mResult );
	}
	else
	{
		// Coarse degree.
		DoManeuver( axis, deg, mCalcAngle.mResult );
	}
}

/*****************************************************************************
 * @param axis
 * @param degManeuver
 *        Degree to rotate.
 * @param degTarget
 *        Degree between pursuer and target.
 *****************************************************************************/
void
PursueAI::DoManeuver( uint axis, fp degManeuver, fp degTarget )
{
	// Which direction to rotate is determined by experimentation.
	Rotate( axis, degManeuver );
	const fp degTarget2 = ReCalcAngle();
	if ( IF_DEG_GT( degTarget2, degTarget ) )
		Rotate( axis, -2 * degManeuver ); // undo first rotation, rotate in opposite direction
}

/*****************************************************************************
 * This should be used to rotate the pursuer's matrix as it updates dependent data.
 *****************************************************************************/
void
PursueAI::Rotate( uint axis, fp deg )
{
	// Rotate pursuer's matrix.
	mPursuer->Rotate( axis, deg );

	// Re-transform the target into the pursuer's local space after changing the matrix.
	TransformTarget();
}

/*****************************************************************************
 * Calculate the 2D angle between an axis of the pursuer and target.
 * For determining the angle of rotation of a pitch/roll/yaw maneuver.
 * @return Angle in degrees.
 * @pre mTargetPos holds target position transformed into pursuer's local space.
 *****************************************************************************/
fp
PursueAI::CalcAngle( int ia, int ib )
{
	// Remember args for ReCalcAngle().
	// Why are indexes stored instead of the indexed values?
	// Because mTargetPos[] will be changed by Rotate()
	// prior to when ReCalcAngle() is called.
	mCalcAngle.mArgIa = ia;
	mCalcAngle.mArgIb = ib;

	// Calculate 2D distance and arc cos of coordinate A from (A,B).
	fp a = mTargetPos[ia];
	fp b = mTargetPos[ib];
	fp dist = Distance( Vector2(a,b) );
	fp x = a / dist;
	fp radian = acos( -x );
	fp degree = rad2deg( radian );
	// NaN can be caused by division or the arg passed to acos().
	if ( UX( IFNAN(degree) ) ) return 0.0;
	return (mCalcAngle.mResult = ABS(degree));
}

/*****************************************************************************
 * Re-calculate 2D angle between pursuer's axis and target.
 * @remarks Which direction to rotated is determined by experimentation.
 * ReCalcAngle() is used to catch if a rotation wrongly increased the angle between
 * the pursuer/target which means the opposite direction was the right maneuver.
 *****************************************************************************/
fp
PursueAI::ReCalcAngle( void )
{
	return CalcAngle( mCalcAngle.mArgIa,
                      mCalcAngle.mArgIb );
}

/*****************************************************************************
 * Select a target.
 *****************************************************************************/
void
PursueAI::SelectTarget( void )
{
	// TODO This is limited to Craft objects.

	// In the list of Craft objects, select the nearest Craft object.
	// Exclude the pursuer, the current target, and destroyed Crafts.
	fp nearestDist = WORLD(SQUARE(1000000.0));
	CHECK_TYPESIG( &gGame.mCraftDlist, TYPESIG_LIB_DLIST );
	for ( const Dlink<Craft>* dlink = gGame.mCraftDlist.ForStart();
          gGame.mCraftDlist.ForTest(dlink);
          gGame.mCraftDlist.ForNext(dlink) )
	{
		Craft* craft = dlink->mObj;
		CHECK_TYPESIG(craft,TYPESIG_DYNA);
		if (   craft != mPursuer
            && craft != mTarget
            && ! craft->IfDestroyed()
            && craft != gGame.mPlayer1Craft
            && craft != gGame.mPlayer2Craft )
		{
			fp dist = DistanceSquaredMatrix( craft->mCm, mPursuer->mCm );
			if ( dist < nearestDist )
			{
				nearestDist = dist;
				mTarget = craft;
			}
		}
	}
}

////////////////////////////////////////////////////////////////////////////////
//////////////////// PursueAI methods called indirectly ////////////////////////
//////////////////// by the state-machine procedure ////////////////////////////
////////////////////////////////////////////////////////////////////////////////

const fp maxUnitCoord = 0.984;	// asin(0.984) = 80'

/*****************************************************************************
 * IfRoll/Yaw/Pitch() return true if angle between pursuer/target warrants a maneuver.
 *****************************************************************************/
bool
PursueAI::IfRoll( void )
{
	// Update target position in the pursuer's local space.
	TransformTargetOnce();

	// Rolling isn't effective if the pursuer's Z axis is pointing at the target.
	fp z = NormalizeCoord( ZZ, mTargetPos );
	if ( UX(z > maxUnitCoord) ) return false;

	return IF_DEG_GT( CalcAngleRoll(), mDegAligned[AXIS_ROLL] );
}
bool
PursueAI::IfPitch( void )
{
	// Update target position in the pursuer's local space.
	TransformTargetOnce();

	// Pitching isn't effective the target is to the left/right
	// by a wide angle (100% ineffective if target perpendicular)
	fp x = NormalizeCoord( XX, mTargetPos );
	if ( UX(ABS(x) > maxUnitCoord) ) return false;

	return IF_DEG_GT( CalcAnglePitch(), mDegAligned[AXIS_PITCH] );
}
bool
PursueAI::IfYaw( void )
{
	// Update target position in the pursuer's local space.
	TransformTargetOnce();

	// Don't yaw until pursuer is flying behind/inline with the target.
	// Yaw is for very small rotations.
	if ( ! IfInlineWithTarget() ) return false;

	// Yawing isn't effective if the target is above/below.
	fp y = NormalizeCoord( YY, mTargetPos );
	if ( UX(ABS(y) > maxUnitCoord) ) return false;

	return IF_DEG_GT( CalcAngleYaw(), mDegAligned[AXIS_YAW] );
}

/*****************************************************************************
 * Return true if pursuer is flying inline and behind its target (on the same vector).
 *****************************************************************************/
bool
PursueAI::IfInlineWithTarget( void )
{
	// Update target position in the pursuer's local space.
	TransformTargetOnce();

	fp z = NormalizeCoord( ZZ, mTargetPos );
	return z < -maxUnitCoord;
}

/*****************************************************************************
 * Do a maneuver: pitch/roll/yaw.
 *****************************************************************************/
bool
PursueAI::Roll( void )
{
	DoManeuver( AXIS_ROLL );
	return true;
}
bool
PursueAI::Pitch( void )
{
	DoManeuver( AXIS_PITCH );
	return true;
}
bool
PursueAI::Yaw( void )
{
	DoManeuver( AXIS_YAW );
	return true;
}

/*****************************************************************************
 * Roll so that the pursuer's wings are oriented with the target's wings.
 * Thereafter, the pursuer can maintain orientation by slightly yawing/pitching
 * (until the target maneuvers wide).
 *****************************************************************************/
bool
PursueAI::RollWithTarget( void )
{
	// Calculate angle between pursuer/target Y axises
	// in the XY plane (ignoring Z dimension).
#define CalcAngleRollWithTarget()				\
	Angle( Vector2(mPursuer->mCm[Yx], mPursuer->mCm[Yy]), \
           Vector2( mTarget->mCm[Yx],  mTarget->mCm[Yy]) );
	// Which direction to rotate is determined by experimentation.
	fp rad = CalcAngleRollWithTarget();
	fp deg = Min( ABS(rad2deg(rad)),		// don't exceed maneuverability rate
                  GetPursuerManeuverability(AXIS_Z) );
	if ( IF_DEG_LT( deg, 4.0 ) )
		return false;						// stop when approx aligned
	Rotate( AXIS_Z, deg );
	fp rad2 = CalcAngleRollWithTarget();	// did angle increase?
	if ( IF_RAD_GT( rad2, rad ) )
		Rotate( AXIS_Z, -2 * deg );			// undo first rotation, rotate in opposite direction
	return true;
}

/*****************************************************************************
 * Translate pursuer forward.
 *****************************************************************************/
bool
PursueAI::Step( void )
{
	mPursuer->Step();
	//TransformTarget(); // would be a discarded calculation
	return true;
}

/*****************************************************************************
 * Adjust speed.
 * If the target is far away, speed up.
 * If the target is near, slow down to avoid overshooting and to increase turn radius.
 *****************************************************************************/
bool
PursueAI::AdjustSpeed( void )
{
	const fp dist = DistanceSquaredMatrix( mPursuer->mCm, mTarget->mCm );

	if      ( dist > WORLD(SQUARE(1000.0)) ) { mPursuer->SetThrottle( 90 ); }
	else if ( dist > WORLD(SQUARE( 600.0)) ) { mPursuer->SetThrottle( 80 ); }
	else if ( dist > WORLD(SQUARE( 500.0)) ) { mPursuer->SetThrottle( 70 ); }
	else if ( dist > WORLD(SQUARE( 400.0)) ) { mPursuer->SetThrottle( 60 ); }
	else if ( dist > WORLD(SQUARE( 300.0)) ) { mPursuer->SetThrottle( 50 ); }
	else if ( dist > WORLD(SQUARE( 200.0)) ) { mPursuer->SetThrottle( 40 ); }
	else                                     { mPursuer->SetThrottle( 30 );	}

	return true;
}

/*****************************************************************************
 * Fire when within range and aligned towards target.
 *****************************************************************************/
bool
PursueAI::Fire( void )
{
	// TODO: This limits the pursuer to firing when it is flying
	// inline and behind the target.  There isn't code to allow
	// the pursuer to fire from any other angles (head-on eg).
	if ( EX(! IfInlineWithTarget()) )
		return true;

	// Pursuer is aligned with target.
	// Is pursuer within range to fire guns?
	const fp dist = DistanceSquaredMatrix( mPursuer->mCm, mTarget->mCm );
	if ( /*dist >= WORLD(SQUARE(300.0))  &&*/  dist <= WORLD(SQUARE(1200.0)) )
	{
		// Limit rate-of-fire (firing guns every frame will cause dropped frames).
		static int milsecPrevFire = 0;
		if ( gEng.GetMilsecTotal() - milsecPrevFire > 150 )
		{
			CombatFireGuns( *mPursuer, true );
			milsecPrevFire = gEng.GetMilsecTotal();
		}
	}

	// Hit target?
	if ( mTarget->IfDestroyed() )
	{
		// Hit target.
		// Select a new target.
		SelectTarget();
	}

	return true;
}

/*****************************************************************************
 * Check if target was destroyed.
 *****************************************************************************/
bool
PursueAI::CheckTarget( void )
{
	if ( UX(mTarget->IfDestroyed()) )
		SelectTarget();
	return true;
}

////////////////////////////////////////////////////////////////////////////////
/////////////// C function interfaces called by the state-machine //////////////
////////////////////////////////////////////////////////////////////////////////

bool
PursueIfRoll( Machine* machine )
{
	MACHINE_C_FUNC( PursueAI, IfRoll )
}
bool
PursueIfPitch( Machine* machine )
{
	MACHINE_C_FUNC( PursueAI, IfPitch )
}
bool
PursueIfYaw( Machine* machine )
{
	MACHINE_C_FUNC( PursueAI, IfYaw )
}
bool
PursueRoll( Machine* machine )
{
	MACHINE_C_FUNC( PursueAI, Roll )
}
bool
PursuePitch( Machine* machine )
{
	MACHINE_C_FUNC( PursueAI, Pitch )
}
bool
PursueYaw( Machine* machine )
{
	MACHINE_C_FUNC( PursueAI, Yaw )
}
bool
PursueIfInlineWithTarget( Machine* machine )
{
	MACHINE_C_FUNC( PursueAI, IfInlineWithTarget )
}
bool
PursueRollWithTarget( Machine* machine )
{
	MACHINE_C_FUNC( PursueAI, RollWithTarget )
}
bool
PursueStep( Machine* machine )
{
	MACHINE_C_FUNC_TRUE( PursueAI, Step )
}
bool
PursueAdjustSpeed( Machine* machine )
{
	MACHINE_C_FUNC_TRUE( PursueAI, AdjustSpeed )
}
bool
PursueFire( Machine* machine )
{
	MACHINE_C_FUNC_TRUE( PursueAI, Fire )
}
bool
PursueCheckTarget( Machine* machine )
{
	MACHINE_C_FUNC_TRUE( PursueAI, CheckTarget )
}
