package classwork;

import java.awt.event.KeyEvent;
import java.io.*;
import javax.media.opengl.*;

import jocode.*;
import jomodel.*;

/**
 * Simple model viewer with basic navigation features
 *   - Loads 3DS or OBJ file
 *   - Renders model with lighting
 *   - Shows vertex normals
 *
 * Keys
 *   - Arrow keys move viewpoint
 *   - Mouse drag to rotate model
 *   - Mouse wheel zooms in/out
 *   - Hit 'N' to turn normal rendering on/off
 *
 * Requires the glmodel package to load model formats into a GL_Object class.
 * Requires JOMaterial.java
 * Requires JOVector.java
 *
 * The glmodel package contains two readers to parse the OBJ and 3DS formats.
 * Two importers convert the parsed  data into vertex and triangle objects
 * that we can use to render the mesh in OpenGL.
 * The GL_Mesh class holds the final mesh that we draw with
 * the renderMesh() function.
 *
 */
public class GLART_6_model extends JOApp {
    private float rotateModelX = 0;
    private float rotateModelY = 0;
    private boolean normalsOn = true;
    private JOMesh obj;

    // camera position and rotation (see handleNavigationKeys())
    float[] cameraPos = new float[] {0,0,100};
    float cameraRotation = 0f;
    final float piover180 = 0.0174532925f;    // A constant used in navigation

    // Material object will hold color values
    JOMaterial material = new JOMaterial();

    // for mouse drag
    JOVector mousePrevPos = null;
    boolean mouseIsDown = false;

    // light position: if last value is 0, then this describes light direction.
    // If 1, then light position.
    float lightPosition[] = { -100f, 100f, 100, 0f };


    /**
     * Main function creates and runs the application.
     */
    public static void main(String args[]) {
    	GLART_6_model app = new GLART_6_model();
        app.run();
    }

    /**
     * Initialize
     */
    public void setup() {
        // color of overall scene lighting
        float ambient[]       = { .2f, .2f, .2f, 1f };
        // color of light source
        float lightDiffuse[]  = { 1f, 1f, .8f, 1f }; // direct light
        float lightSpecular[] = { 1f, 1f, .8f, 1f }; // highlight
        float lightAmbient[]  = { .2f, .2f, .2f, 1f }; // scattered light
        // color of material
        float mtlDiffuse[]      = { 1f, .6f, .5f, 1f };    // red
        float mtlAmbient[]      = { 1f, .6f, .5f, 1f };   // red
        float mtlSpecular[]     = { .8f, .8f, .8f, 1f }; // almost white: very reflective
        float mtlShininess      = 100f;   // 0=no shine,  127=max shine

        // Select the Projection Matrix (controls perspective)
        gl.glMatrixMode(GL.GL_PROJECTION);
        gl.glLoadIdentity();    // Reset The Projection Matrix

        // Define perspective
        glu.gluPerspective(
            30.0f,        // Field Of View
            (float)getWidth() / (float)getHeight(), // aspect ratio
            0.01f,         // near Z clipping plane
            1000.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
        setLight( GL.GL_LIGHT1, lightDiffuse, lightAmbient, lightSpecular, lightPosition );

        // no overall scene lighting
        setAmbientLight(ambient);

        // change the current material settings
        material.setDiffuse(mtlDiffuse);
        material.setAmbient(mtlAmbient);
        material.setSpecular(mtlSpecular);
        material.setShininess(mtlShininess);
        // activate this material
        material.apply();

        // set the background color
        gl.glClearColor(.08f, .08f, .1f, 1);
        
        JOOBJImporter importOBJ = new JOOBJImporter();
        obj = importOBJ.load("models/tri_pyramid.obj"); 
        //obj = importOBJ.load("models/pig.obj");      //"shark.obj"  "castle.obj"  "venus.obj"

        //========== smoothing ================
        // set the smoothing threshold.
        // If two faces meet at an angle greater than the threshold, they will be treated as two separate faces.
        // If the faces meet at less that the threshold, they will be treated as one smooth surface.
        // smoothing angle 0 means that even 1 degree of difference between faces will create a hard edge.
        // smoothing angle 100 means that faces that meet at 0-99 degrees will be smoothed.
        obj.setSmoothingAngle(170f);   // 0=faceted   179=smoothest
        obj.regenerateNormals();
    }

    /**
     * Render the scene.
     */
    public void draw() {
    	handleNavigationKeys();
    	
    	// Clear screen and depth buffer
        gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);

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

        // reset the coordinate system to center of screen
        gl.glLoadIdentity();

    	// Place the viewpoint
		glu.gluLookAt(	// camera position
						cameraPos[0], cameraPos[1], cameraPos[2],
						// look at a point directly in front of camera
						cameraPos[0]- (float) Math.sin(cameraRotation* piover180), cameraPos[1],
						cameraPos[2]- (float) Math.cos(cameraRotation* piover180),
						// which way is up
						0f, 1f, 0f);

		// set the light position (have to do this each render() since
		// we're moving the scene around with navigation, have to update
		// light position. Light pos is transformed just like a vertex).
		setLightPosition(GL.GL_LIGHT1, lightPosition[0], lightPosition[1],	lightPosition[2]);
		
		// draw white sphere at light position
		gl.glDisable(GL.GL_LIGHTING);
		gl.glColor4f(1, 1, .9f, 1);
		gl.glPushMatrix();
		{
			gl.glTranslatef(lightPosition[0], lightPosition[1], lightPosition[2]);
			renderSphere();
		}
		gl.glPopMatrix();
		gl.glEnable(GL.GL_LIGHTING);
		
		gl.glPushMatrix();
		{
			// rotate model (based on mouse drag)
			gl.glRotatef(rotateModelY, 0, 1, 0); // around vertical axis
			gl.glRotatef(rotateModelX, 1, 0, 0); // around horizontal axis
			
			// draw the model
			renderMesh(obj, 0);
			
			// draw normals
			if (normalsOn) {
				renderMeshNormals(obj);
			}
		}
		gl.glPopMatrix();		
    }

    /**
     * Render mesh with normals and texture coordinates.  Loops through
     * all triangles in the mesh object.
     *
     * Several triangles may refer to the same vertex, but each face
     * can have different normals for that vertex.  This allows for
     * sharp edges between faces.
     *
     * @param o  mesh object to render
     */
    public void renderMesh(JOMesh o, int textureHandle)
    {
        JOTriangle t;
        gl.glBindTexture(GL.GL_TEXTURE_2D,textureHandle);
        gl.glBegin(GL.GL_TRIANGLES);
        for (int j = 0; j < o.triangles.length; j++) { // draw all triangles in object
            t = o.triangles[j];

            gl.glTexCoord2f(t.uvw1.x, t.uvw1.y);
            gl.glNormal3f(t.norm1.x, t.norm1.y, t.norm1.z);
            gl.glVertex3f( (float)t.p1.pos.x, (float)t.p1.pos.y, (float)t.p1.pos.z);

            gl.glTexCoord2f(t.uvw2.x, t.uvw2.y);
            gl.glNormal3f(t.norm2.x, t.norm2.y, t.norm2.z);
            gl.glVertex3f( (float)t.p2.pos.x, (float)t.p2.pos.y, (float)t.p2.pos.z);

            gl.glTexCoord2f(t.uvw3.x, t.uvw3.y);
            gl.glNormal3f(t.norm3.x, t.norm3.y, t.norm3.z);
            gl.glVertex3f( (float)t.p3.pos.x, (float)t.p3.pos.y, (float)t.p3.pos.z);
        }
        gl.glEnd();
    }


    public void renderMeshNormals(JOMesh o)
    {
        JOTriangle t;
        gl.glDisable(GL.GL_LIGHTING);
        gl.glColor3f(0,1,0);
        gl.glBegin(GL.GL_LINES);
        {
	        for (int j = 0; j < o.triangles.length; j++) { // draw all triangles in object
	            t = o.triangles[j];
		            gl.glVertex3f( (float)t.p1.pos.x, (float)t.p1.pos.y, (float)t.p1.pos.z);
		            gl.glVertex3f( (float)(t.p1.pos.x+t.norm1.x), (float)(t.p1.pos.y+t.norm1.y), (float)(t.p1.pos.z+t.norm1.z));

		            gl.glVertex3f( (float)t.p2.pos.x, (float)t.p2.pos.y, (float)t.p2.pos.z);
		            gl.glVertex3f( (float)(t.p2.pos.x+t.norm2.x), (float)(t.p2.pos.y+t.norm2.y), (float)(t.p2.pos.z+t.norm2.z));

		            gl.glVertex3f( (float)t.p3.pos.x, (float)t.p3.pos.y, (float)t.p3.pos.z);
		            gl.glVertex3f( (float)(t.p3.pos.x+t.norm3.x), (float)(t.p3.pos.y+t.norm3.y), (float)(t.p3.pos.z+t.norm3.z));
	        }
        }
        gl.glEnd();
        gl.glEnable(GL.GL_LIGHTING);
    }

    /**
     * Adjust the Camera position based on keyboard arrow key input.
     * These are repeating events (camera will move as long as key is held
     * down).
     */
    public void handleNavigationKeys()
    {
        // Turn left
        if (JOApp.isKeyDown(KeyEvent.VK_LEFT)) {
            cameraRotation += 1.0f;
        }
        // Turn right
        if (JOApp.isKeyDown(KeyEvent.VK_RIGHT)) {
            cameraRotation -= 1.0f;
        }
        // move forward in current direction
        if (JOApp.isKeyDown(KeyEvent.VK_UP)) {
            cameraPos[0] -= (float) Math.sin(cameraRotation * piover180) * 1f;
            cameraPos[2] -= (float) Math.cos(cameraRotation * piover180) * 1f;
        }
        // move backward in current direction
        if (JOApp.isKeyDown(KeyEvent.VK_DOWN)) {
            cameraPos[0] += (float) Math.sin(cameraRotation * piover180) * 1f;
            cameraPos[2] += (float) Math.cos(cameraRotation * piover180) * 1f;
        }
        // move camera down
        if (JOApp.isKeyDown(KeyEvent.VK_PAGE_UP)) {
            cameraPos[1] +=  .3f;
        }
        // move camera up
        if (JOApp.isKeyDown(KeyEvent.VK_PAGE_DOWN)) {
            cameraPos[1] -=  .3f;
        }
    }

    /**
     * Key event functions (keyDown() and keyUp() are are called by mainloop()).
     * @param keycode
     */
    public void keyUp(int keycode) {
        // Normals off/on
        if (keycode == KeyEvent.VK_N) {
            normalsOn = !normalsOn;
        }
    }

    public void keyDown(int keycode) {
    }

    /**
     * Mouse event functions (called by handleMouseEvents() by way of mainloop()).
     * @param keycode
     */
    public void mouseMove(int x, int y) {
    	if (mouseIsDown) {
    		int mouseDragDistanceX = (int)(x - mousePrevPos.x);
    		int mouseDragDistanceY = (int)(y - mousePrevPos.y);
        	rotateModelX += ((float)mouseDragDistanceY/(float)getHeight()) * 360f;
        	rotateModelY += ((float)mouseDragDistanceX/(float)getWidth()) * 360f;
        	mousePrevPos.x = x;
        	mousePrevPos.y = y;
       	}
    }

    public void mouseDown(int x, int y) {
    	mouseIsDown = true;
    	mousePrevPos = new JOVector(x,y,0);
    }

    public void mouseUp(int x, int y) {
    	mouseIsDown = false;
    }

    public void mouseWheel(int wheelMoved) {
        // move forward or backward
        cameraPos[0] -= (float) Math.sin(cameraRotation * piover180) * (wheelMoved/50f);
        cameraPos[2] -= (float) Math.cos(cameraRotation * piover180) * (wheelMoved/50f);
    }
}
