import com.jogamp.opengl.GL2;

/**
 * Provides static methods for drawing several 3D shapes in an OpenGL
 * drawing context of type GL2.  The shapes come with optional texture coords.
 */
public class TexturedShapes {

    /**
     * Draws a cube with side 1, centered at the origin, with sides
     * parallel to the coordinate axes.  Texture coords are generated.
     */
    public static void cube(GL2 gl) {
        cube(gl,1,true);
    }

    /**
     * Draws a square with side 1, in the xy plane, centered at the origin, with sides
     * parallel to the coordinate axes.  Texture coords are generated.
     */
    public static void square(GL2 gl) {
        square(gl,1,true);
    }

    /**
     * Draws a disk of radius 0.5 in the xy-plane, centered at the origin.  Texture
     * coordinates are generated.
     */
    public static void circle(GL2 gl) {
        circle(gl,0.5,32,5,true);
    }
    
    /**
     * Draws a ring (or "annulus") in the xy-plane, centered at the origin.  The outer radius
     * of the ring is 0.5 and the inner radius is 0.3.  Texture coordinates are generated.
     */
    public static void ring(GL2 gl) {
        ring(gl,0.3,0.5,32,5,true);
    }

    /**
     * Calls uvSphere(gl,0.5,32,16,true) to draw a unit sphere with texture coords.
     * The sphere is centered at (0,0,0) with its axis along the z-axis.
     */
    public static void uvSphere(GL2 gl) {
        uvSphere(gl,0.5,32,16,true);
    }

    /**
     * Calls uvCylinder(gl,0.5,1,32,10,5,true); to draw a cylinder with diameter and
     * height both equal to 1 and with texture coords.  The cylinder has its base
     * in the xy-plane and its axis along the positive z-axis.
     */
    public static void uvCylinder(GL2 gl) {
        uvCylinder(gl,0.5,1,32,10,5,true);
    }

    /**
     * Calls uvCone(gl,0.5,1,32,10,5,true); to draw a cone with diameter and
     * height both equal to 1 and with texture coords.  The cone has its base
     * in the xy-plane and its axis along the positive z-axis.
     */
    public static void uvCone(GL2 gl) {
        uvCone(gl,0.5,1,32,10,5,true);
    }

    /**
     * Calls uvTorus(gl,0.5,1.0/6,48,72,true); to draw a torus with radii 1/6 and 1/2.  
     * The torus is bisected by the xy-plane and has its axis along the positive 
     * z-axis and its center at (0,0,0).  Texture coords are generated for the toruns.
     */
    public static void uvTorus(GL2 gl) {
        uvTorus(gl,0.5,1.0/6,48,72,true);
    }



    /**
     * Draw a sphere with a given radius, number of slices, and number
     * of stacks.  The number of slices is the number of lines of longitude
     * (like the slices of an orange).  The number of stacks is the number
     * of divisions perpendicular the axis; the lines of latitude are the
     * dividing lines between stacks, so there are stacks-1 lines of latitude.
     * The last parameter tells whether or not to generate texture
     * coordinates for the sphere.  The texture wraps once around the sphere.
     * The sphere is centered at (0,0,0), and its axis lies along the z-axis.
     */
    public static void uvSphere(GL2 gl, double radius, int slices, int stacks, boolean makeTexCoords) {
        if (radius <= 0)
            throw new IllegalArgumentException("Radius must be positive.");
        if (slices < 3)
            throw new IllegalArgumentException("Number of slices must be at least 3.");
        if (stacks < 2)
            throw new IllegalArgumentException("Number of stacks must be at least 2.");
        for (int j = 0; j < stacks; j++) {
            double latitude1 = (Math.PI/stacks) * j - Math.PI/2;
            double latitude2 = (Math.PI/stacks) * (j+1) - Math.PI/2;
            double sinLat1 = Math.sin(latitude1);
            double cosLat1 = Math.cos(latitude1);
            double sinLat2 = Math.sin(latitude2);
            double cosLat2 = Math.cos(latitude2);
            gl.glBegin(GL2.GL_QUAD_STRIP);
            for (int i = 0; i <= slices; i++) {
                double longitude = (2*Math.PI/slices) * i;
                double sinLong = Math.sin(longitude);
                double cosLong = Math.cos(longitude);
                double x1 = cosLong * cosLat1;
                double y1 = sinLong * cosLat1;
                double z1 = sinLat1;
                double x2 = cosLong * cosLat2;
                double y2 = sinLong * cosLat2;
                double z2 = sinLat2;
                gl.glNormal3d(x2,y2,z2);
                if (makeTexCoords)
                    gl.glTexCoord2d(1.0/slices * i, 1.0/stacks * (j+1));
                gl.glVertex3d(radius*x2,radius*y2,radius*z2);
                gl.glNormal3d(x1,y1,z1);
                if (makeTexCoords)
                    gl.glTexCoord2d(1.0/slices * i, 1.0/stacks * j);
                gl.glVertex3d(radius*x1,radius*y1,radius*z1);
            }
            gl.glEnd();
        }
    } // end uvSphere

    /**
     * Draw a cylinder with a given radius, number of slices, number of stacks, and
     * number of rings.  The number of slices is the number of divisions parallel to the 
     * axis (like the slices of an orange).  The number of stacks is the number
     * of divisions perpendicular the axis.  If the number or rings is less than or
     * equal to zero, then the top and bottom caps are not drawn.  If the number of
     * rings is positive, then the top and bottom caps are drawn, and they are divided
     * radially into the specified number of rings for drawing; the number of axial 
     * divisions of the caps is the same as the number of slices.  The last parameter
     * tells whether to generate texture coordinates for the cylinder.  The texture
     * will be wrapped once around the cylinder.  For the top an bottom caps, a circular
     * cutout from the texture is used.  The cylinder has its base in the xy-plane, with 
     * center at (0,0,0), and its axis lies along the positive direction of the z-axis.
     */
    public static void uvCylinder(GL2 gl, double radius, double height,
            int slices, int stacks, int rings, boolean makeTexCoords) {
        if (radius <= 0)
            throw new IllegalArgumentException("Radius must be positive.");
        if (height <= 0)
            throw new IllegalArgumentException("Height must be positive.");
        if (slices < 3)
            throw new IllegalArgumentException("Number of slices must be at least 3.");
        if (stacks < 2)
            throw new IllegalArgumentException("Number of stacks must be at least 2.");
        for (int j = 0; j < stacks; j++) {
            double z1 = (height/stacks) * j;
            double z2 = (height/stacks) * (j+1);
            gl.glBegin(GL2.GL_QUAD_STRIP);
            for (int i = 0; i <= slices; i++) {
                double longitude = (2*Math.PI/slices) * i;
                double sinLong = Math.sin(longitude);
                double cosLong = Math.cos(longitude);
                double x = cosLong;
                double y = sinLong;
                gl.glNormal3d(x,y,0);
                if (makeTexCoords)
                    gl.glTexCoord2d(1.0/slices * i, 1.0/stacks * (j+1));
                gl.glVertex3d(radius*x,radius*y,z2);
                if (makeTexCoords)
                    gl.glTexCoord2d(1.0/slices * i, 1.0/stacks * j);
                gl.glVertex3d(radius*x,radius*y,z1);
            }
            gl.glEnd();
        }
        if (rings > 0) { // draw top and bottom
            gl.glNormal3d(0,0,1);
            for (int j = 0; j < rings; j++) {
                double d1 = (1.0/rings) * j;
                double d2 = (1.0/rings) * (j+1);
                gl.glBegin(GL2.GL_QUAD_STRIP);
                for (int i = 0; i <= slices; i++) {
                    double angle = (2*Math.PI/slices) * i;
                    double sin = Math.sin(angle);
                    double cos = Math.cos(angle);
                    if (makeTexCoords)
                        gl.glTexCoord2d(0.5*(1+cos*d1),0.5*(1+sin*d1));
                    gl.glVertex3d(radius*cos*d1,radius*sin*d1,height);
                    if (makeTexCoords)
                        gl.glTexCoord2d(0.5*(1+cos*d2),0.5*(1+sin*d2));
                    gl.glVertex3d(radius*cos*d2,radius*sin*d2,height);
                }
                gl.glEnd();
            }
            gl.glNormal3d(0,0,-1);
            for (int j = 0; j < rings; j++) {
                double d1 = (1.0/rings) * j;
                double d2 = (1.0/rings) * (j+1);
                gl.glBegin(GL2.GL_QUAD_STRIP);
                for (int i = 0; i <= slices; i++) {
                    double angle = (2*Math.PI/slices) * i;
                    double sin = Math.sin(angle);
                    double cos = Math.cos(angle);
                    if (makeTexCoords)
                        gl.glTexCoord2d(0.5*(1+cos*d2),0.5*(1+sin*d2));
                    gl.glVertex3d(radius*cos*d2,radius*sin*d2,0);
                    if (makeTexCoords)
                        gl.glTexCoord2d(0.5*(1+cos*d1),0.5*(1+sin*d1));
                    gl.glVertex3d(radius*cos*d1,radius*sin*d1,0);
                }
                gl.glEnd();
            }
        }
    } // end uvCylinder

    /**
     * Draw a cone with a given radius, number of slices, number of stacks, and
     * number of rings.  The number of slices is the number of divisions parallel to the 
     * axis (like the slices of an orange).  The number of stacks is the number
     * of divisions perpendicular the axis.  If the number or rings is less than or
     * equal to zero, then the bottom is not drawn.  If the number of
     * rings is positive, then the bottom is drawn, and is divided
     * radially into the specified number of rings for drawing; the number of axial 
     * divisions of the base is the same as the number of slices.  The last
     * parameter tells whether to generate texture coordinates for the cone. The cone
     * has its base in the xy-plane, with center at (0,0,0), and its axis lies
     * along the positive direction of the z-axis. 
     */
    public static void uvCone(GL2 gl, double radius, double height, 
            int slices, int stacks, int rings, boolean makeTexCoords) {
        if (radius <= 0)
            throw new IllegalArgumentException("Radius must be positive.");
        if (height <= 0)
            throw new IllegalArgumentException("Height must be positive.");
        if (slices < 3)
            throw new IllegalArgumentException("Number of slices must be at least 3.");
        if (stacks < 2)
            throw new IllegalArgumentException("Number of stacks must be at least 2.");
        for (int j = 0; j < stacks; j++) {
            double z1 = (height/stacks) * j;
            double z2 = (height/stacks) * (j+1);
            gl.glBegin(GL2.GL_QUAD_STRIP);
            for (int i = 0; i <= slices; i++) {
                double longitude = (2*Math.PI/slices) * i;
                double sinLong = Math.sin(longitude);
                double cosLong = Math.cos(longitude);
                double x = cosLong;
                double y = sinLong;
                double nz = radius/height;
                double normLength = Math.sqrt(x*x+y*y+nz*nz);
                gl.glNormal3d(x/normLength,y/normLength,nz/normLength);
                if (makeTexCoords)
                    gl.glTexCoord2d(1.0/slices * i, 1.0/stacks * (j+1));
                gl.glVertex3d((height-z2)/height*radius*x,(height-z2)/height*radius*y,z2);
                if (makeTexCoords)
                    gl.glTexCoord2d(1.0/slices * i, 1.0/stacks * j);
                gl.glVertex3d((height-z1)/height*radius*x,(height-z1)/height*radius*y,z1);
            }
            gl.glEnd();
        }
        if (rings > 0) {
            gl.glNormal3d(0,0,-1);
            for (int j = 0; j < rings; j++) {
                double d1 = (1.0/rings) * j;
                double d2 = (1.0/rings) * (j+1);
                gl.glBegin(GL2.GL_QUAD_STRIP);
                for (int i = 0; i <= slices; i++) {
                    double angle = (2*Math.PI/slices) * i;
                    double sin = Math.sin(angle);
                    double cos = Math.cos(angle);
                    if (makeTexCoords)
                        gl.glTexCoord2d(0.5*(1+cos*d2),0.5*(1+sin*d2));
                    gl.glVertex3d(radius*cos*d2,radius*sin*d2,0);
                    if (makeTexCoords)
                        gl.glTexCoord2d(0.5*(1+cos*d1),0.5*(1+sin*d1));
                    gl.glVertex3d(radius*cos*d1,radius*sin*d1,0);
                }
                gl.glEnd();
            }
        }
    } // end uvCone

    /**
     * Create a torus (doughnut) lying bisected by the xy-plane, centered at the origin,
     * and with its central axis lying along the z-axis.
     * The first two parameters give the outer and inner radii of the torus.
     * @param outerRadius  The outer radius of the torus.
     *    (Note: if the outerRadius is smaller than the innerRadius, then the
     *    values are swapped, so that in effect the first two parameters are
     *    the two radii in either order.)
     * @param innerRadius  The inner radius of the torus.
     * @param slices The number of slices, like the slices of an orange, that are
     *    used to  approximate the torus.  The slices are cross-sections
     *    of the tube of the torus. Must be 3 or more.
     * @param rings The number of divisions around the circumference of the tube
     *    of the torus.  Must be 3 or more.
     * @param makeTexCoords Tells whether to generate texture coords for the torus.
     *    The texture will wrap once around the torus in each direction.
     */
    public static void uvTorus(GL2 gl, double outerRadius, double innerRadius, 
            int slices, int rings, boolean makeTexCoords) {
        if (outerRadius == innerRadius)
            throw new IllegalArgumentException("Outer and inner radii can't be the same.");
        if (outerRadius < innerRadius) {
            double temp = outerRadius;
            outerRadius = innerRadius;
            innerRadius = temp;
        }
        if (innerRadius < 0)
            throw new IllegalArgumentException("Radius can't be negative.");
        if (slices < 3)
            throw new IllegalArgumentException("Number of slices must be 3 or more.");
        if (rings < 3)
            throw new IllegalArgumentException("Number of rings must be 3 or more.");
        double centerRadius = (innerRadius + outerRadius) / 2;
        double tubeRadius = outerRadius - centerRadius;
        for (int i = 0; i < slices; i++) {
            double s1 = 1.0/slices * i;
            double s2 = 1.0/slices * (i+1);
            double centerCos1 = Math.cos(2*Math.PI*s1);
            double centerSin1 = Math.sin(2*Math.PI*s1);
            double centerCos2 = Math.cos(2*Math.PI*s2);
            double centerSin2 = Math.sin(2*Math.PI*s2);
            gl.glBegin(GL2.GL_QUAD_STRIP);
            for (int j = 0; j <= rings; j++) {
                double t = 1.0/rings * j;
                double cos = Math.cos(2*Math.PI*t - Math.PI);
                double sin = Math.sin(2*Math.PI*t - Math.PI);
                double x1 = centerCos1*(centerRadius + tubeRadius*cos);
                double y1 = centerSin1*(centerRadius + tubeRadius*cos);
                double z1 = sin*tubeRadius;
                gl.glNormal3d(centerCos1*cos,centerSin1*cos,sin);
                if (makeTexCoords)
                    gl.glTexCoord2d(s1,t);
                gl.glVertex3d(x1,y1,z1);
                double x2 = centerCos2*(centerRadius + tubeRadius*cos);
                double y2 = centerSin2*(centerRadius + tubeRadius*cos);
                double z2 = sin*tubeRadius;
                gl.glNormal3d(centerCos2*cos,centerSin2*cos,sin);
                if (makeTexCoords)
                    gl.glTexCoord2d(s2,t);
                gl.glVertex3d(x2,y2,z2);
            }
            gl.glEnd();
        }
    } // end uvTorus

    /**
     * Draws a cube with a specified side length, centered at the origin
     * and with edges paralled to the coordinate axes.  The last parameter
     * tells whether to generated texture coordinates.  The full texture
     * is applied to each face of the cube.
     */
    public static void cube(GL2 gl, double side, boolean makeTexCoords) {
        gl.glPushMatrix();
        gl.glRotatef(-90,-1,0,0);  // This puts the textures in the orientation I want.
        gl.glPushMatrix();
        gl.glTranslated(0,0,side/2);
        square(gl,side,makeTexCoords);  // Each side of the cube is a transformed square.
        gl.glPopMatrix();
        gl.glPushMatrix();
        gl.glRotatef(90,0,1,0);
        gl.glTranslated(0,0,side/2);
        square(gl,side,makeTexCoords);
        gl.glPopMatrix();
        gl.glPushMatrix();
        gl.glRotatef(180,0,1,0);
        gl.glTranslated(0,0,side/2);
        square(gl,side,makeTexCoords);
        gl.glPopMatrix();
        gl.glPushMatrix();
        gl.glRotatef(270,0,1,0);
        gl.glTranslated(0,0,side/2);
        square(gl,side,makeTexCoords);
        gl.glPopMatrix();
        gl.glPushMatrix();
        gl.glRotatef(90,-1,0,0);
        gl.glTranslated(0,0,side/2);
        square(gl,side,makeTexCoords);
        gl.glPopMatrix();
        gl.glPushMatrix();
        gl.glRotatef(-90,-1,0,0);
        gl.glTranslated(0,0,side/2);
        square(gl,side,makeTexCoords);
        gl.glPopMatrix();
        gl.glPopMatrix();
    } // end cube

    /**
     * Draws a square in the xy-plane, with given side length,
     * and edges parallel to the x and y axes.  The third parameter
     * tells whether to generate texture coordinates for the square.
     * The full texture is applied to the square.
     */
    public static void square(GL2 gl, double side, boolean makeTexCoords) {
        double radius = side/2;
        gl.glBegin(GL2.GL_POLYGON);
        gl.glNormal3f(0,0,1);
        if (makeTexCoords)
            gl.glTexCoord2d(0,0);
        gl.glVertex2d(-radius,-radius);
        if (makeTexCoords)
            gl.glTexCoord2d(1,0);
        gl.glVertex2d(radius,-radius);
        if (makeTexCoords)
            gl.glTexCoord2d(1,1);
        gl.glVertex2d(radius,radius);
        if (makeTexCoords)
            gl.glTexCoord2d(0,1);
        gl.glVertex2d(-radius,radius);
        gl.glEnd();
    } // end square

    /**
     * Draw a filled circle in the xy-plane, centered at the origin.  A polygonal
     * approximation of the circle is drawn.
     * @param radius  the radius of the circle
     * @param slices  the number of sides of the polygon
     * @param rings  number of radial subdivisions of the circle
     * @param makeTexCoords tells whether to make texture coordinates for the circle.
     *    A circular cutout from the texture is applied to the circle.
     */
    public static void circle(GL2 gl, double radius, int slices, int rings, boolean makeTexCoords) {
        if (radius <= 0)
            throw new IllegalArgumentException("Radius must be greater than zero.");
        ring(gl,0,radius,slices,rings,makeTexCoords);
    }

    /**
     * Draw a filled ring in the xy-plane, centered at the origin.  The ring is a disk
     * with a smaller disk deleted from its center.  A polygonal
     * approximation of the ring is drawn.
     * @param innerRadius  the radius of the hole in the ring.  Must be greater than or
     *    equal to zero; if the value is zero, then a completely filled disk is drawn.
     * @param outerRradius  the radius of the ring, measured from center to outer edge.
     *    Must be greater than innerRadius.
     * @param slices  the number of sides of the polygon
     * @param rings  number of radial subdivisions of the disk
     * @param makeTexCoords tells whether to make texture coordinates for the circle.
     *    A  cutout from the texture is applied to the disk.
     */
    public static void ring(GL2 gl, double innerRadius, double outerRadius,
                                        int slices, int rings, boolean makeTexCoords) {
        if (innerRadius < 0)
            throw new IllegalArgumentException("innerRadius must be greater than or equal to zero.");
        if (outerRadius <= innerRadius)
            throw new IllegalArgumentException("outerRadius must be greater than innerRadius.");
        if (slices < 3)
            throw new IllegalArgumentException("Number of slices must be 3 or more.");
        if (rings < 1)
            throw new IllegalArgumentException("Number of rings must be 1 or more.");
          gl.glNormal3d(0,0,1);
          double dr = (outerRadius - innerRadius) / rings;
          for (int j = 0; j < rings; j++) {
             double d1 = innerRadius + dr * j;
             double d2 = innerRadius + dr * (j+1);
             gl.glBegin(GL2.GL_QUAD_STRIP);
             for (int i = 0; i <= slices; i++) {
                double angle = (2*Math.PI/slices) * i;
                double sin = Math.sin(angle);
                double cos = Math.cos(angle);
                if (makeTexCoords)
                   gl.glTexCoord2d(0.5*(1+cos*d1/outerRadius),0.5*(1+sin*d1/outerRadius));
                gl.glVertex3d(cos*d1,sin*d1,0);
                if (makeTexCoords)
                   gl.glTexCoord2d(0.5*(1+cos*d2/outerRadius),0.5*(1+sin*d2/outerRadius));
                gl.glVertex3d(cos*d2,sin*d2,0);
             }
             gl.glEnd();
          }
    }

}
