/*
 * Copyright (C) 1997-2009, R3vis Corporation.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
 * USA, or visit http://www.gnu.org/copyleft/lgpl.html.
 *
 * Original Contributor:
 *   Wes Bethel, R3vis Corporation, Marin County, California
 *   http://www.r3vis.com/
 * Additional Contributor(s):
 *
 * The OpenRM project is located at http://openrm.sourceforge.net/.
 */
/*
 * $Id: rmflyui.c,v 1.8 2006/08/08 03:49:07 wes Exp $
 * Version: $Name: v180-alpha-02 $
 * $Revision: 1.8 $
 * $Log: rmflyui.c,v $
 * Revision 1.8  2006/08/08 03:49:07  wes
 * Ergonomic changes to rmaux callbacks.
 *
 * Revision 1.7  2006/08/07 12:49:05  wes
 * Added new parms to callback functions: RMnode and usrData. Added new
 * infrastructure to manage user data.
 *
 * Revision 1.6  2005/06/08 18:33:02  wes
 * Code cleanups to eliminate compiler warnings.
 *
 * Revision 1.5  2005/02/19 16:08:45  wes
 * Distro sync and consolidation.
 *
 * Revision 1.4  2005/01/23 17:11:49  wes
 * Copyright update to 2005.
 *
 * Revision 1.3  2004/01/17 04:07:53  wes
 * Updated copyright line for 2004.
 *
 * Revision 1.2  2003/02/02 02:07:18  wes
 * Updated copyright to 2003.
 *
 * Revision 1.1.1.1  2003/01/28 02:15:23  wes
 * Manual rebuild of rm150 repository.
 *
 * Revision 1.8  2003/01/16 22:21:18  wes
 * Updated all source files to reflect new organization of header files:
 * all header files formerly located in include/rmaux, include/rmi, include/rmv
 * are now located in include/rm.
 *
 * Revision 1.7  2002/04/30 19:39:19  wes
 * Updated return type of rmauxFlyMotionFunc() to be an int rather
 * than void. This will ensure consistency and proper functioning
 * on all platforms.
 *
 * Revision 1.6  2001/03/31 17:09:31  wes
 * v1.4.0-alpha2 checkin.
 *
 * Revision 1.5  2000/12/03 22:36:29  wes
 * First steps towards thread safety.
 *
 * Revision 1.4  2000/04/20 16:22:16  wes
 * JDB modifications: additional documentation, code rearranging.
 *
 * Revision 1.3  2000/04/17 00:06:51  wes
 * Numerous documentation updates and code reorganization courtesy of jdb.
 *
 * Revision 1.2  2000/02/29 23:43:57  wes
 * Compile warning cleanups.
 *
 * Revision 1.1.1.1  2000/02/28 21:29:40  wes
 * OpenRM 1.2 Checkin
 *
 * Revision 1.1.1.1  2000/02/28 17:18:48  wes
 * Initial entry - pre-RM120 release, source base for OpenRM 1.2.
 *
 */


#include <rm/rm.h>
#include <rm/rmaux.h>

/*
 * of the routines in this file, only rmauxFlyUI is intended to be
 * accessible from the application level. the other routines are used
 * to effect inter-frame changes in the camera and to implement resets.
 *
 * 1/15/2000 w.bethel
 */

#ifdef RM_X
#include <X11/cursorfont.h>
#endif

typedef enum RMflyEnum /* PRIVATE */
{
    NEEDS_INITIALIZATION = 0,
    IN_MOTION = 1,
    PAUSED = 2
} RMflyEnum;

typedef struct RMflyStruct /* PRIVATE */
{
    RMflyEnum  flyInMotion;		/* tag for state of fly interface */
    float      flyOrientationScale;	/* used to attenuate incremental rotation */
    float      flyTranslateScale;	/* used to attenuate camera movement */
    
    RMcamera3D flyCamera;		/* working camera, computed on each frame */
    RMvertex3D flyEye;			/* working eye point */
    RMvertex3D flyAtVector;		/* working look-at vector */
    RMvertex3D flyUpVector;		/* working up vector */

    RMmatrix   flyMatrix;		/* accumulation matrix */
    
    double     flyEyeAtMag;		/* magnitude of original eye-at vector */
    double     flyFocalLength;  	/* of the camera */
    int        flyWinWidth;		/* width of display window in pixels */
    int        flyWinHeight;		/* height of display window in pixels */
    float      flyFOVdelta;		/* FOV of camera */

    RMnode    *cameraNode;		/* node containing the original camera */
    RMnode    *drawNode;	/* root of SG to draw on each frame */
    RMvertex3D saveEyeAtVector;		/* saved, normalized eye-at vector */
    RMvertex3D saveUpVector;		/* saved, normalized camera up vector */
    RMcamera3D saveCamera;		/* virgin camera */
} RMflyStruct;

/* static variables - malloc'ed to ensure thread safety */
static RMflyStruct *activeFlyStruct = NULL;
#ifdef RM_X
static Cursor *motionCursor = NULL;
#endif

/* PRIVATE declarations */
void        private_rmauxFlySetup (RMflyStruct *fs, int w, int h);
extern void rmauxInvokeRenderFunc (RMpipe *p, RMnode *n);

/*
 * ----------------------------------------------------
 * @Name rmauxFlyUI
 @pstart
 void rmauxFlyUI (RMnode *cameraNode,
                  RMnode *sgRoot,
	          float orientationScale,
		  float stepScale)
 @pend

 @astart
 RMnode *cameraRoot - (input) the RMnode containing the RMcamera3D that
    will be manipulated with a flight model.

 RMnode *sgRoot - (input) the root of the scene graph that will be
    drawn on each frame. cameraRoot and sgRoot may be the same node.

 float orientationScale - (input) a floating point value between 0.0
    and 1.0 used to attenuate rotations. A good range of value for
    this parameter is 1/30...1/60.

 float stepScale - (input) a floating point value between 0.0 and 1.0
    used to attenuate translational camera motion. A good range of
    values for this parameter is 1/30..1/60. 
 @aend

 @dstart

 rmauxFlyUI builds a set of button to event mappings that are useful
 for terrain flyovers. The fundamental assumption of rmauxFlyUI is
 that the input RMnode contains an RMcamera3D scene parameter. This 3D
 camera will be modified - thus, rmauxFlyUI changes the position of
 the viewer in the scene, but does not change the orientation matrices
 of any objects.
 
 Pressing the left mouse button commences activity (motion), and
 pressing the left mouse button again stops the viewer (abruptly).
 
 Changes in the horizontal location of the pointer cause camera roll
 to change, while changes to the vertical location of the pointer
 affect camera pitch. Heading changes as a function of roll & pitch
 (like flying an aircraft).
 
 The input float parameter orientationScale is used to regulate how
 quickly changes occur. The value of orientationScale is inversely
 proportional to the number of frames required for a given change to
 occur. For example, on each fly frame, we compute the displacement
 between the pointer current position and the center of the window.
 Then, we adjust the camera to a new heading. The adjustment is
 computed such that the new heading will be achieved in
 1/orientationScale frames. Therefore, orientationScale is most often
 a value between 0.0 and 1.0. The closer to 1.0, the more rapid will
 the changes occur. The closer to 0.0, the more slowly they will
 occur.  We may some day incorporate a temporal filter on changes, but
 for now, we don't.

 The input float parameter stepScale regulates the amount of camera
 translation per frame. The amount the camera moves during each frame
 is a function of stepScale multiplied by the cameraFocalDistance
 parameter (by default, the camera focal distance parameter is 1.0,
 which measures units of the distance from the eye point to the view
 reference, or look at point). Increasing the cameraFocalDistance
 parameter has no substantive effect on the view matrix except when
 viewing in stereo.

 Upon entry, rmauxFlyUI checks to see if the input RMnode "cameraNode"
 has a 3d camera as a scene parameter. If none is present, an error message
 isissued and we return. If one is present, we grab a copy of the camera
 and use the copy for the duration of the UI run. Our copy is
 modified, then we set the camera3D scene parameter of the target as
 we fly around. Should the application want to modify the camera, it
 will be necessary to call rmauxFlyUI again to re-register the new
 camera with the FlyUI machinery.

 During operation, the scene graph rooted at the input RMnode "sgRoot"
 is drawn on each frame from a viewpoint that "flies" through the scene.

 We use a crude, static-velocity model (zero acceleration). It's like
 a 3 year old child: either it's completely stopped, or going at full
 speed, with no speed inbetween.

 @dend
 * ----------------------------------------------------
 */
void
rmauxFlyUI (RMnode *cameraNode,
	    RMnode *sgRoot,
	    RMpipe *p,
	    float orientationScale,
	    float stepScale)
{
    RMcamera3D *tmp = NULL;
    
    /* setup fly interface once only */
    if (activeFlyStruct == NULL)
    {
	activeFlyStruct = (RMflyStruct *)malloc(sizeof(RMflyStruct));
	memset((void *)activeFlyStruct, 0, sizeof(RMflyStruct));
    }
    
    /* check to see if the target node has a 3D camera scene parameter */
    if (rmNodeGetSceneCamera3D(cameraNode, &tmp) == RM_WHACKED)
    {
	rmError("rmauxFlyUI expects the input RMnode to have an RMcamera3D scene parameter. no such scene parameter is present in the input RMnode. returning without creating the fly UI.");
	return;
    }

    activeFlyStruct->flyCamera = *tmp;
    rmCamera3DDelete(tmp);

    activeFlyStruct->saveCamera = activeFlyStruct->flyCamera;
    activeFlyStruct->cameraNode = cameraNode;
    activeFlyStruct->drawNode = sgRoot;

#ifdef RM_X
    /* setup fancy X11 cursor */
    if (motionCursor == NULL)
    {
	motionCursor = (Cursor *)malloc(sizeof(Cursor));
	*motionCursor =  XCreateFontCursor(rmxPipeGetDisplay(p), XC_trek);
    }
#endif

    rmauxSetButtonDownCallbackFunc(rmauxFlyToggleMotion, NULL, RM_BUTTON1, RM_NONE_MODMASK);
    rmauxSetButtonDownCallbackFunc(rmauxFlyResetCamera, NULL, RM_BUTTON3, RM_NONE_MODMASK);

    activeFlyStruct->flyOrientationScale = orientationScale;
    activeFlyStruct->flyTranslateScale = stepScale;
}


/*
 * ----------------------------------------------------
 * @Name rmauxFlyResetCamera
 @pstart
 int rmauxFlyResetCamera RMAUX_BUTTON_FUNC_PARMS()
 @pend

 @astart
 the macro RMAUX_BUTTON_CALLBACK_FUNCTION_PARMS() expands to:

 RMpipe *p - a handle to the current RMpipe.

 RMnode *n - the RMnode * provided to rmauxEventLoop.

 void *usrData - a handle to user-data.

 int xbutton, ybutton - the current (x, y) positions of the mouse
    pointer.
 @aend

 @dstart

 This routine restores the 3D camera parameters to what they were when
 rmauxFlyUI was originally called.  It is intended to be called from
 event loop code.

 Returns 1 to the caller, presumably an event loop handler, so that
 processing will continue.

 @dend
 * ----------------------------------------------------
 */
int
rmauxFlyResetCamera RMAUX_BUTTON_CALLBACK_FUNCTION_PARMS()
{
    int winWidth, winHeight;
    
    /* reset the camera to its initial value when called */
    rmNodeSetSceneCamera3D(activeFlyStruct->cameraNode, &(activeFlyStruct->saveCamera));
    rmPipeGetWindowSize(p, &winWidth, &winHeight);
    private_rmauxFlySetup(activeFlyStruct, winWidth, winHeight);

    /* foil compiler warning */
    xbutton = ybutton = 0;

    return(1);
}


/*
 * ----------------------------------------------------
 * @Name rmauxFlyToggleMotion
 @pstart
 int rmauxFlyToggleMotion RMAUX_BUTTON_CALLBACK_FUNCTION_PARMS()
 @pend

 @astart
 the macro RMAUX_BUTTON_CALLBAC_FUNCTION_PARMS() expands to:

 RMpipe *p - a handle to the current RMpipe.
 
 RMnode *n - the "subTree" RMnode * to rmauxEventLoop.
 
 void *usrData - self explanatory

 int xbutton, ybutton - the current (x, y) positions of the mouse
    pointer.
 @aend

 @dstart

 This routine turns on and off motion by manipulating the "idle
 function."  When turning motion off, the idle function is removed
 (nothing happens).  When turned on, we set the idle function to
 rmauxFlyMotionFunc so that we begin to fly.  A 1 is returned to the
 caller.

 @dend
 * ----------------------------------------------------
 */
int
rmauxFlyToggleMotion RMAUX_BUTTON_CALLBACK_FUNCTION_PARMS()
{
/*    int (*funcptr)(RMpipe *, int, int); */
	
    if (activeFlyStruct->flyInMotion != IN_MOTION)	/* start motion */
    {
	int winWidth, winHeight;

/*
	funcptr = rmauxFlyMotionFunc;
	rmauxSetIdleFunc(p, funcptr);
*/
	rmauxSetIdleCallbackFunc(rmauxFlyMotionFunc, NULL);

#ifdef RM_X
	/* X11 gets a nice cursor that will be displayed when we start flying */
	XDefineCursor(rmxPipeGetDisplay(p), rmPipeGetWindow(p), *motionCursor);
#endif
	/*
	 * get the current window size. we're assuming that the
	 * user probably won't do window resizing while they're
	 * flying around, but checking here allows for the possibility
	 * of responding to window resizes when not flying.
	 */
	rmPipeGetWindowSize(p, &winWidth, &winHeight);

	if (activeFlyStruct->flyInMotion == NEEDS_INITIALIZATION)
	    private_rmauxFlySetup(activeFlyStruct, winWidth, winHeight); 

	/* first, we set a state variable, and assign a new idle function */
	activeFlyStruct->flyInMotion = IN_MOTION;
    }
    else  /* stop motion */
    {
	activeFlyStruct->flyInMotion = PAUSED;
	rmauxSetIdleCallbackFunc(NULL, NULL);
#ifdef RM_X
	/* restore X11 cursor */
	XUndefineCursor(rmxPipeGetDisplay(p), rmPipeGetWindow(p));
#endif
    }

    /* foil compiler warning */
    xbutton = ybutton = 0;
    
    return(1);
}


/*
 * ----------------------------------------------------
 * @Name rmauxFlyMotionFunc
 @pstart
 int rmauxFlyMotionFunc RMAUX_BUTTON_CALLBACK_FUNCTION_PARMS()
 @pend

 @astart
 the macro RMAUX_BUTTON_CALLBACK_FUNCTION_PARMS() expands to:

 RMpipe *p - a handle to the current RMpipe.

 RMnode *n - the RMnode * provided to rmauxEventLoop.

 void *usrData - caller-supplied data pointer.

 int xbutton, ybutton - the current (x, y) positions of the mouse 
   pointer.
 @aend

 @dstart

 Given a new cursor location, update the camera to take into account
 the rotational and translational effects of the change from the
 current values and those dictated by the new cursor position.

 A static structure is used to hold state, so this routine (in fact,
 all of rmauxFlyUI) is not thread safe at this time.

 Always returns 1 so that rmauxEventLoop will not terminate.

 @dend
 * ----------------------------------------------------
 */
int
rmauxFlyMotionFunc RMAUX_BUTTON_CALLBACK_FUNCTION_PARMS()
{
    int          xCenter, xDelta;
    int          yCenter, yDelta;
    double       deltaPitch;
    double       deltaRoll;
    double       c, s;
    RMvertex3D   newHeading, newUpVector;
    RMmatrix     mPitch, composite;
    RMmatrix     mRoll;
    RMmatrix     mTmp;
    RMflyStruct *fs;
    
    fs = activeFlyStruct;
    /*
     * first, look at difference between x-coord of the pointer and the
     * center of the window. the horizontal difference between the
     * pointer location and the center of the window will affect roll.
     * roll is a modification to the local +y axis of the camera.
     */
    xCenter = (fs->flyWinWidth >> 1);
    xDelta = -(xbutton - xCenter);
    deltaRoll = (double)xDelta * (double)(fs->flyFOVdelta) * fs->flyOrientationScale;
    deltaRoll = RM_DEGREES_TO_RADIANS(deltaRoll);
    
    /* a change in roll changes the orientation of our viewpoint around the camera's local Y axis */
    c = cos(deltaRoll);
    s = sin(deltaRoll);
    rmMatrixIdentity(&mRoll);

    mRoll.m[0][0] = mRoll.m[2][2] = c;
    mRoll.m[2][0] = s;
    mRoll.m[0][2] = -s;

    /* now modify the pitch as a function of the y coordinate of the pointer and the old at vector */
    yCenter = (fs->flyWinHeight >> 1);
    yDelta = yCenter - ybutton;
    yDelta = yDelta;
    deltaPitch = (double)yDelta * (double)(fs->flyFOVdelta) * fs->flyOrientationScale;
    deltaPitch = RM_DEGREES_TO_RADIANS(deltaPitch);
    
    /* compute new eye-at vector as a function of the old value and the new heading */
    rmMatrixIdentity(&mPitch);

    /* for now, don't use pitch */
    c = cos(deltaPitch);
    s = sin(deltaPitch);

    mPitch.m[1][1] = mPitch.m[2][2] = c;
    mPitch.m[1][2] = s;
    mPitch.m[2][1] = -s;
    rmMatrixMultiply(&mPitch, &mRoll, &composite);  

    rmMatrixMultiply(&(fs->flyMatrix), &composite, &(fs->flyMatrix)); 
    mTmp = (fs->flyMatrix);

    newHeading.x = mTmp.m[0][1];
    newHeading.y = mTmp.m[1][1];
    newHeading.z = mTmp.m[2][1];

    newUpVector.x = mTmp.m[0][2];
    newUpVector.y = mTmp.m[1][2];
    newUpVector.z = mTmp.m[2][2];
			     
    rmVertex3DNormalize(&newHeading);
    rmVertex3DNormalize(&newUpVector);
    
    /* update the new eye point. it's a function of the old
     * eye point, newheading, eye-at magnitude, camera focal length
     * and flyTranslateScale 
     */
    {
	float s;
	
	s = fs->flyTranslateScale * fs->flyFocalLength * fs->flyEyeAtMag;
	
	fs->flyEye.x += s*newHeading.x;
	fs->flyEye.y += s*newHeading.y;
	fs->flyEye.z += s*newHeading.z;
    }

#if 0
    fprintf(stderr,"new eye point is %g, %g, %g, new heading is %g, %g, %g \n", fs->flyEye.x, fs->flyEye.y, fs->flyEye.z, newHeading.x, newHeading.y, newHeading.z);
#endif

    /* convert at vector to a real look at point for the RMcamera3D */
    newHeading.x = (newHeading.x * fs->flyEyeAtMag) + fs->flyEye.x;
    newHeading.y = (newHeading.y * fs->flyEyeAtMag) + fs->flyEye.y;
    newHeading.z = (newHeading.z * fs->flyEyeAtMag) + fs->flyEye.z; 

    rmCamera3DSetAt(&(fs->flyCamera), &newHeading);
    rmCamera3DSetEye(&(fs->flyCamera), &(fs->flyEye));
    rmCamera3DSetUpVector(&(fs->flyCamera), &newUpVector); 
    rmNodeSetSceneCamera3D(fs->cameraNode, &(fs->flyCamera));

    rmauxInvokeRenderFunc(p, fs->drawNode);

    return 1;
}


/* PRIVATE
 *
 * intended only for internal use, fills in an internal structure
 * with info. we need the window width and height from the caller.
 */
void
private_rmauxFlySetup (RMflyStruct *fs,
		       int w,
		       int h)
{
    RMcamera3D *c;
    RMmatrix   *m;

    fs->flyWinWidth = w;
    fs->flyWinHeight = h;
    
    c = &(fs->saveCamera);
    
    rmCamera3DGetEye(c, &(fs->flyEye));
    rmCamera3DGetAt(c, &(fs->flyAtVector));

    rmVertex3DDiff(&(fs->flyAtVector), &(fs->flyEye), &(fs->flyAtVector));
    rmVertex3DMagNormalize(&(fs->flyAtVector), &(fs->flyEyeAtMag));
    fs->saveEyeAtVector = fs->flyAtVector;

    rmCamera3DGetUpVector(c, &(fs->flyUpVector));
    fs->saveUpVector = fs->flyUpVector;
    
    fs->flyFOVdelta = rmCamera3DGetFOV(c);
    fs->flyFocalLength = rmCamera3DGetFocalDistance(c);

    fs->flyFOVdelta = fs->flyFOVdelta / (float)(w); /* 1 pixel = how much heading change? */

    rmMatrixIdentity(&(fs->flyMatrix));

    m = &(fs->flyMatrix);

    m->m[0][1] = fs->flyAtVector.x;
    m->m[1][1] = fs->flyAtVector.y;
    m->m[2][1] = fs->flyAtVector.z;

    m->m[0][2] = fs->flyUpVector.x;
    m->m[1][2] = fs->flyUpVector.y;
    m->m[2][2] = fs->flyUpVector.z;
}

/* EOF */
