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