/*
 * c3dline.c --
 *
 *     This file implements the C3dItemType functions for the line and
 *     point items.
 */

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

/*
 * The following struct is used in place of a C3dItemType for the point and
 * line types defined in this file.
 *
 * Some of the item-type callback functions in this file are used for
 * three-dimensional point and line objects. To tell if it is being
 * called as point or line, each function does:
 *
 *     mode = ((PointLineItemType *)(pItem->pType))->subtype;
 *     assert(mode == POINT || mode == LINE);
 */
typedef struct PointLineItemType PointLineItemType;
struct PointLineItemType {
    C3dItemType common;               /* Must be first! */
    int subtype;                      /* POINT or LINE */
};

#define POINT 1
#define LINE 2

/*
 * This structure contains data for a line or point item that is private to
 * this file. There is one instance per item.
 */
typedef struct LineItem LineItem;
struct LineItem {
    C3dItem common;            /* Common attributes. Must be first! */
    C3dColor color;            /* Color to draw line in */
    int nLine;                 /* Number of line segments in item */
    int *aFinalVertex;         /* Array of final vertices for segments. */
    double width;
};

/*
 *---------------------------------------------------------------------------
 *
 * lineTable --
 *
 *     Return a Tk_OptionTable token configured with options for the line
 *     or point item (both use the same options).
 *
 * Results:
 *     Tk_OptionTable token.
 *
 * Side effects:
 *     None.
 *
 *---------------------------------------------------------------------------
 */
static Tk_OptionTable
lineTable(Tcl_Interp *interp)
{
    static Tk_OptionSpec line_option_array[] = {
        TAGS_OPTION,
        HIDDEN_OPTION,
        C3DOPTION(LineItem, COLOR, color, "#FFFFFF", 0),
        C3DOPTION(LineItem, DOUBLE, width, "1.0", 0),
        {TK_OPTION_END, 0, 0, 0, 0, 0, 0, 0, 0}
    };
    Tk_OptionTable line_options;

    line_options = C3dCreateOptionTable(interp, line_option_array);
    assert(line_options);

    return line_options;
}

/*
 *---------------------------------------------------------------------------
 *
 * pointlineMultiCreate --
 *
 * Results:
 *     None.
 *
 * Side effects:
 *     None.
 *
 *---------------------------------------------------------------------------
 */
static C3dItem *
pointlineMultiCreate(
    C3dWidget *pCanvas,
    int nLine,
    Tcl_Obj **apCoords,
    int point
) {
    int nBytes;
    LineItem *pLine;
    int i;
    int v = 0;
    int nCoords = 0;           /* Total number of coordinates for item */
    Tcl_Interp *interp = pCanvas->interp;

    for (i = 0; i < nLine; i++) {
        int n;
        if (C3dCheckCoords(interp, apCoords[i], point?1:2, -1, &n)) {
            return 0;
        }
        nCoords += n;
    }

    nBytes = sizeof(LineItem)            +       /* LineItem struct */
             sizeof(C3dVertex)*nCoords   +       /* C3dItem.aVertex array */
             sizeof(int)*nLine;                  /* LineItem.aFinalVertex */
    pLine = (LineItem *)C3dAlloc(nBytes);

    memset(pLine, 0, nBytes);
    assert((char *)pLine == (char *)(&pLine->common));
    pLine->common.aVertex = (C3dVertex *)&pLine[1];
    pLine->aFinalVertex = (int *)&pLine->common.aVertex[nCoords];

    for (i = 0; i < nLine; i++) {
        int n;
        C3dCheckCoords(interp, apCoords[i], point?1:2, -1, &n);
        if (C3dSetCoords(interp, apCoords[i], v, (C3dItem *)pLine)) {
            C3dFree((char *)pLine);
            return 0;
        }
        v += n;
        pLine->aFinalVertex[i] = v;
    }
    pLine->nLine = nLine;
    assert(v == nCoords);

    return (C3dItem *)pLine;
}

static C3dItem *
lineMultiCreate(
    C3dWidget *pCanvas,
    int nLine,
    Tcl_Obj **apCoords
) {
    return pointlineMultiCreate(pCanvas, nLine, apCoords, 0);
}

static C3dItem *
pointMultiCreate(
    C3dWidget *pCanvas,
    int nLine,
    Tcl_Obj **apCoords
) {
    return pointlineMultiCreate(pCanvas, nLine, apCoords, 1);
}

/*
 *---------------------------------------------------------------------------
 *
 * pointlineDrawCallback --
 *
 * Results:
 *     None.
 *
 * Side effects:
 *     None.
 *
 *---------------------------------------------------------------------------
 */
void
pointlineDrawCallback(C3dItem *pItem, ClientData clientData)
{
    long iLine = (long)clientData;
    LineItem *pLine = (LineItem *)pItem;
    int finalvertex = pLine->aFinalVertex[iLine];
    int startvertex = (iLine == 0 ? 0 : pLine->aFinalVertex[iLine - 1]);
    int point = (((PointLineItemType *)pItem->pType)->subtype == POINT);
    int transparent = (pLine->color.aChannel[3] != 1.0);
    C3dWidget *pCanvas = pItem->pCanvas;
    int v;

    if (pCanvas->nLight > 0) {
        glDisable(GL_LIGHTING);
    }
    if (transparent) {
        glDepthMask(GL_FALSE);
    }

    glColor4fv(pLine->color.aChannel);
    if (point) {
        glPointSize(pLine->width);
        glBegin(GL_POINTS);
    } else {
        glLineWidth(pLine->width);
        glBegin(GL_LINE_STRIP);
    }
    for (v = startvertex; v < finalvertex; v++) {
        C3dVertex *pVertex = &pItem->aVertex[v];
        glVertex3f(pVertex->x, pVertex->y, pVertex->z);
    }
    glEnd();

    if (pCanvas->nLight > 0) {
        glEnable(GL_LIGHTING);
    }
    if (transparent) {
        glDepthMask(GL_TRUE);
    }
}

/*
 *---------------------------------------------------------------------------
 *
 * pointlineDraw --
 *
 * Results:
 *     None.
 *
 * Side effects:
 *     None.
 *
 *---------------------------------------------------------------------------
 */
static void
pointlineDraw(C3dWidget *pCanvas, C3dItem *p)
{
    long i;
    LineItem *pLine = (LineItem *)p;

    for (i = 0; i < pLine->nLine; i++) {
        ClientData c = (ClientData)i;
        if (pLine->color.aChannel[3] == 1.0) {
            pointlineDrawCallback(p, c);
        } else {
            float lx, ly, lz;
            float cx, cy, cz;
            float fx, fy, fz;
            int v = (i == 0 ? 0 : pLine->aFinalVertex[i - 1]);
            float f;

            lx = pCanvas->options.cameralocation->x;
            ly = pCanvas->options.cameralocation->y;
            lz = pCanvas->options.cameralocation->z;
            cx = pCanvas->options.cameracenter->x;
            cy = pCanvas->options.cameracenter->y;
            cz = pCanvas->options.cameracenter->z;
            fx = p->aVertex[v].x;
            fy = p->aVertex[v].y;
            fz = p->aVertex[v].z;

            f= C3dDot(cx-lx, cy-ly, cz-lz, fx-lx, fy-ly, fz-lz);
            if (f > 0.0) {
                f = f * -1.0;
                C3dItemDrawCallback(pCanvas, pointlineDrawCallback, p, c, f);
            }
        }
    }

}

/*
 *---------------------------------------------------------------------------
 *
 * lineCoords --
 *
 * Results:
 *     None.
 *
 * Side effects:
 *     None.
 *
 *---------------------------------------------------------------------------
 */
static void
lineCoords(C3dItem *pItem, Tcl_Obj *pRet)
{
    LineItem *pLine = (LineItem *)pItem;
    int nBytes;
    Tcl_Obj **apLine;
    Tcl_Obj **apVertex;
    int i;

    nBytes = sizeof(Tcl_Obj *) * pItem->nVertex * 3 +
             sizeof(Tcl_Obj *) * pLine->nLine;
    apLine = (Tcl_Obj **)C3dAlloc(nBytes);
    apVertex = &apLine[pLine->nLine];

    for (i = 0; i < pItem->nVertex; i++) {
        apVertex[i*3 + 0] = Tcl_NewDoubleObj(pItem->aVertex[i].x);
        apVertex[i*3 + 1] = Tcl_NewDoubleObj(pItem->aVertex[i].y);
        apVertex[i*3 + 2] = Tcl_NewDoubleObj(pItem->aVertex[i].z);
    }

    for (i = 0; i < pLine->nLine; i++) {
        int v1 = (i == 0 ? 0 : pLine->aFinalVertex[i - 1]);
        int v2 = pLine->aFinalVertex[i];
        apLine[i] = Tcl_NewListObj(3 * (v2 - v1), &apVertex[v1 * 3]);
    }

    Tcl_SetListObj(pRet, pLine->nLine, apLine);
    C3dFree((char *)apLine);
}

/*
 *---------------------------------------------------------------------------
 *
 * Given a line segment x0,y0 -> x1,y1 figure out how far the point
 * x,y is from that line segment and return that distance.  Let
 * xC,yC be the point along the line x0,y0->x1,y1 that is closest
 * to x,y.  Store in *prPercent that percentage of the distance from
 * x0,y0->x1,y1 where xC,yC occurs.
 *
 */
static double
segmentDistance(
    double x0,
    double y0,
    double x1,
    double y1,
    int x,
    int y,
    double *prPercent
) {
    double dx, dy;    /* Vector from x0,y0 to x1,y1 */
    double cx, cy;    /* Point on line closest to x,y */
    double ex, ey;    /* Vector from x0,y0 to x,y */
    double d;         /* Length of vector dx,dy */
    double e;         /* Length of vector ex,ey */
    double qx, qy;    /* Vector from cx,cy to x,y */
    double f;         /* fraction along x0,y0 to x1,y1 of cx,cy */

    dx = x1 - x0;
    dy = y1 - y0;
    ex = x - x0;
    ey = y - y0;
    if ((dx==0 && dy==0) || (ex==0 && ey==0)) {
         /* x0,y0 to x1,y1 is a zero length line or x,y==x0,y0 */
         *prPercent = 0.0;
         return sqrt(ex*ex + ey*ey);
    }
    f = dx*ex + dy*ey;
    if (f <= 0.0) {
         /* x,y is off the end x0 end */
         *prPercent = 0.0;
         return sqrt(ex*ex + ey*ey);
    }
    e = sqrt(ex*ex + ey*ey);
    d = sqrt(dx*dx + dy*dy);
    f /= d;
    if (f >= e) {
         /* x,y is off the end x1 end */
         *prPercent = 1.0;
         return e;
    }
    f /= d;
    cx = x0 + f*dx;
    cy = y0 + f*dy;
    qx = x - cx;
    qy = y - cy;
    *prPercent = f;
    return sqrt(qx*qx + qy*qy);
}

/*
 *---------------------------------------------------------------------------
 *
 * lineSegment --
 *
 *     Try to find which part of the line is drawn over the
 *     point x,y.  If on the first segment of the line, return
 *     A value between 0.0 and 1.0 depending on where on the
 *     the line the point occurs.  If on the second segment,
 *     return between 1.0 and 2.0.  And so forth.
 */
static double
lineSegment(C3dItem *pItem, int x, int y)
{
    double bestDist = 9.9e9;
    double segment = -1.0;
    struct C3dTransform *pTransform;
    int i;
    double *a2d;
    Tk_Window win = pItem->pCanvas->tkwin;
    float matrix[16];

    /* Project the line vertices into 2D coordinates
    ** stored in a2d[]
    */
    pTransform = C3dTransformProjection(pItem->pCanvas);
    memcpy(matrix, pTransform, sizeof(matrix));
    C3dTransformDelete(pTransform);
    a2d = (double*)ckalloc( 2*sizeof(a2d[0])*pItem->nVertex );
    for(i=0; i<pItem->nVertex; i++){
        float w, x, y, z;
        x = pItem->aVertex[i].x;
        y = pItem->aVertex[i].y;
        z = pItem->aVertex[i].z;
        w = matrix[12]*x + matrix[13]*y + matrix[14]*z + matrix[15];
        a2d[i*2] = (matrix[0]*x + matrix[1]*y + matrix[2]*z + matrix[3])/w;
        a2d[i*2+1] = (matrix[4]*x + matrix[5]*y + matrix[6]*z + matrix[7])/w;
    }
    for(i=0; i<pItem->nVertex; i++){
        a2d[i*2] = 0.5*(a2d[i*2]+1.0)*Tk_Width(win);
        a2d[i*2+1] = 0.5*(a2d[i*2+1]+1.0)*Tk_Height(win);
    }

    /* Find the closest segment */
    for(i=0; i<pItem->nVertex-1; i++){
        double dist;
        double percent;
        dist = segmentDistance(a2d[i*2], a2d[i*2+1], a2d[i*2+2], a2d[i*2+3],
                               x, y, &percent);
        if (dist < bestDist) {
            bestDist = dist;
            segment = i + percent;
            if (dist == 0) break;
        }
    }

    /* Return results */
    ckfree((char*)a2d);
    return segment;
}

/*
 *---------------------------------------------------------------------------
 *
 * C3dLine --
 *
 * Results:
 *     None.
 *
 * Side effects:
 *     None.
 *
 *---------------------------------------------------------------------------
 */
C3dItemType *
C3dLine(void)
{
    static PointLineItemType line = {
        {
            "line",          /* zType    */
            0,               /* isOverlay */
            0,               /* xCreate  */
            lineMultiCreate, /* xMultiCreate  */
            0,               /* xDelete  */
            pointlineDraw,   /* xDraw    */
            lineTable,       /* xTable   */
            0,               /* xStatistics */
            lineCoords,      /* xCoords */
            lineSegment,     /* xSegment */
        },
        LINE
    };
    return (C3dItemType *)&line;
}

/*
 *---------------------------------------------------------------------------
 *
 * C3dPoint --
 *
 * Results:
 *     None.
 *
 * Side effects:
 *     None.
 *
 *---------------------------------------------------------------------------
 */
C3dItemType *
C3dPoint(void)
{
    static PointLineItemType line = {
        {
            "point",          /* zType    */
            0,                /* isOverlay */
            0,                /* xCreate  */
            pointMultiCreate, /* xMultiCreate  */
            0,                /* xDelete  */
            pointlineDraw,    /* xDraw    */
            lineTable,        /* xTable   */
            0,                /* xStatistics */
            lineCoords        /* xCoords */
       },
       POINT
    };
    return (C3dItemType *)&line;
}
