/*****************************************************************************
 * $LastChangedDate: 2010-06-26 21:01:07 -0400 (Sat, 26 Jun 2010) $
 * @file
 * @author  Jim E. Brooks  http://www.palomino3d.org
 * @brief   Missiles.
 *//*
 * LEGAL:   COPYRIGHT (C) 2008 JIM E. BROOKS
 *          THIS SOURCE CODE IS RELEASED UNDER THE TERMS
 *          OF THE GNU GENERAL PUBLIC LICENSE VERSION 2 (GPL 2).
 *****************************************************************************/

#define GAME_MISSILE_CC 1
#include "base/module.hh"
using namespace base;
#include "math/module.hh"
#include "math/funcs_trig.hh"
#include "math/funcs_vector.hh"
#include "math/funcs_transform.hh"
#include "math/matrix.hh"
#include "math/matrix_funcs.hh"
using namespace math;
#include "graph/module.hh"
#include "graph/model_cache.hh"
using namespace graph;
#include "object/module.hh"
#include "object/dyna.hh"
using namespace object;
#include "world/module.hh"
using namespace world;
#include "physics/module.hh"
using namespace physics;
#include "control/module.hh"
#include "control/defs_axis.hh"
using namespace control;
#include "fx/module.hh"
#include "fx/fx.hh"
using namespace fx;
#include "sound/module.hh"
using namespace sound;
#include "game/module.hh"
#include "game/missile.hh"
#include "game/defs.hh"

namespace game {

namespace defs {
const SpeedKPH     MISSILE_INITIAL_SPEED_KPH( 1500.0f );
const Meter        MISSILE_LENGTH_GEO( 3.0f );
const Meter        MISSILE_HIT_DISTANCE_GEO( 15.0f );
const Meter        MISSILE_SAFE_DISTANCE_GEO( 100.0f );
const Meter        MISSILE_MAX_DISTANCE_GEO( 4500.0f );  // phony
const string       MISSILE_MODEL_FILENAME = "models/misc/aim9.ac.gz";
const fp           MISSILE_SCALE = 1.0f;
const Milliseconds MISSILE_PARTICLES_LIFETIME( 30*1000 );
const Milliseconds MISSILE_EXPLOSION_LIFETIME( 30*1000 );
const Meter        MISSILE_EXPLOSION_RADIUS_GEO( 40.0f );
const Radian       MISSILE_MANEUVER_ANGLE_ROLL(  Deg2Rad( 10.0f ) );  // per tick
const Radian       MISSILE_MANEUVER_ANGLE_PITCH( Deg2Rad( 3.0f ) );   // per tick
} // namespace defs

////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////  Missile  ///////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

/*****************************************************************************
 * ctor/dtor.
 *****************************************************************************/
Missile::Missile( shptr<Object> target,
                  Object&       launcher,
                  const Speed   launcherSpeed,
                  const Meter   particleRadiusGeo,
                  const bool    malfunction )
:   Projectile(MakeGraph(),
               launcher,
               Missile::ComputeInitialSpeed( launcherSpeed ),
               defs::MISSILE_MAX_DISTANCE_GEO,
               defs::MISSILE_SAFE_DISTANCE_GEO),
    mTarget(target),
    mRotationAxis(AXIS_ROLL),
    mManeuverAngleRoll(defs::MISSILE_MANEUVER_ANGLE_ROLL),
    mManeuverAnglePitch(defs::MISSILE_MANEUVER_ANGLE_PITCH),
    mPredictedManeuverAngleSign(1.0f),
    mHitDistance(world::conv::Meters2Sim(defs::MISSILE_HIT_DISTANCE_GEO)),
    mMalfunction(malfunction)
{
CHECK_TYPESIG(target,TYPESIG_OBJECT);
CHECK_TYPESIG(&launcher,TYPESIG_OBJECT);

    SET_TYPESIG(this,TYPESIG_MISSILE);

    // Create particle system for smoke trail.
    GET_FX().MakeSmokeTrail( defs::MISSILE_PARTICLES_LIFETIME, this );
}

shptr<Graph>
Missile::MakeGraph( void )
{
    // Load and orient 3D model of missile.
    RotationList rotationList;
    rotationList.AddRotation( RotationList::Rotation( YY, Degree(-90.0f) ) );
    return GET_MODEL_CACHE().LoadModel( defs::MISSILE_MODEL_FILENAME,
                                        defs::MISSILE_LENGTH_GEO,
                                        GeoVertex(),
                                        rotationList,
                                        defs::MISSILE_SCALE,
                                        ModelCache::LOAD_COPY );
}

Missile::~Missile()
{
    INVALIDATE_TYPESIG(this,TYPESIG_MISSILE);
}

/*****************************************************************************
 * Compute initial speed of missile.
 *
 * Implementation:
 * In this simulation, the missile instantly reaches its maximum speed.
 * The reason is artificial: the missile very quickly flies away from its launcher
 * to prevent the launcher flying into its own missile!
 *****************************************************************************/
Speed  // CLASS_METHOD
Missile::ComputeInitialSpeed( const Speed launcherSpeed )
{
    const Speed missileSpeed = physics::conv::KPH2Speed( defs::MISSILE_INITIAL_SPEED_KPH );
    return missileSpeed + launcherSpeed;
}

/*****************************************************************************
 * @return Forward displacement (step) of missile to move in this frame/tick.
 *****************************************************************************/
WorldVertex
Missile::Guide( const fp step1 )
{
CHECK_TYPESIG(this,TYPESIG_MISSILE);
CHECK_TYPESIG(mTarget,TYPESIG_OBJECT);

    Matrix            missileMatrix = GetMatrix();
    const WorldVertex missilePos    = missileMatrix.GetPosition();
    const WorldVertex targetPos     = mTarget->GetPosition();

    // Projectile::HandleCollision() does collision-detection.
    // Additionally, missile will explode if it moves near the target as a real missile would.

    // Hit target?
    if ( mHit )
    {
        return WorldVertex(0,0,0);
    }
    // If missile moves extremely close to target, then let it collide/explode.
    else if ( (Distance(targetPos,missilePos) < mHitDistance) )
    {
        // Treat as collision.
        HandleCollision( mTarget );
        return WorldVertex(0,0,0);
    }
    // Hasn't hit anything yet.
    else
    {
        // Roll or pitch missile.
        switch ( mRotationAxis )
        {
            case AXIS_ROLL:
            {
                Maneuver( missileMatrix,  // IN/OUT
                          AXIS_ROLL, AXIS_PITCH,
                          mManeuverAngleRoll,
                          targetPos );
            }
            break;

            case AXIS_PITCH:
            {
                Maneuver( missileMatrix,  // IN/OUT
                          AXIS_PITCH, AXIS_ROLL,
                          mManeuverAnglePitch,
                          targetPos );
            }
            break;
        
            default:
            break;  // else continue same direction
        }

        // Compute direction.
        return WorldVertex( missileMatrix[Xz] * step1,    // XX
                            missileMatrix[Yz] * step1,    // YY
                            missileMatrix[Zz] * step1 );  // ZZ
    }
}

/*****************************************************************************
 * subroutine
 * This maneuvers the missile by recomputing its matrix orientation. 
 *****************************************************************************/
void
Missile::Maneuver( Matrix&           matrix,  /*IN/OUT*/
                   const uint        rotationAxis,
                   const uint        nextRotationAxis,
                   Radian            maneuverAngle, 
                   const WorldVertex targetPos )
{
CHECK_TYPESIG(this,TYPESIG_MISSILE);
CHECK_TYPESIG(mTarget,TYPESIG_OBJECT);
ASSERT_AXIS3(rotationAxis);
ASSERT_AXIS3(nextRotationAxis);

//..............................................................................
// Macros.

// Compute matrix to transform a world vertex into missile's local space.
#define MISSILE_TRANSPOSE_MATRIX() \
    MatrixTranspose( transposeMatrix, matrix );

// Transpose target's position into missile's local space.
#define COMPUTE_TRANSPOSE_TARGET_POS() \
    TranslateRotate<Vector3,WorldVertex,Matrix>( targetPos, transposeMatrix );

// Compute angle between 2 axises of missile (in its local space)
// and the target position (transposed into missile's local space).
#define COMPUTE_ANGLE_MISSILE_AND_TARGET()                                                                      \
    (rotationAxis == AXIS_ROLL) ? Angle3( Vector3( transposeTargetPos[XX], transposeTargetPos[YY], 0.0f ),     \
                                          Vector3( 0.0f, 1.0f, 0.0f  ) )                                       \
    /* AXIS_PITCH */            : Angle3( Vector3( 0.0f, transposeTargetPos[YY], transposeTargetPos[ZZ] ),     \
                                          Vector3( 0.0f, 0.0f, -1.0f ) );

//..............................................................................

    // Transform target into missile's local space.
    Matrix transposeMatrix;
    MISSILE_TRANSPOSE_MATRIX();
    Vector3 transposeTargetPos = COMPUTE_TRANSPOSE_TARGET_POS();

    // Compute angle between an axis of missile and target.
    Radian angle = COMPUTE_ANGLE_MISSILE_AND_TARGET();

    // Use maximum maneuver angle or actual angle if actual angle is smaller.
    // That is, maneuver angle will narrow.
    if ( ABS(angle) < maneuverAngle )
    {
        // This axis of rotation has narrowed as far as it can go.
        // Switch state to maneuver in another axis-of-rotation.
        maneuverAngle = angle;
        mRotationAxis = nextRotationAxis;  // next state
    }

    // Optimize by rotating the same direction as last time
    // to avoid reverse-rotation case.
    maneuverAngle = Radian(fp(maneuverAngle) * mPredictedManeuverAngleSign);
    const Radian minAngle = Deg2Rad( 0.0025f );
    if ( ABS(fp(angle)) > fp(minAngle) )
    {
        // Rotate in one direction.  If the angle increased by mistake,
        // then rotate in the other direction (tracking is done by trial-and-error).
        MatrixRotateLocal( matrix, rotationAxis, maneuverAngle );

        // Update after recomputing matrix.
        MISSILE_TRANSPOSE_MATRIX();
        transposeTargetPos = COMPUTE_TRANSPOSE_TARGET_POS();

        Radian angle2 = COMPUTE_ANGLE_MISSILE_AND_TARGET();
        if ( ABS(fp(angle2)) > ABS(fp(angle)) )
        {
            mPredictedManeuverAngleSign *= -1.0f;
            MatrixRotateLocal( matrix, rotationAxis, -maneuverAngle * 2.0f );
        }
    }

    // FYI: This will cause spiraling as if missile was malfunctioning.
  //mRotationAxis = nextRotationAxis;  // next state

    // Commit rotations.
    SetMatrix( matrix );
}

/*****************************************************************************
 * Explode.
 *****************************************************************************/
void
Missile::Explode( void )
{
CHECK_TYPESIG(this,TYPESIG_MISSILE);

    // Graphics.
    GET_FX().MakeExplosion( defs::MISSILE_EXPLOSION_LIFETIME,
                            GetPosition(),
                            world::conv::Meters2Sim( defs::MISSILE_EXPLOSION_RADIUS_GEO ) );

    // Sound.
    GET_SOUND().Play( "hit.wav.gz", sound::defs::PLAY_ONCE );
}

} // namespace game
