/*
 * @(#)FourD.java
 *
 * Name: FourD - Display 4-dimensional wireframe models
 * Version: Java Applet V1.3, last revised: 2007-02-01
 * Author: Harry J. Smith, 19628 Via Monte Dr., Saratoga CA 95070
 * Copyright (c) 1999-2007 by author, All rights reserved
 *
 * Based on ThreeD.java by Sun Microsystems, Inc.
 */

/* A set of classes to parse, represent and display 4D wireframe models
 * represented in Wavefront .obj format.
 */
import java.applet.Applet;
import java.awt.Graphics;
import java.awt.Color;
import java.awt.Event;
import java.awt.Image;
import java.awt.Dimension;
import java.io.StreamTokenizer;
import java.io.InputStream;
import java.io.IOException;
import java.net.URL;
import java.io.Reader; // new
import java.io.BufferedReader; // new
import java.io.InputStreamReader; // new

class FileFormatException extends Exception {
	public FileFormatException(String s) {
		super(s);
	}
}

// The representation of a 4D model
class Model4D {
	float vert[];
	int tvert[];
	int nvert, maxvert;
	int con[];
	int ncon, maxcon;
	boolean transformed;
	Matrix4D mat;
	float xmin, xmax, ymin, ymax, zmin, zmax, hmin, hmax;

	Model4D () {
		mat = new Matrix4D ();
	}	

	// Create a 4D model by parsing an input stream
	Model4D (InputStream is) throws IOException, FileFormatException {
		this();
		Reader r = new BufferedReader(new InputStreamReader(is)); // new
		StreamTokenizer st = new StreamTokenizer(r); // new
		st.eolIsSignificant(true);
		st.commentChar('#');
	scan:
		while (true) {
			switch (st.nextToken()) {
				default:
				break scan;
			case StreamTokenizer.TT_EOL:
				break;
			case StreamTokenizer.TT_WORD:
				if ("v".equals(st.sval)) {
					double x = 0, y = 0, z = 0, h = 0;
					if (st.nextToken() == StreamTokenizer.TT_NUMBER) {
						x = st.nval;
						if (st.nextToken() == StreamTokenizer.TT_NUMBER) {
							y = st.nval;
							if (st.nextToken() == StreamTokenizer.TT_NUMBER) {
								z = st.nval;
								if (st.nextToken() == StreamTokenizer.TT_NUMBER)
									h = st.nval;
							}
						}
					}
					addVert((float) x, (float) y, (float) z, (float) h);
					while (st.ttype != StreamTokenizer.TT_EOL &&
							st.ttype != StreamTokenizer.TT_EOF)
						st.nextToken();
				} else {
					if ("f".equals(st.sval) || "fo".equals(st.sval) || "l".equals(st.sval)) {
						int start = -1;
						int prev = -1;
						int n = -1;
						while (true)
							if (st.nextToken() == StreamTokenizer.TT_NUMBER) {
								n = (int) st.nval;
								if (prev >= 0)
									add(prev - 1, n - 1);
								if (start < 0)
									start = n;
								prev = n;
							} else 
								if (st.ttype == '/')
									st.nextToken();
								else
									break;
						if (start >= 0)
							add(start - 1, prev - 1);
						if (st.ttype != StreamTokenizer.TT_EOL)
							break scan;
					} else {
						while (st.nextToken() != StreamTokenizer.TT_EOL
								&& st.ttype != StreamTokenizer.TT_EOF);
					}
				}
			}
		}
		is.close();
		if (st.ttype != StreamTokenizer.TT_EOF)
			throw new FileFormatException(st.toString());
	} // Model4D

	// Add a vertex to this model
	int addVert(float x, float y, float z, float h) {
		int i = nvert;
		if (i >= maxvert)
			if (vert == null) {
				maxvert = 100;
				vert = new float[maxvert * 4];
			} else {
				maxvert *= 2;
				float nv[] = new float[maxvert * 4];
				System.arraycopy(vert, 0, nv, 0, vert.length);
				vert = nv;
			}
		i *= 4;
		vert[i] = x;
		vert[i + 1] = y;
		vert[i + 2] = z;
		vert[i + 3] = h;
		return nvert++;
	}

	// Add a line from vertex p1 to vertex p2
	void add(int p1, int p2) {
		int i = ncon;
		if (p1 >= nvert || p2 >= nvert)
			return;
		if (i >= maxcon)
			if (con == null) {
				maxcon = 100;
				con = new int[maxcon];
			} else {
				maxcon *= 2;
				int nv[] = new int[maxcon];
				System.arraycopy(con, 0, nv, 0, con.length);
				con = nv;
			}
		if (p1 > p2) {
			int t = p1;
			p1 = p2;
			p2 = t;
		}
		con[i] = (p1 << 16) | p2;
		ncon = i + 1;
	}

	// Transform all the points in this model
	void transform() {
		if (transformed || nvert <= 0)
			return;
		if (tvert == null || tvert.length < nvert * 4)
			tvert = new int[nvert*4];
		mat.transform(vert, tvert, nvert);
		transformed = true;
	}

	// Quick Sort implementation
	private void quickSort(int a[], int left, int right) {
		int leftIndex = left;
		int rightIndex = right;
		int partionElement;
		if ( right > left) {
			/* Arbitrarily establishing partition element as the midpoint of
			 * the array.
			 */
			partionElement = a[ ( left + right ) / 2 ];

			// loop through the array until indices cross
			while( leftIndex <= rightIndex ) {
				/* find the first element that is greater than or equal to 
				 * the partionElement starting from the leftIndex.
				 */
				while( ( leftIndex < right ) && ( a[leftIndex] < partionElement ) )
					++leftIndex;

				/* find an element that is smaller than or equal to 
				 * the partionElement starting from the rightIndex.
				 */
				while( ( rightIndex > left ) && 
						( a[rightIndex] > partionElement ) )
					--rightIndex;

				// if the indexes have not crossed, swap
				if( leftIndex <= rightIndex ) {
					swap(a, leftIndex, rightIndex);
					++leftIndex;
					--rightIndex;
				}
			}

			/* If the right index has not reached the left side of array
			 * must now sort the left partition.
			 */
			if( left < rightIndex )
				quickSort( a, left, rightIndex );

			/* If the left index has not reached the right side of array
			 * must now sort the right partition.
			 */
			if( leftIndex < right )
				quickSort( a, leftIndex, right );
		}
	} // quickSort

	private void swap(int a[], int i, int j) {
		int T;
		T = a[i]; 
		a[i] = a[j];
		a[j] = T;
	}

	// eliminate duplicate lines
	void compress() {
		int limit = ncon;
		int c[] = con;
		quickSort(con, 0, ncon - 1);
		int d = 0;
		int pp1 = -1;
		for (int i = 0; i < limit; i++) {
			int p1 = c[i];
			if (pp1 != p1) {
				c[d] = p1;
				d++;
			}
			pp1 = p1;
		}
		ncon = d;
	}

	static Color gr[];

	/* Paint this model to a graphics context. It uses the matrix associated
	 * with this model to map from model space to screen space.
	 */
	void paint(Graphics g) {
		if (vert == null || nvert <= 0)
			return;
		transform();
		if (gr == null) {
			gr = new Color[16];
			for (int i = 0; i < 16; i++) {
				int grey = (int) (170*(1-Math.pow(i/15.0, 2.3)));
				gr[i] = new Color(grey, grey, grey);
			}
		}
		int lg = 0;
		int lim = ncon;
		int c[] = con;
		int v[] = tvert;
		if (lim <= 0 || nvert <= 0)
			return;
		for (int i = 0; i < lim; i++) {
			int T = c[i];
			int p1 = ((T >> 16) & 0xFFFF) * 4;
			int p2 = (T & 0xFFFF) * 4;
			int grey = v[p1 + 2] + v[p2 + 2];
			if (grey < 0)
			grey = 0;
			if (grey > 15)
			grey = 15;
			if (grey != lg) {
				lg = grey;
				g.setColor(gr[grey]);
			}
			g.drawLine(v[p1], v[p1 + 1], v[p2], v[p2 + 1]);
		}
		g.setColor(Color.black);
	}

	// Find the bounding box of this model
	void findBB() {
		if (nvert <= 0)
			return;
		float v[] = vert;
		float xmin = v[0], xmax = xmin;
		float ymin = v[1], ymax = ymin;
		float zmin = v[2], zmax = zmin;
		float hmin = v[3], hmax = hmin;
		for (int i = nvert * 4; (i -= 4) > 0;) {
			float x = v[i];
			if (x < xmin)
				xmin = x;
			if (x > xmax)
				xmax = x;
			float y = v[i + 1];
			if (y < ymin)
				ymin = y;
			if (y > ymax)
				ymax = y;
			float z = v[i + 2];
			if (z < zmin)
				zmin = z;
			if (z > zmax)
				zmax = z;
			float h = v[i + 3];
			if (h < hmin)
				hmin = h;
			if (h > hmax)
				hmax = h;
		}
		this.xmax = xmax;
		this.xmin = xmin;
		this.ymax = ymax;
		this.ymin = ymin;
		this.zmax = zmax;
		this.zmin = zmin;
		this.hmax = hmax;
		this.hmin = hmin;
	}
} // end of class Model4D

// An applet to put a 4D model into a page
public class FourD extends Applet implements Runnable {
	Model4D md;
	boolean painted = true;
	float xfac;
	int prevx, prevy;
	float xtheta, ytheta;
	float scalefudge = 1;
	Matrix4D amat = new Matrix4D();
	String mdname = null;
	String message = null;

	// define double buffering image
	private Image m_image;	// offscreen image
	private Graphics m_g;	// associated graphics object
	Dimension m_dimImage;	// size of offscreen image

	// initial state
	float xya = 27, yza = 0, xza = 316, yha = 136, xha = 324;

	public void init() {
		// create a graphics image to paint to
				// size() was deprecated, changes to getSize() // new
		m_dimImage = new Dimension(getSize().width, getSize().height);
		m_image = createImage(getSize().width, getSize().height);
		m_g = m_image.getGraphics();
	
		mdname = getParameter("model");
		try {
			scalefudge = Float.valueOf(getParameter("scale")).floatValue();
			xya = Float.valueOf(getParameter("xya")).floatValue();
			yza = Float.valueOf(getParameter("yza")).floatValue();
			xza = Float.valueOf(getParameter("xza")).floatValue();
			yha = Float.valueOf(getParameter("yha")).floatValue();
			xha = Float.valueOf(getParameter("xha")).floatValue();
		} catch(Exception e){};
		amat.xhrot(xha);
		amat.yhrot(yha);
		amat.xzrot(xza);
		amat.yzrot(yza);
		amat.xyrot(xya);
		if (mdname == null)
			mdname = "model.obj4";
		resize(getSize().width <= 20 ? 400 : getSize().width,
				getSize().height <= 20 ? 400 : getSize().height);
	}

	public void run() {
		InputStream is = null;
		try {
			Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
			is = new URL(getDocumentBase(), mdname).openStream();
			Model4D m = new Model4D (is);
			md = m;
			m.findBB();
			m.compress();
			float xw = m.xmax - m.xmin;
			float yw = m.ymax - m.ymin;
			float zw = m.zmax - m.zmin;
			float hw = m.hmax - m.hmin;
			if (yw > xw)
				xw = yw;
			if (zw > xw)
				xw = zw;
			if (hw > xw)
				xw = hw;
			float f1 = getSize().width / xw;
			float f2 = getSize().height / xw;
			xfac = 0.5f * (f1 < f2 ? f1 : f2) * scalefudge;
		} catch(Exception e) {
			md = null;
			message = e.toString();
		}
		try {
			if (is != null)
				is.close();
		} catch(Exception e) {}
		repaint();
	}

	public void start() {
		if (md == null && message == null)
			new Thread(this).start();
	}

	public void stop() {
	}

	public boolean mouseDown(Event e, int x, int y) { // mouseDown deprecated ??
		prevx = x;
		prevy = y;
		return true;
	}

	public boolean mouseDrag(Event e, int x, int y) { // mouseDrag deprecated ??
		float xtheta = (prevy - y) * 360.0f / getSize().width;
		float ytheta = (x - prevx) * 360.0f / getSize().height;
		xtheta /= 2;
		ytheta /= 2;
		if (e.controlDown()) {
			xya += ytheta;
			xya += xtheta; xya %= 360; if (xya < 0) xya += 360;
		} else {
			if (e.metaDown() || e.shiftDown()) {
				xha += ytheta; xha %= 360; if (xha < 0) xha += 360;
				yha += xtheta; yha %= 360; if (yha < 0) yha += 360;
			} else {
				xza += ytheta; xza %= 360; if (xza < 0) xza += 360;
				yza += xtheta; yza %= 360; if (yza < 0) yza += 360;
			}
		}
		amat.unit();
		amat.xhrot(xha);
		amat.yhrot(yha);
		amat.xzrot(xza);
		amat.yzrot(yza);
		amat.xyrot(xya);
		if (painted) {
			painted = false;
			repaint();
		}
		prevx = x;
		prevy = y;
		return true;
	}

	public void update(Graphics g) {
		paint(g);
	}

	public void paint(Graphics g) {
		if (md != null) {
			md.mat.unit();
			md.mat.translate(-(md.xmin + md.xmax) / 2,
					 -(md.ymin + md.ymax) / 2,
					 -(md.zmin + md.zmax) / 2,
					 -(md.hmin + md.hmax) / 2);
			md.mat.mult(amat);
			md.mat.scale(xfac, -xfac, 16 * xfac / getSize().width, xfac);
			md.mat.translate(getSize().width / 2, getSize().height / 2, 8, 16);
			md.transformed = false;

			// clear the off-screen image (not the on-screen one)
			Color colFG = getForeground();
//			Color colBG = Color.white;
			Color colBG = new Color(255, 255, 224); // ffffe0
			m_g.setColor(colBG);
			m_g.fillRect(0, 0, m_dimImage.width, m_dimImage.height);
			m_g.setColor(colFG);

			md.paint(m_g);
			m_g.drawString("xy=" + (int)(xya), 5, 15);
			m_g.drawString("yz=" + (int)(yza), 5, 30);
			m_g.drawString("xz=" + (int)(xza), 5, 45);
			m_g.drawString("yh=" + (int)(yha), 5, 60);
			m_g.drawString("xh=" + (int)(xha), 5, 75);
			g.drawImage(m_image, 0, 0, null);
			setPainted();
		} else {
			if (message != null) {
				g.drawString("Error in model:", 3, 20);
				g.drawString(message, 10, 40);
			}
		}
	}

	private synchronized void setPainted() {
		painted = true;
		notifyAll();
	}
} // end of class FourD
