/*
 * c3dlight.c --
 *
 *     This file implements the C3dItemType functions for the light item.
 */

#include "c3d.h"
#include <assert.h>
#include <math.h>


typedef struct LightOptions LightOptions;
struct LightOptions {
    int horizon;

    C3dColor ambient;
    C3dColor diffuse;
    C3dColor specular;

    double constantattenuation;
    double linearattenuation;
    double quadraticattenuation;
    double spotexponent;
    double spotcutoff;

    GLfloat position[4];
    GLfloat spotdirection[4];
};
#define OPTIONS_STRUCT(pItem) (LightOptions *)&(((C3dVertex *)&pItem[1])[2])

/*
 * Map between integers 0..7 and OpenGL light identifiers
 * GL_LIGHT0..GL_LIGHT7.
 */
int OglLightEnum[CANVAS3D_MAX_LIGHTS] = {
    GL_LIGHT0, GL_LIGHT1, GL_LIGHT2, GL_LIGHT3,
    GL_LIGHT4, GL_LIGHT5, GL_LIGHT6, GL_LIGHT7
};

/*
 *---------------------------------------------------------------------------
 *
 * lightTable --
 *
 * Results:
 *     None.
 *
 * Side effects:
 *     None.
 *
 *---------------------------------------------------------------------------
 */
static Tk_OptionTable
lightTable(Tcl_Interp *interp)
{

#define LIGHT_OPTION(t, n, d) {                                          \
    TK_OPTION_ ## t, "-" #n, 0, 0, d, -1,                                \
    sizeof(C3dVertex)*2 + sizeof(C3dItem) + offsetof(LightOptions, n),  \
    0, 0, 0                                                              \
}

    static Tk_OptionSpec light_option_array[] = {
        /* Common options */
        TAGS_OPTION,
        // ITEM_OPTION(BOOLEAN, hidden, "false", 0),
        HIDDEN_OPTION,

        /* Light specific options */
        LIGHT_OPTION(BOOLEAN, horizon,              "false"),
        LIGHT_OPTION(COLOR,   ambient,              "#000000"),
        LIGHT_OPTION(COLOR,   diffuse,              "#FFFFFF"),
        LIGHT_OPTION(COLOR,   specular,             "#FFFFFF"),
        LIGHT_OPTION(DOUBLE,  constantattenuation,  "1.0"),
        LIGHT_OPTION(DOUBLE,  linearattenuation,    "0.0"),
        LIGHT_OPTION(DOUBLE,  quadraticattenuation, "0.0"),
        LIGHT_OPTION(DOUBLE,  spotexponent,         "0.0"),
        LIGHT_OPTION(DOUBLE,  spotcutoff,           "180.0"),

        /* End of array marker */
        {TK_OPTION_END, 0, 0, 0, 0, 0, 0, 0, 0}
    };
    Tk_OptionTable light_options;

    light_options = C3dCreateOptionTable(interp, light_option_array);
    assert(light_options);

    return light_options;
}

/*
 *---------------------------------------------------------------------------
 *
 * lightCreate --
 *
 *     Allocate and return a pointer to a new light item. The first
 *     parameter is the widget object, and the second is a coordinate list
 *     consisting of a single coordinate - the location of the light
 *     source.
 *
 *     This function adds a pointer to the new light object to the
 *     pCanvas->aLight[] array and increments pCanvas->nLight.
 *
 *     If this function fails, either because of a problem with the
 *     coordinates list supplied, or because there are already the maximum
 *     number of allowable light sources present in the scene, NULL is
 *     returned and an error message written to pCanvas->interp.
 *
 * Results:
 *     Pointer to new light item if successful, NULL if an error occurs.
 *
 * Side effects:
 *     Modifies pCanvas->nLight and pCanvas->aLight[].
 *
 *---------------------------------------------------------------------------
 */
static C3dItem *
lightCreate(C3dWidget *pCanvas, Tcl_Obj *pCoords)
{
    C3dItem *pLight;
    int nBytes;
    int nCoords;
    Tcl_Interp *interp = pCanvas->interp;

    /* If the maximum number of light sources have already been added to
     * the scene, creating a new one is an error.
     */
    if (pCanvas->nLight == CANVAS3D_MAX_LIGHTS) {
        Tcl_ResetResult(interp);
        Tcl_AppendResult(interp, "Maximum light source count exceeded", 0);
        return 0;
    }

    if (C3dCheckCoords(interp, pCoords, 1, 2, &nCoords)) {
        return 0;
    }

    nBytes = sizeof(C3dItem) +              /* C3dItem struct */
             2 * sizeof(C3dVertex) +        /* C3dItem.aVertex array */
             sizeof(LightOptions);          /* Item options structure */
    pLight = (C3dItem *)C3dAlloc(nBytes);
    memset(pLight, 0, nBytes);
    pLight->aVertex = (C3dVertex *)&pLight[1];

    if (C3dSetCoords(interp, pCoords, 0, pLight)) {
        C3dFree((char *)pLight);
        return 0;
    }
    if (nCoords == 1) {
        pLight->aVertex[1].x = pLight->aVertex[0].x;
        pLight->aVertex[1].y = pLight->aVertex[0].y;
        pLight->aVertex[1].z = pLight->aVertex[0].z - 1.0;
    }

    assert(pCanvas->nLight < 8);
    pCanvas->aLight[pCanvas->nLight++] = pLight;

    return pLight;
}

/*
 *---------------------------------------------------------------------------
 *
 * lightDelete --
 *
 * Results:
 *     None.
 *
 * Side effects:
 *     None.
 *
 *---------------------------------------------------------------------------
 */
static void
lightDelete(C3dItem *pItem)
{
    int i;
    C3dWidget *pCanvas = pItem->pCanvas;
    int seen = 0;

    assert(pCanvas->nLight > 0);

    for (i = 0; i < pCanvas->nLight; i++) {
        if (seen) {
            pCanvas->aLight[i-1] = pCanvas->aLight[i];
        } else if (pCanvas->aLight[i] == pItem) {
            seen = 1;
        }
    }
    assert(seen);

    pCanvas->nLight--;
    C3dFree((char *)pItem);
}

/*
 *---------------------------------------------------------------------------
 *
 * lightDraw --
 *
 * Results:
 *     None.
 *
 * Side effects:
 *     None.
 *
 *---------------------------------------------------------------------------
 */
static void
lightDraw(
    C3dWidget *pCanvas,
    C3dItem *pItem,
    int ogllight
) {
    C3dVertex *aVertex = pItem->aVertex;
    LightOptions *pOptions = (LightOptions *)OPTIONS_STRUCT(pItem);

    glEnable(ogllight);

    /* Set the position of the light-source. */
    pOptions->position[0] = aVertex[0].x;
    pOptions->position[1] = aVertex[0].y;
    pOptions->position[2] = aVertex[0].z;
    pOptions->position[3] = (pOptions->horizon ? 0.0 : 1.0);
    glLightfv(ogllight, GL_POSITION, pOptions->position);

    /* Set the diffuse, ambient and specular components of the light. */
    glLightfv(ogllight, GL_AMBIENT, pOptions->ambient.aChannel);
    glLightfv(ogllight, GL_DIFFUSE, pOptions->diffuse.aChannel);
    glLightfv(ogllight, GL_SPECULAR, pOptions->specular.aChannel);

    /* Set the attenuation constants for the light source. */
    glLightf(ogllight, GL_CONSTANT_ATTENUATION, pOptions->constantattenuation);
    glLightf(ogllight, GL_LINEAR_ATTENUATION, pOptions->linearattenuation);
    glLightf(ogllight, GL_QUADRATIC_ATTENUATION,pOptions->quadraticattenuation);

    /* Spotlight parameters */
    glLightf(ogllight, GL_SPOT_EXPONENT, pOptions->spotexponent);
    glLightf(ogllight, GL_SPOT_CUTOFF, pOptions->spotcutoff);
    pOptions->spotdirection[0] = aVertex[1].x - aVertex[0].x;
    pOptions->spotdirection[1] = aVertex[1].y - aVertex[0].y;
    pOptions->spotdirection[2] = aVertex[1].z - aVertex[0].z;
    pOptions->spotdirection[3] = pOptions->position[3];
    glLightfv(ogllight, GL_SPOT_DIRECTION, pOptions->spotdirection);
}

/*
 *---------------------------------------------------------------------------
 *
 * C3dDrawLights --
 *
 * Results:
 *     None.
 *
 * Side effects:
 *     None.
 *
 *---------------------------------------------------------------------------
 */
int
C3dDrawLights(C3dWidget *pCanvas)
{
    assert((sizeof(pCanvas->aLight) / sizeof(pCanvas->aLight[0]))
	== (sizeof(OglLightEnum) / sizeof(OglLightEnum[0])));

    /* If one or more light-source items have been added to the scene, then
     * enable Open-GL lighting calculations. Otherwise, disable them.
     */
    if (pCanvas->nLight > 0) {
        glEnable(GL_LIGHTING);
        glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, GL_TRUE);
    } else {
        glDisable(GL_LIGHTING);
    }

    /* Configure the scene light-sources, if any. If there are no light
     * sources, we don't need to bother disabling all the lights, because
     * the entire Open-GL lighting model has been disabled above.
     */
    if (pCanvas->nLight > 0) {
        int i;
        for (i = 0; i < CANVAS3D_MAX_LIGHTS; i++) {
            C3dItem *pLight = pCanvas->aLight[i];
            if (i < pCanvas->nLight && !pLight->hidden) {
                lightDraw(pCanvas, pLight, OglLightEnum[i]);
            } else {
                glDisable(OglLightEnum[i]);
            }
        }
    }

    return TCL_OK;
}

/*
 *---------------------------------------------------------------------------
 *
 * C3dLight --
 *
 *     Return a pointer to a C3dItemType structure populated with callbacks
 *     for the light item.
 *
 * Results:
 *     See above.
 *
 * Side effects:
 *     None.
 *
 *---------------------------------------------------------------------------
 */
C3dItemType *
C3dLight(void)
{
    static C3dItemType light = {
        "light",        /* zType    */
        0,              /* isOverlay */
        lightCreate,    /* xCreate  */
        0,              /* xMultiCreate  */
        lightDelete,    /* xDelete  */
        0,              /* xDraw    */
        lightTable,     /* xTable   */
        0,              /* xStatistics */
        0               /* xCoords */
    };
    return &light;
}
