/*
 * c3dcamera.c --
 *
 */
#include "c3d.h"
#include <assert.h>
#include <math.h>
#include <string.h>

typedef struct BoundingSphereContext BoundingSphereContext;

/*
 * This struct is passed as the clientData from C3dBoundingSphere() to
 * boundingsphereSearchCb(). The two-pass algorithm used to determine the
 * sphere is as follows:
 *
 * The first pass loops through all items to be included in the sphere and
 * fills in the values of the xmin, xmax etc. with the maximum and minimum
 * x, y and z values of any vertex of any item. This defines a "bounding
 * cube". The center point of the bounding cube is used as the center of
 * the returned sphere.
 *
 * For the second pass variables x, y and z are set to the center of the
 * sphere. Variable maxdistsquare is set to the square of the maximum
 * distance of this point (x, y, z) from any vertex of any item. We use the
 * square instead of the distance to avoid potentially expensive sqrt()
 * invocations (not sure if this actually saves any cycles, but it can't
 * hurt).
 *
 * Once the second pass is finished, the radius of the returned sphere is
 * calculated as sqrt(maxdistsquare).
 *
 * Variable 'state' may take the values 0, 1 or 2. They are interpreted as
 * follows:
 *
 *     0: First pass, values of xmin etc. are not valid.
 *     1: First pass, xmin etc. are valid.
 *     2: Second pass.
 */
struct BoundingSphereContext {
    int state;

    float xmin;
    float ymin;
    float zmin;
    float xmax;
    float ymax;
    float zmax;

    float x;
    float y;
    float z;
    float maxdistsquare;
};


/*
 *---------------------------------------------------------------------------
 *
 * boundingsphereSearchCb --
 *
 * Results:
 *     None.
 *
 * Side effects:
 *     None.
 *
 *---------------------------------------------------------------------------
 */
static int
boundingsphereSearchCb(ClientData clientData, C3dItem *pItem)
{
    BoundingSphereContext *pContext = (BoundingSphereContext *)clientData;

    if (pItem->pType->xDraw && pItem->nVertex > 0) {
        int i = 0;
        switch (pContext->state) {
            case 0:
                pContext->xmin = pContext->xmax = pItem->aVertex[0].x;
                pContext->ymin = pContext->ymax = pItem->aVertex[0].y;
                pContext->zmin = pContext->zmax = pItem->aVertex[0].z;
                pContext->state = i = 1;
                /* Fall through */
            case 1:
                for (; i < pItem->nVertex; i++) {
                    pContext->xmin = MIN(pContext->xmin, pItem->aVertex[i].x);
                    pContext->ymin = MIN(pContext->ymin, pItem->aVertex[i].y);
                    pContext->zmin = MIN(pContext->zmin, pItem->aVertex[i].z);
                    pContext->xmax = MAX(pContext->xmax, pItem->aVertex[i].x);
                    pContext->ymax = MAX(pContext->ymax, pItem->aVertex[i].y);
                    pContext->zmax = MAX(pContext->zmax, pItem->aVertex[i].z);
                }
                break;
            case 2:
                for (i = 0; i < pItem->nVertex; i++) {
                    float xdiff = (pContext->x - pItem->aVertex[i].x);
                    float ydiff = (pContext->y - pItem->aVertex[i].y);
                    float zdiff = (pContext->z - pItem->aVertex[i].z);
                    float d = xdiff*xdiff + ydiff*ydiff + zdiff*zdiff;
                    pContext->maxdistsquare = MAX(d, pContext->maxdistsquare);
                }
                break;
        }
    }
    return TCL_OK;
}

/*
 *---------------------------------------------------------------------------
 *
 * C3dBoundingSphere --
 *
 * Results:
 *     None.
 *
 * Side effects:
 *     None.
 *
 *---------------------------------------------------------------------------
 */
int
C3dBoundingSphere(
    C3dWidget *pCanvas,
    Tcl_Obj *pSearch,
    int *pSphereValid,
    float *pR,
    float *pX,
    float *pY,
    float *pZ
) {
    BoundingSphereContext sContext;
    int rc;

    memset(&sContext, 0, sizeof(BoundingSphereContext));

    rc = C3dSearch(pCanvas, pSearch, 0, boundingsphereSearchCb, &sContext);
    if (rc != TCL_OK) {
        return TCL_ERROR;
    }

    assert(sContext.state == 0 || sContext.state == 1);
    if (sContext.state) {
        *pSphereValid = 1;
    } else {
        *pSphereValid = 0;
        return TCL_OK;
    }

    sContext.state = 2;
    sContext.x = (sContext.xmin + sContext.xmax) / 2.0;
    sContext.y = (sContext.ymin + sContext.ymax) / 2.0;
    sContext.z = (sContext.zmin + sContext.zmax) / 2.0;

    rc = C3dSearch(pCanvas, pSearch, 0, boundingsphereSearchCb, &sContext);
    assert(rc == TCL_OK);

    *pR = sqrt(sContext.maxdistsquare);
    *pX = sContext.x;
    *pY = sContext.y;
    *pZ = sContext.z;

    return TCL_OK;
}

/*
 *---------------------------------------------------------------------------
 *
 * C3dDrawCamera --
 *
 *     This function is called before drawing any light source or OpenGL
 *     items to "position the camera". More technically, all required
 *     transforms are applied to the PROJECTION matrix.
 *
 *     The current matrix mode when this function exits is GL_PROJECTION.
 *
 * Results:
 *     None.
 *
 * Side effects:
 *     Applies transformations to the PROJECTION matrix.
 *
 *---------------------------------------------------------------------------
 */
void
C3dDrawCamera(C3dWidget *pCanvas)
{
    C3dTransform *pTransform;
    GLdouble matrix[16];

    pTransform = C3dTransformProjection(pCanvas);
    C3dTransformToMatrix(pTransform, matrix);
    C3dTransformDelete(pTransform);

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glMultMatrixd(matrix);
}

/*
 *---------------------------------------------------------------------------
 *
 * C3dCameraTransform --
 *
 * Results:
 *     None.
 *
 * Side effects:
 *     None.
 *
 *---------------------------------------------------------------------------
 */
int
C3dCameraTransform(
    C3dWidget *pCanvas,
    C3dTransform *pTransform
) {
    C3dVertex upVertex;

    upVertex.x =
        pCanvas->options.cameralocation->x +
        pCanvas->options.cameraup->x;
    upVertex.y =
        pCanvas->options.cameralocation->y +
        pCanvas->options.cameraup->y;
    upVertex.z =
        pCanvas->options.cameralocation->z +
        pCanvas->options.cameraup->z;

    C3dTransformApply(pTransform, &upVertex);
    C3dTransformApply(pTransform, pCanvas->options.cameralocation);
    C3dTransformApply(pTransform, pCanvas->options.cameracenter);

    pCanvas->options.cameraup->x =
        upVertex.x - pCanvas->options.cameralocation->x;
    pCanvas->options.cameraup->y =
        upVertex.y - pCanvas->options.cameralocation->y;
    pCanvas->options.cameraup->z =
        upVertex.z - pCanvas->options.cameralocation->z;

    C3dDrawWhenIdle(pCanvas, STATE_REDRAW_ALL);
    return TCL_OK;
}
