/*==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==*/

/////////////////////////////////////////////////////////////////////////////////////////
//
// INCLUDES
//
/////////////////////////////////////////////////////////////////////////////////////////

// singular
#include "plAvTaskSeek.h"

// local
#include "plAvBrainHuman.h"
#include "plAGAnim.h"
#include "plArmatureMod.h"
#include "plAvatarMgr.h"
#include "plAvCallbackAction.h"

// other
#include "plMessage/plAvatarMsg.h"
#include "pnMessage/plCameraMsg.h"
#include "pnInputCore/plControlEventCodes.h"
#include "plPipeline/plDebugText.h"
#include "plStatusLog/plStatusLog.h"
#include "pnSceneObject/plCoordinateInterface.h"
#include "hsTimer.h"
#include "plgDispatch.h"

/////////////////////////////////////////////////////////////////////////////////////////
//
// PROTOTYPES
//
/////////////////////////////////////////////////////////////////////////////////////////
float QuatAngleDiff(const hsQuat &a, const hsQuat &b);
void MakeMatrixUpright(hsMatrix44 &mat);

/////////////////////////////////////////////////////////////////////////////////////////
//
// DEFINES
//
/////////////////////////////////////////////////////////////////////////////////////////

#define kSeekTimeout 5.0f
#define kRotSpeed 1.0f      // normal rotation speed is 1.0 radians per second
#define kFloatSpeed 3.0f
#define kMaxRadiansPerSecond 1.5

#define kDefaultShuffleRange 0.5f
#define kDefaultMaxSidleRange 4.0f
#define kDefaultMaxSidleAngle 0.2f

hsBool plAvTaskSeek::fLogProcess = false;

/////////////////////////////////////////////////////////////////////////////////////////
//
// IMPLEMENTATION
//
/////////////////////////////////////////////////////////////////////////////////////////

void plAvTaskSeek::IInitDefaults()
{
    fSeekObject = nil;
    fMovingTarget = false;
    fAlign = kAlignHandle;
    fAnimName = nil;
    fPosGoalHit = false;
    fRotGoalHit = false;
    fStillPositioning = true;
    fStillRotating = true;
    fShuffleRange = kDefaultShuffleRange;
    fMaxSidleRange = kDefaultMaxSidleRange;
    fMaxSidleAngle = kDefaultMaxSidleAngle;
    fFlags = kSeekFlagForce3rdPersonOnStart;  
    fState = kSeekRunNormal;
    fNotifyFinishedKey = nil;
}
// plAvTaskSeek ------------
// -------------
plAvTaskSeek::plAvTaskSeek() {}

plAvTaskSeek::plAvTaskSeek(plAvSeekMsg *msg)
{
    IInitDefaults();

    fAlign = msg->fAlignType;
    fAnimName = msg->fAnimName;

    plKey &target = msg->fSeekPoint;
    if (target)
        SetTarget(target);
    else
        SetTarget(msg->fTargetPos, msg->fTargetLookAt);
    
    if (msg->UnForce3rdPersonOnFinish())
        fFlags |= kSeekFlagUnForce3rdPersonOnFinish;
    else
        fFlags &= ~kSeekFlagUnForce3rdPersonOnFinish;

    if (msg->Force3rdPersonOnStart())
        fFlags |= kSeekFlagForce3rdPersonOnStart;
    else
        fFlags &= ~kSeekFlagForce3rdPersonOnStart;

    if (msg->NoWarpOnTimeout())
        fFlags |= kSeekFlagNoWarpOnTimeout;
    else
        fFlags &= ~kSeekFlagNoWarpOnTimeout;

    if (msg->RotationOnly())
    {
        fFlags |= kSeekFlagRotationOnly;
        fStillPositioning = false;
        fPosGoalHit = true;
    }
    else
        fFlags &= ~kSeekFlagRotationOnly;

    fNotifyFinishedKey = msg->fFinishKey;
}

// plAvTaskSeek ------------------------
// -------------
plAvTaskSeek::plAvTaskSeek(plKey target)
{
    IInitDefaults();

    SetTarget(target);
}

// plAvTaskSeek -------------------------------------------------------------------------------------------
// -------------
plAvTaskSeek::plAvTaskSeek(plKey target, plAvAlignment align, const char *animName, bool moving)
{
    IInitDefaults();

    fMovingTarget = moving;
    fAlign = align;
    fAnimName = animName;

    SetTarget(target);
}

void plAvTaskSeek::SetTarget(plKey target)
{
    hsAssert(target, "Bad key to seek task");
    if(target)
    {
        fSeekObject = plSceneObject::ConvertNoRef(target->ObjectIsLoaded());
    }
    else
    {
        fSeekObject = nil;
    }
}
    
void plAvTaskSeek::SetTarget(hsPoint3 &pos, hsPoint3 &lookAt)
{
    fSeekPos = pos;
    hsVector3 up(0.f, 0.f, 1.f);
    hsScalar angle = hsATan2(lookAt.fY - pos.fY, lookAt.fX - pos.fX) + hsScalarPI / 2;
    fSeekRot.SetAngleAxis(angle, up);
}

// Start -----------------------------------------------------------------------------------------
// ------
hsBool plAvTaskSeek::Start(plArmatureMod *avatar, plArmatureBrain *brain, double time, hsScalar elapsed)
{
    plAvBrainHuman *huBrain = plAvBrainHuman::ConvertNoRef(brain);
    hsAssert(huBrain, "Seek task only works on human brains");

    plAvatarMgr::GetInstance()->GetLog()->AddLine("Starting SMART SEEK");
    //controller needs to know we are seeking. prevents controller from interacting with exclusion regions
    
    if (avatar->GetController() )
        avatar->GetController()->SetSeek(true);
    fStartTime = time;
    if(huBrain)
    {
        avatar->SuspendInput();     // stop accepting input from the user, but queue any messages
                                    // ...and save our current input state.
        
        ILimitPlayersInput(avatar);
        
        if (plAvOneShotTask::fForce3rdPerson && avatar->IsLocalAvatar() && (fFlags & plAvSeekMsg::kSeekFlagForce3rdPersonOnStart))
        {
            // create message
            plCameraMsg* pMsg = TRACKED_NEW plCameraMsg;
            pMsg->SetBCastFlag(plMessage::kBCastByExactType);
            pMsg->SetBCastFlag(plMessage::kNetPropagate, false);
            pMsg->SetCmd(plCameraMsg::kResponderSetThirdPerson);
            plgDispatch::MsgSend( pMsg );   // whoosh... off it goes
        }       

        huBrain->IdleOnly(); // Makes sure to kill jumps too. Just calling ClearInputFlags isn't enough
        IUpdateObjective(avatar);
        return true;
    }
    else
    {
        return false;
    }
}

// Process -------------------------------------------------------------------------------------------
// --------
hsBool plAvTaskSeek::Process(plArmatureMod *avatar, plArmatureBrain *brain, double time, hsScalar elapsed)
{
    if (fState == kSeekAbort)
        return false;
    
    plAvBrainHuman *uBrain = plAvBrainHuman::ConvertNoRef(brain);
    if (uBrain)
    {
        if (fMovingTarget)
        {
            IUpdateObjective(avatar);
        }
        
        IAnalyze(avatar);
        hsBool result = IMoveTowardsGoal(avatar, uBrain, time, elapsed);
        if (fLogProcess)
            DumpToAvatarLog(avatar);
        return result;
    }

    return false;
}

// Finish ---------------------------------------------------------------------------------------
// -------
void plAvTaskSeek::Finish(plArmatureMod *avatar, plArmatureBrain *brain, double time, hsScalar elapsed)
{
    plAvBrainHuman *huBrain = plAvBrainHuman::ConvertNoRef(brain);
    
    if(huBrain)
    {
        // this will process any queued input messages so if the user pressed or released a key while we were busy, we'll note it now.
        avatar->ResumeInput();  
        IUndoLimitPlayersInput(avatar);
        
        if (plAvOneShotTask::fForce3rdPerson && avatar->IsLocalAvatar() && (fFlags & plAvSeekMsg::kSeekFlagUnForce3rdPersonOnFinish))
        {
            // create message
            plCameraMsg* pMsg = TRACKED_NEW plCameraMsg;
            pMsg->SetBCastFlag(plMessage::kBCastByExactType);
            pMsg->SetBCastFlag(plMessage::kNetPropagate, false);
            pMsg->SetCmd(plCameraMsg::kResponderUndoThirdPerson);
            plgDispatch::MsgSend( pMsg );   // whoosh... off it goes
        }
        
        avatar->SynchIfLocal(hsTimer::GetSysSeconds(), false);
    }

    if (fNotifyFinishedKey)
    {
        plAvTaskSeekDoneMsg *msg = TRACKED_NEW plAvTaskSeekDoneMsg(avatar->GetKey(), fNotifyFinishedKey);
        msg->fAborted = (fState == kSeekAbort);
        msg->Send();
    }
    plAvatarMgr::GetInstance()->GetLog()->AddLine("Finished SMART SEEK");   
    //inform controller we are done seeking
    if (avatar->GetController())
        avatar->GetController()->SetSeek(false);
}

void plAvTaskSeek::LeaveAge(plArmatureMod *avatar)
{
    fSeekObject = nil;
    fState = kSeekAbort;
}

// IAnalyze ----------------------------------------
// ---------
hsBool plAvTaskSeek::IAnalyze(plArmatureMod *avatar)
{
    avatar->GetPositionAndRotationSim(&fPosition, &fRotation);
    fGoalVec.Set(&(hsScalarTriple)(fSeekPos - fPosition));
    hsVector3 normalizedGoalVec(fGoalVec);
    normalizedGoalVec.Normalize();

    fDistance = hsSquareRoot(fGoalVec.fX * fGoalVec.fX + fGoalVec.fY * fGoalVec.fY);

    if(fDistance < 3.0f)
    {
        // we're in "near target" mode
        fMinFwdAngle = .5f;         // walk forward if target's in 90' cone straight ahead
        fMaxBackAngle = -.2f;       // walk backward if target's in a 144' cone behind
    }
    else
    {
        // we're in "far target" mode
        fMinFwdAngle = .2f;         // walk forward if target's in a 144' cone ahead
        fMaxBackAngle = -2.0;       // disable backing up if goal is far out (-1 is the minimum usable value here)
    }
    
    hsQuat invRot = fRotation.Conjugate();
    hsPoint3 globFwd = invRot.Rotate(&kAvatarForward);
    hsPoint3 globRight = invRot.Rotate(&kAvatarRight);

    hsPoint3 locGoalVec = fRotation.Rotate(&fGoalVec);

    fDistForward = -(locGoalVec.fY);
    fDistRight = -(locGoalVec.fX);

    fAngForward = globFwd.InnerProduct(normalizedGoalVec);
    fAngRight = globRight.InnerProduct(normalizedGoalVec);
    return true;
}

// IMoveTowardsGoal --------------------------------------------------------------
// -----------------
hsBool plAvTaskSeek::IMoveTowardsGoal(plArmatureMod *avatar, plAvBrainHuman *brain,
                                      double time, hsScalar elapsed)
{
    bool stillRunning = true;
    avatar->ClearInputFlags(false, false);

    double duration = time - fStartTime;

    if(duration > kSeekTimeout)
    {
        if (fFlags & kSeekFlagNoWarpOnTimeout)
        {
            fState = kSeekAbort;
            return false;
        }
        fSeekRot.Normalize();
        avatar->SetPositionAndRotationSim(&fSeekPos, &fSeekRot);
        IAnalyze(avatar); // Recalcs fPosition, fDistance, etc.
        hsStatusMessage("Timing out on smart seek - jumping to target.");
        stillRunning = false;

        // We just set the pos/rot, so we know these are hit.
        fPosGoalHit = true;
        fRotGoalHit = true;
    }

    if (!(fDistance > fShuffleRange))
        fPosGoalHit = true;

    if (!fPosGoalHit)
    {
        bool right = fAngRight > 0.0f;
        bool inSidleRange = fDistance < fMaxSidleRange;
        
        bool sidling = fabs(fDistRight) > fabs(fDistForward) && inSidleRange;

        if(sidling)
        {
            if(right)
                avatar->SetStrafeRightKeyDown();
            else
                avatar->SetStrafeLeftKeyDown();
        }
        else
        {
            if(fAngForward < fMaxBackAngle)
                avatar->SetBackwardKeyDown();

            else
            {
                if(fAngForward > fMinFwdAngle)
                    avatar->SetForwardKeyDown();

                if(right)
                    avatar->SetTurnRightKeyDown();
                else
                    avatar->SetTurnLeftKeyDown();
            }
        }
    }
    else 
    {
        if (!(QuatAngleDiff(fRotation, fSeekRot) > .1))
            fRotGoalHit = true;

        if (!fRotGoalHit)
        {
            hsQuat invRot = fSeekRot.Conjugate();
            hsPoint3 globFwd = invRot.Rotate(&kAvatarForward);
            globFwd = fRotation.Rotate(&globFwd);
        
            if (globFwd.fX < 0)
                avatar->SetTurnRightKeyDown();
            else
                avatar->SetTurnLeftKeyDown();
        }
    }       

    if (fPosGoalHit && fRotGoalHit)
        stillRunning = ITryFinish(avatar, brain, time, elapsed);

    return stillRunning;
}


// ITRYFINISH
bool plAvTaskSeek::ITryFinish(plArmatureMod *avatar, plAvBrainHuman *brain, double time, hsScalar elapsed)
{
    hsBool animsDone = brain->IsMovementZeroBlend();

    hsPoint3 newPosition = fPosition;
    hsQuat newRotation = fRotation;

    if (!(fFlags & kSeekFlagRotationOnly) && (fStillPositioning || !animsDone))
        fStillPositioning = IFinishPosition(newPosition, avatar, brain, time, elapsed);
    if (fStillRotating || !animsDone)
        fStillRotating = IFinishRotation(newRotation, avatar, brain, time, elapsed);

    newRotation.Normalize();
    if (hsCheckBits(fFlags, kSeekFlagRotationOnly))
        avatar->SetPositionAndRotationSim(nil, &newRotation);
    else
        avatar->SetPositionAndRotationSim(&newPosition, &newRotation);

    return fStillPositioning || fStillRotating || !animsDone;
}

hsBool plAvTaskSeek::IFinishPosition(hsPoint3 &newPosition,
                                     plArmatureMod *avatar, plAvBrainHuman *brain,
                                     double time, hsScalar elapsed)
{
    // While warping, we might be hovering just above the ground. Don't want that to
    // trigger any falling behavior.
    if(brain&&brain->fCallbackAction)
    {
        
        brain->fCallbackAction->ResetAirTime();
    }
    // how far will we translate this frame?
    float thisDist = kFloatSpeed * elapsed;
    // what percentage of the remaining distance will we cover?
    float thisPct = (fDistance ? thisDist / fDistance : 1.f);
    
    if(thisPct > 0.9f)
    {
        // we're pretty much done; just hop the rest of the way
        newPosition = fSeekPos;
        return false; // we're done
    }
    else
    {
        // move incrementally toward the target position
        hsVector3 thisMove = fGoalVec * thisPct;
        newPosition = fPosition + thisMove;
        return true;        // we're still processing
    }
    return true;
}



// IFinishRotation --------------------------------------
// ----------------
hsBool plAvTaskSeek::IFinishRotation(hsQuat &newRotation,
                                     plArmatureMod *avatar, plAvBrainHuman *brain,
                                     double time, hsScalar elapsed)
{
    // we're pretty much done; just hop the rest of the way
    newRotation = fSeekRot;
    return false;
}

// IUpdateObjective ----------------------------------------
// -----------------
hsBool plAvTaskSeek::IUpdateObjective(plArmatureMod *avatar)
{
    // This is an entirely valid case. It just means our goal is fixed.
    if (fSeekObject == nil)
        return true;

    // goal here is to express the target matrix in the avatar's PHYSICAL space
    hsMatrix44 targL2W = fSeekObject->GetLocalToWorld();
    const plCoordinateInterface* subworldCI = nil;
    if (avatar->GetController())
        subworldCI = avatar->GetController()->GetSubworldCI();
    if (subworldCI)
        targL2W = subworldCI->GetWorldToLocal() * targL2W;

    MakeMatrixUpright(targL2W);

    switch(fAlign)
    {
        // match our handle to the target matrix at the end of the given animation
        // This case isn't currently used but will be important someday. The idea
        // is that you have a target point and an animation, and you want to seek
        // the avatar to a point where he can start playing the animation and wind
        // up, after the animation completes, at the target location.
        // Hence "AlignHandleAnimEnd" = "align the avatar so the animation will
        // end on the target."
        case kAlignHandleAnimEnd:
            {
                hsMatrix44 adjustment;
                plAGAnim *anim = avatar->FindCustomAnim(fAnimName);
                // don't need to do this every frame; the animation doesn't change.
                // *** cache the adjustment;
                GetStartToEndTransform(anim, nil, &adjustment, "Handle");   // actually getting end-to-start
                // ... but we do still need to multiply by the (potentially changed) target
                targL2W = targL2W * adjustment;
            }
            break;
        case kAlignHandle:      // targetMat is already correct
        default:
            break;
    };

    GetPositionAndRotation(targL2W, &fSeekPos, &fSeekRot);

    return true;
}



// DumpDebug -----------------------------------------------------------------------------------------------------
// ----------
void plAvTaskSeek::DumpDebug(const char *name, int &x, int&y, int lineHeight, char *strBuf, plDebugText &debugTxt)
{
    sprintf(strBuf, "duration: %.2f pos: (%.3f, %.3f, %.3f) goalPos: (%.3f, %.3f, %.3f) ",
            hsTimer::GetSysSeconds() - fStartTime,
            fPosition.fX, fPosition.fY, fPosition.fZ, fSeekPos.fX, fSeekPos.fY, fSeekPos.fZ);
    debugTxt.DrawString(x, y, strBuf);
    y += lineHeight;

    sprintf(strBuf, "positioning: %d rotating %d goalVec: (%.3f, %.3f, %.3f) dist: %.3f angFwd: %.3f angRt: %.3f",
            fStillPositioning, fStillRotating, fGoalVec.fX, fGoalVec.fY, fGoalVec.fZ, fDistance, fAngForward, fAngRight);
    debugTxt.DrawString(x, y, strBuf);
    y += lineHeight;
    
    sprintf(strBuf, " distFwd: %.3f distRt: %.3f shufRange: %.3f sidAngle: %.3f sidRange: %.3f, fMinWalk: %.3f",
            fDistForward, fDistRight, fShuffleRange, fMaxSidleAngle, fMaxSidleRange, fMinFwdAngle);
    debugTxt.DrawString(x, y, strBuf);
    y += lineHeight;
}

void plAvTaskSeek::DumpToAvatarLog(plArmatureMod *avatar)
{
    plStatusLog *log = plAvatarMgr::GetInstance()->GetLog();
    char strBuf[256];
    avatar->GetMoveKeyString(strBuf);
    log->AddLine(strBuf);

    sprintf(strBuf, "    duration: %.2f pos: (%.3f, %.3f, %.3f) goalPos: (%.3f, %.3f, %.3f) ",
            hsTimer::GetSysSeconds() - fStartTime,
            fPosition.fX, fPosition.fY, fPosition.fZ, fSeekPos.fX, fSeekPos.fY, fSeekPos.fZ);
    log->AddLine(strBuf);
    
    sprintf(strBuf, "    positioning: %d rotating %d goalVec: (%.3f, %.3f, %.3f) dist: %.3f angFwd: %.3f angRt: %.3f",
            fStillPositioning, fStillRotating, fGoalVec.fX, fGoalVec.fY, fGoalVec.fZ, fDistance, fAngForward, fAngRight);
    log->AddLine(strBuf);

    sprintf(strBuf, "    distFwd: %.3f distRt: %.3f shufRange: %.3f sidAngle: %.3f sidRange: %.3f, fMinWalk: %.3f",
            fDistForward, fDistRight, fShuffleRange, fMaxSidleAngle, fMaxSidleRange, fMinFwdAngle);
    log->AddLine(strBuf);
}

/////////////////////////////////////////////////////////////////////////////////////////
//
// LOCALS
//
/////////////////////////////////////////////////////////////////////////////////////////

// QuatAngleDiff ------------------------------------
// --------------
float QuatAngleDiff(const hsQuat &a, const hsQuat &b)
{
    hsScalar theta;     /* angle between A and B */
    hsScalar cos_t;     /* sine, cosine of theta */

    /* cosine theta = dot product of A and B */
    cos_t = a.Dot(b);

    /* if B is on opposite hemisphere from A, use -B instead */
    if (cos_t < 0.0)
    {
        cos_t = -cos_t;
    } 

    // Calling acos on 1.0 is returning an undefined value. Need to check for it.
    hsScalar epsilon = 0.00001;
    if (hsABS(cos_t - 1.f) < epsilon)
        return 0;

    theta   = hsACosine(cos_t);
    return theta;
}

// MakeMatrixUpright -------------------------------------------
// ------------------
// ensure that the z axis of the given matrix points at the sky.
// does not orthonormalize
// man, I could have sworn I did this somewhere else...
void MakeMatrixUpright(hsMatrix44 &mat)
{
    mat.fMap[0][2] = 0.0f;          // eliminate any z in the x axis
    mat.fMap[1][2] = 0.0f;          // eliminate any z in the y axis
    mat.fMap[2][0] = 0.0f; mat.fMap[2][1] = 0.0f; mat.fMap[2][2] = 1.0f;    // z axis = pure sky
}