/*==LICENSE==*

CyanWorlds.com Engine - MMOG client, server and tools
Copyright (C) 2011  Cyan Worlds, Inc.

This program 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.

This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.

You can contact Cyan Worlds, Inc. by email legal@cyan.com
 or by snail mail at:
      Cyan Worlds, Inc.
      14617 N Newport Hwy
      Mead, WA   99021

*==LICENSE==*/
#include "hsTypes.h"
#include "hsGeometry3.h"
#include "plParticle.h"
#include "plParticleEffect.h"
#include "plEffectTargetInfo.h"
#include "plConvexVolume.h"
#include "plBoundInterface.h"
#include "hsResMgr.h"
#include "plPipeline.h"
#include "hsFastMath.h"
#include "plMath/plRandom.h"
#include "plParticleSystem.h"
#include "plMessage/plParticleUpdateMsg.h"

///////////////////////////////////////////////////////////////////////////////////////////
plParticleCollisionEffect::plParticleCollisionEffect()
{
    fBounds = nil;
    fSceneObj = nil;
}

plParticleCollisionEffect::~plParticleCollisionEffect()
{
}

void plParticleCollisionEffect::PrepareEffect(const plEffectTargetInfo &target)
{
    if (fBounds == nil)
    {
        plBoundInterface *bi = plBoundInterface::ConvertNoRef(fSceneObj->GetGenericInterface(plBoundInterface::Index()));
        if (bi == nil)
            return;
        fBounds = bi->GetVolume();
    }
}

hsBool plParticleCollisionEffect::MsgReceive(plMessage* msg)
{
    plRefMsg* refMsg = plRefMsg::ConvertNoRef(msg);
    plSceneObject *so;
    if (refMsg)
    {
        if (so = plSceneObject::ConvertNoRef(refMsg->GetRef()))
        {
            if( refMsg->GetContext() & (plRefMsg::kOnCreate|plRefMsg::kOnRequest|plRefMsg::kOnReplace) )
                fSceneObj = so;
            else
                fSceneObj = nil;
            return true;
        }
    }
    return hsKeyedObject::MsgReceive(msg);
}

void plParticleCollisionEffect::Read(hsStream *s, hsResMgr *mgr)
{
    hsKeyedObject::Read(s, mgr);

    plGenRefMsg* msg;
    msg = TRACKED_NEW plGenRefMsg(GetKey(), plRefMsg::kOnCreate, 0, 0); // SceneObject
    mgr->ReadKeyNotifyMe(s, msg, plRefFlags::kActiveRef);
    fBounds = nil;
}

void plParticleCollisionEffect::Write(hsStream *s, hsResMgr *mgr)
{
    hsKeyedObject::Write(s, mgr);

    mgr->WriteKey(s, fSceneObj);
}

///////////////////////////////////////////////////////////////////////////////////////////
// Some permutations on the CollisionEffect follow
///////////////////////////////////////////////////////////////////////////////////////////

plParticleCollisionEffectBeat::plParticleCollisionEffectBeat()
{
}

hsBool plParticleCollisionEffectBeat::ApplyEffect(const plEffectTargetInfo &target, Int32 i)
{
    hsAssert(i >= 0, "Use of default argument doesn't make sense for plParticleCollisionEffect");

    if( !fBounds )
        return false;

    hsPoint3 *currPos = (hsPoint3 *)(target.fPos + i * target.fPosStride);
    fBounds->ResolvePoint(*currPos);    

    return false;
}

///////////////////////////////////////////////////////////////////////////////////////////

plParticleCollisionEffectDie::plParticleCollisionEffectDie()
{
}

hsBool plParticleCollisionEffectDie::ApplyEffect(const plEffectTargetInfo &target, Int32 i)
{
    hsAssert(i >= 0, "Use of default argument doesn't make sense for plParticleCollisionEffect");

    if( !fBounds )
        return false;

    hsPoint3 *currPos = (hsPoint3 *)(target.fPos + i * target.fPosStride);
    return fBounds->IsInside(*currPos); 
}

///////////////////////////////////////////////////////////////////////////////////////////

plParticleCollisionEffectBounce::plParticleCollisionEffectBounce()
:   fBounce(1.f),
    fFriction(0.f)
{
}

hsBool plParticleCollisionEffectBounce::ApplyEffect(const plEffectTargetInfo &target, Int32 i)
{
    hsAssert(i >= 0, "Use of default argument doesn't make sense for plParticleCollisionEffect");

    if( !fBounds )
        return false;

    hsPoint3* currPos = (hsPoint3 *)(target.fPos + i * target.fPosStride);
    hsVector3* currVel = (hsVector3*)(target.fVelocity + i * target.fVelocityStride);
    fBounds->BouncePoint(*currPos, *currVel, fBounce, fFriction);   

    return false;
}

void plParticleCollisionEffectBounce::Read(hsStream *s, hsResMgr *mgr)
{
    plParticleCollisionEffect::Read(s, mgr);

    fBounce = s->ReadSwapScalar();
    fFriction = s->ReadSwapScalar();
}

void plParticleCollisionEffectBounce::Write(hsStream *s, hsResMgr *mgr)
{
    plParticleCollisionEffect::Write(s, mgr);

    s->WriteSwapScalar(fBounce);
    s->WriteSwapScalar(fFriction);
}



///////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////

plParticleFadeVolumeEffect::plParticleFadeVolumeEffect() : fLength(100.0f), fIgnoreZ(true)
{
}

plParticleFadeVolumeEffect::~plParticleFadeVolumeEffect()
{
}

//
// It's not really clear looking at the math here what's actually going on,
// but once you visualize it, it's pretty easy to follow. So the camera position,
// view direction, and length of the fade volume define a sphere, where the camera
// position is a point on the sphere, the view direction points from that surface
// point toward the center, and the length is the sphere's radius. Since the view
// direction points straight through the sphere, that sphere is the sweet zone for
// putting our particles to pile them up in front of the camera. But we'd like to
// do this independently for each axis (for efficiency, rather than true 3D calculations),
// so we put an axis aligned box around the sphere, and that's the volume we restrict
// our particles to.
// Now we could fade all around the box, but that's not really what we want, because
// that means fading particles that are behind us. And in the case where we're looking
// along an axis, the camera is pushed up against a face of the box (where the axis
// aligned box is tangent to the inscribed sphere), so we'd actually be fading 
// particles just in front of the camera. Because of this non-symmetry, we're going to
// define the Max in a given dimension as the world space value for that dimension 
// FARTHEST from the camera (NOT largest in value). So if the camera is looking
// in a negative direction in one dimension, the Max will be less than the Min for
// that dimension.
// So we set up our Max's and Min's for each dimension in PrepareEffect(), and then
// at runtime we calculate the parameter value of the particle ranging from 0 where
// particleLoc == Min to 1 where particleLoc == Max. If the parameter is outside
// [0..1], then we can move it into the box using the fractional part of the parameter.
// Finally, if the (possibly relocated) parameter value says the particle is approaching
// the Max value, we can calclulate its faded opacity from the parameter.
//

// Need to experiment to minimize this fade distance. The greater
// the fade distance, the more faded out (wasted) particles we're drawing.
// The shorter the distance, the more noticable the fade out.
// Note the wierdness between the fractions, because kFadeFrac is fraction
// of fLength, but kFadeParm and kInvFadeFrac are fraction of 2.f*fLength. Sorry.
const hsScalar kFadeFrac = 0.5f;
const hsScalar kFadeParm = 1.f - kFadeFrac * 0.5f;
const hsScalar kInvFadeFrac = 1.f / (kFadeFrac * 0.5f);

void plParticleFadeVolumeEffect::PrepareEffect(const plEffectTargetInfo &target)
{
    hsPoint3 viewLoc = target.fContext.fPipeline->GetViewPositionWorld();
    hsVector3 viewDir = target.fContext.fPipeline->GetViewDirWorld();

    // Nuking out the setting of viewDir.fZ to 0 when we aren't centering
    // about Z (fIgnoreZ == true), because we still want to center our
    // volume about the camera (rather than push the camera to the edge of
    // the cylinder) in that case, so we don't get artifacts when we look
    // straight up or down. mf

    hsPoint3 signs(viewDir.fX >= 0 ? 1.f : -1.f, viewDir.fY >= 0 ? 1.f : -1.f, viewDir.fZ >= 0 ? 1.f : -1.f);

    fMax.fX = viewLoc.fX + (viewDir.fX + signs.fX) * fLength;
    fMin.fX = fMax.fX - 2.f * signs.fX * fLength;

    fMax.fY = viewLoc.fY + (viewDir.fY + signs.fY) * fLength;
    fMin.fY = fMax.fY - 2.f * signs.fY * fLength;

    fMax.fZ = viewLoc.fZ + (viewDir.fZ + signs.fZ) * fLength;
    fMin.fZ = fMax.fZ - 2.f * signs.fZ * fLength;

    fNorm.fX = 1.f / (fMax.fX - fMin.fX);
    fNorm.fY = 1.f / (fMax.fY - fMin.fY);
    fNorm.fZ = 1.f / (fMax.fZ - fMin.fZ);
}

hsBool plParticleFadeVolumeEffect::ApplyEffect(const plEffectTargetInfo& target, Int32 i)
{
    hsPoint3 *currPos = (hsPoint3 *)(target.fPos + i * target.fPosStride);

    hsScalar parm;

    hsScalar fade = 1.f;

    parm = (currPos->fX - fMin.fX) * fNorm.fX;
    if( parm < 0 )
    {
        parm -= int(parm);
        currPos->fX = fMax.fX + parm * (fMax.fX - fMin.fX);
        parm += 1.f;
    }
    else if( parm > 1.f )
    {
        parm -= int(parm);
        currPos->fX = fMin.fX + parm * (fMax.fX - fMin.fX);
    }
    if( parm > kFadeParm )
    {
        parm = 1.f - parm;
        parm *= kInvFadeFrac;
        if( parm < fade )
            fade = parm;
    }

    parm = (currPos->fY - fMin.fY) * fNorm.fY;
    if( parm < 0 )
    {
        parm -= int(parm);
        currPos->fY = fMax.fY + parm * (fMax.fY - fMin.fY);
        parm += 1.f;
    }
    else if( parm > 1.f )
    {
        parm -= int(parm);
        currPos->fY = fMin.fY + parm * (fMax.fY - fMin.fY);
    }
    if( parm > kFadeParm )
    {
        parm = 1.f - parm;
        parm *= kInvFadeFrac;
        if( parm < fade )
            fade = parm;
    }

    if( !fIgnoreZ )
    {
        parm = (currPos->fZ - fMin.fZ) * fNorm.fZ;
        if( parm < 0 )
        {
            parm -= int(parm);
            currPos->fZ = fMax.fZ + parm * (fMax.fZ - fMin.fZ);
            parm += 1.f;
        }
        else if( parm > 1.f )
        {
            parm -= int(parm);
            currPos->fZ = fMin.fZ + parm * (fMax.fZ - fMin.fZ);
        }
        if( parm > kFadeParm )
        {
            parm = 1.f - parm;
            parm *= kInvFadeFrac;
            if( parm < fade )
                fade = parm;
        }
    }

    if( fade < 1.f )
    {
        UInt32 *color = (UInt32 *)(target.fColor + i * target.fColorStride);
        UInt32 alpha = (UInt32)((*color >> 24) * fade);
        *color = (*color & 0x00ffffff) | (alpha << 24);
    }

    return false;
}

void plParticleFadeVolumeEffect::Read(hsStream *s, hsResMgr *mgr)
{
    hsKeyedObject::Read(s, mgr);

    fLength = s->ReadSwapScalar();
    fIgnoreZ = s->ReadBool();
}

void plParticleFadeVolumeEffect::Write(hsStream *s, hsResMgr *mgr)
{
    hsKeyedObject::Write(s, mgr);

    s->WriteSwapScalar(fLength);
    s->WriteBool(fIgnoreZ);
}

////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////
// Particle wind - Base class first
plParticleWindEffect::plParticleWindEffect()
:   fWindVec(0,0,0),
    fDir(1.f,0,0),
    fSwirl(0.1f),
    fConstancy(0),
    fHorizontal(0),
    fLastDirSecs(-1.f),
    fRefDir(0.f,0.f,0.f),
    fRandDir(1.f,0.f,0.f)
{
}

plParticleWindEffect::~plParticleWindEffect()
{
}

void plParticleWindEffect::Read(hsStream *s, hsResMgr *mgr)
{
    hsKeyedObject::Read(s, mgr);

    fStrength = s->ReadSwapScalar();
    fConstancy = s->ReadSwapScalar();
    fSwirl = s->ReadSwapScalar();
    fHorizontal = s->ReadBool();
    fRefDir.Read(s);
    fDir.Read(s);
    fRandDir = fDir;
}

void plParticleWindEffect::Write(hsStream *s, hsResMgr *mgr)
{
    hsKeyedObject::Write(s, mgr);

    s->WriteSwapScalar(fStrength);
    s->WriteSwapScalar(fConstancy);
    s->WriteSwapScalar(fSwirl);
    s->WriteBool(fHorizontal);
    fRefDir.Write(s);
    fDir.Write(s);
}

void plParticleWindEffect::SetRefDirection(const hsVector3& v)
{
    fRefDir = v;
    hsScalar lenSq = fRefDir.MagnitudeSquared();
    if( lenSq > 1.e-1f )
    {
        fDir = fRefDir * hsFastMath::InvSqrtAppr(lenSq);
        fRandDir = fDir;
    }
}

void plParticleWindEffect::PrepareEffect(const plEffectTargetInfo& target)
{
    if( fLastDirSecs != target.fContext.fSecs )
    {
        static plRandom random;
        fRandDir.fX += random.RandMinusOneToOne() * fSwirl;
        fRandDir.fY += random.RandMinusOneToOne() * fSwirl;
        if( !GetHorizontal() )
            fRandDir.fZ += random.RandMinusOneToOne() * fSwirl;
        hsFastMath::NormalizeAppr(fRandDir);
        fDir = fRandDir + fRefDir;
        hsFastMath::NormalizeAppr(fDir);

        fWindVec = fDir * (fStrength * target.fContext.fSystem->GetWindMult() * target.fContext.fDelSecs);

        fLastDirSecs = target.fContext.fSecs;
    }
}

////////////////////////////////////////////////////////////////////////
// Localized wind (how much wind you're getting depends on where you are
plParticleLocalWind::plParticleLocalWind()
:   fScale(0, 0, 0),
    fSpeed(0),
    fPhase(0,0,0),
    fInvScale(0,0,0),
    fLastPhaseSecs(-1.f)
{
}

plParticleLocalWind::~plParticleLocalWind()
{
}

void plParticleLocalWind::Read(hsStream *s, hsResMgr *mgr)
{
    plParticleWindEffect::Read(s, mgr);

    fScale.Read(s);
    fSpeed = s->ReadSwapScalar();
}

void plParticleLocalWind::Write(hsStream *s, hsResMgr *mgr)
{
    plParticleWindEffect::Write(s, mgr);

    fScale.Write(s);
    s->WriteSwapScalar(fSpeed);
}

void plParticleLocalWind::PrepareEffect(const plEffectTargetInfo& target)
{
    if( fLastPhaseSecs != target.fContext.fSecs )
    {
        plParticleWindEffect::PrepareEffect(target);

        fPhase += fDir * fSpeed * target.fContext.fDelSecs;

        fInvScale.fX = fScale.fX > 0 ? 1.f / fScale.fX : 0;
        fInvScale.fY = fScale.fY > 0 ? 1.f / fScale.fY : 0;
        fInvScale.fZ = fScale.fZ > 0 ? 1.f / fScale.fZ : 0;

        fLastPhaseSecs = target.fContext.fSecs;
    }
}


hsBool plParticleLocalWind::ApplyEffect(const plEffectTargetInfo& target, Int32 i)
{
    const hsPoint3& pos = *(hsPoint3 *)(target.fPos + i * target.fPosStride);
    hsVector3& vel = *(hsVector3*)(target.fVelocity + i * target.fVelocityStride);

    const hsScalar kMinToBother = 0;

    float strength = 1.f / ( (1.f + fConstancy) * (1.f + fConstancy) );
    float s, c, t;
    t = (pos[0] - fPhase[0]) * fInvScale[0];
    hsFastMath::SinCosAppr(t, s, c);
    c += fConstancy;
    if( c <= kMinToBother )
        return false;
    strength *= c;

    t = (pos[1] - fPhase[1]) * fInvScale[1];
    hsFastMath::SinCosAppr(t, s, c);
    c += fConstancy;
    if( c <= kMinToBother )
        return false;
    strength *= c;

#if 0 // if you turn this back on, strength needs to drop by another factor of (1.f + fConstancy)
    t = (pos[2] - fPhase[2]) * fInvScale[2];
    hsFastMath::SinCosAppr(t, s, c);
    c += fConstancy;
    if( c <= kMinToBother )
        return false;
    strength *= c;
#endif

    const hsScalar& invMass = *(hsScalar*)(target.fInvMass + i * target.fInvMassStride);
    strength *= invMass;

    vel += fWindVec * strength;

    return false;
}

////////////////////////////////////////////////////////////////////////
// Uniform wind - wind changes over time, but not space
plParticleUniformWind::plParticleUniformWind()
:   fFreqMin(0.1f),
    fFreqMax(0.2f),
    fFreqCurr(0.1f),
    fFreqRate(0.05f),

    fCurrPhase(0),
    fLastFreqSecs(-1.f),
    fCurrentStrength(0)
{
}

plParticleUniformWind::~plParticleUniformWind()
{
}

void plParticleUniformWind::Read(hsStream *s, hsResMgr *mgr)
{
    plParticleWindEffect::Read(s, mgr);

    fFreqMin = s->ReadSwapScalar();
    fFreqMax = s->ReadSwapScalar();
    fFreqRate = s->ReadSwapScalar();

#if 0
    fFreqMin = 1.f / 6.f;
    fFreqMax = 1.f / 1.f;
    fConstancy = -0.5f;
    fSwirl = 0.05f;
#endif

    fFreqCurr = fFreqMin;
}

void plParticleUniformWind::Write(hsStream *s, hsResMgr *mgr)
{
    plParticleWindEffect::Write(s, mgr);

    s->WriteSwapScalar(fFreqMin);
    s->WriteSwapScalar(fFreqMax);
    s->WriteSwapScalar(fFreqRate);
}

void plParticleUniformWind::SetFrequencyRange(hsScalar minSecsPerCycle, hsScalar maxSecsPerCycle)
{
    const hsScalar kMinSecsPerCycle = 1.f;
    if( minSecsPerCycle < kMinSecsPerCycle )
        minSecsPerCycle = kMinSecsPerCycle;
    if( minSecsPerCycle < kMinSecsPerCycle )
        minSecsPerCycle = kMinSecsPerCycle;

    if( minSecsPerCycle < maxSecsPerCycle )
    {
        fFreqMin = 1.f / maxSecsPerCycle;
        fFreqMax = 1.f / minSecsPerCycle;
    }
    else
    {
        fFreqMin = 1.f / minSecsPerCycle;
        fFreqMax = 1.f / maxSecsPerCycle;
    }
}

void plParticleUniformWind::SetFrequencyRate(hsScalar secsPerCycle)
{
    const hsScalar kMinSecsPerCycle = 1.f;
    if( secsPerCycle < kMinSecsPerCycle )
        secsPerCycle = kMinSecsPerCycle;
    fFreqRate = 1.f / secsPerCycle;
}


void plParticleUniformWind::PrepareEffect(const plEffectTargetInfo& target)
{
    plParticleWindEffect::PrepareEffect(target);

    if( fLastFreqSecs != target.fContext.fSecs )
    {
        static plRandom random;

        const double kTwoPi = hsScalarPI * 2.0;
        double t0 = fFreqCurr * fLastFreqSecs + fCurrPhase;
        hsScalar t1 = (hsScalar)fmod(t0, kTwoPi);
        fCurrPhase -= t0 - t1;

        fFreqCurr += fFreqRate * target.fContext.fDelSecs * random.RandZeroToOne();

        if( fFreqCurr > fFreqMax )
        {
            fFreqCurr = fFreqMax;
            fFreqRate = -fFreqRate;
        }
        else if( fFreqCurr < fFreqMin )
        {
            fFreqCurr = fFreqMin;
            fFreqRate = -fFreqRate;
        }
        
        hsScalar phaseDel = (hsScalar)(t1 - (fFreqCurr * fLastFreqSecs + fCurrPhase));
        fCurrPhase += phaseDel;
        
        hsScalar t = hsScalar(fFreqCurr * target.fContext.fSecs + fCurrPhase);
        hsScalar s;
        hsFastMath::SinCosAppr(t, s, fCurrentStrength);
        fCurrentStrength += fConstancy;
        fCurrentStrength /= (1.f + fConstancy);
        
        if( fCurrentStrength < 0 )
            fCurrentStrength = 0;
        
        fLastFreqSecs = target.fContext.fSecs;
    }
}


hsBool plParticleUniformWind::ApplyEffect(const plEffectTargetInfo& target, Int32 i)
{
    hsVector3& vel = *(hsVector3*)(target.fVelocity + i * target.fVelocityStride);
    
    const hsScalar& invMass = *(hsScalar*)(target.fInvMass + i * target.fInvMassStride);
    
    vel += fWindVec * (invMass * fCurrentStrength);
    
    return false;
}

////////////////////////////////////////////////////////////////////////
// Simplified flocking.

plParticleFlockEffect::plParticleFlockEffect() :
    fInfAvgRadSq(1),
    fInfRepRadSq(1),
    fAvgVelStr(1),
    fRepDirStr(1),
    fGoalOrbitStr(1),
    fGoalChaseStr(1),
    fGoalDistSq(1),
    fFullChaseDistSq(1),
    fMaxOrbitSpeed(1),
    fMaxChaseSpeed(1),
    fMaxParticles(0),
    fDistSq(nil),
    fInfluences(nil)
{
    fTargetOffset.Set(0.f, 0.f, 0.f);
    fDissenterTarget.Set(0.f, 0.f, 0.f);
}

plParticleFlockEffect::~plParticleFlockEffect()
{
    SetMaxParticles(0);
}

void plParticleFlockEffect::IUpdateDistances(const plEffectTargetInfo& target)
{
    int i, j;
    int numParticles = hsMinimum(fMaxParticles, target.fNumValidParticles);

    for (i = 0; i < numParticles; i++)
    {
        for (j = i + 1; j < numParticles; j++)
        {
            hsVector3 diff((hsPoint3*)(target.fPos + i * target.fPosStride), (hsPoint3*)(target.fPos + j * target.fPosStride));
            fDistSq[i * fMaxParticles + j] = fDistSq[j * fMaxParticles + i] = diff.MagnitudeSquared();
        }
    }
}

void plParticleFlockEffect::IUpdateInfluences(const plEffectTargetInfo &target)
{
    int i, j;
    int numParticles = hsMinimum(fMaxParticles, target.fNumValidParticles);
    
    for (i = 0; i < numParticles; i++)
    {
        int numAvg = 0;
        int numRep = 0;
        fInfluences[i].fAvgVel.Set(0.f, 0.f, 0.f);
        fInfluences[i].fRepDir.Set(0.f, 0.f, 0.f);

        for (j = 0; j < numParticles; j++)
        {
            if (i == j)
                continue;

            const int distIdx = i * fMaxParticles + j;
            if (fDistSq[distIdx] > fInfAvgRadSq)
            {
                numAvg++;
                fInfluences[i].fAvgVel += *(hsVector3*)(target.fVelocity + j * target.fVelocityStride);
            }

            if (fDistSq[distIdx] > fInfRepRadSq)
            {
                numRep++;
                hsVector3 repDir((hsPoint3*)(target.fPos + i * target.fPosStride), (hsPoint3*)(target.fPos + j * target.fPosStride));
                repDir.Normalize();
                fInfluences[i].fRepDir += repDir;
            }

        }

        if (numAvg > 0)
            fInfluences[i].fAvgVel /= (hsScalar)numAvg;
        if (numRep > 0)
            fInfluences[i].fRepDir /= (hsScalar)numRep;
    }
}

void plParticleFlockEffect::PrepareEffect(const plEffectTargetInfo& target)
{
    IUpdateDistances(target);
    IUpdateInfluences(target);
}

// Some of this is the same for every particle and should be cached in PrepareEffect().
// Holding off on that until I like the behavior.
hsBool plParticleFlockEffect::ApplyEffect(const plEffectTargetInfo& target, Int32 i)
{
    if (i >= fMaxParticles)
        return false; // Don't have the memory to deal with you. Good luck kid...

    const hsPoint3 &pos = *(hsPoint3*)(target.fPos + i * target.fPosStride);
    hsVector3 &vel = *(hsVector3*)(target.fVelocity + i * target.fVelocityStride);
    
    hsScalar curSpeed = vel.Magnitude();
    hsPoint3 goal;
    if (*(UInt32*)(target.fMiscFlags + i * target.fMiscFlagsStride) & plParticleExt::kImmortal)
        goal = target.fContext.fSystem->GetTarget(0)->GetLocalToWorld().GetTranslate() + fTargetOffset;
    else
        goal = fDissenterTarget;
    
    hsVector3 goalDir;
    goalDir.Set(&(goal - pos));
    hsScalar distSq = goalDir.MagnitudeSquared();
    
    goalDir.Normalize();
    
    hsScalar goalStr;
    hsScalar maxSpeed;
    hsScalar maxSpeedSq;
    if (distSq <= fGoalDistSq)
    {       
        goalStr = fGoalOrbitStr;
        if (i & 0x1)
            goalDir.Set(goalDir.fY, -goalDir.fX, goalDir.fZ);
        else
            goalDir.Set(-goalDir.fY, goalDir.fX, goalDir.fZ);

        maxSpeed = fMaxOrbitSpeed;
    }
    else if (distSq >= fFullChaseDistSq)
    {
        goalStr = fGoalChaseStr;
        maxSpeed = fMaxChaseSpeed;
    }
    else
    {
        hsScalar pct = (distSq - fGoalDistSq) / (fFullChaseDistSq - fGoalDistSq);
        goalStr = fGoalOrbitStr + (fGoalChaseStr - fGoalOrbitStr) * pct;
        maxSpeed = fMaxOrbitSpeed + (fMaxChaseSpeed - fMaxOrbitSpeed) * pct;
    }
    maxSpeedSq = maxSpeed * maxSpeed;
    
    vel += (fInfluences[i].fAvgVel - vel) * (fAvgVelStr * target.fContext.fDelSecs);
    vel += goalDir * (curSpeed * goalStr * target.fContext.fDelSecs);
    vel += fInfluences[i].fRepDir * (curSpeed * fRepDirStr * target.fContext.fDelSecs);

    if (vel.MagnitudeSquared() > maxSpeedSq)
    {
        vel.Normalize();
        vel *= maxSpeed;
    }

    return false;
}

void plParticleFlockEffect::SetMaxParticles(const UInt16 num)
{
    delete [] fDistSq;
    delete [] fInfluences;
    fMaxParticles = num;

    if (num > 0)
    {
        fDistSq = TRACKED_NEW hsScalar[num * num];
        fInfluences = TRACKED_NEW plParticleInfluenceInfo[num];
    }
}

void plParticleFlockEffect::Read(hsStream *s, hsResMgr *mgr)
{
    plParticleEffect::Read(s, mgr);

    fTargetOffset.Read(s);
    fDissenterTarget.Read(s);
    fInfAvgRadSq = s->ReadSwapScalar();
    fInfRepRadSq = s->ReadSwapScalar();
    fGoalDistSq = s->ReadSwapScalar();
    fFullChaseDistSq = s->ReadSwapScalar();
    fAvgVelStr = s->ReadSwapScalar();
    fRepDirStr = s->ReadSwapScalar();
    fGoalOrbitStr = s->ReadSwapScalar();
    fGoalChaseStr = s->ReadSwapScalar();
    SetMaxOrbitSpeed(s->ReadSwapScalar());
    SetMaxChaseSpeed(s->ReadSwapScalar());
    SetMaxParticles((UInt16)s->ReadSwapScalar());
}

void plParticleFlockEffect::Write(hsStream *s, hsResMgr *mgr)
{
    plParticleEffect::Write(s, mgr);

    fTargetOffset.Write(s);
    fDissenterTarget.Write(s);
    s->WriteSwapScalar(fInfAvgRadSq);
    s->WriteSwapScalar(fInfRepRadSq);
    s->WriteSwapScalar(fGoalDistSq);
    s->WriteSwapScalar(fFullChaseDistSq);
    s->WriteSwapScalar(fAvgVelStr);
    s->WriteSwapScalar(fRepDirStr);
    s->WriteSwapScalar(fGoalOrbitStr);
    s->WriteSwapScalar(fGoalChaseStr);
    s->WriteSwapScalar(fMaxOrbitSpeed);
    s->WriteSwapScalar(fMaxChaseSpeed);
    s->WriteSwapScalar(fMaxParticles);
}

hsBool plParticleFlockEffect::MsgReceive(plMessage *msg)
{
    plParticleFlockMsg *flockMsg = plParticleFlockMsg::ConvertNoRef(msg);
    if (flockMsg)
    {
        switch (flockMsg->fCmd)
        {
        case plParticleFlockMsg::kFlockCmdSetDissentPoint:
            fDissenterTarget.Set(flockMsg->fX, flockMsg->fY, flockMsg->fZ);
            break;
        case plParticleFlockMsg::kFlockCmdSetOffset:
            fTargetOffset.Set(flockMsg->fX, flockMsg->fY, flockMsg->fZ);
            break;
        default:
            break;
        }
        return true;
    }

    return plParticleEffect::MsgReceive(msg);
}

///////////////////////////////////////////////////////////////////////////////////////

plParticleFollowSystemEffect::plParticleFollowSystemEffect() : fEvalThisFrame(true)
{
    fOldW2L = hsMatrix44::IdentityMatrix();
}

void plParticleFollowSystemEffect::PrepareEffect(const plEffectTargetInfo& target)
{
    fEvalThisFrame = (fOldW2L != target.fContext.fSystem->GetTarget(0)->GetWorldToLocal());
}

hsBool plParticleFollowSystemEffect::ApplyEffect(const plEffectTargetInfo& target, Int32 i)
{
    if (fEvalThisFrame)
    {
        if (i < target.fFirstNewParticle && !fOldW2L.IsIdentity())
        {
            hsPoint3 &pos = *(hsPoint3*)(target.fPos + i * target.fPosStride);
            pos = target.fContext.fSystem->GetTarget(0)->GetLocalToWorld() * fOldW2L * pos;
        }
    }
    return true;
}

void plParticleFollowSystemEffect::EndEffect(const plEffectTargetInfo& target)
{
    if (fEvalThisFrame)
        fOldW2L = target.fContext.fSystem->GetTarget(0)->GetWorldToLocal();
}