package classwork;

import javax.media.opengl.*;
import javax.media.opengl.glu.*;
import java.awt.event.*;
import jocode.*;
import jomodel.*;

/**
 * GLART_5_transparent_material_solution.java
 *
 * Draw a row of transparent textured spheres.
 *
 * This demo fixes the front-to-back drawing problem that transparent
 * shapes have.  We need to draw the objects behind transparent objects
 * first, then draw the transparent objects in front, so that we can 
 * see the background objects through the transparent objects.
 * See GLART_sphere_lit_alpha.java for an example of the Z order
 * issue when rendering with alpha.
 *
 * In this case, we'll find the current Z depth of each transparent 
 * sphere by "projecting" the sphere position through the modelview,
 * projection and viewport matrices.  In effect we're reproducing
 * what OpenGL does when it transforms vertices before drawing
 * them on screen.  This will give us the Z depth of each sphere
 * as it appears on screen.
 *
 * Once we know the Z depth we can draw the transparent spheres in 
 * the correct back-to-front order.  The Z depth furthest away from 
 * the eye will be 1 and the Z depth closest to the eye will be 0.
 * We will to draw objects in order from the high Z depth to the low
 * Z depth.
 */
public class GLART_5_transparent_material_solution extends JOApp {
    float rotation = 0;

    // Material objects will hold surface color values
    JOMaterial clearMtl = new JOMaterial();
    JOMaterial opaqueMtl = new JOMaterial();

    // texture handle (a number that refers to an allocated texture)
    int marbleTextureHandle = 0;

	// An array holds world positions of the spheres
	// The ObjectPosition class is defined at the end of this file
    ObjectPosition spherePositions[];


    /**
     * Main function just creates and runs the application.
     */
    public static void main(String args[]) {
    	GLART_5_transparent_material_solution app = new GLART_5_transparent_material_solution();
    	displayWidth = 800;
    	displayHeight = 600;
        app.run();
    }

    /**
     * Initialize OpenGL
     */
    public void setup() {
        // Select the Projection Matrix (controls perspective)
        gl.glMatrixMode(GL.GL_PROJECTION);
        gl.glLoadIdentity();    // Reset The Projection Matrix

        // Define perspective
        glu.gluPerspective(
            45.0f,        // Field Of View
            (float)getWidth() / (float)getHeight(), // aspect ratio
            0.1f,         // near Z clipping plane
            100.0f);      // far Z clipping plane

        // Select The Modelview Matrix (controls model orientation)
        gl.glMatrixMode(GL.GL_MODELVIEW);

        // make sure OpenGL correctly layers objects
        gl.glEnable(GL.GL_DEPTH_TEST);

        // OpenGL won't draw backward facing triangles ("back faces")
        gl.glEnable(GL.GL_CULL_FACE);

        // turn lighting on (does not create a light)
        gl.glEnable(GL.GL_LIGHTING);

        // Create a light
        // diffuse is the color of direct light from this light source
        // specular is the hightlight color
        // ambient is the color of scattered light from this source
        // position is where the light is, or it's direction
        float lightDiffuse[]  = { .9f, .9f, .8f, 1f }; // direct light
        float lightSpecular[] = { .9f, .9f, .8f, 1f }; // highlight
        float lightAmbient[]  = { .1f, .1f, .0f, 1f }; // scattered light
        float lightPosition[] = { -4f, 4f, 6, 1f };    // Last value is 1: this is light position.
        setLight( GL.GL_LIGHT1, lightDiffuse, lightAmbient, lightSpecular, lightPosition );

        // overall scene lighting
        setAmbientLight( new float[] { .7f, .7f, .7f, 1f } );

        // make the transparent material
        clearMtl.setDiffuse(  new float[] { .9f, .95f, .9f, .5f });    // white/green  .5f in the fourth position is ALPHA value
        clearMtl.setAmbient(  new float[] { .7f, .75f, .7f, 1f });     // light gray greenish
        clearMtl.setSpecular( new float[] { .9f, .9f, .9f, 1f });      // almost white: very reflective
        clearMtl.setShininess(127f);   // 0=no shine,  127=max shine

        // make the opaque material
        opaqueMtl.setDiffuse(new float[] { 1f, .3f, .3f, 1f }); // reddish
        opaqueMtl.setAmbient(new float[] { .8f, .2f, .2f, 1f });   // reddish

        // set the background color
        gl.glClearColor(.1f, .1f, .23f, 1);

        // blending
        gl.glEnable(GL.GL_BLEND);
        gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);

        // Force normals to length 1
        gl.glEnable(GL.GL_NORMALIZE);

        // Draw specular highlghts on top of textures (GL12.GL_SINGLE_COLOR to reset)
        gl.glLightModeli(GL.GL_LIGHT_MODEL_COLOR_CONTROL, GL.GL_SEPARATE_SPECULAR_COLOR );

        // Create a texture from the image
        marbleTextureHandle = makeTexture("images/marble.jpg");

        // positions of 3 transparent spheres
		spherePositions = new ObjectPosition[3];
		spherePositions[0] = new ObjectPosition( 0f, 0f, -2f);
		spherePositions[1] = new ObjectPosition( 0f, 0f, 0f);
		spherePositions[2] = new ObjectPosition( 0f, 0f, 2f);
    }

    /**
     * Render the scene.
     */
    public void draw() {
        // reset the coordinate system to center of screen
        gl.glLoadIdentity();

        // Where is the 'eye'
        glu.gluLookAt(
            -3f, 0f, 6f,   // eye position
            0f, 0f, 0f,    // target to look at
            0f, 1f, 0f);   // which way is up

        // rotate scene
        gl.glRotatef(rotation, 0,1,0);

		// Clear screen and depth buffer
        gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);

		//-----------------------------------------------------------------------------
        // 1) Sort the transparent objects by their Z depth
        //
        // I do this with a simple but somewhat kludgey approach.  I draw the spheres
        // exactly as they appear in the scene and check their screen Z positions (in
        // other words the Z depth).  Then I sort the spheres by that Z depth, and
        // clear the screen.
        //-----------------------------------------------------------------------------

		// get the Z depths of the three spheres
		for (int i=0; i < spherePositions.length; i++) {
			spherePositions[i].Zdepth = drawSphereAt(spherePositions[i].x, spherePositions[i].y, spherePositions[i].z);
		}

		// sort the sphere positions by Zdepth (back to front)
		sortSpheresByZ(spherePositions);

		// Clear screen and depth buffer again
        gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);

		//-----------------------------------------------------------------------------
        // 2) Draw opaque stuff first 
        // Depth test is ON, so these objects will be written into depth buffer
        //-----------------------------------------------------------------------------

        // activate opaque material
        opaqueMtl.apply();

        // draw sphere at one end of line
		drawSphereAt(0f,0f,-4f);

        // draw sphere at other end of line
		drawSphereAt(0f,0f,4f);

		//-----------------------------------------------------------------------------
        // 3) Draw transparent stuff second
        // Depth test is ON, so depth test will take place for the opaque objects that
		// we'we drawn so far. We make the depth buffer read-only: the transparent 
		// objects will not be written to the depth buffer, so they will not overwrite 
		// what is behind them.
        //-----------------------------------------------------------------------------

        // activate transparent material
        clearMtl.apply();

        // make depth buffer read-only before drawing transparent objects
        gl.glDepthMask(false);

		// draw the transparent spheres from highest Z (farthest from the eye) to lowest Z (closest to the eye)
		for (int i=0; i < spherePositions.length; i++) {
			drawSphereAt(spherePositions[i].x, spherePositions[i].y, spherePositions[i].z);
		}

        // make depth buffer writable again
        gl.glDepthMask(true);
    }

	/**
	 * Translate to the given world position and draw a sphere.
	 * Return the Z depth of that position.
	 */
    public float drawSphereAt(float x, float y, float z) {
		JOVector projectedPoint;
		gl.glPushMatrix();
        {
			// shift to sphere position
        	gl.glTranslatef(x,y,z);
        	// draw double-sided sphere
        	draw2SidedSphere();
        	// "project" the sphere position onto the screen so we can get it's Z depth.
        	// The projectPoint() function returns the screen position of the sphere  
        	// after it has been transformed by the gluLookAt(), glRotate(), glTranslate()  
        	// and gluPerspective() commands.  We project 0,0,0 because the sphere is 
        	// drawn at the origin (it is translated and rotated afterwards)
			projectedPoint = projectPoint(0,0,0);
        }
        gl.glPopMatrix();
        // Return the Z depth of the sphere
        return projectedPoint.z;
    }

	/**
	 * Draw a sphere with another slightly smaller sphere inside.  The inner sphere faces
	 * inside, creating the effect that the sphere is a glass shell and can reflect
	 * light both on the outer surface and the inner surface.
	 */
    public void draw2SidedSphere() {
        gl.glPushMatrix();
        {
        	// draw inside-facing surface a little smaller
        	gl.glScalef(.99f,.99f,.99f);
        	renderSphereInside();
        }
        gl.glPopMatrix();
        gl.glPushMatrix();
        {
        	// draw outside-facing sphere a little bigger
        	gl.glScalef(1.01f, 1.01f, 1.01f);
        	renderSphere();
        }
        gl.glPopMatrix();
    }

    /**
     * call glu functions to create geometry for a 1 unit sphere, with normals.
     * NOTE: this is not optimized at all.
     */
	public static void renderSphereInside() {
        GLUquadric s = glu.gluNewQuadric();
        glu.gluQuadricOrientation(s, GLU.GLU_INSIDE);  // normals point inwards
        glu.gluQuadricTexture(s, true);
	    glu.gluSphere(s, 1, 48, 48);  // run GL commands to draw sphere
	}

    public static void renderSphere() {
        GLUquadric s = glu.gluNewQuadric();
        glu.gluQuadricOrientation(s, GLU.GLU_OUTSIDE);  // normals point outwards
        glu.gluQuadricTexture(s, true);
	    glu.gluSphere(s, 1, 48, 48);  // run GL commands to draw sphere
    }

    /**
     * Return the screen position of a world position.
     *  
     * "Project" a point from world space into screen space.  This replicates the "transformation
     * pipeline", the process OpenGL goes through to place geometry onto the screen.  We call
     * glu.gluProject(), a utility function that transforms a world xyz coordinate into
     * screen space, based on the current modelview matrix, projection matrix, and viewport values.
     *
     * Remember: in screen space, Z is a number from 0-1 (not the same as world space Z)
     */
    public JOVector projectPoint(float x, float y, float z) {
        float[] resultf = new float[4];
        project(x, y, z, resultf);
        return new JOVector(resultf[0],resultf[1],resultf[2]);
    }

	/**
	 * Uses a basic bubble sort to sort the sphere positions by Z depth. When
	 * we're done the positions array will be ordered from highest Z depth
	 * to lowest.  Higher Z depths are further away from the eye (background).
	 */
	public void sortSpheresByZ(ObjectPosition[] worldPositions) {
		int n = worldPositions.length;
		for (int pass=1; pass < n; pass++) {
			for (int i=0; i < n-pass; i++) {
				if (worldPositions[i].Zdepth < worldPositions[i+1].Zdepth) {
					ObjectPosition temp = worldPositions[i+1];
					worldPositions[i+1] = worldPositions[i];
					worldPositions[i] = temp;

				}
			}
		}
	}

    /**
     * rotate scene if right or left arrow keys are pressed
     */
    public void keyDown(int keycode) {
        if(keycode == KeyEvent.VK_RIGHT) {
            rotation -= 2f;
        }
        else if(keycode == KeyEvent.VK_LEFT) {
        	rotation += 2f;
        }
    }

	// ---------------------------------------------------------------
	// A class to keep track of one object's Z depth, and its position 
	// in the world.  This information is used to draw the objects in
	// order by Z depth (necessary when rendering transparent objects).
	// ---------------------------------------------------------------

    class ObjectPosition {
		public float Zdepth;
		public float x;
		public float y;
		public float z;

		public ObjectPosition(float x_, float y_, float z_) {
			x = x_;
			y = y_;
			z = z_;
		}
	}
}
