/*
	Rogue Trader.
	Copyright (c) 2007, Darragh Coy.
	All rights reserved.
	Original work(s), source code & author(s) details available at: http://www.darraghcoy.com

	Redistribution and use in source and binary forms, with or without modification, are permitted 
	provided that the following conditions are met:

		* 	Redistributions of source code must retain the above copyright notice, this list of 
			conditions and the following disclaimer.
		* 	Redistributions in binary form must reproduce the above copyright notice, this list of 
			conditions and the following disclaimer in the documentation.
		* 	Neither the name of the author nor the names of contributors may be used to endorse or 
			promote products derived from this software without specific prior written permission.
		* 	The use of any third party libraries, modules or data files provided with the source 
			form of the program must follow and respect the individual licensing terms of these 
			components. You do not have permission to modify or circumvent the licensing terms for 
			provided third party components unless the third party licenses specifically allow this. 
			If you do not agree to the terms of provided third party components then you do not have 
			permission to use them in your derived work(s).
		* 	The terms of this license do not apply to separate (but related) distributions if supplied 
			by the author(s), only to the contents of this source distribution, derived works, and the 
			binaries produced by either modified or unmodified versions of the source code. The terms 
			of this license do not also apply to any third party libraries or components provided with 
			the source distribution. Such third party elements are licensed under their own separate 
			agreements.

	THIS SOFTWARE IS PROVIDED BY DARRAGH COY ''AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, 
	BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
	ARE DISCLAIMED. IN NO EVENT SHALL DARRAGH COY BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
	EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 
	OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 
	OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 
	IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

//##############################################################################
//##############################################################################
//##############################################################################
//##############################################################################
//
//
// Class Name:   Laser
// Purpose:      Simple class that describes a laser weapon and handles drawing
//               of laser beams and hitting of other objects and such.
//
//
//##############################################################################
//##############################################################################
//##############################################################################
//##############################################################################

#include "Laser.h"

//------------------------------------------------------------------------------
// Constant initialisation
//------------------------------------------------------------------------------

const float Laser::MAX_HEAT                             = 2000.0f;      // maximum amount of heat a laser can have
const float Laser::MIN_COOL_TIME                        = 0.25f;        // minimum amount of time a laser must cool for when first overheated

const float Laser::BEAM_WIDTH                           = 2.0f;         // width of the laser beam when drawn
const float Laser::BEAM_WIDTH_RANDOMNESS                = 0.5f;         // Amount the width of the beam can randomnly vary
const float Laser::BEAM_WIDTH_RANDOM_CHANGE_INTERVAL    = 0.03f;        // how much time in seconds must pass before the laser beams width can be randomnly changed again

const float Laser::HIT_PARTICLE_INTERVAL    = 0.06f;        // minimum amount of time that must pass in between spawning particles when the laser Laser::HITs something
const int   Laser::HIT_PARTICLE_COUNT       = 32;           // number of Laser::HIT particles to spawn at a time
const float Laser::HIT_PARTICLE_SIZE1       = 32.0f;        // hit particle: size 1
const float Laser::HIT_PARTICLE_SIZE2       = 16.0f;        // hit particle: size 2
const float Laser::HIT_PARTICLE_VEL1        = 150.0f;       // hit particle: velocity 1
const float Laser::HIT_PARTICLE_VEL2        = 100.0f;       // hit particle: velocity 2
const float Laser::HIT_PARTICLE_TIME1       = 0.8f;         // hit particle: time to live 1
const float Laser::HIT_PARTICLE_TIME2       = 0.5f;         // hit particle: time to live 2
const float Laser::HIT_PARTICLE_R1          = 1.0f;         // hit particle: color red1
const float Laser::HIT_PARTICLE_G1          = 0.9f;         // hit particle: color green1
const float Laser::HIT_PARTICLE_B1          = 0.0f;         // hit particle: color blue1
const float Laser::HIT_PARTICLE_R2          = 1.0f;         // hit particle: color red2
const float Laser::HIT_PARTICLE_G2          = 0.5f;         // hit particle: color green2
const float Laser::HIT_PARTICLE_B2          = 0.0f;         // hit particle: color blue2

const float Laser::LASER_NOISE_DISTANCE     = 96000.0f;     // distance from which lasers can be heard

/*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

Name:     Laser::Laser()
Purpose:  Laser constructor. Initialises the laser properties
Params:   as commented.
Returns:  n/a

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*/

Laser::Laser
(
    float strenght,         // how much damage the laser does
    float coolingAmount,    // how much cooling the laser has
    float colorR,           // laser color: r
    float colorG,           // laser color: g
    float colorB            // laser color: b
)
{
    // Initialise the lasers properties:

    this->strenght          = strenght;
    this->coolingAmount     = coolingAmount;
    this->colorR            = colorR;
    this->colorG            = colorG;
    this->colorB            = colorB;

    // Set the laser as not firing:

    this->firing = false;

    // Set the current heat of the laser to 0

    this->currentHeat = 0;

    // Set the amount of time left till lasers can fire again after cooling to 0

    this->coolingTimeLeft = 0;

    // Set the currennt random amount to add to the width of the beam:

    this->beamWidthRandomness = mlRandomFloat() * BEAM_WIDTH_RANDOMNESS;

    // Set the time since the last batch of hit particles were spawned:

    this->timeSinceHitParticles = HIT_PARTICLE_INTERVAL;

    // Set the time since the last random change in the beams width to 0

    this->timeSinceRandomWidthChange = 0;
}

/*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

Name:     Laser::resetCurrentHeat()
Purpose:  Sets the current heat of the laser to zero
Params:   none
Returns:  none

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*/

void Laser::resetCurrentHeat()
{
    // reset the cooling time left and heat of the laser

    this->coolingTimeLeft   = 0;
    this->currentHeat       = 0;
}

/*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

Name:     Laser::cool()
Purpose:  Cools down the laser.
Params:   Scene that the laser belongs to. Time the last frame took in seconds.
Returns:  none

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*/

void Laser::cool( GameScene * scene , float frameTime )
{
    // Abort if scene given is null

    if ( scene == 0 ) return;

    // If laser was previously firing: stop the current laser sound in the scene:

    if ( firing == true ) scene->deleteSound( sndLaserID );

    // Cool down the laser by cooling amount:

    this->currentHeat -= this->coolingAmount * frameTime;

    // If current heat is negative then clamp to zero:

    if ( currentHeat < 0 ) currentHeat = 0;

    // Set the laser to not firing:

    this->firing = false;
}

/*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

Name:     Laser::fire()
Purpose:  Fires the laser from given point and in given direction, inflicting
          damage on whatever is hit.
Params:   as commented
Returns:  none

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*/

void Laser::fire
(
    GameScene  *    scene,          // scene in which the laser is being fired
    GameObject *    firer,          // object that fired the laser- will not be hit by the laser
    float           frameTime,      // time the last frame took in seconds
    float           originX,        // point from which it is being fired: x
    float           originY,        // point from which it is being fired: y
    float           originZ,        // point from which it is being fired: z
    float           directionX,     // direction in which it is being fired: x (does not need to be normalised)
    float           directionY,     // direction in which it is being fired: y (does not need to be normalised)
    float           directionZ      // direction in which it is being fired: z (does not need to be normalised)
)
{
    //--------------------------------------------------------------------------
    // Abort if null scene given:
    //--------------------------------------------------------------------------

    if ( scene == 0  ) return;

    //--------------------------------------------------------------------------
    // Increment the time since the last batch of laser hit particles if the
    // amount is not already passed the minimum time needed to pass before
    // spawning particles again:
    //--------------------------------------------------------------------------

    if ( timeSinceHitParticles < HIT_PARTICLE_INTERVAL )
    {
        timeSinceHitParticles += frameTime;
    }

    //--------------------------------------------------------------------------
    // If the laser heat level is at the current maximum then do not fire the weapon:
    //--------------------------------------------------------------------------

    if ( currentHeat >= MAX_HEAT )
    {
        // Set the cooling time left to the minimum amount of the time the laser must cool for after overheating

        coolingTimeLeft = MIN_COOL_TIME;

        // Cannot fire, laser too hot: cool and abort firing:

        cool(scene,frameTime); return;
    }
    else
    {
        //----------------------------------------------------------------------
        // See if the laser has to spend more time cooling before firing again:
        //----------------------------------------------------------------------

        if ( coolingTimeLeft <= 0 )
        {
            //------------------------------------------------------------------
            // Allowed fire laser: set beam start point and heat up laser
            //------------------------------------------------------------------

            // If the laser was not previously firing then create a laser noise in the scene in which the laser was fired

            if ( firing == false )
            {
                // Create the laser sound in the scene

                this->sndLaserID = scene->addSound
                (
                    SoundManager::getSndLaser(),    // sound to add
                    originX,                        // sound position
                    originY,                        // sound position
                    originZ,                        // sound position
                    LASER_NOISE_DISTANCE,           // distance over which sound can be heard
                    1,                              // frequency of sound
                    true                            // looping sound ?
                );
            }
            else
            {
                // Laser already firing: update laser sound position

                scene->updateSoundPosition
                (
                    sndLaserID,     // ID of sound to update
                    originX,        // sound position
                    originY,        // sound position
                    originZ         // sound position
                );
            }

            // set start point for the laser beam

            this->beamStartX = originX;
            this->beamStartY = originY;
            this->beamStartZ = originZ;

            // set the laser as firing

            this->firing = true;

            // Heat up the laser according to laser strenght amount:

            this->currentHeat += this->strenght * frameTime;

            // But also cool it by its cooling factor:

            this->currentHeat -= this->coolingAmount * frameTime;

            // If current heat negative then clamp:

            if ( currentHeat < 0 ) currentHeat = 0;

            //------------------------------------------------------------------
            // If enough time has passed since the last random change in the beam
            // width then randomnly change the width of the beam again:
            //------------------------------------------------------------------

            if ( timeSinceRandomWidthChange >= BEAM_WIDTH_RANDOM_CHANGE_INTERVAL )
            {
                // Time to randomnly change beam width again:

                this->beamWidthRandomness = mlRandomFloat() * BEAM_WIDTH_RANDOMNESS;

                // Set the time passed since the last random change to zero again

                timeSinceRandomWidthChange = 0;
            }
            else
            {
                // Not enough time has passed to change beam width. Increment the time passed:

                timeSinceRandomWidthChange += frameTime;
            }
        }
        else
        {
            //------------------------------------------------------------------
            // Laser must cool for a little while longer before re firing:
            // decrement cooling time left:
            //------------------------------------------------------------------

            coolingTimeLeft -= frameTime;

            // Cool the laser

            cool(scene,frameTime);

            // Abort firing

            return;

        }   // end else of if ( coolingTimeLeft <= 0 )

    }   // end else of if ( currentHeat >= MAX_HEAT )

    //--------------------------------------------------------------------------
    // Fill in arrays to hold the origin and direction for a ray cast:
    //--------------------------------------------------------------------------

    float origin[3] =
    {
        originX,
        originY,
        originZ
    };

    float direction[3] =
    {
        directionX,
        directionY,
        directionZ
    };

    // Save the hit point here:

    float hitPoint[3];

    //--------------------------------------------------------------------------
    // Do a ray cast from the given point in the given direction and see what object it hits:
    //--------------------------------------------------------------------------

    GameObject * hitObject = scene->rayCast
    (
        firer,      // object to exclude from the ray cast: do not want to hit the object which fired the laser
        origin,
        direction,
        hitPoint
    );

    //--------------------------------------------------------------------------
    // Now see if an object was hit:
    //--------------------------------------------------------------------------

    if ( hitObject != 0 )
    {
        //----------------------------------------------------------------------
        // Hit an object:
        //----------------------------------------------------------------------

        // Damage the object hit

        hitObject->damage( this->strenght * frameTime , firer );

        // Set the end point of the laser as the hit point:

        this->beamEndX = hitPoint[0];
        this->beamEndY = hitPoint[1];
        this->beamEndZ = hitPoint[2];

        //----------------------------------------------------------------------
        // If enough time has passed since the last batch of hit particles then
        // spawn another bunch at the hit point:
        //----------------------------------------------------------------------

        if ( timeSinceHitParticles >= HIT_PARTICLE_INTERVAL )
        {
            // Spawn a bunch of particles at the hit point

            scene->createExplosion
            (
                hitPoint[0],            // explosion position
                hitPoint[1],
                hitPoint[2],
                HIT_PARTICLE_R1,        // 1st explosion color
                HIT_PARTICLE_G1,
                HIT_PARTICLE_B1,
                HIT_PARTICLE_R2,        // 2nd explosion color
                HIT_PARTICLE_G2,
                HIT_PARTICLE_B2,
                HIT_PARTICLE_SIZE1,     // 1st particle size
                HIT_PARTICLE_SIZE2,     // 2nd particle size
                HIT_PARTICLE_VEL1,      // 1st particle velocity
                HIT_PARTICLE_VEL2,      // 2nd particle velocity
                HIT_PARTICLE_TIME1,     // 1st particle live time
                HIT_PARTICLE_TIME2,     // 2nd particle live time
                HIT_PARTICLE_COUNT      // number of particles to spawn
            );

            // Create a laser hit sound in the scene:

            scene->addSound
            (
                SoundManager::getSndLaserHit(), // sound to add
                hitPoint[0],                    // sound position
                hitPoint[1],                    // sound position
                hitPoint[2],                    // sound position
                LASER_NOISE_DISTANCE,           // distance over which sound can be heard
                1,                              // frequency of sound
                false                           // looping sound ?
            );

            // Set the time passed since the last batch of particles to zero

            timeSinceHitParticles = 0;
        }
    }
    else
    {
        //----------------------------------------------------------------------
        // Did not hit an object:
        //----------------------------------------------------------------------

        // Make a normalised copy of the direciton vector:

        float vector[3] =
        {
            direction[0],
            direction[1],
            direction[2]
        };

        mlNormalise3d(vector);

        // Now scale it by a large amount:

        vector[0] *= 999999;
        vector[1] *= 999999;
        vector[2] *= 999999;

        // Now set the beam end point:

        beamEndX = beamStartX + vector[0];
        beamEndY = beamStartY + vector[1];
        beamEndZ = beamStartZ + vector[2];

    }   // end else of if ( hitObject != 0 )
}

/*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

Name:     Laser::draw()
Purpose:  Draws the laser beam. Will not do anything however if the laser is not
          currently firing
Params:   camera the scene is being viewed from
Returns:  none

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*/

void Laser::draw ( Camera * camera )
{
    //--------------------------------------------------------------------------
    // If the laser is not firing then do not bother:
    //--------------------------------------------------------------------------

    if ( firing == false ) return;

    //--------------------------------------------------------------------------
    // Save the opengl enables state:
    //--------------------------------------------------------------------------

    glPushAttrib( GL_ENABLE_BIT );

    //--------------------------------------------------------------------------
    // Now setup the opengl state for beam drawing
    //--------------------------------------------------------------------------

    glDisable( GL_CULL_FACE );
    glDisable( GL_LIGHTING  );
    glEnable ( GL_BLEND     );

    glDepthMask(GL_FALSE);
    glBlendFunc( GL_SRC_ALPHA , GL_ONE );

    //--------------------------------------------------------------------------
    // Get the position of the camera
    //--------------------------------------------------------------------------

    float cameraPos[3]; camera->getPosition(cameraPos);

    //--------------------------------------------------------------------------
    // Get a vector going from the beams position to the cameras position
    //--------------------------------------------------------------------------

    float vectorToCamera[3] =
    {
        cameraPos[0] - beamStartX,
        cameraPos[1] - beamStartY,
        cameraPos[2] - beamStartZ
    };

    //--------------------------------------------------------------------------
    // Get the beams un normalised direction vector
    //--------------------------------------------------------------------------

    float beamVector[3] =
    {
        beamEndX - beamStartX,
        beamEndY - beamStartY,
        beamEndZ - beamStartZ
    };

    //--------------------------------------------------------------------------
    // Declare a vector that will hold the direction to expand the beam outwards
    //--------------------------------------------------------------------------

    float expandVector[3];

    //--------------------------------------------------------------------------
    // Do a cross product with the vector to the camera. this will give a vector
    // to expand the thickness of the beam outwards along
    //--------------------------------------------------------------------------

    mlCross3d( vectorToCamera , beamVector , expandVector);

    //--------------------------------------------------------------------------
    // Check the magnitude of this vector. If it is very small then that means
    // that the beam and vectorToCamera vectors were almost colinear and hence
    // the expand vector will vary wildy since the camera is nearly or is on the
    // line of the beam. if this is the case then do not draw the beam:
    //--------------------------------------------------------------------------

    if ( mlMagnitude3d(expandVector) < 0.01f ) return;

    //--------------------------------------------------------------------------
    // Normalise the expand vector
    //--------------------------------------------------------------------------

    mlNormalise3d( expandVector );

    //--------------------------------------------------------------------------
    // Set the beam color and select the beam texture
    //--------------------------------------------------------------------------

    glColor4f(colorR,colorG,colorB,1); GLTextureManager::getTexBeam()->bind();

    //--------------------------------------------------------------------------
    // Now draw the beam, expanded outwards along the expansion vector:
    //--------------------------------------------------------------------------

    glBegin( GL_QUADS );

        glTexCoord2f( 0 , 0 );

        glVertex3f
        (
            beamStartX - expandVector[0] * ( BEAM_WIDTH + beamWidthRandomness ),
            beamStartY - expandVector[1] * ( BEAM_WIDTH + beamWidthRandomness ),
            beamStartZ - expandVector[2] * ( BEAM_WIDTH + beamWidthRandomness )
        );

        glTexCoord2f( 0 , 1 );

        glVertex3f
        (
            beamStartX + expandVector[0] * ( BEAM_WIDTH + beamWidthRandomness ),
            beamStartY + expandVector[1] * ( BEAM_WIDTH + beamWidthRandomness ),
            beamStartZ + expandVector[2] * ( BEAM_WIDTH + beamWidthRandomness )
        );

        glTexCoord2f( 1 , 1 );

        glVertex3f
        (
            beamEndX + expandVector[0] * ( BEAM_WIDTH + beamWidthRandomness ),
            beamEndY + expandVector[1] * ( BEAM_WIDTH + beamWidthRandomness ),
            beamEndZ + expandVector[2] * ( BEAM_WIDTH + beamWidthRandomness )
        );

        glTexCoord2f( 1 , 0 );

        glVertex3f
        (
            beamEndX - expandVector[0] * ( BEAM_WIDTH + beamWidthRandomness ),
            beamEndY - expandVector[1] * ( BEAM_WIDTH + beamWidthRandomness ),
            beamEndZ - expandVector[2] * ( BEAM_WIDTH + beamWidthRandomness )
        );

    glEnd();

    //--------------------------------------------------------------------------
    // Restore prior opengl enables state
    //--------------------------------------------------------------------------

    glPopAttrib();
}
