/******************************************************************************
 *{@C
 *      Copyright:      2006-2018 Paul Obermeier (obermeier@tcl3d.org)
 *
 *                      See the file "Tcl3D_License.txt" for information on
 *                      usage and redistribution of this file, and for a
 *                      DISCLAIMER OF ALL WARRANTIES.
 *
 *      Module:         Tcl3D -> tcl3dOgl
 *      Filename:       tcl3dUtilArcBall.c
 *
 *      Author:         Paul Obermeier
 *
 *      Description:    Arcball functions for mouse manipulation.
 *
 *                      This C implementation has been derived from the ArcBall
 *                      class contained in NeHe's lesson 48. 
 *
 *                      It's original copyright and version history are:
 *                      (C) 1999-2003 Tatewake.com
 *                      08/17/2003 - (TJG) - Creation
 *                      09/23/2003 - (TJG) - Bug fix and optimization
 *                      09/25/2003 - (TJG) - Version for NeHe Basecode users
 *
 *****************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#include "tcl3dVecMath.h"
#include "tcl3dUtilArcBall.h"

/*
 * Arcball sphere constants:
 * Diameter is       2.0f
 * Radius is         1.0f
 * Radius squared is 1.0f
 */

/* assuming IEEE-754 (float), which i believe has max precision of 7 bits */
# define Epsilon 1.0e-5

static void mapToSphere (ArcBall b, float mx, float my, float *newVec)
{
    float tmpX, tmpY;
    float length;

    /* Copy mouse parameter into temporaries */
    tmpX = mx;
    tmpY = my;

    /* Adjust point coords and scale down to range of [-1 ... 1] */
    tmpX =        (tmpX * b->adjustWidth)  - 1.0f;
    tmpY = 1.0f - (tmpY * b->adjustHeight);

    /* Compute the square of the length of the vector to the point
     * from the center
     */
    length = (tmpX * tmpX) + (tmpY * tmpY);

    /* If the point is mapped outside of the sphere ... 
     * (length > radius squared)
     */
    if (length > 1.0f) {
        float norm;

        /* Compute a normalizing factor (radius / sqrt(length)) */
        norm = 1.0f / sqrt (length);

        /* Return the "normalized" vector, a point on the sphere */
        newVec[0] = tmpX * norm;
        newVec[1] = tmpY * norm;
        newVec[2] = 0.0f;
    } else {   /* Else it's on the inside */
        /* Return a vector to a point mapped inside the sphere
         * sqrt(radius squared - length)
         */
        newVec[0] = tmpX;
        newVec[1] = tmpY;
        newVec[2] = sqrt (1.0f - length);
    }
}

ArcBall tcl3dNewArcBall (float newWidth, float newHeight)
{
    ArcBallStruct *pArcBall;

    pArcBall = (ArcBallStruct *) malloc (sizeof (ArcBallStruct));
    if (!pArcBall) {
        return NULL;
    }

    /* Clear initial values */
    pArcBall->stVec[0] = pArcBall->stVec[1] = pArcBall->stVec[2] = 0.0f;
    pArcBall->enVec[0] = pArcBall->enVec[1] = pArcBall->enVec[2] = 0.0f;

    /* Set initial bounds */
    tcl3dSetArcBallBounds (pArcBall, newWidth, newHeight);

    return pArcBall;
}

void tcl3dDeleteArcBall (ArcBall b)
{
    if (b) {
        free (b);
    }
    return;
}

void tcl3dSetArcBallBounds (ArcBall b, float newWidth, float newHeight)
{
    if (newWidth <= 1.0f ) {
        newWidth = 2.0f;
    }
    if (newHeight <= 1.0f ) {
        newHeight = 2.0f;
    }

    /* Set adjustment factor for width/height */
    b->adjustWidth  = 1.0f / ((newWidth  - 1.0f) * 0.5f);
    b->adjustHeight = 1.0f / ((newHeight - 1.0f) * 0.5f);
}

/* Mouse down: Supply mouse position in mx and my */
void tcl3dArcBallClick (ArcBall b, float mx, float my)
{
    /* Map the point to the sphere */
    mapToSphere (b, mx, my, b->stVec);
}

/* Mouse drag, calculate rotation: Supply mouse position in mx and my */
void tcl3dArcBallDrag (ArcBall b, float mx, float my, float * newRot)
{
    /* Map the point to the sphere */
    mapToSphere (b, mx, my, b->enVec);

    /* Return the quaternion equivalent to the rotation */
    if (newRot) {
        float Perp[3];

        /* Compute the vector perpendicular to the begin and end vectors */
        tcl3dVec3fCrossProduct (b->enVec, b->stVec, Perp);

        /* Compute the length of the perpendicular vector */
        if (tcl3dVec3fLength (Perp) > Epsilon) {
            /* If its non-zero, we're ok, so return the perpendicular
             * vector as the transform after all
             */
            newRot[0] = Perp[0];
            newRot[1] = Perp[1];
            newRot[2] = Perp[2];
            /* In the quaternion values, w is cosine (theta / 2),
             * where theta is rotation angle
             */
            newRot[3] = tcl3dVec3fDotProduct (b->stVec, b->enVec);
        } else {
            /* If it is zero, the begin and end vectors coincide,
             * so return an identity transform
             */
            newRot[0] = newRot[1] = newRot[2] = newRot[3] = 0.0f;
        }
    }
}
