package jocode;


import java.util.*;
import javax.media.opengl.*;

import jomodel.*;


/**
 * JOLine creates a quad-strip line from a series of points.  To use:
 * <PRE>
 * draw() {
 *    JOLine L = new JOLine();
 *    L.setWidth(20);
 *    L.point(0,0);
 *    L.point(5,5);
 *    L.point(0,10);
 *    L.point(5,15);
 *    L.close();   // if you want a closed shape
 *    L.draw();
 * }
 * </PRE>
 * <P>
 * NOTE: gets ugly when points are very close, or reverse direction sharply.  Uses
 * my home-grown trigonometry... probably not the best way to do this, but it works
 * in most cases.
 * <P>
 * ALSO NOTE: the line will be drawn with the active texture.  To draw an untextured
 * line, call activateTexture(0) or disable textures (gl.glDisable(GL.GL_TEXTURE_2D)
 * before calling line.draw().
 */
public class JOLine {
    ArrayList<JOVector> points;  // holds each point in line (see point())
    float w2 = 1f;               // half of line width
    boolean changed = false;     // true if line was changed since last rebuild()
    float uvfactor = 100f;       // how to tile texture on line: if uvfactor=100 texture will repeat every 100 units

    float[][] verts;  // to hold vertex positions
    private int vc;   // vertex counter


    public JOLine() {
        clear();
    }

    public JOLine(float lineWidth) {
        clear();
        setWidth(lineWidth);
    }

    /**
     * reset the line
     */
    public void clear() {
        points = new ArrayList<JOVector>();
        verts = null;
    }

    /**
     * Add an xy point to the line.
     */
    public void point(float x, float y) {
        JOVector p = new JOVector(x,y,0);
        /*
        // eliminate segments shorter than 10 units
        // in hand drawn lines very short segments (around 1 unit long) give erratic results
        if (points.size() > 0) {
            if ( JOVector.sub(p,(JOVector)points.get(points.size()-1)).length() < 10) {
                return;
            }
        }
        */
        points.add(p);
        changed = true;
    }

    /**
     * Close the line by adding a line segment that connects the last point to the first.
     */
    public void close() {
        int numPoints = points.size();
        if (numPoints >= 3) {
            JOVector firstP = (JOVector)points.get(0);       // first point in shape
            JOVector lastP = (JOVector)points.get(points.size()-1);  // last point in shape
            // close the shape if not already closed
            if (!lastP.equals(firstP)) {
                point(firstP.x, firstP.y);
            }
        }
    }

    /**
     * set line width
     */
    public void setWidth(float w) {
        w2 = w/2f;
        changed = true;
    }

    /**
     * return line width
     */
    public float getWidth() {
        return w2 * 2;
    }

    /**
     * return number of points in this line
     */
    public int getNumPoints() {
        return points.size();
    }

    /**
     * return the given xy point
     */
    public JOVector getPoint(int n) {
        return (n > 0 && n < points.size())? (JOVector)points.get(n) : null;
    }

    /**
     * set texture tiling factor.  Texture will repeat every <uvfactor> units.
     * If a line is 50 units long, and uvfactor is 10, then the texture will
     * repeat five times along the length of the line.
     */
    public void setUVfactor(float tilefactor) {
        uvfactor = tilefactor;
        changed = true;
    }

    /**
     * regenerate the line geometry from the added points.
     */
    public void rebuild() {
        makeLine();
    }

    /**
     * create the line geometry from point positions
     */
    public void makeLine() {
        // need at least two points
        if (points.size() < 2) {
            return;
        }

        // create array to hold vertex positions and reset vert counter
        verts = new float[(points.size()*4)-4][3];
        vc = 0;

        JOVector firstP = (JOVector) points.get(0);
        JOVector lastP = (JOVector) points.get(points.size()-1);

        for (int i=0; i < points.size()-1; i++) {
            JOVector p1 = (JOVector) points.get(i);
            JOVector p2 = (JOVector) points.get(i+1);
            // default prev and next line segment directions are the same as this segment direction
            JOVector v1 = new JOVector(p2, p1);
            JOVector v2 = new JOVector(p2, p1);
            // get end directions of line segment (will change the shape of the segment so it joins with next segment)
            // special case: first segment of closed shape
            if (i == 0 && firstP.equals(lastP))  {
                // if first point and last point are the same, first segment will join last
                v1 = new JOVector(lastP,  (JOVector)points.get(points.size()-2));
            }
            // previous line segment direction
            if (i > 0) {
                v1 = new JOVector(p1, (JOVector)points.get(i-1));
            }
            // next line segment direction
            if (i < points.size()-2) {
                v2 = new JOVector((JOVector)points.get(i+2), p2);
            }
            // draw the line segment
            makeSegment(p1,p2,v1,v2);
        }

        // if first point and last point are the same, join last segment to first
        if (firstP.equals(lastP)) {
            // move last vert to same position as first
            verts[verts.length-1][0] = verts[0][0];
            verts[verts.length-1][1] = verts[0][1];
            // move second to last vert to same position as second
            verts[verts.length-2][0] = verts[1][0];
            verts[verts.length-2][1] = verts[1][1];
        }

        changed = false;
    }

    /**
     * Draw one segment from a line.  The segment is a quad created by
     * finding normals to the line vector.  The line is drawn in the XY plane.
     * In the diagram below, qv1 thru qv4 are the four quad vectors.  N1 and N2 are
     * normals to the line vector and Z axis.  The normals are averaged with the
     * previous and next line segments so that this segment will join gracefully
     * with the previous and next.
     * <PRE>
     *      qv4   p2    qv3
     *        +----o----+
     *         -N2 |  N2
     *             |
     *             |          line segment p1-p2
     *             |
     *         -N1 |  N1
     *        +----o----+
     *      qv1   p1    qv2
     * </PRE>
     *
     * @param p1      start point of line segment
     * @param p2      end point of line segment
     * @param prevV   direction of previous line segment
     * @param postV   direction of next line segment
     */
    public void makeSegment(JOVector p1, JOVector p2, JOVector prevV, JOVector postV) {
        // vectors of line segments
        JOVector v0 = prevV;                   // direction of prior line segment
        JOVector v1 = JOVector.sub(p2,p1);    // direction of segment we're drawing now
        JOVector v2 = postV;                   // direction of next line segment

        // normals to form the line joins
        JOVector N1 = getNormal(v0,v1,w2);     // average normal for previous and current segment
        JOVector N2 = getNormal(v1,v2,w2);     // average normal for current and next segment

        // make the four quad verts for the segment
        JOVector qv2 = JOVector.add(p1, N1);
        JOVector qv1 = JOVector.add(p1, N1.reverse());
        JOVector qv3 = JOVector.add(p2, N2);
        JOVector qv4 = JOVector.add(p2, N2.reverse());

        // add the verts to array
        verts[vc][0] = qv1.x;       verts[vc][1] = qv1.y;       verts[vc][2] = qv1.z;  vc++;
        verts[vc][0] = qv2.x;       verts[vc][1] = qv2.y;       verts[vc][2] = qv2.z;  vc++;
        verts[vc][0] = qv3.x;       verts[vc][1] = qv3.y;       verts[vc][2] = qv3.z;  vc++;
        verts[vc][0] = qv4.x;       verts[vc][1] = qv4.y;       verts[vc][2] = qv4.z;  vc++;
    }

    /**
     * Make an average normal to the two vectors with the right length to
     * make a join for a line <width> wide.
     * <PRE>
     *           + - - - - - -
     *           |\ normal
     *           | \
     *           |  o-----------o line b
     *              |
     *              |
     *              |
     *              o line a
     * </PRE>
     * @param a
     * @param b
     * @return
     */
    public JOVector getNormal(JOVector a, JOVector b, float width) {
        // Z axis
        JOVector Z = new JOVector(0,0,1);
        // get normal to line a and b (normals are in the XY plane)
        JOVector cross1 = JOVector.crossProduct(a,Z);
        JOVector cross2 = JOVector.crossProduct(b,Z);
        // average the two normals
        JOVector crossAvg = JOVector.add(cross1,cross2).div(2).normalize();
        // adjust the length of the normal
        float N1 = getNormalLen(cross1,crossAvg,width);
        crossAvg.mult(N1);
        return crossAvg;
    }

    /**
     * Return the length of the normal needed to make a join in a line
     * of the given width.
     * @param v1
     * @param v2
     * @param width
     * @return
     */
    public float getNormalLen(JOVector v1, JOVector v2, float width) {
        float A = 180 - JOVector.angleXY(v1,v2);
        float normlen = width/(float)Math.cos(Math.toRadians(A));
        return normlen;
    }

    /**
     * for debugging
     * @param v
     * @param x
     * @param y
     */
    public void drawV(JOVector v, float x, float y) {
        JOApp.gl.glColor3f(0,1,1);
        JOApp.gl.glPushMatrix(); {
            JOApp.gl.glBegin(GL.GL_LINES);
            JOApp.gl.glVertex3f(x,y,0);
            JOApp.gl.glVertex3f(x+v.x, y+v.y, 0);
            JOApp.gl.glEnd();
        }
        JOApp.gl.glPopMatrix();
    }

    /**
     * draw the mesh geometry created by rebuild()
     */
    public void draw() {
        // rebuild if points were added or width changed
        if (changed) {
            makeLine();
        }
        // line is not empty, draw it
        if (verts != null && verts.length >= 0) {
            // normal is on Z axis
            JOApp.gl.glNormal3f(0,0,1);

            // draw the quads
            for (int i=0; i < verts.length; i+=4) {
                JOApp.gl.glBegin(GL.GL_QUADS);
                {
                    JOApp.gl.glTexCoord2f(verts[i+0][0]/uvfactor,verts[i+0][1]/uvfactor);
                    JOApp.gl.glVertex3f(verts[i+0][0],verts[i+0][1],verts[i+0][2]);

                    JOApp.gl.glTexCoord2f(verts[i+1][0]/uvfactor,verts[i+1][1]/uvfactor);
                    JOApp.gl.glVertex3f(verts[i+1][0],verts[i+1][1],verts[i+1][2]);

                    JOApp.gl.glTexCoord2f(verts[i+2][0]/uvfactor,verts[i+2][1]/uvfactor);
                    JOApp.gl.glVertex3f(verts[i+2][0],verts[i+2][1],verts[i+2][2]);

                    JOApp.gl.glTexCoord2f(verts[i+3][0]/uvfactor,verts[i+3][1]/uvfactor);
                    JOApp.gl.glVertex3f(verts[i+3][0],verts[i+3][1],verts[i+3][2]);
                }
                JOApp.gl.glEnd();
            }
        }
    }

    /**
     * draw the mesh geometry created by rebuild() as a 3D shape (extrude the line shape
     * along the Z axis).
     */
    public void draw3D(float height) {
        // rebuild if points were added or width changed
        if (changed) {
            makeLine();
        }
        // if line is not empty, draw it
        if (verts != null && verts.length > 0) {
            float z = height;   // how high to extrude line
            JOVector n;        // normal of quad

            // draw the quads
            for (int i=0; i < verts.length; i+=4) {
                // quad facing forward
                JOApp.gl.glNormal3f(0,0,1);
                JOApp.gl.glBegin(GL.GL_QUADS);
                {
                    JOApp.gl.glTexCoord2f(verts[i+0][0]/uvfactor,verts[i+0][1]/uvfactor);
                    JOApp.gl.glVertex3f(verts[i+0][0],verts[i+0][1],verts[i+0][2]);

                    JOApp.gl.glTexCoord2f(verts[i+1][0]/uvfactor,verts[i+1][1]/uvfactor);
                    JOApp.gl.glVertex3f(verts[i+1][0],verts[i+1][1],verts[i+1][2]);

                    JOApp.gl.glTexCoord2f(verts[i+2][0]/uvfactor,verts[i+2][1]/uvfactor);
                    JOApp.gl.glVertex3f(verts[i+2][0],verts[i+2][1],verts[i+2][2]);

                    JOApp.gl.glTexCoord2f(verts[i+3][0]/uvfactor,verts[i+3][1]/uvfactor);
                    JOApp.gl.glVertex3f(verts[i+3][0],verts[i+3][1],verts[i+3][2]);
                }
                JOApp.gl.glEnd();
                
                // quad facing backward
                JOApp.gl.glNormal3f(0,0,-1);
                JOApp.gl.glBegin(GL.GL_QUADS);
                {
                    JOApp.gl.glTexCoord2f(verts[i+1][0]/uvfactor,verts[i+1][1]/uvfactor);
                    JOApp.gl.glVertex3f(verts[i+1][0],verts[i+1][1],verts[i+1][2]-z);

                    JOApp.gl.glTexCoord2f(verts[i+0][0]/uvfactor,verts[i+0][1]/uvfactor);
                    JOApp.gl.glVertex3f(verts[i+0][0],verts[i+0][1],verts[i+0][2]-z);

                    JOApp.gl.glTexCoord2f(verts[i+3][0]/uvfactor,verts[i+3][1]/uvfactor);
                    JOApp.gl.glVertex3f(verts[i+3][0],verts[i+3][1],verts[i+3][2]-z);

                    JOApp.gl.glTexCoord2f(verts[i+2][0]/uvfactor,verts[i+2][1]/uvfactor);
                    JOApp.gl.glVertex3f(verts[i+2][0],verts[i+2][1],verts[i+2][2]-z);
                }
                JOApp.gl.glEnd();
                
                // quad facing side 1  (calc normal from three points on side plane)
                n = JOVector.getNormal(new JOVector(verts[i+3][0],verts[i+3][1],verts[i+3][2]-z),
                                        new JOVector(verts[i+0]),
                                        new JOVector(verts[i+3]));
                JOApp.gl.glNormal3f(n.x,n.y,n.z);
                JOApp.gl.glBegin(GL.GL_QUADS);
                {
                    JOApp.gl.glTexCoord2f(0/uvfactor,verts[i+0][1]/uvfactor);
                    JOApp.gl.glVertex3f(verts[i+0][0],verts[i+0][1],verts[i+0][2]-z);

                    JOApp.gl.glTexCoord2f(z/uvfactor,verts[i+0][1]/uvfactor);
                    JOApp.gl.glVertex3f(verts[i+0][0],verts[i+0][1],verts[i+0][2]);

                    JOApp.gl.glTexCoord2f(z/uvfactor,verts[i+3][1]/uvfactor);
                    JOApp.gl.glVertex3f(verts[i+3][0],verts[i+3][1],verts[i+3][2]);

                    JOApp.gl.glTexCoord2f(0/uvfactor,verts[i+3][1]/uvfactor);
                    JOApp.gl.glVertex3f(verts[i+3][0],verts[i+3][1],verts[i+3][2]-z);
                }
                JOApp.gl.glEnd();
                
                // quad facing side 2 (calc normal from three points on side plane)
                n = JOVector.getNormal(new JOVector(verts[i+1][0],verts[i+1][1],verts[i+1][2]-z),
                                        new JOVector(verts[i+1]),
                                        new JOVector(verts[i+2]));
                JOApp.gl.glNormal3f(n.x,n.y,n.z);
                JOApp.gl.glBegin(GL.GL_QUADS);
                {
                    JOApp.gl.glTexCoord2f(0/uvfactor,verts[i+1][1]/uvfactor);
                    JOApp.gl.glVertex3f(verts[i+1][0],verts[i+1][1],verts[i+1][2]);

                    JOApp.gl.glTexCoord2f(z/uvfactor,verts[i+1][1]/uvfactor);
                    JOApp.gl.glVertex3f(verts[i+1][0],verts[i+1][1],verts[i+1][2]-z);

                    JOApp.gl.glTexCoord2f(z/uvfactor,verts[i+2][1]/uvfactor);
                    JOApp.gl.glVertex3f(verts[i+2][0],verts[i+2][1],verts[i+2][2]-z);

                    JOApp.gl.glTexCoord2f(0/uvfactor,verts[i+2][1]/uvfactor);
                    JOApp.gl.glVertex3f(verts[i+2][0],verts[i+2][1],verts[i+2][2]);

                }
                JOApp.gl.glEnd();
            }
        }
    }

    /**
     * render the line into a display list and return the list ID.
     */
    public int makeDisplayList() {
        int DLID = JOApp.beginDisplayList();
        draw();
        JOApp.endDisplayList();
        return DLID;
    }

    /**
     * return a JOLine shaped into a rectangle.
     */
    public static JOLine makeRectangle(float x, float y, float w, float h, float linewidth) {
        JOLine rect = new JOLine(linewidth);
        rect.point(x, y);
        rect.point(x+w, y);
        rect.point(x+w, y+h);
        rect.point(x, y+h);
        rect.close();
        return rect;
    }

    /**
     * return a JOLine shaped into a circle.  Can also make triangle, square, pentagon, etc. by
     * giving a low number for numSegments.
     */
    public static JOLine makeCircle(float x, float y, float radius, int numSegments, float linewidth) {
        int s = 0;     // start
        int e = 360;   // end
        int stepSize = 360/numSegments;   // degrees per segment
        JOLine L = new JOLine(linewidth);
        // add first point
        float ts = (float) Math.sin(Math.toRadians(s));
        float tc = (float) Math.cos(Math.toRadians(s));
        L.point(x+tc*radius, y+ts*radius);
        // add intermediate points, snap to {step} degrees
        while ( (s = ((s+stepSize)/stepSize)*stepSize) < e) {
            ts = (float) Math.sin(Math.toRadians(s));
            tc = (float) Math.cos(Math.toRadians(s));
            L.point(x+tc*radius, y+ts*radius);
        }
        // add last point
        L.close();
        return L;
    }
}
