import java.awt.BorderLayout;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.media.j3d.Appearance;
import javax.media.j3d.BranchGroup;
import javax.media.j3d.Canvas3D;
import javax.media.j3d.Geometry;
import javax.media.j3d.GeometryArray;
import javax.media.j3d.GeometryUpdater;
import javax.media.j3d.IndexedTriangleArray;
import javax.media.j3d.J3DGraphics2D;
import javax.media.j3d.Locale;
import javax.media.j3d.PolygonAttributes;
import javax.media.j3d.RenderingAttributes;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.vecmath.Color3f;
import org.apache.commons.math3.linear.MatrixUtils;
import org.apache.commons.math3.linear.RealMatrix;
import org.apache.commons.math3.util.FastMath;

import com.sun.j3d.utils.geometry.Text2D;
import com.sun.j3d.utils.universe.ConfiguredUniverse;
import com.sun.j3d.utils.universe.SimpleUniverse;

public class notusingtransform3D {

	public static void main(String[] args) {
		System.setProperty("sun.awt.noerasebackground", "true");
		new notusingtransform3D();
	}

	float x = 0, y = 0, z = 0, dX = 1, dY = 1, dZ = 1;
	double rotX = 0, rotY = 0, rotZ = 0, dRotX = 0.05, dRotY = 0.05, dRotZ = 0.05;
	double zoomScaleX = 0, zoomScaleY = 0;

	IndexedTriangleArray[] cube_one, cube_two, cube_three;

	String legendText = "a";

	float[] arrayOfRefs_one, arrayOfRefs_two, arrayOfRefs_three;

	static int[] cube_idx = { // from
								// https://en.wikibooks.org/wiki/OpenGL_Programming/Modern_OpenGL_Tutorial_05
			// front
			0, 1, 2, 2, 3, 0,
			// right
			1, 5, 6, 6, 2, 1,
			// back
			7, 6, 5, 5, 4, 7,
			// left
			4, 0, 3, 3, 7, 4,
			// bottom
			4, 5, 1, 1, 0, 4,
			// top
			3, 2, 6, 6, 7, 3 };

	static float[] cube_colors = {
			// front colors
			1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f,
			// back colors
			1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f };

	float[][] cube1_pt = { { -2, +0, -10 }, { -1, +0, -10 }, { -1, +1, -10 }, { -2, +1, -10 }, { -2, +0, -11 },
			{ -1, +0, -11 }, { -1, +1, -11 }, { -2, +1, -11 } };

	float[][] cube2_pt = { { -3, +0, -14 }, { -1, +0, -14 }, { -3, +2, -14 }, { -1, +2, -14 }, { -3, +2, -12 },
			{ -3, +0, -12 }, { -1, +2, -12 }, { -1, +0, -12 }, };

	float[][] cube3_pt = { { -2, +0, -15 }, { -2, +3, -15 }, { -1, +0, -15 }, { -1, +3, -15 }, { -2, +3, -17 },
			{ -1, +3, -17 }, { -2, +0, -17 }, { -1, +0, -17 }, };

	public RealMatrix translationM(double tx, double ty, double tz) {
		double[][] matrixData = { { 1, 0, 0, tx }, { 0, 1, 0, ty }, { 0, 0, 1, tz }, { 0, 0, 0, 1 } };
		RealMatrix mat = MatrixUtils.createRealMatrix(matrixData);
		return mat;
	}

	public RealMatrix rotateX(double fi) {
		double[][] matrixData = { { 1, 0, 0, 0 }, { 0, FastMath.cos(fi), -FastMath.sin(fi), 0 },
				{ 0, FastMath.sin(fi), FastMath.cos(fi), 0 }, { 0, 0, 0, 1 } };
		RealMatrix mat = MatrixUtils.createRealMatrix(matrixData);
		return mat;
	}

	public RealMatrix rotateY(double fi) {
		double[][] matrixData = { { FastMath.cos(fi), 0, FastMath.sin(fi), 0 }, { 0, 1, 0, 0 },
				{ -FastMath.sin(fi), 0, FastMath.cos(fi), 0 }, { 0, 0, 0, 1 } };
		RealMatrix mat = MatrixUtils.createRealMatrix(matrixData);
		return mat;
	}

	public RealMatrix rotateZ(double fi) {
		double[][] matrixData = { { FastMath.cos(fi), -FastMath.sin(fi), 0, 0 },
				{ FastMath.sin(fi), FastMath.cos(fi), 0, 0 }, { 0, 0, 1, 0 }, { 0, 0, 0, 1 } };
		RealMatrix mat = MatrixUtils.createRealMatrix(matrixData);
		return mat;
	}

	public RealMatrix scale(double scaleX, double scaleY, double scaleZ) {
		double[][] matrixData = { { scaleX, 0, 0, 0 }, { 0, scaleY, 0, 0 }, { 0, 0, scaleZ, 0 }, { 0, 0, 0, 1 } };
		RealMatrix mat = MatrixUtils.createRealMatrix(matrixData);
		return mat;
	}

	public RealMatrix quaternion(float x, float y, float z, float w) {
		double[][] quatData = { { x }, { y }, { z }, { w } };
		RealMatrix quat = MatrixUtils.createRealMatrix(quatData);
		return quat;
	}

	public void translateArray(float[] cube, float deltaX, float deltaY, float deltaZ) {
		RealMatrix quat;
		for (int i = 0; i < 8; i++) {
			quat = quaternion(cube[i * 3 + 0], cube[i * 3 + 1], cube[i * 3 + 2], 1);
			quat = quat.preMultiply(translationM(deltaX, deltaY, deltaZ));
			cube[i * 3 + 0] = (float) quat.getEntry(0, 0);
			cube[i * 3 + 1] = (float) quat.getEntry(1, 0);
			cube[i * 3 + 2] = (float) quat.getEntry(2, 0);

		}
	}

	public void rotateArrayX(float[] cube, double fi) {
		RealMatrix quat;
		for (int i = 0; i < 8; i++) {
			quat = quaternion(cube[i * 3 + 0], cube[i * 3 + 1], cube[i * 3 + 2], 1);
			quat = quat.preMultiply(rotateX(fi));
			cube[i * 3 + 0] = (float) quat.getEntry(0, 0);
			cube[i * 3 + 1] = (float) quat.getEntry(1, 0);
			cube[i * 3 + 2] = (float) quat.getEntry(2, 0);
		}
	}

	public void rotateArrayY(float[] cube, double fi) {
		RealMatrix quat;
		for (int i = 0; i < 8; i++) {
			quat = quaternion(cube[i * 3 + 0], cube[i * 3 + 1], cube[i * 3 + 2], 1);
			quat = quat.preMultiply(rotateY(fi));
			cube[i * 3 + 0] = (float) quat.getEntry(0, 0);
			cube[i * 3 + 1] = (float) quat.getEntry(1, 0);
			cube[i * 3 + 2] = (float) quat.getEntry(2, 0);
		}
	}

	public void rotateArrayZ(float[] cube, double fi) {
		RealMatrix quat;
		for (int i = 0; i < 8; i++) {
			quat = quaternion(cube[i * 3 + 0], cube[i * 3 + 1], cube[i * 3 + 2], 1);
			quat = quat.preMultiply(rotateZ(fi));
			cube[i * 3 + 0] = (float) quat.getEntry(0, 0);
			cube[i * 3 + 1] = (float) quat.getEntry(1, 0);
			cube[i * 3 + 2] = (float) quat.getEntry(2, 0);
		}
	}

	public void zoom(float[] cube, double scaleX, double scaleY, double scaleZ) {
		RealMatrix quat;
		for (int i = 0; i < 8; i++) {
			quat = quaternion(cube[i * 3 + 0], cube[i * 3 + 1], cube[i * 3 + 2], 1);
			quat = quat.preMultiply(scale(scaleX, scaleY, scaleZ));
			cube[i * 3 + 0] = (float) quat.getEntry(0, 0);
			cube[i * 3 + 1] = (float) quat.getEntry(1, 0);
			cube[i * 3 + 2] = (float) quat.getEntry(2, 0);
		}
	}

	public static IndexedTriangleArray[] ccr1(float cube_pts[][]) {
		float[] flat_pts = flatten(cube_pts);
		IndexedTriangleArray[] dozenOfSeparateTriangles = new IndexedTriangleArray[12];
		for (int i = 0; i < dozenOfSeparateTriangles.length; i++) {
			IndexedTriangleArray triangleGeometry = new IndexedTriangleArray(3,
					GeometryArray.COORDINATES | GeometryArray.COLOR_3 | GeometryArray.BY_REFERENCE, 3);
			triangleGeometry.setCapability(GeometryArray.ALLOW_REF_DATA_WRITE);
			ccr2(triangleGeometry, flat_pts, i);
			dozenOfSeparateTriangles[i] = triangleGeometry;
		}
		return dozenOfSeparateTriangles;
	}

	public static float[] flatten(float cube_pts[][]) {
		float[] flat_pts = new float[cube_pts.length * 3];
		for (int j = 0; j < cube_pts.length; j++) {
			flat_pts[j * 3 + 0] = cube_pts[j][0];
			flat_pts[j * 3 + 1] = cube_pts[j][1];
			flat_pts[j * 3 + 2] = cube_pts[j][2];
		}
		return flat_pts;
	}

	public static void ccr2(IndexedTriangleArray geom, float flat_pts[], int i) {
		geom.setCoordRefFloat(flat_pts);
		geom.setCoordinateIndices(0, subsetOf3(cube_idx, i * 3));
		geom.setColorRefFloat(cube_colors);
		geom.setColorIndices(0, subsetOf3(cube_idx, i * 3));
	}

	public static int[] subsetOf3(int[] cubeIndices, int startIndex) {
		int[] subset = new int[3];
		subset[0] = cubeIndices[startIndex];
		subset[1] = cubeIndices[startIndex + 1];
		subset[2] = cubeIndices[startIndex + 2];
		return subset;
	}

	public void sortZAscending(float[][] array) {
		for (int i = 0; i < array.length; i++) {

		}
	}

	static Appearance app;

	public notusingtransform3D() {

		Canvas3D canv = new Canvas3D(ConfiguredUniverse.getPreferredConfiguration()) {
			// this call is called by the renderer thread and should do a bunch of set
			// appearance and draw geometry calls
			public void renderField(int fieldDesc) {
				getGraphicsContext3D().setAppearance(app);
				for (int i = 0; i < 12; i++) {
					getGraphicsContext3D().draw(cube_one[i]);
				}

				// note we might keep using the same appearance, but we could easily swap to
				// something new; with a texture for example
				// getGraphicsContext3D().draw(cube_two);

				// notice that the cube order is now fixed to the order of these draw calls,
				// nothing in the z order will alter that.
			}

			// for fun we can now draw on the rendered image and show details
			@Override
			public void postRender() {
				J3DGraphics2D g = getGraphics2D();

				// draw a cross hair
				g.drawLine((this.getWidth() / 2) - 5, (this.getHeight() / 2), (this.getWidth() / 2) + 5,
						(this.getHeight() / 2));
				g.drawLine((this.getWidth() / 2), (this.getHeight() / 2) - 5, (this.getWidth() / 2),
						(this.getHeight() / 2) + 5);

				g.drawString("This is an example String " + System.currentTimeMillis(), 50, 20);

				// etc e.g.
				// g.drawImage(getBufferedImage(), 10, 50, null);

				g.flush(false);
			}
		};

		SimpleUniverse uni = new SimpleUniverse(canv);
		BranchGroup group = new BranchGroup();
		app = new Appearance();
		Locale loc = new Locale(uni);
		cube_one = ccr1(cube1_pt);
		arrayOfRefs_one = cube_one[0].getCoordRefFloat();

		// cube_two = ccr1(cube2_pt);
		// cube_twoData = cube_two.getCoordRefFloat();
		// cube_three = ccr1(cube3_pt);
		// cube_threeData = cube_three.getCoordRefFloat();

		// shapes are no longer needed, we are using the appearance and geometries
		// directly in the immediate render method
		// Shape3D cube1 = new Shape3D(cube_one, app); // creates Shape3D from Geometry
		// and Appearance
		// Shape3D cube2 = new Shape3D(cube_two, app);
		// Shape3D cube3 = new Shape3D(cube_three, app);
		Text2D legend = new Text2D(legendText, new Color3f(1, 1, 1), "Arial", 12, 0);

		// turn off the Z buffer
		RenderingAttributes ra = new RenderingAttributes();
		ra.setDepthBufferEnable(false);
		app.setRenderingAttributes(ra);

		// turn off front facing detection (render either side of a triangle)
		PolygonAttributes pa = new PolygonAttributes();
		pa.setCullFace(PolygonAttributes.CULL_NONE);
		app.setPolygonAttributes(pa);

		// group.addChild(cube1); // adding Shape3D to BranchGroup
		// group.addChild(cube2);
		// group.addChild(cube3);
		// lets keep rendering the legend normally
		group.addChild(legend);

		loc.addBranchGraph(group);
		// uni.getViewingPlatform().setNominalViewingTransform();

		uni.getCanvas().addKeyListener(new KeyListener() {

			private void flush() {
				// https://docs.oracle.com/cd/E17802_01/j2se/javase/technologies/desktop/java3d/forDevelopers/j3dguide/Immediate.doc.html
				// mixed mode optimizes and waits to be told that the scene has changed, this
				// call tells it the scene needs to be redrawn, or we could add a behavior
				canv.getGraphicsContext3D().flush(false);
			}

			@Override
			public void keyTyped(KeyEvent e) {
			}

			@Override
			public void keyReleased(KeyEvent e) {
			}

			@Override
			public void keyPressed(KeyEvent e) {
				if (e.getKeyCode() == KeyEvent.VK_A) {
					x -= dX;
					System.out.println("x = " + x);
					cube_one[0].updateData(new GeometryUpdater() {
						@Override
						public void updateData(Geometry geom) {
							translateArray(arrayOfRefs_one, dX, 0, 0);
							legendText = "x = " + x;
							flush();
						}
					});
					for (int i = 1; i < cube_one.length; i++) {
						cube_one[i].updateData(new GeometryUpdater() {
							@Override
							public void updateData(Geometry geom) {
							}
						});
					}
				}
				if (e.getKeyCode() == KeyEvent.VK_D) {
					x += dX;
					System.out.println("x = " + x);
					cube_one[0].updateData(new GeometryUpdater() {
						@Override
						public void updateData(Geometry geom) {
							translateArray(arrayOfRefs_one, -dX, 0, 0);
							flush();
						}
					});
					for (int i = 1; i < cube_one.length; i++) {
						cube_one[i].updateData(new GeometryUpdater() {
							@Override
							public void updateData(Geometry geom) {
							}
						});
					}
				}
				if (e.getKeyCode() == KeyEvent.VK_Q) {
					cube_one[0].updateData(new GeometryUpdater() {
						@Override
						public void updateData(Geometry geom) {
							translateArray(arrayOfRefs_one, 0, dY, 0);
							flush();
						}
					});
					for (int i = 1; i < cube_one.length; i++) {
						cube_one[i].updateData(new GeometryUpdater() {
							@Override
							public void updateData(Geometry geom) {
							}
						});
					}
				}
				if (e.getKeyCode() == KeyEvent.VK_E) {
					cube_one[0].updateData(new GeometryUpdater() {
						@Override
						public void updateData(Geometry geom) {
							translateArray(arrayOfRefs_one, 0, -dY, 0);
							flush();
						}
					});
					for (int i = 1; i < cube_one.length; i++) {
						cube_one[i].updateData(new GeometryUpdater() {
							@Override
							public void updateData(Geometry geom) {
							}
						});
					}
				}
				if (e.getKeyCode() == KeyEvent.VK_W) {
					cube_one[0].updateData(new GeometryUpdater() {
						@Override
						public void updateData(Geometry geom) {
							translateArray(arrayOfRefs_one, 0, 0, dZ);
							flush();
						}
					});
					for (int i = 1; i < cube_one.length; i++) {
						cube_one[i].updateData(new GeometryUpdater() {
							@Override
							public void updateData(Geometry geom) {
							}
						});
					}
				}
				if (e.getKeyCode() == KeyEvent.VK_S) {
					cube_one[0].updateData(new GeometryUpdater() {
						@Override
						public void updateData(Geometry geom) {
							translateArray(arrayOfRefs_one, 0, 0, -dZ);
							flush();
						}
					});
					for (int i = 1; i < cube_one.length; i++) {
						cube_one[i].updateData(new GeometryUpdater() {
							@Override
							public void updateData(Geometry geom) {
							}
						});
					}
				}
				if (e.getKeyCode() == KeyEvent.VK_DOWN) {
					cube_one[0].updateData(new GeometryUpdater() {
						@Override
						public void updateData(Geometry geom) {
							rotateArrayX(arrayOfRefs_one, dRotX);
							flush();
						}
					});
					for (int i = 1; i < cube_one.length; i++) {
						cube_one[i].updateData(new GeometryUpdater() {
							@Override
							public void updateData(Geometry geom) {
							}
						});
					}
				}
				if (e.getKeyCode() == KeyEvent.VK_UP) {
					cube_one[0].updateData(new GeometryUpdater() {
						@Override
						public void updateData(Geometry geom) {
							rotateArrayX(arrayOfRefs_one, -dRotX);
							flush();
						}
					});
					for (int i = 1; i < cube_one.length; i++) {
						cube_one[i].updateData(new GeometryUpdater() {
							@Override
							public void updateData(Geometry geom) {
							}
						});
					}
				}
				if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
					cube_one[0].updateData(new GeometryUpdater() {
						@Override
						public void updateData(Geometry geom) {
							rotateArrayY(arrayOfRefs_one, dRotY);
							flush();
						}
					});
					for (int i = 1; i < cube_one.length; i++) {
						cube_one[i].updateData(new GeometryUpdater() {
							@Override
							public void updateData(Geometry geom) {
							}
						});
					}
				}
				if (e.getKeyCode() == KeyEvent.VK_LEFT) {
					cube_one[0].updateData(new GeometryUpdater() {
						@Override
						public void updateData(Geometry geom) {
							rotateArrayY(arrayOfRefs_one, -dRotY);
							flush();
						}
					});
					for (int i = 1; i < cube_one.length; i++) {
						cube_one[i].updateData(new GeometryUpdater() {
							@Override
							public void updateData(Geometry geom) {
							}
						});
					}
				}
				if (e.getKeyCode() == KeyEvent.VK_C) {
					cube_one[0].updateData(new GeometryUpdater() {
						@Override
						public void updateData(Geometry geom) {
							rotateArrayZ(arrayOfRefs_one, dRotZ);
							flush();
						}
					});
					for (int i = 1; i < cube_one.length; i++) {
						cube_one[i].updateData(new GeometryUpdater() {
							@Override
							public void updateData(Geometry geom) {
							}
						});
					}
				}
				if (e.getKeyCode() == KeyEvent.VK_V) {
					cube_one[0].updateData(new GeometryUpdater() {
						@Override
						public void updateData(Geometry geom) {
							rotateArrayZ(arrayOfRefs_one, -dRotZ);
							flush();
						}
					});
					for (int i = 1; i < cube_one.length; i++) {
						cube_one[i].updateData(new GeometryUpdater() {
							@Override
							public void updateData(Geometry geom) {
							}
						});
					}
				}
				if (e.getKeyCode() == KeyEvent.VK_Z) {
					cube_one[0].updateData(new GeometryUpdater() {
						@Override
						public void updateData(Geometry geom) {
							zoom(arrayOfRefs_one, 0.95, 0.95, 1);
							flush();
						}
					});
					for (int i = 1; i < cube_one.length; i++) {
						cube_one[i].updateData(new GeometryUpdater() {
							@Override
							public void updateData(Geometry geom) {
							}
						});
					}
				}
				if (e.getKeyCode() == KeyEvent.VK_X) {
					cube_one[0].updateData(new GeometryUpdater() {
						@Override
						public void updateData(Geometry geom) {
							zoom(arrayOfRefs_one, 1.05, 1.05, 1);
							flush();
						}
					});
					for (int i = 1; i < cube_one.length; i++) {
						cube_one[i].updateData(new GeometryUpdater() {
							@Override
							public void updateData(Geometry geom) {
							}
						});
					}
				}
			}
		});

		// make it visible, because we hand in a canvas3D we also have to get the canvas
		// (AWT object) on to the screen now
		JFrame j3dJFrames = new JFrame();
		j3dJFrames.setDefaultCloseOperation(j3dJFrames.EXIT_ON_CLOSE);
		j3dJFrames.getContentPane().setLayout(new BorderLayout());
		j3dJFrames.setSize(256, 256);

		// Put the Canvas3D into a JPanel.
		JPanel j3dJPanels = new JPanel();
		j3dJPanels.setLayout(new BorderLayout());
		j3dJPanels.add("Center", canv);
		j3dJFrames.getContentPane().add("Center", j3dJPanels);

		j3dJFrames.setVisible(true);

	}
}