/**	
	Company:		Shout Interactive
	Project:		Shout3D 1.0
	Class:			ExaminePanel
	Date:			September 15, 1999
	Description:	Class for ExaminePanel
	(C) Copyright Shout Interactive, Inc. - 1997/1998/1999 - All rights reserved
 */

package applets;

import java.applet.*;
import java.awt.*;
import java.awt.image.*;
import java.io.*;
import java.util.Date;
import java.net.URL;
import shout3d.core.*;
import shout3d.math.*;
import shout3d.*;

/**
 * A Shout3DPanel which allows examination of an item. It is similar
 * to but not exactly like, VRML "EXAMINE" mode.
 * 
 * @author Jim Stewartson
 * @author Paul Isaacs
 * @author Dave Westwood
 */

public class ExaminePanel extends Shout3DPanel implements RenderObserver, DeviceObserver {

	// cached reference to the initial viewpoint
	Viewpoint camera;
	Node[]    pathToCameraParent = null;
	// cached reference to the scene root
    Transform root;
		
	// variables for the last x and y positions of the mouse
	float lastX = 0;
	float lastY = 0;
	
	// a cached MouseInput
	MouseInput mi;

	// variables for doing the examine calculations
	float[] distanceVector = new float[3];
	float BACKUP_SLACK = 0.1f;

	Quaternion cameraQuat = new Quaternion();
	Quaternion transQuat = new Quaternion();
	
	float cameraHeadingSpeed = 0;
	float cameraPitchSpeed = 0;
	float cameraZoomSpeed = 0;
	
	float cameraHeading = 3.14f;
	float cameraPitch = 0.2f;
	float cameraRoll = 0;
	float cameraDist = 0;
	
	float[] bboxMin;
	float[] bboxMax;
	float[] bboxCenter;
	
	/**
	 * Constructs me
	 */
	public ExaminePanel(Shout3DApplet applet){
		super(applet);
	}
	
	/**
	 * Remove observers when done with the panel
	 */
	public void finalize()throws Throwable {
		applet.getDeviceListener().removeDeviceObserver(this, "DeviceInput");
		applet.getRenderer().removeRenderObserver(this);		
		super.finalize();
	}
	
	/**
	 * Overrides Shout3DPanel.customInitialize()
	 */
    public void customInitialize() {
		camera = (Viewpoint)(applet.getCurrentBindableNode("Viewpoint"));
        root = (Transform)getScene();
		
		// register for device input
		applet.getDeviceListener().addDeviceObserver(this, "DeviceInput", null);
		// register for rendering notification
		applet.getRenderer().addRenderObserver(this, null);		
	}
	
	/**
	 * DeviceObserver method
	 */
	public boolean onDeviceInput(DeviceInput di, Object userData){
		if (di instanceof MouseInput){
			mi = (MouseInput)di;
			switch (mi.which){
			case MouseInput.DOWN:
				// reset the x and y
				lastX = mi.x;
				lastY = mi.y;
				break;
			case MouseInput.DRAG:
				// adjust the camera rotation delta to the distance the
				// user dragged
				cameraHeadingSpeed -= (float)(mi.x - lastX)/30f;
				// if the control key is down
				if ((mi.modifiers & DeviceInput.CTRL_MASK) != 0){
					// zoom based on y
					cameraZoomSpeed += (float)(mi.y - lastY)/20f;
				}
				else {
					// otherwise tumble 
					cameraPitchSpeed += (float)(mi.y - lastY)/30f;
				}
				// reset the x and y
				lastX = mi.x;
				lastY = mi.y;
				break;
			case MouseInput.UP:
				// stop rotating the camera
				cameraHeadingSpeed = 0;
				cameraPitchSpeed = 0;
				cameraZoomSpeed = 0;
				break;
				
			}
		}
		// return false, let other entities handle the event too.
		return false;
	}
	
	/**
	 * Utility function for getting the distance between two points
	 */
	float getDistance(float[] from, float[] to){
		float x = from[0] - to[0];
		float y = from[1] - to[1];
		float z = from[2] - to[2];
		float dist = (float)Math.sqrt(x*x + y*y + z*z);
		return dist;
	}
	
	/**
	 * RenderObserver function. 
	 * Does the camera movement calculations after each render.
	 */
	public void onPostRender(Renderer r, Object userData){
		if (bboxMin == null){
			// This is the first render.  Establish camera position based on 
			// starting orientation and bbox size.
			
			// Get the center of the current scene's bbox, in world space
			Searcher s = getNewSearcher();
			s.setNode(root);
			Node[] pathToRoot = s.searchFirst(root);
			s.setNode(camera);
			Node[] pathToCamera = s.searchFirst(root);
			
			bboxMin = root.getWorldBBoxMin(pathToRoot);
			bboxMax = root.getWorldBBoxMax(pathToRoot);
			bboxCenter = new float[3];
			
			// Get the path to the camera's parent, if path has length > 1
			if (pathToCamera != null && pathToCamera.length > 1){
				pathToCameraParent = new Node[pathToCamera.length - 1];
				System.arraycopy(pathToCamera, 0, pathToCameraParent, 0, pathToCameraParent.length);
			}
					 
			bboxCenter[0] = ((bboxMax[0] - bboxMin[0])/2f)+bboxMin[0];
			bboxCenter[1] = ((bboxMax[1] - bboxMin[1])/2f)+bboxMin[1];
			bboxCenter[2] = ((bboxMax[2] - bboxMin[2])/2f)+bboxMin[2];	
			
			performInitialCameraPlacement();
		}
		// change the camera's heading, pitch and distance 
		cameraHeading += cameraHeadingSpeed/getFramesPerSecond();
		cameraPitch += cameraPitchSpeed/getFramesPerSecond();
		float distanceToSceneCenter = getDistance(camera.position.getValue(), bboxCenter);
		distanceToSceneCenter += cameraZoomSpeed/getFramesPerSecond();
		
									 
		// distance from target to camera
		distanceVector[0] = 0;
		distanceVector[1] = 0;
		distanceVector[2] = -distanceToSceneCenter;
		
		// set the eulers of the camera and set camera orientation
		cameraQuat.setEulers(cameraHeading-3.14f, 
				  -cameraPitch, -cameraRoll);
		
		// get the axis angle from the quat and set the camera orientation
		float[] camRot = new float[4];
		cameraQuat.getAxisAngle(camRot);
		camera.orientation.setValue(camRot);
		
		// transform the distance vector by an oriented quaternion 
		transQuat.setEulers(cameraHeading, cameraPitch, cameraRoll);
		transQuat.xform(distanceVector);

		// Add the transformed distance vector to the model position
		camera.position.set1Value(0, bboxCenter[0]+distanceVector[0]);
		camera.position.set1Value(1, bboxCenter[1]+distanceVector[1]);
		camera.position.set1Value(2, bboxCenter[2]+distanceVector[2]);
	}
	
	public void onPreRender(Renderer r, Object userData){
	}
	
			
	public void performInitialCameraPlacement() {
		// Perform initial camera placement.
		// Start at the bbox center:
		float[] cameraPos = { bboxCenter[0], bboxCenter[1], bboxCenter[2]};
		// Move back far enough that the largest of width/height/depth of the bbox can be fully seen within the field of view
		// These are given by the equations:   tan(fieldOfView/2) = (width/2)/xbasedZdist;
		//                                     tan(fieldOfView/2) = (height/2)/ybasedZdist;
		//                                     tan(fieldOfView/2) = (depth/2)/zbasedZdist;
		float xbasedZdist = (float)((bboxMax[0] - bboxMin[0])/(2f * Math.tan(camera.fieldOfView.getValue() / 2f)));
		float ybasedZdist = (float)((bboxMax[1] - bboxMin[1])/(2f * Math.tan(camera.fieldOfView.getValue() / 2f)));
		float zbasedZdist = (float)((bboxMax[2] - bboxMin[2])/(2f * Math.tan(camera.fieldOfView.getValue() / 2f)));
		// Use the bigger of the two distances, with some slack for good measure:
		if (xbasedZdist > ybasedZdist && xbasedZdist > zbasedZdist)
			cameraPos[2] += (1f + BACKUP_SLACK) * xbasedZdist;
		else if ( ybasedZdist > zbasedZdist)
			cameraPos[2] += (1f + BACKUP_SLACK) * ybasedZdist;
		else
			cameraPos[2] += (1f + BACKUP_SLACK) * zbasedZdist;
		// If needed, transform position into camera parent space:
		if (pathToCameraParent != null){
			// Second argument means get matrix going from top to bottom of path.
			float[] xfMat = MatUtil.getMatrixAlongPath(pathToCameraParent, false);
			MatUtil.multVecMatrix(xfMat, cameraPos);
		}
		// Now  set it the position in the camera
		camera.position.setValue(cameraPos);
		
		// Set the initial cameraHeading and cameraPitch from the current values in the camera.
		Quaternion initCamQuat = new Quaternion();
		initCamQuat.setAxisAngle(camera.orientation.getValue());
		float[] headingPitchRoll = new float[3];
		initCamQuat.getEulers(headingPitchRoll);
		cameraHeading = headingPitchRoll[0] + 3.14f;
		cameraPitch = -headingPitchRoll[1];
	}
}
