/*
 * cd3options.c --
 *
 *     This file contains the implementation of various custom Tk option
 *     types used by the widget and the items it contains.
 *
 *-------------------------------------------------------------------------
 */

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

#ifdef ANDROID
static int
power_of_two(input)
    int input;
{
    int value = 1;
    while (value < input) {
        value <<= 1;
    }
    return value;
}
#endif

/*
 *---------------------------------------------------------------------------
 *
 * colorSet --
 *
 * Results:
 *     None.
 *
 * Side effects:
 *     None.
 *
 *---------------------------------------------------------------------------
 */
static int
colorSet(
    ClientData clientData,         /* Not used */
    Tcl_Interp *interp,            /* Report errors here */
    Tk_Window tkwin,               /* Widget window */
    Tcl_Obj **valuePtr,            /* Pointer to new tags list Tcl_Obj* */
    char *recordPtr,               /* Pointer to record */
    int intOffset,                 /* Offset to start of C3dColor struct */
    char *saveIntPtr,              /* Not used */
    int flags                      /* Not used */
) {
    Tcl_Obj *pObj = *valuePtr;
    Tcl_Obj **aChannel;
    int nChannel;
    double r, g, b;
    double a = 1.0;
    C3dColor *pColor = (C3dColor *)(recordPtr + intOffset);
    XColor *xcolor;

    if (
        TCL_OK == Tcl_ListObjGetElements(0, pObj, &nChannel, &aChannel) &&
        (nChannel == 3 || nChannel ==4) &&
        TCL_OK == Tcl_GetDoubleFromObj(0, aChannel[0], &r) &&
        TCL_OK == Tcl_GetDoubleFromObj(0, aChannel[1], &g) &&
        TCL_OK == Tcl_GetDoubleFromObj(0, aChannel[2], &b) &&
        (nChannel == 3 || TCL_OK == Tcl_GetDoubleFromObj(0, aChannel[3], &a))
    ) {
        pColor->aChannel[0] = r;
        pColor->aChannel[1] = g;
        pColor->aChannel[2] = b;
        pColor->aChannel[3] = a;
        return TCL_OK;
    }

    xcolor = C3dAllocColorFromObj(interp, tkwin, pObj);
    if (!xcolor) {
        return TCL_ERROR;
    }

    pColor->aChannel[0] = ((float)xcolor->red / 65535.0);
    pColor->aChannel[1] = ((float)xcolor->green / 65535.0);
    pColor->aChannel[2] = ((float)xcolor->blue / 65535.0);
    pColor->aChannel[3] = 1.0;

    C3dFreeColor(xcolor);
    return TCL_OK;
}

/*
 *---------------------------------------------------------------------------
 *
 * colorRestore --
 *
 * Results:
 *     None.
 *
 * Side effects:
 *     None.
 *
 *---------------------------------------------------------------------------
 */
static void
colorRestore(
    ClientData clientData,
    Tk_Window tkwin,
    char *internalPtr,               /* C3dColor* structure */
    char *saveInternalPtr            /* Not used */
) {
    C3dColor *pColor = (C3dColor *)internalPtr;
    colorSet(0, 0, tkwin, &pColor->pObj, internalPtr, 0, 0, 0);
}

/*
 *---------------------------------------------------------------------------
 *
 * tagsSet --
 *
 * Results:
 *     None.
 *
 * Side effects:
 *     None.
 *
 *---------------------------------------------------------------------------
 */
static int
tagsSet(
    ClientData clientData,         /* Not used */
    Tcl_Interp *interp,            /* Report errors here */
    Tk_Window tkwin,               /* Not used */
    Tcl_Obj **valuePtr,            /* Pointer to new tags list Tcl_Obj* */
    char *recordPtr,               /* Pointer to record */
    int intOffset,                 /* Offset to C3dItem variable */
    char *saveIntPtr,              /* Pointer to 8 bytes of save space */
    int flags                      /* Not used */
) {
    Tcl_Obj *pList = *valuePtr;
    C3dItem *pItem = (C3dItem *)(recordPtr + intOffset);
    C3dWidget *pCanvas = pItem->pCanvas;
    Tcl_Obj **aTag;
    int nTag;
    int i;

    assert(pCanvas);

    if (Tcl_ListObjGetElements(interp, pList, &nTag, &aTag)) {
        return TCL_ERROR;
    }

    C3dRemoveTagFromItem(pCanvas, 0, pItem);

    for (i = 0; i < nTag; i++) {
         if (C3dAddTagToItem(pCanvas, aTag[i], pItem)) {
             return TCL_ERROR;
         }
    }
    return TCL_OK;
}

/*
 *---------------------------------------------------------------------------
 *
 * tagsGet --
 *
 * Results:
 *     None.
 *
 * Side effects:
 *     None.
 *
 *---------------------------------------------------------------------------
 */
static Tcl_Obj *
tagsGet(
    ClientData clientData,              /* Not used */
    Tk_Window tkwin,                    /* Not used */
    char *recordPtr,                    /* Pointer to C3dItem */
    int internalOffset                  /* Not used */
) {
    C3dItem *pItem = (C3dItem *)(recordPtr + internalOffset);
    Tcl_Obj *pRet = Tcl_NewObj();

    if (C3dGetTags(pItem, pRet)) {
        return 0;
    }

    return pRet;
}

/*
 *---------------------------------------------------------------------------
 *
 * teximageGet --
 *
 * Results:
 *     None.
 *
 * Side effects:
 *     None.
 *
 *---------------------------------------------------------------------------
 */
static Tcl_Obj *
teximageGet(
    ClientData clientData,              /* Not used */
    Tk_Window tkwin,                    /* Not used */
    char *recordPtr,                    /* Pointer to C3dItem */
    int internalOffset                  /* Offset to C3dTexture* */
) {
    C3dTexture *pTex = *(C3dTexture **)(recordPtr + internalOffset);
    if (pTex) {
        return pTex->pObj;
    }
    return Tcl_NewStringObj("", -1);
}

/*
 *---------------------------------------------------------------------------
 *
 * teximageSet --
 *
 * Results:
 *     None.
 *
 * Side effects:
 *     None.
 *
 *---------------------------------------------------------------------------
 */
static int
teximageSet(
    ClientData clientData,         /* Not used */
    Tcl_Interp *interp,            /* Report errors here */
    Tk_Window tkwin,               /* Widget window */
    Tcl_Obj **valuePtr,            /* Pointer to image Tcl_Obj* */
    char *recordPtr,               /* Pointer to record */
    int intOffset,                 /* Offset to C3dTexture* variable */
    char *saveIntPtr,              /* Pointer to 8 bytes of save space */
    int flags                      /* Not used */
) {
    Tk_PhotoHandle photo;
    Tk_PhotoImageBlock block;
    int w;
    int h;
    int x;
    int y;
    int *pL;
    int *pS;
    int mul;
    C3dTexture *pTex;
    const char *zPhoto = Tcl_GetString(*valuePtr);
    C3dTexture **ppTexture = (C3dTexture **)(recordPtr + intOffset);

    if (saveIntPtr) {
        *((C3dTexture **)saveIntPtr) = *ppTexture;
    }

    *ppTexture = 0;
    if (strlen(zPhoto) == 0) {
        return TCL_OK;
    }

    photo = Tk_FindPhoto(interp, zPhoto);
    if (!photo) {
        return TCL_ERROR;
    }
    Tk_PhotoGetImage(photo, &block);

    /* Set w and h to the width and height of the texture. One (but not
     * both) of w and h may be different from block.width and block.height.
     */
    w = block.width;
    h = block.height;
    pL = (w > h) ? &w : &h;
    pS = (w > h) ? &h : &w;
    for (mul = 2; (mul * *pS) <= *pL; mul++);
    *pL = (mul - 1) * *pS;
#ifdef ANDROID
    /* OpenGLES 1.1 limitation: must be power of 2 */
    w = power_of_two(w);
    h = power_of_two(h);
#endif

    pTex = (C3dTexture *)C3dAlloc(
            sizeof(C3dTexture) +
            w * h * 4 * sizeof(GLubyte)
    );
    pTex->iWidth = w;
    pTex->iHeight = h;
    pTex->aTexels = (GLubyte *)(&pTex[1]);
    pTex->pObj = *valuePtr;
    C3dIncrRefCount(pTex->pObj);

    for (y = 0; y < w; y++) {
        for (x = 0; x < w; x++) {
            int xin = x;
            int yin = y;
            unsigned char *zIn;
            GLubyte *zOut = &pTex->aTexels[(y * w + x) * 4];

            if (block.width != w) {
                xin = ((x * block.width) / w);
            }
            if (block.height != h) {
                yin = ((y * block.height) / h);
            }
            zIn = &block.pixelPtr[xin*block.pixelSize + yin*block.pitch];

            zOut[0] = zIn[block.offset[0]];
            zOut[1] = zIn[block.offset[1]];
            zOut[2] = zIn[block.offset[2]];
            zOut[3] = zIn[block.offset[3]];
        }
    }

    *ppTexture = pTex;
    return TCL_OK;
}

/*
 *---------------------------------------------------------------------------
 *
 * teximageFree --
 *
 * Results:
 *     None.
 *
 * Side effects:
 *     None.
 *
 *---------------------------------------------------------------------------
 */
static void
teximageFree(
    ClientData clientData,
    Tk_Window tkwin,
    char *internalPtr
) {
    C3dTexture *pTexture = *((C3dTexture **)internalPtr);
    if (pTexture) {
        C3dDecrRefCount(pTexture->pObj);
        C3dFree((char *)pTexture);
        *((C3dTexture **)internalPtr) = 0;
    }
}

/*
 *---------------------------------------------------------------------------
 *
 * teximageRestore --
 *
 * Results:
 *     None.
 *
 * Side effects:
 *     None.
 *
 *---------------------------------------------------------------------------
 */
static void
teximageRestore(
    ClientData clientData,
    Tk_Window tkwin,
    char *internalPtr,               /* Pointer to C3dItem */
    char *saveInternalPtr            /* Pointer to Tcl_Obj*[2]. */
) {
    C3dTexture **pOrig = (C3dTexture **)internalPtr;
    C3dTexture **pSave = (C3dTexture **)saveInternalPtr;
    *pOrig = *pSave;
}

/*
 *---------------------------------------------------------------------------
 *
 * texcoordsSet --
 *
 * Results:
 *     None.
 *
 * Side effects:
 *     None.
 *
 *---------------------------------------------------------------------------
 */
static int
texcoordsSet(
    ClientData clientData,         /* Not used */
    Tcl_Interp *interp,            /* Report errors here */
    Tk_Window tkwin,               /* Widget window */
    Tcl_Obj **valuePtr,            /* Pointer to image Tcl_Obj* */
    char *recordPtr,               /* Pointer to record */
    int intOffset,                 /* Offset to C3dTexture* variable */
    char *saveIntPtr,              /* Pointer to 8 bytes of save space */
    int flags                      /* Not used */
) {
    C3dTextureCoords **ppCoords = (C3dTextureCoords **)(recordPtr + intOffset);
    C3dTextureCoords *pCoords;
    Tcl_Obj **apObj;
    int nObj;
    int ii;

    if (saveIntPtr) {
        *((C3dTextureCoords **)saveIntPtr) = *ppCoords;
    }
    *ppCoords = 0;

    if (Tcl_ListObjGetElements(interp, *valuePtr, &nObj, &apObj)) {
        return TCL_ERROR;
    }
    if (nObj == 0) {
        return TCL_OK;
    }
    if (nObj % 2) {
        Tcl_ResetResult(interp);
        Tcl_AppendResult(interp,"Uneven number of values in -texcoords list",0);
        return TCL_ERROR;
    }

    pCoords = (C3dTextureCoords *)C3dAlloc(
            sizeof(C3dTextureCoords) +
            sizeof(float) * nObj
    );
    pCoords->aCoords = (float *)(&pCoords[1]);
    pCoords->nCoords = nObj / 2;
    for (ii = 0; ii < nObj; ii++) {
        double val;
        if (Tcl_GetDoubleFromObj(interp, apObj[ii], &val)) {
            C3dFree((char *)pCoords);
            return TCL_ERROR;
        }
        pCoords->aCoords[ii] = val;
    }
    pCoords->pObj = *valuePtr;
    C3dIncrRefCount(pCoords->pObj);
    *ppCoords = pCoords;

    return TCL_OK;
}

/*
 *---------------------------------------------------------------------------
 *
 * texcoordsGet --
 *
 * Results:
 *     None.
 *
 * Side effects:
 *     None.
 *
 *---------------------------------------------------------------------------
 */
static Tcl_Obj *
texcoordsGet(
    ClientData clientData,              /* Not used */
    Tk_Window tkwin,                    /* Not used */
    char *recordPtr,                    /* Pointer to C3dItem */
    int iOffset                         /* Offset to C3dTexture* */
) {
    C3dTextureCoords *pCoords = *(C3dTextureCoords **)(recordPtr + iOffset);
    if (pCoords) {
        return pCoords->pObj;
    }
    return Tcl_NewStringObj("", -1);
}

/*
 *---------------------------------------------------------------------------
 *
 * texcoordsFree --
 *
 * Results:
 *     None.
 *
 * Side effects:
 *     None.
 *
 *---------------------------------------------------------------------------
 */
static void
texcoordsFree(
    ClientData clientData,
    Tk_Window tkwin,
    char *internalPtr
) {
    C3dTextureCoords *pCoords = *((C3dTextureCoords **)internalPtr);
    if (pCoords) {
        C3dDecrRefCount(pCoords->pObj);
        C3dFree((char *)pCoords);
    }
}

/*
 *---------------------------------------------------------------------------
 *
 * texcoordsRestore --
 *
 * Results:
 *     None.
 *
 * Side effects:
 *     None.
 *
 *---------------------------------------------------------------------------
 */
static void
texcoordsRestore(
    ClientData clientData,
    Tk_Window tkwin,
    char *internalPtr,               /* Pointer to C3dItem */
    char *saveInternalPtr            /* Pointer to Tcl_Obj*[2]. */
) {
    C3dTextureCoords **pOrig = (C3dTextureCoords **)internalPtr;
    C3dTextureCoords **pSave = (C3dTextureCoords **)saveInternalPtr;
    *pOrig = *pSave;
}

/*
 *---------------------------------------------------------------------------
 *
 * C3dCreateOptionTable --
 *
 *     This function is a wrapper around Tk_CreateOptionTable(). See the
 *     manpage of that function for an overview of functionality.
 *
 *     Before calling Tk_CreateOptionTable(), this function makes the
 *     following modifications to pSpec:
 *
 *     1) Any option with type TK_OPTION_COLOR is changed to a custom
 *        option using colorSet() and colorRestore(). The internalOffset
 *        value of the option is assumed to be the offset to a C3dColor
 *        struct.
 *
 *     2) Any option with name "-tags" is changed to a custom option that
 *        uses tagsSet(), tagsGet(), tagsRestore() and tagsFree(). The
 *        internalOffset is assumed to be the offset to a C3dTag* variable.
 *        If it is not already, the objOffset is set to -1 (do not use).
 *
 *     3) Any option with name "-teximage" is changed to a custom option
 *        that uses teximageSet(), teximageGet(), teximageRestore() and
 *        teximageFree(). The internal offset is assumed to be the offset to
 *        a variable of type C3dTexture*.
 *
 * Results:
 *     If successful, a Tk_OptionTable identifier. Otherwise NULL.
 *
 * Side effects:
 *     Modifies the array pointed to by pSpec.
 *
 *---------------------------------------------------------------------------
 */
Tk_OptionTable
C3dCreateOptionTable(Tcl_Interp *interp, Tk_OptionSpec *pSpec)
{
    static Tk_ObjCustomOption colorOption = {
        "c3dcolor",
        colorSet,
        0,
        colorRestore,
        0,
        0
    };
    static Tk_ObjCustomOption tagsOption = {
        "tags",
        tagsSet,
        tagsGet,
        0,
        0,
        0
    };
    static Tk_ObjCustomOption teximageOption = {
        "teximage",
        teximageSet,
        teximageGet,
        teximageRestore,
        teximageFree,
        0
    };
    static Tk_ObjCustomOption texcoordsOption = {
        "texcoords",
        texcoordsSet,
        texcoordsGet,
        texcoordsRestore,
        texcoordsFree,
        0
    };
    Tk_OptionSpec *p;

    for (p = pSpec; p->type != TK_OPTION_END; p++) {
        /* Modify color options. */
        if (p->type == TK_OPTION_COLOR) {
            p->type = TK_OPTION_CUSTOM;
            p->clientData = (ClientData)&colorOption;
            p->objOffset = p->internalOffset + Tk_Offset(C3dColor, pObj);
        }

        /* Modify -tags options. */
        else if (p->optionName && 0 == strcmp(p->optionName, "-tags")) {
            p->type = TK_OPTION_CUSTOM;
            p->clientData = (ClientData)&tagsOption;
            p->objOffset = -1;
        }

        /* Modify -teximage options. */
        else if (p->optionName && 0 == strcmp(p->optionName, "-teximage")) {
            p->type = TK_OPTION_CUSTOM;
            p->clientData = (ClientData)&teximageOption;
            p->objOffset = -1;
        }

        /* Modify -texcoords options. */
        else if (p->optionName && 0 == strcmp(p->optionName, "-texcoords")) {
            p->type = TK_OPTION_CUSTOM;
            p->clientData = (ClientData)&texcoordsOption;
            p->objOffset = -1;
        }
    }

    /* Now that the Tk_OptionSpec array has been updated, invoke
     * Tk_CreateOptionTable() to create the table based on the modified
     * template.
     */
    return Tk_CreateOptionTable(interp, pSpec);
}

