/*
 * c3dtext.c --
 *
 *     This file implements the C3dItemType functions for the 2dtext item.
 */

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

#define NUM_AA_BITMAPS 4

typedef struct TextItem TextItem;
struct TextItem {
    C3dItem common;

    /* The following variables are set using options. */
    C3dColor color;
    Tk_Font font;
    const char *text;
    int anchor;
    double layer;

    /* Not set by options. */
    int w;                              /* Width of rendered text. */
    int h;                              /* Height of rendered text. */
    GLubyte *aBitmap[NUM_AA_BITMAPS];   /* Bitmaps of rendered text. */
    C3dOverlayLink overlay;
};

/*
 *---------------------------------------------------------------------------
 *
 * adjustForAnchor --
 *
 *     This function modifies an (x, y) coordinate pair to account for the
 *     -anchor option of the text item.
 *
 *     When this function is called, pX and pY point to the x and y
 *     coordinates as configured by the user.  Argument 'anchor' contains
 *     the value of the -anchor option. The width and height of the
 *     rendered text are passed as the w and h arguments, respectively.
 *
 *     The values of *pX and pY are modified so they refer to the location
 *     of the south-west corner of the rendered text.
 *
 * Results:
 *     None.
 *
 * Side effects:
 *     None.
 *
 *---------------------------------------------------------------------------
 */
static void
adjustForAnchor(
    float *pX,                 /* Pointer to x-coordinate */
    float *pY,                 /* Pointer to y-coordinate */
    int w,                     /* Width of rendered text */
    int h,                     /* Height of rendered text */
    int anchor                 /* Value of -anchor switch */
) {
    switch (anchor) {
        case TK_ANCHOR_N:
        case TK_ANCHOR_S:
        case TK_ANCHOR_CENTER:
            *pX -= ((float)w / 2.0);
            break;
        case TK_ANCHOR_SE:
        case TK_ANCHOR_E:
        case TK_ANCHOR_NE:
            *pX -= (float)w;
            break;
    }
    switch (anchor) {
        case TK_ANCHOR_CENTER:
        case TK_ANCHOR_E:
        case TK_ANCHOR_W:
            *pY += ((float)h / 2.0);
            break;
        case TK_ANCHOR_N:
        case TK_ANCHOR_NW:
        case TK_ANCHOR_NE:
            *pY += (float)h;
            break;
    }
}

/*
 *---------------------------------------------------------------------------
 *
 * textOverlay --
 *
 *     Return a pointer to the C3dOverlayLink structure allocated along
 *     with this text item.
 *
 * Results:
 *     Pointer to C3dOverlayLink struct.
 *
 * Side effects:
 *     None.
 *
 *---------------------------------------------------------------------------
 */
static C3dOverlayLink *
textOverlay(
    C3dItem *pItem
) {
    return &((TextItem *)pItem)->overlay;
}

/*
 *---------------------------------------------------------------------------
 *
 * textTable --
 *
 *     Return the Tk_OptionTable used to implement the itemconfigure and
 *     itemcget commands for the text item type.
 *
 * Results:
 *     Option table for text items.
 *
 * Side effects:
 *     None.
 *
 *---------------------------------------------------------------------------
 */
static Tk_OptionTable
textTable(
    Tcl_Interp *interp
) {
    static Tk_OptionSpec text_option_array[] = {
        TAGS_OPTION,
        HIDDEN_OPTION,
        C3DOPTION(TextItem, COLOR, color, "white", 0),
        C3DOPTION(TextItem, FONT, font, "Helvetica", 0),
        C3DOPTION(TextItem, STRING, text, "", 0),
        C3DOPTION(TextItem, ANCHOR, anchor, "center", 0),
        C3DOPTION(TextItem, DOUBLE, layer, "1.0", 0),
        {TK_OPTION_END, 0, 0, 0, 0, 0, 0, 0, 0}
    };
    Tk_OptionTable text_options;

    text_options = C3dCreateOptionTable(interp, text_option_array);
    assert(text_options);

    return text_options;
}

/*
 *---------------------------------------------------------------------------
 *
 * textCreate --
 *
 *     Create a new text item and return a pointer to it.
 *
 * Results:
 *     Pointer to newly allocated C3dItem structure containing an
 *     unconfigured text item.
 *
 * Side effects:
 *     None.
 *
 *---------------------------------------------------------------------------
 */
static C3dItem *
textCreate(
    C3dWidget *pCanvas,           /* Widget the item will be a part of */
    Tcl_Obj *pCoords              /* 2dCoordList */
) {
    TextItem *pText;
    int nBytes;
    Tcl_Obj **aCoord;
    Tcl_Size nCoord;
    double x, y;
    Tcl_Interp *interp = pCanvas->interp;

    /* The 2dCoordList argument for a text item must be a list of the
     * form {<x> <y>}. Check this is the case and store the two list
     * elements in variables x and y.
     *
     * If there is some problem with the 2dCoordList, then leave an error
     * in pCanvas->interp and return NULL.
     */
    if (Tcl_ListObjGetElements(interp, pCoords, &nCoord, &aCoord)) {
        return 0;
    }
    if (nCoord != 2) {
        Tcl_ResetResult(interp);
        Tcl_AppendResult(interp,
                "Text items require a single two-dimensional coordinate", 0);
        return 0;
    }
    if (Tcl_GetDoubleFromObj(interp, aCoord[0], &x) ||
        Tcl_GetDoubleFromObj(interp, aCoord[1], &y)
    ) {
        return 0;
    }

    /* Allocate the TextItem struct and enough space for the single element
     * aVertex array. All values are intially zero. The caller will
     * initialise option values based on the default values in the option
     * table (see textTable()).
     */
    nBytes = sizeof(TextItem) +              /* C3dItem struct */
             sizeof(C3dVertex);              /* aVertex array */
    pText = (TextItem *)C3dAlloc(nBytes);
    memset(pText, 0, nBytes);
    pText->common.aVertex = (C3dVertex *)&pText[1];
    pText->common.aVertex[0].x = x;
    pText->common.aVertex[0].y = y;
    pText->common.aVertex[0].z = 0.0;

    return (C3dItem *)pText;
}

/*
 *---------------------------------------------------------------------------
 *
 * pixelToBit --
 *
 *     Return an integer between 0 and (NUM_AA_BITMAPS-1) representing the
 *     "darkness" of the pixel at (x, y) in image pImage. Zero is returned
 *     for a pixel that is not colored at all, (NUM_AA_BITMAPS-1) is
 *     returned for a pixel that is very dark.
 *
 * Results:
 *     None.
 *
 * Side effects:
 *     None.
 *
 *---------------------------------------------------------------------------
 */
static int
pixelToBit(
    Display *pDisplay,
    Colormap cmap,
    XImage *pImage,
    int x,
    int y
) {
    unsigned long pixel = XGetPixel(pImage, x, y);
    unsigned short r, g, b;

    /* TODO: Other visuals other than true color. */
    r = (pixel & 0x00FF0000) >> 8;
    g = (pixel & 0x0000FF00) >> 0;
    b = (pixel & 0x000000FF) << 8;

#if NUM_AA_BITMAPS == 1
    return ((b+g+r) < 130000) ? 1 : 0;
#else
    return (196608 - (b+g+r)) / (196608 / NUM_AA_BITMAPS);
#endif
}

/*
 *---------------------------------------------------------------------------
 *
 * textCreateBitmap --
 *
 *     Create the Open GL bitmaps for text item pItem from the X windows
 *     image pImage (width w, height h). The image has been created by
 *     rendering the text to a pixmap with background color WhitePixel()
 *     and foreground color BlackPixel().
 *
 * Results:
 *     None.
 *
 * Side effects:
 *     None.
 *
 *---------------------------------------------------------------------------
 */
static void
textCreateBitmap(
    C3dWidget *pCanvas,
    TextItem *pItem,
    XImage *pImage,
    int w,
    int h
) {
    int i;
    int j;
    int k;
    int nBytes;           /* Number of bytes to allocate for bitmap. */
    int b;                /* Number of bytes to allocate for bitmap. */

    Tk_Window win = pCanvas->tkwin;
    Colormap cmap = Tk_Colormap(win);
    Display *pDisplay = Tk_Display(win);

    if (pItem->aBitmap[0]) {
        C3dFree((char *)pItem->aBitmap[0]);
        pItem->aBitmap[0] = 0;
    }

    /* Allocate enough space for bitmaps with the start of each row aligned
     * to 4-byte boundaries. The format of the bitmaps used by this item
     * assumes the OpenGL state is set as follows:
     *
     *     glPixelStorei(GL_PACK_LSB_FIRST, 0);
     *     glPixelStorei(GL_PACK_ALIGNMENT, 4);
     */
    nBytes = h * 8 * ((((w+7) / 8) + 7) / 8);
    pItem->aBitmap[0] = (GLubyte *)C3dAlloc(NUM_AA_BITMAPS * nBytes);
    for (k = 1; k < NUM_AA_BITMAPS; k++) {
        pItem->aBitmap[k] = &pItem->aBitmap[0][nBytes * k];
    }

    b = 0;
    for (j = h-1; j >= 0; j--) {
        for (i = 0; i < w; i += 8) {
            GLbyte byte[NUM_AA_BITMAPS + 1];

            memset(byte, 0, (NUM_AA_BITMAPS+1) * sizeof(GLbyte));
            switch (w - i) {
                default:
                    byte[pixelToBit(pDisplay, cmap, pImage, i+7, j)] |= 0x01;
                case 7:
                    byte[pixelToBit(pDisplay, cmap, pImage, i+6, j)] |= 0x02;
                case 6:
                    byte[pixelToBit(pDisplay, cmap, pImage, i+5, j)] |= 0x04;
                case 5:
                    byte[pixelToBit(pDisplay, cmap, pImage, i+4, j)] |= 0x08;
                case 4:
                    byte[pixelToBit(pDisplay, cmap, pImage, i+3, j)] |= 0x10;
                case 3:
                    byte[pixelToBit(pDisplay, cmap, pImage, i+2, j)] |= 0x20;
                case 2:
                    byte[pixelToBit(pDisplay, cmap, pImage, i+1, j)] |= 0x40;
                case 1:
                    byte[pixelToBit(pDisplay, cmap, pImage, i+0, j)] |= 0x80;
            };

            for (k = 0; k < NUM_AA_BITMAPS; k++) {
                pItem->aBitmap[k][b] = byte[k+1];
            }
            b++;
        }
        b = 4 * ((b + 3) / 4);
    }
}

/*
 *---------------------------------------------------------------------------
 *
 * textDrawOpenGL--
 *
 *     Draw the text by converting it to one or more bitmaps, which can
 *     then be rendered using OpenGL commands. This method is only used if
 *     the widget -saveunder option is set to "none".
 *
 * Results:
 *     None.
 *
 * Side effects:
 *     May allocate bitmaps at TextItem.aBitmap. These will be eventually
 *     deleted by textDelete().
 *
 *---------------------------------------------------------------------------
 */
static void
textDrawOpenGL(
    C3dItem *pItem,
    ClientData clientData
) {
    C3dWidget *pCanvas = (C3dWidget *)clientData;
    int k;
    GLfloat color[4];
    GLfloat alpha;                        /* Alpha channel of the text color */
    TextItem *pText = (TextItem *)pItem;
    Tk_Window win = pCanvas->tkwin;
    Display *pDisplay = Tk_Display(win);
    float x = pItem->aVertex[0].x;
    float y = pItem->aVertex[0].y;
    float xbitmap = 0.0;
    float ybitmap = 0.0;
    C3dTransform *pOrtho;
    GLdouble mOrtho[16];

    if (!(pItem->flags & ITEMFLAG_CACHE_VALID)) {
        int w;           /* Width of generated bitmap, in pixels */
        int h;           /* Height of generated bitmap, in pixels */
        Pixmap pix;      /* Drawable to render text to. */
        Tk_FontMetrics metrics;
        XImage *pImage;
        const char *zText = pText->text;
        int nText = strlen(zText);
        Tk_Font font = pText->font;

        GC gc = 0;
        XGCValues gc_values;
        int mask;

        /* Figure out the width and height of the rendered text. */
        w = Tk_TextWidth(pText->font, zText, nText);
        Tk_GetFontMetrics(pText->font, &metrics);
        h = metrics.ascent + metrics.descent;
        pText->w = w;
        pText->h = h;

        if (w > 0 && h > 0) {
            /* Allocate a pixmap to render text into. Clear the background to
             * white. The text is drawn on top in black.
             */
            pix = C3dGetPixmap(pDisplay, Tk_WindowId(win), w, h, Tk_Depth(win));
            assert(pix);
            gc_values.foreground = WhitePixel(pDisplay, Tk_ScreenNumber(win));
            mask = GCForeground;
            gc = C3dGetGC(win, mask, &gc_values);
            XFillRectangle(pDisplay, pix, gc, 0, 0, w, h);
            C3dFreeGC(pDisplay, gc);

            /* Render the text to the pixmap */
            gc_values.foreground = BlackPixel(pDisplay, Tk_ScreenNumber(win));
            gc_values.font = Tk_FontId(font);
            mask = GCForeground | GCFont;
            gc = C3dGetGC(win, mask, &gc_values);
            Tk_DrawChars(pDisplay,pix,gc,font,zText,nText,0,metrics.ascent);
            C3dFreeGC(pDisplay, gc);

            /* Retrieve an XImage of the rendered text. */
            pImage = XGetImage(pDisplay, pix, 0, 0, w, h, AllPlanes, ZPixmap);
            assert(pImage);

            /* Call textCreateBitmap() to create the bitmap(s) for the rendered
             * text. Then destroy the XImage and pixmap. Once the bitmap(s)
             * have been created they are no longer useful.
             */
            textCreateBitmap(pCanvas, pText, pImage, w, h);
            XDestroyImage(pImage);
            C3dFreePixmap(pDisplay, pix);
        }

        pItem->flags |= ITEMFLAG_CACHE_VALID;
    }

    if (pText->w <= 0 || pText->h <= 0) {
        return;
    }

    if (!pCanvas->options.enablealpha) {
        glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    }

    /* Set the projection matrix to the standard widget 'overlay' matrix. */
    glMatrixMode(GL_PROJECTION);
    glPushMatrix();
    glLoadIdentity();
    pOrtho = C3dTransformOverview(pCanvas);
    C3dTransformToMatrix(pOrtho, mOrtho);
    glMultMatrixd(mOrtho);
    C3dTransformDelete(pOrtho);

    glMatrixMode(GL_MODELVIEW);
    glDisable(GL_LIGHTING);

#ifndef ANDROID
    glPixelStorei(GL_PACK_LSB_FIRST, 0);
#endif
    glPixelStorei(GL_PACK_ALIGNMENT, 4);

    memcpy(color, pText->color.aChannel, sizeof(GLfloat)*3);
    alpha = pText->color.aChannel[3];
    if (!pCanvas->options.enablealpha) {
        alpha = 1.0;
    }

    /* Variables x and y currently store the x and y coordinates passed to
     * the item when it was created, or set using the [.win coords]
     * command.
     *
     * How these coordinates are interpreted depends on the value of the
     * -anchor option. This call modifies the values of x and y so that
     * they specify the location of the bottom-left corner of the text, in
     * screen coordinates.
     */
    adjustForAnchor(&x, &y, pText->w, pText->h, pText->anchor);

    if (pCanvas->eSelectMode) {
        /* glshim wants float */
        glRectf(x, y - pText->h, x + pText->w, y);
    } else {
        if (x < 0.0) {
            xbitmap = -1.0 * x;
            x = 0.0;
        }
        if (y >= pCanvas->options.height) {
            ybitmap = y - pCanvas->options.height;
            y = pCanvas->options.height - 1;
        }
        for (k = 0; k < NUM_AA_BITMAPS; k++) {
            GLubyte *pBitmap = pText->aBitmap[k];
            color[3] = (alpha / (double)NUM_AA_BITMAPS) * (double)(k+1);
            glColor4f(color[0], color[1], color[2], color[3]);
            /* glshim wants float */
            glRasterPos2f(x, y);
            glBitmap(pText->w, pText->h, xbitmap, ybitmap, 0, 0, pBitmap);
        }
    }

    glMatrixMode(GL_PROJECTION);
    glPopMatrix();
    glMatrixMode(GL_MODELVIEW);
#ifdef ANDROID
    /* glshim: this draws the bitmaps */
    glViewport(0, 0, pCanvas->options.width, pCanvas->options.height);
#endif

    if (!pCanvas->options.enablealpha) {
        glDisable(GL_BLEND);
    }
}

/*
 *---------------------------------------------------------------------------
 *
 * textDraw --
 *
 *     Draw the text item. Text items can be drawn one of two ways,
 *     depending on the value of the widget -saveunder option. If
 *     -saveunder is other than "none", then we can draw directly onto the
 *     drawable C3dWidget.pixmap.
 *
 *     If -saveunder is "none", then we convert the image of the rendered
 *     text to one or more bitmaps and draw these using OpenGL commands.
 *     This is done by textDrawOpenGL().
 *
 * Results:
 *     None.
 *
 * Side effects:
 *     None.
 *
 *---------------------------------------------------------------------------
 */
static void
textDrawCallback(
    C3dItem *pItem,
    ClientData clientData
) {
    C3dWidget *pCanvas = (C3dWidget *)clientData;
    if (!pCanvas->eSelectMode && pCanvas->options.saveunder) {
        TextItem *pText = (TextItem *)pItem;

        GC gc;
        XGCValues gc_values;
        XColor color;
        XColor *pColor;
        float x, y;
        int w, h;
        Display *pDisplay = Tk_Display(pCanvas->tkwin);
        const char *zText = pText->text;
        int nText = strlen(zText);
        Tk_Font font = pText->font;
        Tk_FontMetrics fontMetrics;

        color.red = 65535 * pText->color.aChannel[0];
        color.green = 65535 * pText->color.aChannel[1];
        color.blue = 65535 * pText->color.aChannel[2];
        pColor = C3dGetColorByValue(pCanvas->tkwin, &color);
        assert(pColor);

        gc_values.foreground = pColor->pixel;
        gc_values.font = Tk_FontId(pText->font);

        Tk_MeasureChars(pText->font, pText->text, nText, -1, 0, &w);
        Tk_GetFontMetrics(pText->font, &fontMetrics);
        h = fontMetrics.ascent + fontMetrics.descent;
        x = pItem->aVertex[0].x;
        y = pItem->aVertex[0].y - fontMetrics.descent;

        adjustForAnchor(&x, &y, w, h, pText->anchor);

        gc = C3dGetGC(pCanvas->tkwin, GCForeground | GCFont, &gc_values);
        Tk_DrawChars(pDisplay, pCanvas->pixmap, gc, font, zText, nText, x, y);

        C3dFreeColor(pColor);
        C3dFreeGC(pDisplay, gc);
    } else {
        textDrawOpenGL(pItem, pCanvas);
    }
}

static void
textDraw(
    C3dWidget *pCanvas,
    C3dItem *pItem
) {
    TextItem *p = (TextItem *)pItem;
    C3dItemDrawCallback(pCanvas, textDrawCallback, pItem, pCanvas, p->layer);
}

/*
 *---------------------------------------------------------------------------
 *
 * textDelete --
 *
 *     Delete the text item. It is assumed that Tk_FreeConfigOptions() has
 *     already been called. This function deletes the structure pointed to
 *     by pItem.
 *
 * Results:
 *     None.
 *
 * Side effects:
 *     Renders pItem unusable.
 *
 *---------------------------------------------------------------------------
 */
static void
textDelete(
    C3dItem *pItem
) {
    TextItem *pText = (TextItem *)pItem;
    if (pText->aBitmap[0]) {
        C3dFree((char *)pText->aBitmap[0]);
    }
    C3dFree((char *)pText);
}

/*
 *---------------------------------------------------------------------------
 *
 * C3dText --
 *
 *     Return a pointer to the C3dItemType structure for text items.
 *
 * Results:
 *     None.
 *
 * Side effects:
 *     None.
 *
 *---------------------------------------------------------------------------
 */
C3dItemType *
C3dText(void)
{
    static C3dItemType text = {
        "2dtext",      /* zType    */
        textOverlay,   /* isOverlay */
        textCreate,    /* xCreate  */
        0,             /* xMultiCreate  */
        textDelete,    /* xDelete  */
        textDraw,      /* xDraw    */
        textTable,     /* xTable   */
        0,               /* xStatistics */
        0                /* xCoords */
    };
    return &text;
}
