package j3deditor.bin.hierarchy;


import j3deditor.bin.hierarchy.util.HNodeType;
import j3deditor.bin.hierarchy.util.J3DeSerializable;
import j3deditor.bin.hierarchy.util.VirtualCamera;

import j3deditor.bin.util.ByteConverter;

import com.sun.j3d.utils.universe.ViewingPlatform;

import java.io.InputStream;
import java.io.OutputStream;

import javax.media.j3d.Transform3D;

import javax.vecmath.Matrix3f;
import javax.vecmath.Vector3f;


/**
 * Virtual camera that manages an instance of
 * <code>com.sun.j3d.utils.universe.ViewingPlatform</code>
 * bounded to this camera and manipulates its
 * <code>javax.media.j3d.TransformGroup</code> object.
 *
 * @author Risto Seene
 * 
 * @see com.sun.j3d.utils.universe.ViewingPlatform
 */
public class J3DeCamera extends HierarchyNode implements J3DeSerializable, VirtualCamera{

	/** <code>ViewingPlatform</code> bounded to this virtual camera */
	private ViewingPlatform cam;


	/** number of <code>J3DeCamera</code> nodes */
	private static long numOfNodes = 0;


	/** represents the virtual focus of this camera */
	private Vector3f focLocation;

	/** location of this camera */
	private Vector3f camLocation;

	/** the vector between camera and focus */
	private Vector3f difference;

	/** the orientation vector of this camera */
	private Vector3f vector;


	/** angle of the focus around Y-axis of the scene */
	private float angle = 130;

	/** angle between <code>difference</code> vector and XZ-surface of the scene coordinates */
	private float angleZ = 30;

	/** distance between focus and this camera */
	private float zoomValue = 15;


	/**
	 * Creates an instance of <code>J3DeCamera</code>.
	 */
	public J3DeCamera(){
		super("Camera", HNodeType.CAMERA, numOfNodes);
		numOfNodes++;

		focLocation = new Vector3f(0, 0, 0);
		difference = new Vector3f(0, 0, 15);

		initAttributes();
		setCameraLocation();
	}


	/**
	 * Initializes the transformation attributes of this virtual camera
	 */
	private void initAttributes(){
		if(zoomValue < 1){
			zoomValue = 1;
		}

		camLocation = new Vector3f();
		vector = new Vector3f();

		calculateVector();
		moveFocus(0, 0);
		rotateCamera(0, 0);
		zoomCamera(0);
	}


	/**
	 * Applies the transformation attributes to
	 * <code>ViewingPlatform</code> bounded to this virtual camera
	 */
	private void setCameraLocation(){
		camLocation.add(difference, focLocation);

		Transform3D t = new Transform3D();
		t = setRotation(t);
		t.setTranslation(camLocation);
		try{
			cam.getViewPlatformTransform().setTransform(t);
		} catch(Throwable t1){
		}
	}


	/**
	 * Calculates the orientation vector for camera
	 */
	private void calculateVector(){
		vector.set(difference);
		vector.normalize();
	}


	/**
	 * Bounds the given <code>ViewingPlatform</code> to
	 * this virtual camera.
	 *
	 * @param cam
	 *	<code>ViewingPlatform</code> to be bounded
	 */
	public void setCamera(ViewingPlatform cam){
		this.cam = cam;

		setCameraLocation();
	}

	/**
	 * Moves the virtual focus of the camera.
	 *
	 * @param move1
	 *	movement of the focus along the X-axis of the camera.
	 *	Difference, not absolute position
	 * @param move2
	 *	movement of the focus along the Y-axis of the camera.
	 *	Difference, not absolute position
	 */
	public void moveFocus(float move1, float move2){
		calculateVector();
		Matrix3f matrix = getVectorMatrix();

		focLocation.setX(focLocation.getX() + move1 * matrix.getElement(0, 0) - move2 * matrix.getElement(1, 0));
		focLocation.setY(focLocation.getY() + move1 * matrix.getElement(0, 1) - move2 * matrix.getElement(1, 1));
		focLocation.setZ(focLocation.getZ() - move1 * matrix.getElement(0, 2) - move2 * matrix.getElement(1, 2));

		setCameraLocation();
	}

	/**
	 * Zooms the camera in/out.
	 *
	 * @param zoom
	 *	the amount to zoom
	 */
	public void zoomCamera(float zoom){
		zoom = zoom * zoomValue;

		if(zoom < 0 && zoomValue < 1){
			zoom = 0;
		} else if(zoom > 0 && zoomValue > 100){
			zoom = 0;
		}

		difference.setX(difference.getX() + zoom * vector.getX());
		difference.setY(difference.getY() + zoom * vector.getY());
		difference.setZ(difference.getZ() + zoom * vector.getZ());

		calculateVector();
		setCameraLocation();

		zoomValue = difference.length();
	}


	/**
	 * Rotates the camera.
	 *
	 * @param angle
	 *	rotation around the Y-axis of the scene.
	 *	Difference, not absolute angle
	 * @param angleZ
	 *	the change of the angle of the X-axis of camera.
	 *	Difference, not absolute angle	
	 */
	public void rotateCamera(float angle, float angleZ){
		this.angle += angle;
		if(this.angle > 360){
			this.angle -= 360;
		} else if(this.angle < 0){
			this.angle += 360;
		}

		this.angleZ += angleZ;
		if(this.angleZ > 90){
			this.angleZ = 90;
		} else if(this.angleZ < -90){
			this.angleZ = -90;
		}

		float dist1 = pythagoras(difference.getX(), difference.getZ());

		difference.setX(dist1 * (float)Math.cos(this.angle * Math.PI / 180));
		difference.setZ(dist1 * (float)Math.sin(this.angle * Math.PI / 180));
		calculateVector();

		float dist2 = difference.length();
		difference.setY(dist2 * (float)Math.sin(this.angleZ * Math.PI / 180));

		float distXZ = dist2 * (float)Math.cos(this.angleZ * Math.PI / 180);

		difference.setX(distXZ * (float)Math.cos(this.angle * Math.PI / 180));
		difference.setZ(distXZ * (float)Math.sin(this.angle * Math.PI / 180));
		calculateVector();

		zoomValue = dist2;
		setCameraLocation();
	}

	/**
	 * Calculates the length of the hypotenuse of the right-angled triangle
	 * with the given catheti
	 *
	 * @param x
	 *	one of the cathetus
	 * @param y
	 *	one of the cathetus
	 * @return Returns the length of the hypotenuse
	 */
	private float pythagoras(float x, float y){
		return (float)Math.sqrt(x * x + y * y);
	}


	/**
	 * Calculates orientation vectors of the axes
	 * of this virtual camera
	 *
	 * @return Returns orientation vectors of the axes
	 * 	of this camera as 3x3 matrix
	 */
	private Matrix3f getVectorMatrix(){
		Vector3f vector = new Vector3f(-this.vector.getX(), this.vector.getY(), this.vector.getZ());
		Vector3f vectorX = new Vector3f(vector.getZ(), 0, -vector.getX());
		vectorX.normalize();
		Vector3f vectorY = new Vector3f();
		vectorY.cross(vector, vectorX);

		Matrix3f matrix = new Matrix3f(vectorX.getX(), vectorX.getY(), vectorX.getZ(), vectorY.getX(), vectorY.getY(), vectorY.getZ(), vector.getX(), vector.getY(), vector.getZ());

		return matrix;
	}


	/**
	 * Applies the rotation of this virtual camera
	 * to the given <code>Transform3D</code> object
	 *
	 * @param t
	 *	<code>Transform3D</code> object the rotation will
	 *	be applied to
	 * @return Returns the given object after the rotation
	 *	is applied to it
	 */
	private Transform3D setRotation(Transform3D t){
		t.rotY(-(angle - 90) * Math.PI / 180);

		Transform3D rotY = new Transform3D();
		rotY.rotX(-angleZ * Math.PI / 180);

		t.mul(rotY);

		return t;
	}


	/**
	 * Writes the camera attributes to the given <code>OutputStream</code>
	 * as byte arrays.
	 *
	 * @param out
	 *	stream to be written to
	 * @throws Exception
	 *	if unable to write to the given stream
	 */
	public void writeData(OutputStream out) throws Exception{
		out.write(ByteConverter.getBytes(angle));
		out.write(ByteConverter.getBytes(angleZ));
		out.write(ByteConverter.getBytes(focLocation.getX()));
		out.write(ByteConverter.getBytes(focLocation.getY()));
		out.write(ByteConverter.getBytes(focLocation.getZ()));
		out.write(ByteConverter.getBytes(difference.getX()));
		out.write(ByteConverter.getBytes(difference.getY()));
		out.write(ByteConverter.getBytes(difference.getZ()));
	}


	/**
	 * Reads bytes from the given <code>InputStream</code> and tries
	 * to convert them to the attributes.
	 *
	 * @param in
	 *	stream to be read from
	 * @throws Exception
	 *	if unable to read from the given
	 *	stream or unable to convert the byte arrays to attributes
	 */
	public void readData(InputStream in) throws Exception{
		byte[] bytes = new byte[4];
		in.read(bytes);
		angle = ByteConverter.toFloat(bytes);
		in.read(bytes);
		angleZ = ByteConverter.toFloat(bytes);
		in.read(bytes);
		focLocation.setX(ByteConverter.toFloat(bytes));
		in.read(bytes);
		focLocation.setY(ByteConverter.toFloat(bytes));
		in.read(bytes);
		focLocation.setZ(ByteConverter.toFloat(bytes));
		in.read(bytes);
		difference.setX(ByteConverter.toFloat(bytes));
		in.read(bytes);
		difference.setY(ByteConverter.toFloat(bytes));
		in.read(bytes);
		difference.setZ(ByteConverter.toFloat(bytes));

		initAttributes();
		setCameraLocation();
	}


	/**
	 * Resets the position and orientation of this camera.
	 */
	public void resetCameraPosition(){
		angle = 130;
		angleZ = 30;
		zoomValue = 15;
		initAttributes();
		setCameraLocation();
	}

}
