/*

$Id: Space.java,v 1.6 2008-07-28 19:26:11 harmen Exp $

Play a stereoscopic 4d game around the hypercube (a.k.a. tesseract).
See http://www.harmwal.nl/hypercube/

Copyright (C) 1998, 2006  Harmen van der Wal - http://www.harmwal.nl

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

*/

package nl.harmwal.hypercube;

import java.applet.*;
import java.util.*;
import java.awt.*;
import java.awt.event.*;

interface Spatial {
    void sum(int dimension, double value);
    void sum(Point point);
    void sum(Point point, double value);
    void multiply(double value);
    void mirror(int dimension);
    void transfigure(int dimension, double value);
    void rotate(int dimension1, int dimension2, double angle);
    void relative(Orientation orientation);
    void project();
    Spatial getRelative(Orientation orientation);
    Spatial getProjected(Orientation orientation);
    void draw(Orientation orientation, ColorScheme colorScheme, Graphics graphics);
    void setColor(int colorType);
}

class Point implements Spatial {

    double[] coordinates;
    int colorType = ColorScheme.NORMAL;

    Point(int dimensions) {
	coordinates = new double[dimensions];
    }

    Point(double[] coordinates) {
	this.coordinates = (double[])coordinates.clone();
    }
   
    int getDimensions() {
	return coordinates.length;
    }

    double getCoordinate(int d) {
	return coordinates[d];
    }

    double[] copyCoordinates() {
	return (double[])coordinates.clone();
    }
    
    Point copyPoint() {
	return new Point((double[])coordinates.clone());
    }

    double distance() { // to origin
	double distance = 0;
	for (int d = 0; d < coordinates.length; d++) {
	    distance += coordinates[d] * coordinates[d];
	}
	distance = Math.sqrt(distance);
	return distance;
    }

    double distance(Point point) {
	double[] coordinates = point.copyCoordinates();

	double distance = 0;
	for (int d = 0; d < coordinates.length; d++) {
	    distance += Math.pow(this.coordinates[d] - coordinates[d], 2);
	}
	distance = Math.sqrt(distance);
	return distance;
    }

    boolean inside(double value) {
	boolean inside = true;
	for (int d = 0; d < coordinates.length; d++) {
	    if (Math.abs(coordinates[d]) > value) {
		inside = false;
	    }
	}
	return inside;
    }

    double angle(int dimension1, int dimension2) {
	double angle = 0;
	if (coordinates[dimension1] == 0) {
	    if (coordinates[dimension2] > 0) {
		angle = Math.PI / 2;
	    }else{
		angle = -Math.PI / 2;
	    }
	}else {
	    angle = Math.atan(coordinates[dimension2]/coordinates[dimension1]);
	}

	if (Math.abs(Math.cos(angle) - coordinates[dimension1]) > 
	    Math.abs(Math.cos(angle + Math.PI) - coordinates[dimension1])) {
	    angle += Math.PI;
	}

 	if (angle > Math.PI) {
 	    angle = angle - 2 * Math.PI;
 	}

	return angle;
    }

    public void sum(int dimension, double value) {
	coordinates[dimension] += value;
    }

    public void sum(Point point) {
	double[] coordinates = point.copyCoordinates();
	
	for (int d = 0; d < coordinates.length; d++) {
	    this.coordinates[d] += coordinates[d];
	}
    }

    public void sum(Point point, double value) {
	double[] coordinates = point.copyCoordinates();	
	double distance = point.distance();

	for (int d = 0; d < coordinates.length; d++) {
	    this.coordinates[d] += coordinates[d] * value / distance;
	}
    }

    Point difference(Point point) {
	double[] coordinates = new double[this.coordinates.length];
	for (int d = 0; d < coordinates.length; d++) {
	    coordinates[d] = this.coordinates[d] - point.getCoordinate(d);
	}
	return new Point(coordinates);
    }

    public void multiply(double value) {
	for (int d = 0; d < coordinates.length; d++) {
	    coordinates[d] *= value;
	}
    }

    public void mirror(int dimension) {
	coordinates[dimension] *= -1;
    }

    public void rotate(int dimension1, int dimension2, double angle) {
	double tan = Math.sqrt(Math.pow(coordinates[dimension1], 2) + 
			       Math.pow(coordinates[dimension2], 2));
	angle += angle(dimension1, dimension2);
	coordinates[dimension1] = tan * Math.cos(angle);
	coordinates[dimension2] = tan * Math.sin(angle);
    }

    public void relative(Orientation orientation) {
	Point point = orientation.relative(this);
	coordinates = point.copyCoordinates();
    }

   public void transfigure(int dimension) {
       double[] coordinates = new double[this.coordinates.length - 1];
       int d2 = 0;
	    
	for (int d = 0; d < this.coordinates.length; d++) {
	    if (d != dimension) {
		coordinates[d2++] = this.coordinates[d];
	    }
	}
	this.coordinates = coordinates;
   }

   public void transfigure(int dimension, double value) {
       double[] coordinates = new double[this.coordinates.length + 1];
       int d2 = 0;
	    
       for (int d = 0; d < coordinates.length; d++) {
	   if (d == dimension) {
	       coordinates[d] = value;
	   }else{
	       coordinates[d] = this.coordinates[d2++];
	   }
       }
       this.coordinates = coordinates;
   }

    public Spatial getRelative(Orientation orientation) {
	Point point = orientation.relative(this);
	point.setColor(colorType);
	return (Spatial)point;
    }

    public Spatial getProjected(Orientation orientation) {
	Spatial spatial = getRelative(orientation);
	spatial.project();
	return spatial;
    }

    public void project() {

	double[] coordinates = new double[this.coordinates.length - 1];

	for (int d = 0; d < coordinates.length; d++) {
	    coordinates[d] = this.coordinates[d + 1] / 
		Math.abs(this.coordinates[0]);
	}

	this.coordinates = coordinates;
    }

    public void draw(Orientation orientation, ColorScheme colorScheme,
		     Graphics graphics) {
	Point point = orientation.relative(this);
	double[] coordinates = point.copyCoordinates();

	int x = (int)point.getCoordinate(0);
	int y = (int)point.getCoordinate(1);

	graphics.setColor(colorScheme.scheme[colorType]);
	graphics.drawLine(x, y, x, y);
    }

    public void setColor(int colorType) {
	this.colorType = colorType;
    }

    public String toString() {
	StringBuffer str = new StringBuffer("Point");
	for (int d = 0; d < coordinates.length; d++) {
	    str.append(" " + coordinates[d] + " ");
	}
	return str.toString();
    }
}

class Sphere extends Point {

    double size;

    Sphere(Point point, double size) {
       super(point.getDimensions());
       sum(point);
       this.size = size;
    }
    
    double getSize() {
	return size;
    }

    public Spatial getRelative(Orientation orientation) {

	Point point = orientation.relative(this);
	Sphere sphere = new Sphere(point, size);
	sphere.setColor(colorType);
	return (Spatial)sphere;
    }

    // The closer you are to a sphere, the less you see of it
    public void project() {
	double tan = distance();
	double sin = size;
	double cos = Math.sqrt(Math.pow(tan, 2) - Math.pow(sin, 2));
	double angle = Math.atan(sin/cos);
	tan = cos;
	sin = tan * Math.sin(angle);
	cos = tan * Math.cos(angle);
	size = sin/cos;
	super.project();
    }

    public void draw(Orientation orientation, ColorScheme colorScheme,
		     Graphics graphics) {

	Point point = orientation.relative(this);
	double[] coordinates = point.copyCoordinates();

	int x = (int)point.getCoordinate(0);
	int y = (int)point.getCoordinate(1);

	Point radius = this.copyPoint();
	radius.sum(0, size);
	radius = orientation.relative(radius);
	int r = Math.max(1, (int)radius.distance(point));

	graphics.setColor(colorScheme.scheme[colorType]);
	graphics.drawOval(x - r, y - r, (int)2 * r, (int)2 * r);
    }

    public String toString() {
	StringBuffer str = new StringBuffer("Sphere: ");
	str.append(super.toString());
	str.append(", size " + size);
	return str.toString();
    }
}

// Alternative way of drawing a sphere (for the cursor)
class Plus extends Sphere {

    Plus(Point point, double size) {
	super(point, size);
    }

    public Spatial getRelative(Orientation orientation) {

	Point point = orientation.relative(this);
	Plus plus = new Plus(point, size);
	plus.setColor(colorType);
	return (Spatial)plus;
    }

    public void draw(Orientation orientation, ColorScheme colorScheme,
		     Graphics graphics) {

	Point point = orientation.relative(this);
	double[] coordinates = point.copyCoordinates();

	int x = (int)point.getCoordinate(0);
	int y = (int)point.getCoordinate(1);

	Point radius = this.copyPoint();
	radius.sum(0, size);
	radius = orientation.relative(radius);
	int r = Math.max(1, (int)radius.distance(point));

	graphics.setColor(colorScheme.scheme[colorType]);
	graphics.drawLine(x - r, y, x + r, y);
	graphics.drawLine(x, y - r, x, y + r);
    }
}

// Glue.
class Body implements Spatial {

    Vector vector = new Vector();
    int colorType = ColorScheme.NORMAL;

    // Don't add spatials twice!
    void add(Spatial spatial) {
	vector.addElement(spatial);
    }

    public void sum(int dimension, double value) {
	Enumeration e = vector.elements();
	while(e.hasMoreElements()) {
	    Spatial spatial = (Spatial)e.nextElement();
	    spatial.sum(dimension, value);
	}
    }

    public void sum(Point point) {
	Enumeration e = vector.elements();
	while(e.hasMoreElements()) {
	    Spatial spatial = (Spatial)e.nextElement();
	    spatial.sum(point);
	}
    }

    public void sum(Point point, double value) {
	Enumeration e = vector.elements();
	while(e.hasMoreElements()) {
	    Spatial spatial = (Spatial)e.nextElement();
	    spatial.sum(point, value);
	}
    }

    public void multiply(double value) {
	Enumeration e = vector.elements();
	while(e.hasMoreElements()) {
	    Spatial spatial = (Spatial)e.nextElement();
	    spatial.multiply(value);
	}
    }

    public void mirror(int dimension) {
	Enumeration e = vector.elements();
	while(e.hasMoreElements()) {
	    Spatial spatial = (Spatial)e.nextElement();
	    spatial.mirror(dimension);
	}
    }

    public void rotate(int dimension1, int dimension2, double angle) {
	Enumeration e = vector.elements();
	while(e.hasMoreElements()) {
	    Spatial spatial = (Spatial)e.nextElement();
	    spatial.rotate(dimension1, dimension2, angle);
	}
    }

    public void relative(Orientation orientation) {
	Enumeration e = vector.elements();
	while(e.hasMoreElements()) {
	    Spatial spatial = (Spatial)e.nextElement();
	    spatial.relative(orientation);
	}
    }

    public void transfigure(int dimension, double value) {
	Enumeration e = vector.elements();
	while(e.hasMoreElements()) {
	    Spatial spatial = (Spatial)e.nextElement();
	    spatial.transfigure(dimension, value);
	}
    }

    public Spatial getRelative(Orientation orientation) {
	Body body = new Body();
	Enumeration e = vector.elements();
	while(e.hasMoreElements()) {
	    Spatial spatial = (Spatial)e.nextElement();
	    body.add(spatial.getRelative(orientation));
	}
	return (Spatial)body;
    }

    public Spatial getProjected(Orientation orientation) {
	Spatial spatial = getRelative(orientation);
	spatial.project();
	return spatial;
    }

    public void project() {
	Enumeration e = vector.elements();
	while(e.hasMoreElements()) {
	    Spatial spatial = (Spatial)e.nextElement();
	    spatial.project();
	}
    }

    public void draw(Orientation orientation, ColorScheme colorScheme,
		     Graphics graphics) {
	Enumeration e = vector.elements();
	while(e.hasMoreElements()) {
	    Spatial spatial = (Spatial)e.nextElement();
	    spatial.draw(orientation, colorScheme, graphics);
	}
    }

    public void setColor(int colorType) {
	this.colorType = colorType;
    }
}

class Line extends Body {

    Point point1, point2;

    Line(Point point1, Point point2) {
	this.point1 = point1;
	this.point2 = point2;
	add(point1);
	add(point2);
    }

    // Coordinate of a point if this line is ax in a coordinate system
    double relative(Point point) {
	int dimensions = point.getDimensions();
	double[] position = new double[dimensions];
	double[] direction = new double[dimensions];
	double numerator = 0, denominator = 0, parameter;

	for (int d = 0; d < dimensions; d++) {
	    // Parameter representation of the line.
	    // y = a * x + b
	    position[d] = point1.getCoordinate(d);
	    direction[d] = point2.getCoordinate(d) - point1.getCoordinate(d);
	    
	    // Parameter for shortest distance between point and line.
	    numerator += direction[d] * (point.getCoordinate(d) - position[d]);
	    denominator += Math.pow(direction[d], 2);
	}
	parameter = numerator/denominator;

	return parameter;
    }

    Point point(double parameter) {
	double[] coordinates = new double[point1.getDimensions()];
	for (int d = 0; d < coordinates.length; d++) {
	    coordinates[d] = point1.getCoordinate(d) + 
		parameter * (point2.getCoordinate(d) - 
			     point1.getCoordinate(d));
	}
	return new Point(coordinates);
    }

    public Spatial getRelative(Orientation orientation) {
	Point point1 = orientation.relative(this.point1);
	Point point2 = orientation.relative(this.point2);
	Line line = new Line(point1, point2);
	line.setColor(colorType);
	return (Spatial)line;
    }

    public void project(Orientation orientation) {
	point1.project();
	point2.project();
    }

    public void draw(Orientation orientation, ColorScheme colorScheme,
		     Graphics graphics) {

	Point point1 = orientation.relative(this.point1);
	Point point2 = orientation.relative(this.point2);
	int x1 = (int)point1.getCoordinate(0);
	int y1 = (int)point1.getCoordinate(1);
	int x2 = (int)point2.getCoordinate(0);
	int y2 = (int)point2.getCoordinate(1);


	// Too hackish.
	if (colorType != ColorScheme.DISABLED) {
	    graphics.setColor(colorScheme.scheme[colorType]);
	    graphics.drawLine(x1, y1, x2, y2);
	}

	if (colorType == ColorScheme.TARGET || 
	    colorType == ColorScheme.DISABLED ||
	    colorType == ColorScheme.WINDOW) {

	    if (colorType == ColorScheme.DISABLED) {
		graphics.setColor(colorScheme.scheme[ColorScheme.TARGET]);
	    }
	    graphics.drawOval(x1 - 2, y1 - 2, 4, 4);
	    graphics.drawOval(x2 - 2, y2 - 2, 4, 4);
	}
    }

    public String toString() {
	StringBuffer str = new StringBuffer("Line\n");
	str.append(point1.toString());
	str.append("\n");
	str.append(point2.toString());
	return str.toString();
    }
}

// Coordinate system.
class Orientation extends Body {

    Point[] points;
    Line[] lines;
    int dimensions;

    Orientation(int dimensions) {
	this.dimensions = dimensions;

	double[] coordinates = new double[dimensions];
	points = new Point[dimensions + 1];
 	points[0] = new Point(coordinates);
	add(points[0]);
 	for (int d = 1; d < dimensions + 1; d++) {
	    coordinates[d - 1] += 1;
	    points[d] = new Point(coordinates);
	    add(points[d]);
	    coordinates[d - 1] = 0;
	}
	makeLines();
    }

    Orientation(Point[] points) {
	this.points = (Point[])points.clone();
	for (int p = 0; p < points.length; p++) {
	    add(points[p]);
	}
	makeLines();
    }

    void makeLines() {
	lines = new Line[points.length - 1];
	for (int d = 0; d < lines.length; d++) {
	    lines[d] = new Line(points[0], points[d + 1]);
	}
    }

    Point[] copyPoints() { // deep copy
	Point[] points = new Point[this.points.length];
	for (int p = 0; p < points.length; p++) {
	    points[p] = this.points[p].copyPoint();
	}
	return points;
    }

    Orientation copyOrientation() {
	return new Orientation(copyPoints());
    }

    double getSize() {
	return points[0].distance(points[1]);
    }

    void sumRelative(int dimension, double value) {
	double[] coordinates = new double[points.length - 1];
	for (int d = 0; d < coordinates.length; d++) {
	    coordinates[d] += (points[dimension + 1].getCoordinate(d) -
			       points[0].getCoordinate(d)) * value;
	}
	sum(new Point(coordinates));
    }

    void multiplyRelative(double value) {
	double[] coordinates = new double[points.length - 1];

	for (int p = 1; p < points.length; p++) {
	    for (int d = 0; d < coordinates.length; d++) {
		coordinates[d] = points[0].getCoordinate(d) + 
		    (points[p].getCoordinate(d) - 
		     points[0].getCoordinate(d)) * value;
	    }
	    points[p] = new Point(coordinates);
	}
	makeLines();
    }

    boolean inside(Point point) {
	Point relative = relative(point);
	double size = points[0].distance(points[1]);
	return relative.inside(size);
    }

    // Return point with coordinates relative to this orientation
    Point relative(Point point) {

	double[] coordinates = new double[point.getDimensions()];
	for (int d = 0; d < coordinates.length; d++)
	    {
		coordinates[d] = lines[d].relative(point);
	    }
	return new Point(coordinates);
    }

    void generate(double[] coordinates, int dimension, Body body) {
	for (int l = 0; l < lines.length; l++) {
	    body.add(lines[l]);
	}
    }

    public Spatial getRelative(Orientation orientation) {
	Body body = new Body();
	Body body2 = new Body();
	generate(new double[dimensions], 0, body2);
	body.add(body2.getRelative(orientation));
	return (Spatial)body;
    }

    public void draw(Orientation orientation, ColorScheme colorScheme,
		     Graphics graphics)
    {
	Body body = new Body();
	generate(new double[dimensions], 0, body);
	body.draw(orientation, colorScheme, graphics);
    }

    public String toString() {
	StringBuffer str = new StringBuffer("Orientation\n");
	for (int p = 0; p < points.length; p++) {
	    str.append(points[p] + "\n");
	}
	return str.toString();
    }
}

class Cube extends Orientation {

    Cube(int dimensions) {
	super(dimensions);
    }

    // Can't nest an arbitrary number of loops, 
    // so use recursion to generate points and lines.
    // Points and lines will be relative to this orientation

    void generate(double[] coordinates, int dimension, Body body) {
	create(coordinates, dimension, -1, body);
	create(coordinates, dimension, +1, body);
    }

    void create(double[] coordinates, int dimension, int sign, Body body) {
	coordinates[dimension] = sign;
	
	if (dimension < coordinates.length - 1) {
	    generate(coordinates, ++dimension, body);
	}else{

	    //Point point = new Point(coordinates);
	    //point.relative(this);
	    //System.out.println(point);
	    //body.add(point);
	    //Sphere sphere = new Sphere(point, 0.15 / getSize());
	    //body.add(sphere);

	    line(coordinates, body);
	}
    }

    void line(double[] coordinates, Body body) {
	double[] coordinates2 = new double[coordinates.length];
	for (int d = 0; d < coordinates.length; d++) {
	    if (coordinates[d] < 0) { // No double lines
		System.arraycopy(coordinates, 0, 
				 coordinates2, 0, coordinates.length);
		coordinates2[d] = -coordinates[d];
		Point point1 = new Point(coordinates);
		Point point2 = new Point(coordinates2);
		point1.relative(this);
		point2.relative(this);
		Line line = new Line(point1, point2);
		//System.out.println(line);
		line.setColor(colorType);
		body.add(line);
	    }
	}
    }
}    

// Alternative way of drawing a cube.
class Cross extends Cube {

    Cross(int dimensions) {
	super(dimensions);
    }

    void line(double[] coordinates, Body body) {
	double[] coordinates2 = new double[coordinates.length];
	for (int d = 0; d < coordinates.length; d++) {
	    if (coordinates[d] < 0) { // No double lines
		System.arraycopy(coordinates, 0, 
				 coordinates2, 0, coordinates.length);
		for (int d2 = 0; d2 < coordinates.length; d2++) {
		    coordinates2[d2] = -coordinates[d2];
		}
		Point point1 = new Point(coordinates);
		Point point2 = new Point(coordinates2);
		point1.relative(this);
		point2.relative(this);
		Line line = new Line(point1, point2);
		//System.out.println(line);
		line.setColor(colorType);
		body.add(line);
	    }
	}
    }
}

// Windows and targets on cubes.
class Spot extends Body {

    int dimensions;

    double size;
    Orientation orientation;
    int dimension;
    int side;
    Point point;

    Spot(Orientation orientation, double size, int dimension, int side) {
	this.orientation = orientation; // cube orientation
	this.size = size;
	add(orientation);
	this.dimensions = orientation.dimensions;
	random(dimension, side);
    }

    Spot(Orientation orientation, double size, int dimension, int side,
	 Point point) {
	this.orientation = orientation;
	this.size = size;
	this.dimension = dimension;
	this.side = side;
	add(orientation);

	this.point = point.copyPoint();
	this.point.transfigure(dimension);

	this.dimensions = orientation.dimensions;
    }

    void random(int dimension, int side) {
	this.dimension = dimension;
	this.side = side;

	point = new Point(dimensions - 1);
	for (int d = 0; d < point.getDimensions(); d++) {
	    point.sum(d, Math.random() / (2 - size) - (1 - 2 * size));
	}
    }

    boolean inside(Point point) {
	Point center = this.point.copyPoint();
	center.transfigure(dimension, side);
	center.relative(orientation);

	boolean inside = true;
	for (int d = 0; d < point.getDimensions(); d++) {
	    if (Math.abs(point.getCoordinate(d) - center.getCoordinate(d)) >
		size) {
		inside = false;
	    }
	}
	return inside;
    }
}

// The cursor can move through this window.
// Note: the ball bounces off it.
class Window extends Spot {

    Window(Orientation orientation, double size, int dimension, int side) {
	super(orientation, size, dimension, side);
	colorType = ColorScheme.WINDOW;
    }

    public Spatial getRelative(Orientation orientation) {
	Body body = new Body();

	//Point point = this.point.copyPoint();
	//point.transfigure(dimension, side);
	//point.relative(this.orientation);
	//body.add(point.project(orientation));

   	Cube cube = new Cube(dimensions - 1);
	cube.multiplyRelative(1/size);
	cube.setColor(colorType);
	Body body2 = new Body();
	double[] coordinates = new double[dimensions - 1];
	cube.generate(coordinates, 0, body2);
   	body2.sum(this.point);
 	body2.transfigure(dimension, side);
 	body2.relative(this.orientation);
 	body.add(body2.getRelative(orientation));

	return (Spatial)body;
    }
}

// Target for the ball.
class Target extends Spot {

    boolean enabled;
    Space space;

    Target(Orientation orientation, double size, int dimension, int side, Space space) {
	super(orientation, size, dimension, side);
	enabled = true;
	this.space = space;
    }

    Target(Orientation orientation, double size, int dimension, int side, Point point, Space space) {
	super(orientation, size, dimension, side, point);
	this.space = space;
    }

    void random(int dimension, int side) {
	super.random(dimension, side);
	enabled = true;
    }

    boolean inside(Point point) {
	if (!enabled) {
	    return false;
	}

	boolean returnValue = super.inside(point);

	if (returnValue) {

	    //System.out.println("Ball hit target!");

	    space.hit.play();
	    if (space.score.hit()) {
		space.gameover.play();
	    }

	    enabled = false;
	}

	return returnValue;
    }

    public void disable() {
	enabled = false;
    }

    public Spatial getRelative(Orientation orientation) {
	Body body = new Body();

	//Point point = this.point.copyPoint();
	//point.transfigure(dimension, side);
	//point.relative(this.orientation);
	//body.add(point.project(orientation));

   	Cross cross = new Cross(dimensions - 1);
	cross.multiplyRelative(1/size);

	if (enabled) {
	    cross.setColor(ColorScheme.TARGET);
	    
	}else{
	    cross.setColor(ColorScheme.DISABLED);
	}

	Body lines = new Body();
	double[] coordinates = new double[dimensions - 1];
	cross.generate(coordinates, 0, lines);
	lines.sum(this.point);
	lines.transfigure(dimension, side);
	lines.relative(this.orientation);
	body.add(lines.getRelative(orientation));

	return (Spatial)body;
    }
}

// The Ball gets kicked by the Cursor, bounces off the cube and the
// cursor.
class Ball extends Sphere {

    Point movement;

    boolean drawLines = false;
    Point bouncePoint;
    Point centerPoint;
    Body centerLines;
    Body bounceLines;
    Body counterLines;
    Body bounceCrosses;

    Space space;

    Ball(Point point, double size, Space space) {
	super(point, size);
	initBodies();

	movement = new Point(space.dimensions);

	this.space = space;
    }

    private void initBodies() {
	centerLines = new Body();
	bounceLines = new Body();
	counterLines = new Body();
	bounceCrosses = new Body();
    }

    boolean kick(Point oldPosition, Point newPosition) {
	if (distance(newPosition) < size) {
	    movement = difference(newPosition);
	    // Kick with constant force
	    movement.multiply(2 * 0.05 / movement.distance());
	    // Variabe force: oldPosition.distance(newPosition)
	    
	    centerPoint = copyPoint();
	    bouncePoint = copyPoint();
	    initBodies();
	    
	    Line line = new Line(oldPosition.copyPoint(), this.copyPoint());
	    line.setColor(ColorScheme.BOUNCE);
	    centerLines.add(line);

	    drawLines = true;

	    return true;
	}else{
	    return false;
	}
    }

    void update(Orientation cube, Cursor cursor, Target target) {
	Point oldPosition = copyPoint();
	Point newPosition = copyPoint();

	newPosition.sum(movement);

	// Bounce of a side or the cursor, whichever is closest.
	Point relative = cube.relative(newPosition);
	Point centerPoint = null; // center of ball when it bounces
	Point bouncePoint = null; // where the ball touches when it bounces
 	Target bounceCross = null;
	
	double distance;
	double minimumDistance = 1;
	// Cube
	for (int d = 0; d < coordinates.length; d++) {
	    distance = Math.abs(1 - Math.abs(relative.coordinates[d]));
	    if (distance < size) {
		if (distance < minimumDistance) {
		    minimumDistance = distance;
		    bouncePoint = relative.copyPoint();
		    bouncePoint.coordinates[d] = 
			Math.round(bouncePoint.coordinates[d]); 
		    Orientation orientation = 
			new Orientation(coordinates.length);
		    bouncePoint = orientation.relative(bouncePoint);
		    centerPoint = relative.copyPoint();
		    int side = (int)bouncePoint.coordinates[d];
		    bounceCross = new Target(cube, 0.05, d, side, bouncePoint, space);
		}
	    }
	}

	// Cursor
	Point cursorPosition = 
	    cursor.copyOrientation().relative(new Point(space.dimensions));
	distance = newPosition.distance(cursorPosition);
	if (distance < size) {
	    if (distance < minimumDistance) {
		minimumDistance = distance;
		bouncePoint = cursorPosition.copyPoint();
		centerPoint = relative.copyPoint();
	    }
	}

	if (bouncePoint != null) {
	    Point counterMovement = centerPoint.difference(bouncePoint);
	    Line line = new Line(bouncePoint, centerPoint);
	    line.setColor(ColorScheme.BOUNCE);
	    counterLines.add(line);
	    double parameter = line.relative(this);
	    Point projectedPoint = line.point(parameter);
	    double difference = centerPoint.distance(projectedPoint);
	    
	    counterMovement.multiply(2 * difference / 
				     counterMovement.distance());
	    movement.sum(counterMovement);
	    
	    Line centerLine = new Line(this.centerPoint, centerPoint);
	    Line bounceLine = new Line(this.bouncePoint, bouncePoint);
	    centerLine.setColor(ColorScheme.TRACK);
	    bounceLine.setColor(ColorScheme.TRACK);
	    centerLines.add(centerLine);
	    bounceLines.add(bounceLine);
	    this.centerPoint = centerPoint;
	    this.bouncePoint = bouncePoint;
	    if (bounceCross != null) {
		bounceCross.setColor(ColorScheme.NORMAL);
		bounceCrosses.add(bounceCross);

		if (target.inside(bouncePoint))
		    {
			//System.out.println("Ball hit (possibly disabled) target");
		    }
	    }

	    space.bounce.play();

	}
		
	sum(movement);
	movement.multiply(0.98); // decrease speed
    }

    void dontDrawLines() {
	drawLines = false;
    }

    public Spatial getRelative(Orientation orientation) {
	Body body = new Body();
	
	if (drawLines) {
	    Line line = new Line(centerPoint, this.copyPoint());
	    //Line line = new Line(bouncePoint, this.copyPoint());
	    line.setColor(ColorScheme.BOUNCE);
	    body.add(line.getRelative(orientation));
  
	    //body.add(bounceLines.getRelative(orientation));
	    body.add(centerLines.getRelative(orientation));
	    //body.add(bounceCrosses.getRelative(orientation));
	    body.add(counterLines.getRelative(orientation));
	}

	body.add(super.getRelative(orientation));

	return (Spatial)body;
    }
}

// Walk around the cube and catch the cursor.
class Guard extends Sphere {

    int steps = 100;
    double range = Space.MAXCOORDINATE;
    int step = 0;
    int dimension = 0;
    int direction = -1;

    Line line;
    boolean drawLine = false;

    Space space;

    Guard(Point point, double size, Space space) {
	super(point, size);
	this.space = space;
    }

    void update(Cursor cursor) {
	boolean remDrawLine = drawLine;

	step = ++step;
	if (step > steps)
	    {
		step = 0;
		if (dimension == 0) {
		    dimension = 1;
		}else{
		    dimension = 0;
		    if (direction == -1) {
			direction = 1;
		    }else{
			direction = -1;
		    }
		}
	    }

	this.coordinates[dimension] = (1 - 2 * dimension) * direction * range;
	this.coordinates[1 - dimension] = 
	    direction * range - step * 2 * range * direction / steps;

	Point point = 
	    cursor.copyOrientation().relative(new Point(getDimensions()));

	line = new Line(point, this);
	line.setColor(ColorScheme.ERROR);
	
	remDrawLine = drawLine;
	drawLine = false;
	for (int d = 0; d < getDimensions(); d++)
	    {
		if (((this.getCoordinate(d) > 1) && 
		     (point.getCoordinate(d) > 1)) ||
		    ((this.getCoordinate(d) < -1) && 
		     (point.getCoordinate(d) < -1))) {
		    drawLine = true;
		    if (!remDrawLine) {
			space.gotcha.play();
		    }
		    space.score.gotcha();
		}
	    }
    }

    public Spatial getRelative(Orientation orientation) {
	Body body = new Body();
	
	if (drawLine) {
	    body.add(line.getRelative(orientation));
	}

	body.add(super.getRelative(orientation));

	return (Spatial)body;
    }
}

// Player position.
class Cursor extends Orientation {

    final double size = 0.1;

    // Cursor tail on window-pass/cube-bump
    boolean drawTail = true;
    boolean lastChangeIsMove = false;
    double lastMove = 0;
    Point arrowTail, arrowHead, newTail;
    Line arrow;

    Cursor(int dimensions) {
	super(dimensions);
	arrowTail = copyOrientation().relative(new Point(dimensions));
	arrowHead = copyOrientation().relative(new Point(dimensions));
	newTail = copyOrientation().relative(new Point(dimensions));
	arrow = new Line(arrowTail, arrowHead);
    }

    Cursor(Point[] points) {
	super(points);
    }

    Cursor copyCursor() {
	return new Cursor(copyPoints());
    }

    // Move forward/backwards, as viewed from a distance.
    void move(double value, double viewDistance) {

	if (viewDistance == 0) { // dimensions < 3
	    sum(0, value);
	}else{
	    Orientation orientation = copyOrientation();
	    orientation.sum(0, viewDistance);
	    Point direction = orientation.copyPoints()[0];
	    sum(direction, value);
	}

	if ((lastMove >= 0 && value < 0) ||
	    (lastMove <= 0 && value > 0)) {
	    changeTail();
	}
	if (!lastChangeIsMove) {
	    drawTail = false;
	}
	lastMove = value;
	lastChangeIsMove = true;
    }

    public void relative(Orientation orientation) {
	changeTail();
	super.relative(orientation);
    }

    void changeTail() {
	if (lastChangeIsMove) {
	    newTail = copyOrientation().relative(new Point(dimensions));
	    lastChangeIsMove = false;
	}
    }
	
    void setArrow() {
	arrowTail = newTail;
	arrowHead = copyOrientation().relative(new Point(dimensions));
	arrow = new Line(arrowTail, arrowHead);
	arrow.setColor(ColorScheme.OK);
	drawTail = true;
    }

    void setArrowColor(int colorType) {
	arrow.setColor(colorType);
    }

    public Spatial getRelative(Orientation orientation) {
	Body body = new Body();
	Point point = copyOrientation().relative(new Point(dimensions));

  	Plus plus = new Plus(point, 0.1);
	plus.setColor(colorType);
	body.add(plus.getRelative(orientation));

	if (drawTail) {
	    //Sphere head = new Sphere(arrowHead, 0.05);
	    //body.add(head.getRelative(orientation));
	    body.add(arrow.getRelative(orientation));
	}

	return (Spatial)body;
    }

    public void draw(Orientation orientation, ColorScheme colorScheme,
		     Graphics graphics)
    {
	Point point = copyOrientation().relative(new Point(dimensions));
	Sphere sphere = new Sphere(point, 0.1);

	sphere.setColor(colorType);
	sphere.draw(orientation, colorScheme, graphics);
    }
}

class ColorScheme {

    // color scheme index

    static int BACKGROUND = 0;
    static int NORMAL = 1;
    static int ERROR = 2;
    static int OK = 3;
    static int MARK = 4;
    static int DISABLED = 5;
    static int WINDOW = 6;
    static int TARGET= 7;
    static int TRACK = 8;
    static int BOUNCE = 9;

    Color[] scheme = new Color[10];

    Color[] filter(int position, int green) {

	Color[] newScheme = new Color[10];

	// background
	newScheme[0] = new Color(scheme[0].getRed(), scheme[0].getGreen(), scheme[0].getBlue());

	// foreground
	switch (position) {
	case 0: // green or blue
	    for (int index = 1; index < 10; index++) {
		newScheme[index] =  new Color(0, scheme[index].getGreen(), scheme[index].getBlue());
	    }
	    break;
	case 2: // red
	    for (int index = 1; index < 10; index++) {
		newScheme[index] =  new Color(scheme[index].getRed(), scheme[index].getGreen() * green, 0);
	    }
	    break;
	    
	default:
	    // unaltered colors
	    newScheme = scheme;
	}
	
	return newScheme;
    }
}

class Score {

    int factor = 1000; // milliseconds
    int fps = 20; // frames per second

    int numHits = 5;

    int points = 100 * factor;
    int hits = 0;

    void update() {
	if (hits < numHits) {
	    points = points - 1 * factor / fps;
	}
    }

    void bump() {
	if (hits < numHits) {
	    points = points - 10 * factor;
	}
    }

    void gotcha() {
	if (hits < numHits) {
	    points = points - 5 * factor / fps;
	}
    }

    boolean hit() {
	if (hits < numHits) {
	    points = points + 100 * factor;
	    hits = hits + 1;
	    if (hits == numHits) {
		return true;
	    }
	}
	return false;
    }


    public String toString() {
	if (hits == numHits) {
	    return new String(hits + "/" + points / factor +" done.");
	}
	return new String(hits + "/" + points / factor);

    }

}

class Sound {

    AudioClip clip;
    boolean enabled = false;;

    Sound(AudioClip clip) {
	this.clip = clip;
    }


    void enable(boolean value) {
	enabled = value;
    }

    void play() {
	if (clip != null && enabled) {
	    clip.play();
	}
    }
}

public class Space extends Canvas implements ItemListener, Runnable{

    final static double MAXCOORDINATE = 1.25;

    int dimensions;

    Applet applet;
    Frame frame;
    Panel containerPanel;
    Panel controlPanel;
    Choice colorChoice;
    Choice stereoChoice;

    Checkbox detachBox, swapBox, soundBox;

    boolean stereo = true;
    int swap = -1;
    int notAnaglyph = 0;
    boolean detach = false;
    boolean sound = false;

    ColorScheme[] colorScheme, whiteScheme, blackScheme, grayScheme, greenScheme, blueScheme;

    boolean running = true;
    boolean focus = false;
    boolean pauzed = false;
    long lostFocusTime;

    Body[][] scene;
    Sphere playfield;
    Cursor cursor;
    Cube cube;
    Window window;
    Target target;
    Ball ball;
    Guard guard;

    double viewDistance = 2;
    double eyeDistance = 0.1;

    double cursorLock = 0;

    int lastX, lastY;
    double unit, unit2;

    Dimension offDimension;
    Image offImage;
    Graphics offGraphics;

    Sound bump, kick, bounce, pass, hit, gotcha, gameover;

    Score score = new Score();
    
    Space(int dimensions,  String stereoString, boolean paramSwap, boolean sound, final Applet applet) {

	this.dimensions = dimensions;
	this.sound = sound;
	if (paramSwap) {
	    swap = 1;
	}else{
	    swap = -1;
	}

	this.applet = applet;

	bump = new Sound(Global.bump);
	kick = new Sound(Global.kick);
	bounce = new Sound(Global.bounce);
	pass = new Sound(Global.pass);
	hit = new Sound(Global.hit);
	gotcha = new Sound(Global.gotcha);
	gameover = new Sound(Global.gameover);

	toggleSound(sound);

	cursor = new Cursor(dimensions);
	cube = new Cube(dimensions);
	window = new Window(cube, 0.2, 1, 1);
	target = new Target(cube, 0.3, 1, -1, this);

	ball = new Ball(new Point(dimensions), 0.3, this);
	for (int d = 0; d < dimensions; d++) {
	    ball.sum(d, -0.5);
	}

	guard = new Guard(new Point(dimensions), 0.1, this);
	guard.sum(0, -1.25);
	guard.sum(1, -1.25);

	Point score = new Point(dimensions);
	score.sum(dimensions - 1, -1.25);

	scene = new Body[dimensions + 1][3];
	scene[dimensions][1] = new Body();
	scene[dimensions][1].add(cursor);
	scene[dimensions][1].add(cube);
	scene[dimensions][1].add(window);
	scene[dimensions][1].add(target);
	scene[dimensions][1].add(ball);
	scene[dimensions][1].add(guard);

	playfield = 
	    new Sphere(new Point(dimensions),
		       Math.sqrt(dimensions * 
				 Math.pow(MAXCOORDINATE, 2)));

	//scene[dimensions][1].add(playfield);

	containerPanel = new Panel();
	containerPanel.setLayout(new BorderLayout());
	controlPanel = new Panel();
	controlPanel.setLayout(new FlowLayout());

	whiteScheme = new ColorScheme[3];
	whiteScheme[1] = new ColorScheme();
	whiteScheme[1].scheme[ColorScheme.BACKGROUND] = Color.white;
	whiteScheme[1].scheme[ColorScheme.NORMAL] = Color.black;
	whiteScheme[1].scheme[ColorScheme.ERROR] = Color.red;
	whiteScheme[1].scheme[ColorScheme.OK] = Color.green;
	whiteScheme[1].scheme[ColorScheme.MARK] = Color.blue;
	whiteScheme[1].scheme[ColorScheme.DISABLED] = Color.lightGray;
	whiteScheme[1].scheme[ColorScheme.WINDOW] = Color.green;
	whiteScheme[1].scheme[ColorScheme.TARGET] = Color.blue;
	whiteScheme[1].scheme[ColorScheme.TRACK] = Color.blue;
	whiteScheme[1].scheme[ColorScheme.BOUNCE] = Color.cyan;
	whiteScheme[0] = new ColorScheme();
	whiteScheme[2] = new ColorScheme();
	whiteScheme[0].scheme = whiteScheme[1].filter(0, 1);
	whiteScheme[2].scheme = whiteScheme[1].filter(2, 1);

	//unused
	blackScheme = new ColorScheme[3];
	blackScheme[1] = new ColorScheme();
	blackScheme[1].scheme[ColorScheme.BACKGROUND] = Color.black;
	blackScheme[1].scheme[ColorScheme.NORMAL] = Color.lightGray;
	blackScheme[1].scheme[ColorScheme.ERROR] = Color.red;
	blackScheme[1].scheme[ColorScheme.OK] = Color.green;
	blackScheme[1].scheme[ColorScheme.MARK] = Color.yellow;
	blackScheme[1].scheme[ColorScheme.DISABLED] = Color.gray;
	blackScheme[1].scheme[ColorScheme.WINDOW] = Color.lightGray;
	blackScheme[1].scheme[ColorScheme.TARGET] = Color.yellow;
	blackScheme[1].scheme[ColorScheme.TRACK] = Color.gray;
	blackScheme[1].scheme[ColorScheme.BOUNCE] = Color.lightGray;
	blackScheme[0] = new ColorScheme();
	blackScheme[2] = new ColorScheme();
	blackScheme[0].scheme = blackScheme[1].filter(0, 1);
	blackScheme[2].scheme = blackScheme[1].filter(2, 1);

	// Unused
	grayScheme = new ColorScheme[3];
	grayScheme[1] = new ColorScheme();
	grayScheme[1].scheme[ColorScheme.BACKGROUND] = Color.lightGray;
	grayScheme[1].scheme[ColorScheme.NORMAL] = Color.black;
	grayScheme[1].scheme[ColorScheme.ERROR] = Color.red;
	grayScheme[1].scheme[ColorScheme.OK] = Color.yellow;
	grayScheme[1].scheme[ColorScheme.MARK] = Color.white;
	grayScheme[1].scheme[ColorScheme.DISABLED] = Color.gray;
	grayScheme[1].scheme[ColorScheme.WINDOW] = Color.black;
	grayScheme[1].scheme[ColorScheme.TARGET] = Color.black;
	grayScheme[1].scheme[ColorScheme.TRACK] = Color.white;
	grayScheme[1].scheme[ColorScheme.BOUNCE] = Color.yellow;
	grayScheme[0] = new ColorScheme();
	grayScheme[2] = new ColorScheme();
	grayScheme[0].scheme = grayScheme[1].filter(0, 1);
	grayScheme[2].scheme = grayScheme[1].filter(2, 1);

	// Anaglyphs can be monochrome and colored:
	// http://en.wikipedia.org/wiki/Anaglyph_image
	// If it can be done by using primitive graphics animation too,
	// remains to be seen...
	// For now I copied the colors from
	// http://dogfeathers.com/java/hyprcube.html
	// but unlike Mark Newbold, I like red/green glasses best.

	Color background = new Color(222, 222, 222);
	Color foreground = new Color (255, 239, 0);
	greenScheme = new ColorScheme[3];
	greenScheme[1] = new ColorScheme();
	greenScheme[1].scheme[ColorScheme.BACKGROUND] = background;
	greenScheme[1].scheme[ColorScheme.NORMAL] = foreground;
	greenScheme[1].scheme[ColorScheme.ERROR] = foreground;
	greenScheme[1].scheme[ColorScheme.OK] = foreground;
	greenScheme[1].scheme[ColorScheme.MARK] = foreground;
	greenScheme[1].scheme[ColorScheme.DISABLED] = foreground;
	greenScheme[1].scheme[ColorScheme.WINDOW] = foreground;
	greenScheme[1].scheme[ColorScheme.TARGET] = foreground;
	greenScheme[1].scheme[ColorScheme.TRACK] = foreground;
	greenScheme[1].scheme[ColorScheme.BOUNCE] = foreground;
	greenScheme[0] = new ColorScheme();
	greenScheme[2] = new ColorScheme();
	greenScheme[0].scheme = greenScheme[1].filter(swap * 1 + 1, 0);
	greenScheme[2].scheme = greenScheme[1].filter(swap * -1 + 1, 0);

	background = new Color(0, 84, 0);
	foreground = new Color (188, 84, 255);
	blueScheme = new ColorScheme[3];
	blueScheme[1] = new ColorScheme();
	blueScheme[1].scheme[ColorScheme.BACKGROUND] = background;
	blueScheme[1].scheme[ColorScheme.NORMAL] = foreground;
	blueScheme[1].scheme[ColorScheme.ERROR] = foreground;
	blueScheme[1].scheme[ColorScheme.OK] = foreground;
	blueScheme[1].scheme[ColorScheme.MARK] = foreground;
	blueScheme[1].scheme[ColorScheme.DISABLED] = foreground;
	blueScheme[1].scheme[ColorScheme.WINDOW] = foreground;
	blueScheme[1].scheme[ColorScheme.TARGET] = foreground;
	blueScheme[1].scheme[ColorScheme.TRACK] = foreground;
	blueScheme[1].scheme[ColorScheme.BOUNCE] = foreground;
	blueScheme[0] = new ColorScheme();
	blueScheme[2] = new ColorScheme();
	blueScheme[0].scheme = blueScheme[1].filter(swap * -1 + 1, 1);
	blueScheme[2].scheme = blueScheme[1].filter(swap * 1 + 1, 1);

	colorScheme = whiteScheme;

	// Unused
	colorChoice = new Choice();
	colorChoice.addItem("white");
	colorChoice.addItem("black");
	colorChoice.addItem("gray");
	colorChoice.addItem("white");
	colorChoice.addItem("green");
	colorChoice.addItem("blue");
	colorChoice.select("white");
	//controlPanel.add(colorChoice);
	colorChoice.addItemListener((ItemListener)this);

	stereoSelect(stereoString);
	stereoChoice = new Choice();
	stereoChoice.addItem("normal");
	stereoChoice.addItem("cross eyed");
 	stereoChoice.addItem("anaglyph green");
 	stereoChoice.addItem("anaglyph blue");
	stereoChoice.select(stereoString);
	controlPanel.add(stereoChoice);
	stereoChoice.addItemListener((ItemListener)this);

	swapBox = new Checkbox("swap");
	swapBox.setState(false);
	controlPanel.add(swapBox);
	swapBox.addItemListener((ItemListener)this);

	soundBox = new Checkbox("sound");
	soundBox.setState(sound);
	controlPanel.add(soundBox);
	soundBox.addItemListener((ItemListener)this);

	containerPanel.add("Center", this);
	containerPanel.add("North", controlPanel);

	frame = new Frame(new String(dimensions + "d-Cube"));
	frame.setSize(600, 400);

	frame.addWindowListener(new WindowAdapter() {
		public void windowClosing(WindowEvent e) {
		    if (applet == null) {
			running = false;
			resume();
			if (Global.applet == null){
			    System.exit(0); 
			}
		    }else{
			toggleDetached();
		    }
		}
	    });
	
	if (applet != null) {
	    applet.removeAll();
	    detachBox = new Checkbox("detach");
	    controlPanel.add(detachBox);
	    detachBox.addItemListener((ItemListener)this);
	    applet.setLayout(new BorderLayout());
	    applet.add("Center", containerPanel);
	}else{
	    frame.add(containerPanel);
	    frame.show();
	}

	addKeyListener(new KeyAdapter() {
		public void keyPressed(KeyEvent e) {
		    //System.out.println(e);
		    keyPress(e);
		}
	    });
	
	addKeyListener(new KeyAdapter() {
		public void keyReleased(KeyEvent e) {
		    //System.out.println(e);
		    keyRelease(e);
		}
	    });
	
	addMouseListener(new MouseAdapter() {

		public void mousePressed(MouseEvent e) {
		    //System.out.println(e);
		    requestFocus();
		    mousePress(e);
		}
	    });

	addMouseListener(new MouseAdapter() {

		public void mouseReleased(MouseEvent e) {
		    //System.out.println(e);
		    requestFocus();
		    mouseRelease(e);
		}
	    });

	addMouseMotionListener(new MouseMotionAdapter() {
		public void mouseDragged(MouseEvent e) {
		    //System.out.println(e);
		    mouseDrag(e);
		}
	    });

	addFocusListener(new FocusAdapter() {
		public void focusGained(FocusEvent e) {
		    //System.out.println(e);
		    focus = true;
		    resume();
		}
		public void focusLost(FocusEvent e) {
		    //System.out.println(e);
		    focus = false;
		    lostFocusTime = System.currentTimeMillis();
		}	    

	    });

	// Click to start when embedded.
	if (applet == null) {
	    requestFocus();
	}

	new Thread(this).start();

	gameover.play();

    }

    synchronized void resume(){
	notifyAll();
    }

    synchronized void pauze() {

	while ((!focus || pauzed) && running) {
	    try{
		wait();
	    }catch(InterruptedException e){}
    	}
    }

    public void run() {

	long previousTimer, timer = System.currentTimeMillis(),
	runTime, frameTime = 50, sleepTime = 0;

	running = true;
	while(running) {

	    previousTimer = timer;
	    timer = System.currentTimeMillis();
	    runTime = timer - previousTimer - sleepTime;
	    sleepTime = Math.max(0, frameTime - runTime);
	    //System.out.println(runTime + " " + sleepTime);
	    try {
		// Pauze on applet-stop and lost-focus-with-delay
		if ((!focus && (timer - lostFocusTime > 10000)) || pauzed) {
		    pauze();
		}else{
		    Thread.sleep(sleepTime);
		}
	    }catch(InterruptedException e){}

	    step();
	    repaint();
	}

	frame.dispose();
	//System.out.println("Thread stopped.");
    }

    void stereoSelect(String str) {

	if (str.equals("normal")){
	    stereo = false;
	}else{
	    stereo = true;
	}

	if (str.indexOf("anaglyph") != -1) {
	    notAnaglyph = 0;

	    if (str.indexOf("green") != -1) {
		colorScheme = greenScheme;
	    }

	    if (str.indexOf("blue") != -1) {
		colorScheme = blueScheme;
	    }

	}else{
	    notAnaglyph = 1;
	    colorScheme = whiteScheme;
	}

	

    }

    public void itemStateChanged(ItemEvent e) {

	//System.out.println(e);

	Object source = e.getItemSelectable();


	// unused
	if (source == colorChoice) {
	    if (colorChoice.getSelectedItem().equals("white")) {
		colorScheme = whiteScheme;
	    }
	    if (colorChoice.getSelectedItem().equals("black")) {
		colorScheme = blackScheme;
	    }
	    if (colorChoice.getSelectedItem().equals("gray")) {
		colorScheme = grayScheme;
	    }
	    if (colorChoice.getSelectedItem().equals("green")) {
		colorScheme = greenScheme;
	    }
	    if (colorChoice.getSelectedItem().equals("blue")) {
		colorScheme = blueScheme;
	    }

	}

	if (source == stereoChoice) {
	    stereoSelect(stereoChoice.getSelectedItem());
	}


	if (source == swapBox) {

	    if (e.getStateChange() == ItemEvent.DESELECTED) {
		swap = -1;
	    }
	    if (e.getStateChange() == ItemEvent.SELECTED) {
		swap = 1;
	    }

	    greenScheme[0].scheme = greenScheme[1].filter(swap * 1 + 1, 0);
	    greenScheme[2].scheme = greenScheme[1].filter(swap * -1 + 1, 0);

	    blueScheme[0].scheme = blueScheme[1].filter(swap * -1 + 1, 1);
	    blueScheme[2].scheme = blueScheme[1].filter(swap * 1 + 1, 1);
	    
	}

	if (source == soundBox) {

	    if (e.getStateChange() == ItemEvent.DESELECTED) {
		sound = false;
	    }
	    if (e.getStateChange() == ItemEvent.SELECTED) {
		sound = true;
	    }
	    
	    toggleSound(sound);

	}

	if (source == detachBox) {
	    toggleDetached();
	}

	repaint();
    }

    void mousePress(MouseEvent e) {
	lastX = e.getX();
	lastY = e.getY();
    }

    // Angle of a mousemove that takes a stereoscopic canvas into account
    double angle(MouseEvent e) {
	Dimension dimension = getSize();
	int x = e.getX();
	int y = e.getY();

	int x1 = lastX;
	int x2 = x;
	double width = dimension.width;
	if (stereo && notAnaglyph == 1) {
	    width /= 2;
	    if (x > width) {
		x1 -= width;
		x2 -= width;
	    }
	}
	double[] coordinates = new double[2];
	coordinates[0] = x1 - width / 2;
	coordinates[1]= lastY - dimension.height / 2;
	Point point1 = new Point(coordinates);
	double angle1 = point1.angle(0, 1);
	coordinates[0] = x2 - width / 2;
	coordinates[1]= y - dimension.height / 2;
	Point point2 = new Point(coordinates);
	double angle2 = point2.angle(0, 1);

	return angle2 - angle1;
    }

    void mouseDrag(MouseEvent e) {
	Dimension dimension = getSize();

	double x = (e.getX() - lastX) / unit * Math.PI / 2;
	double y = (e.getY() - lastY) / unit * Math.PI / 2;
	double angle = angle(e);

	Orientation orientation = new Orientation(dimensions);

	int getModResult = 0;
	try {
	    getModResult = ((Integer)Global.getMod.invoke(e, new Object[0])).intValue();
	}catch(Exception e2){
	    e2.printStackTrace();
	}

	if ((e.getModifiers() & InputEvent.SHIFT_MASK) != 0) {
	    move(-(e.getY() - lastY) / unit2);
	}else if ((getModResult & Global.button2) != 0) {
	    if (dimensions > 3) {
		//orientation.rotate(0, 3, angle);
		orientation.rotate(0, 3, -y);
	    }
	    cursor.relative(orientation);
	}else if ((getModResult & Global.button3) != 0) {
	    if (dimensions > 3) {
		orientation.rotate(3, 1, -x);
		orientation.rotate(3, 2, y);
	    }else{
		orientation.rotate(1, 2, angle);
	    }
	    cursor.relative(orientation);
	}else{
	    if (dimensions > 2)
		{
		    orientation.rotate(0, 1, -x);
		    orientation.rotate(0, 2, y);
		}else{
		    orientation.rotate(0, 1, angle);
		}
	    cursor.relative(orientation);
	}

	lastX = e.getX();
	lastY = e.getY();

	repaint();
    }

    void mouseRelease(MouseEvent e) {
	cursorLock = 0;
    }

    void keyPress(KeyEvent e) {

	Orientation orientation = new Orientation(dimensions);
	int dimension1 = 0;
	int dimension2 = 0;
	double value = 0;

	if ((e.getModifiers() & InputEvent.SHIFT_MASK) != 0) {
	    value = 0.05;
	    switch (e.getKeyCode()) {		    
	    case KeyEvent.VK_UP:
		break;
	    case KeyEvent.VK_DOWN:
		value = -value;
		break;
	    default:
		value = 0;
	    }
	    move(value);
	} else if (((e.getModifiers() & InputEvent.META_MASK) != 0) ||
		   ((e.getModifiers() & InputEvent.ALT_MASK) != 0)) {
	    value = -2 * Math.PI / 128;
	    dimension1 = 0;
	    dimension2 = 3;
	    switch (e.getKeyCode()) {
	    case KeyEvent.VK_LEFT:
		value = -value;
	    case KeyEvent.VK_RIGHT:
		break;
	    case KeyEvent.VK_UP:
		value = -value;
	    case KeyEvent.VK_DOWN:
		break;
	    default:
		value = 0;
	    }
	    if (dimensions > 3) {
		orientation.rotate(dimension1, dimension2, value);
		cursor.relative(orientation);
	    }
	}else if ((e.getModifiers() & InputEvent.CTRL_MASK) != 0) {
	    value = - 2 * Math.PI / 128;
	    dimension1 = dimensions - 1;
	    switch (e.getKeyCode()) {
	    case KeyEvent.VK_LEFT:
		value = -value;
	    case KeyEvent.VK_RIGHT:
		dimension2 = Math.min(1, dimensions - 2);
		break;
	    case KeyEvent.VK_DOWN:
		value = -value;
	    case KeyEvent.VK_UP:
		dimension2 = Math.min(2, dimensions - 2);
		break;
	    default:
		value = 0;
	    }
	    orientation.rotate(dimension1, dimension2, value);
	    cursor.relative(orientation);
	    
	}else{
	    value = -2 * Math.PI / 128;
	    switch (e.getKeyCode()) {
	    case KeyEvent.VK_LEFT:
		value = -value;
	    case KeyEvent.VK_RIGHT:
		dimension2 = 1;
		if (dimensions == 2) {
		    value = - value;
		}
		break;
	    case KeyEvent.VK_DOWN:
		value = -value;
	    case KeyEvent.VK_UP:
		dimension2 = 2;
		if (dimensions == 2) {
		    value = -value;
		    dimension2 = 1;
		}
		break;
	    case KeyEvent.VK_PAGE_UP:
		viewDistance = viewDistance - 0.02;
		if (viewDistance < MAXCOORDINATE) {
		    viewDistance = MAXCOORDINATE;
		}
		value = 0;
		break;
	    case KeyEvent.VK_PAGE_DOWN:
		viewDistance = viewDistance + 0.02;
		value = 0;
		break;
	    case KeyEvent.VK_HOME:
		eyeDistance = eyeDistance - 0.002;
		if (eyeDistance < 0) {
		    eyeDistance = 0;
		}
		value = 0;
		//System.out.println(eyeDistance);
		break;
	    case KeyEvent.VK_END:
		eyeDistance = eyeDistance + 0.002;
		value = 0;
		//System.out.println(eyeDistance);
		break;
	    default:
		value = 0;
	    }
	    orientation.rotate(dimension1, dimension2, value);
	    cursor.relative(orientation);
	}
	repaint();
    }

    void toggleSound(boolean sound) {

	bump.enable(sound);
	kick.enable(sound);
	bounce.enable(sound);
	pass.enable(sound);
	gotcha.enable(sound);
	hit.enable(sound);
	gameover.enable(sound);
    }
    
    void toggleDetached() {
	if (detach) {
	    applet.setLayout(new BorderLayout());
	    applet.add("Center", containerPanel);
	    applet.validate();
	    applet.doLayout();
	    frame.dispose();
	    detach = false;
	}else{
	    applet.remove(containerPanel);
	    frame.add(containerPanel);	    
	    frame.show();
	    detach = true;
	}
	detachBox.setState(detach);
    }

    void keyRelease(KeyEvent e) {
	if (e.getKeyCode() == KeyEvent.VK_SHIFT) {
	    cursorLock = 0;
	}
    }

    public Dimension getPreferredSize() {
	return getParent().getSize();
    }

    public void paint(Graphics graphics) {
	update(graphics);
    }

    void move(double value) {

	if ((value > 0 && cursorLock < 0) ||
	    (value < 0 && cursorLock > 0)) {
	    cursorLock = 0;
	}

	if (cursorLock != 0 || value == 0.0) {
	    return;
	}

	cursor.setColor(ColorScheme.NORMAL);

	ball.dontDrawLines();

// 	if (Math.abs(value) > 0.05) {
// 	    System.out.println("too fast: " + value);
// 	}
	    
	if (value > 0.05) {
	    value = 0.05;
	}
	if (value < -0.05) {
	    value = -0.05;
	}

	Cursor scratch = cursor.copyCursor();
	Point oldPosition = 
	    scratch.copyOrientation().relative(new 
					       Point(dimensions));
	double perspective = viewDistance * playfield.getSize();
	if (dimensions < 3) {
	    perspective = 0;
	}
    
	scratch.move(value, perspective);

	Point newPosition = 
	    scratch.copyOrientation().relative(new 
					       Point(dimensions));

	if (newPosition.distance() > playfield.getSize()) {
	    //System.out.println("Too far! (Playfield boundary)");
	    cursor.setColor(ColorScheme.MARK);
	    cursorLock = value;
	}

	if (cube.inside(oldPosition) != cube.inside(newPosition)) {

	    cursor.setArrow();

	    if (window.inside(oldPosition)) {
		//System.out.println("Passed window");

		cursor.setArrowColor(ColorScheme.OK);

		pass.play();

		// Move window and target (if it is disabled).
		int dimension, side;
		do {
		    dimension = (int)(Math.random() * dimensions);
		    side = (int)(Math.round(Math.random()) * 2 - 1);
		}while ((dimension == window.dimension && 
			 side == window.side) ||
			(dimension == target.dimension && 
			 side == target.side));
			
		int oldDimension = window.dimension;
		int oldSide = window.side;
		window.random(dimension, side);

		if (!target.enabled) {
		    do {
			dimension = 
			    (int)(Math.random() * dimensions);
			side = (int)(Math.round(Math.random()) * 2 - 1);
		    }while ((dimension == oldDimension && side == oldSide) ||
			    (dimension == window.dimension && 
			     side == window.side) ||
			    (dimension == target.dimension && 
			     side == target.side));

		    target.random(dimension, side);
		}

	    }else{
		//System.out.println("Ouch! (Cube boundary)");
		cursor.setColor(ColorScheme.ERROR);
		cursor.setArrowColor(ColorScheme.ERROR);
		cursorLock = value;

		bump.play();
		score.bump();

	    }
	}

	if (ball.kick(oldPosition, newPosition)) {
	    //System.out.println("Kick! (Ball boundary)");
	    cursor.setColor(ColorScheme.MARK);
	    cursorLock = value;

	    kick.play();

	}

	if (cursorLock == 0) {
	    cursor.move(value, perspective);
	}

    }

    public void step() {
	ball.update(cube, cursor, target);
	guard.update(cursor);
	score.update();

    }

    public void update(Graphics graphics) {

	Dimension dimension = getSize();
	Color color = null;

	// Create the offscreen graphics context, if no good one exists.
	if ((offGraphics == null) || 
	    (dimension.width != offDimension.width) || 
	    (dimension.height != offDimension.height)) {
	    offDimension = dimension;
	    offImage = createImage(dimension.width, dimension.height);
	    offGraphics = offImage.getGraphics();
	}

	offGraphics.setColor(colorScheme[notAnaglyph * 1].scheme[ColorScheme.BACKGROUND]);
	//offGraphics.setColor(colorScheme[1].scheme[ColorScheme.BACKGROUND]);

	offGraphics.fillRect(0, 0, dimension.width, dimension.height);

	Orientation orientation = new Orientation(dimensions);
	Orientation cursorOrientation = cursor.copyOrientation();
	orientation.sum(cursorOrientation.copyPoints()[0]);
	orientation.relative(cursorOrientation);

	Sphere playfield = 
	    new Sphere(new Point(dimensions), 
		       Math.sqrt(dimensions * 
				 Math.pow(MAXCOORDINATE, 2)));

	double playfieldSize = playfield.getSize(); // for unit2
	Sphere[] playfieldY = new Sphere[3];

	int maxY = 1;
	if (!stereo) {
	    maxY = 0;
	}

	if (dimensions == 2) {
	    // Don't project, just add relative image to scene.
	    for (int y = -1 * maxY; y <= 1 * maxY; y++) {
		scene[dimensions - 1][y + 1] = new Body();
		scene[dimensions - 1][y + 1].add(scene[dimensions][1].getRelative(orientation));
		scene[dimensions - 1][y + 1].rotate(0, 1, - Math.PI / 2);
	    }
	}else{
	    scene[dimensions - 1][1] = scene[dimensions][1];
	}

	for (int d = dimensions - 1; d > 1; d--) {

	    // Project to n-1 space
	    
	    // View from a distance
	    orientation.sumRelative(0, viewDistance * playfield.getSize());
	    
	    // Only calculate 3d->2d stereoscopic images
	    int dimY = 0;
	    if (d == 2) {
		dimY = 1;
	    }

	    // mono & stereo views
	    for (int y = -1 * maxY * dimY ; y <= 1 * maxY * dimY; y++) {
		Orientation orientationY = orientation.copyOrientation();
		orientationY.sumRelative(1, y * eyeDistance * playfield.getSize());
		scene[d - 1][y + 1] = new Body();
		scene[d - 1][y + 1].add(scene[d][1].getProjected(orientationY));
		playfieldY[y + 1] = (Sphere)playfield.getProjected(orientationY);
	    }

	    // Next projection round
	    if (d > 1) {
		// View the projected image from within
		orientation = new Orientation(d);
		// Let the last dimension be the first,
		// so the others retain their familiar meaning 
		// in the projected image.
		// (Does this actually work for dimensions > 4?)
		for(int d2 = 0; d2 < d - 1; d2++) {
		    orientation.rotate(d2, d2 + 1, - Math.PI / 2);
		}
		playfield = playfieldY[1];
	    }
	}

	// Adjust to canvas size and draw
	double height = dimension.height / 2;
	double width = 0;
	for (int y = -1 * maxY; y <= 1 * maxY; y++) {
	    if (y * notAnaglyph == 0) {
		width = dimension.width / 2;
	    }else{
		width = dimension.width / 4;
	    }
	    double size = Math.min(height, width);
	    Orientation canvasOrientation = new Orientation(2);
	    canvasOrientation.mirror(1); // y is upside-down on Java canvas
	    canvasOrientation.sumRelative(0, dimension.width / 2 + notAnaglyph * swap * y * size);
	    canvasOrientation.sumRelative(1, -dimension.height / 2);

	    if (playfieldY[y + 1] == null)
		playfieldY[y + 1] = playfield;

	    canvasOrientation.multiplyRelative(size / 
					       playfieldY[y + 1].getSize());
	    orientation = new Orientation(2);
	    orientation.relative(canvasOrientation);
	    orientation.sum(0, playfieldY[y + 1].copyPoint().getCoordinate(0));

	    if ((!stereo && y == 0) || (stereo && y != 0)) {

		scene[1][y + 1].draw(orientation, colorScheme[(1 - notAnaglyph) * y + 1], offGraphics);
		unit = size / playfield.getSize();
 		unit2 = size / playfieldSize;

		//offGraphics.drawString(score.toString(), (int)(dimension.width / 2 + notAnaglyph * swap * y * size + y), (int)(dimension.height/2 + size));

		offGraphics.drawString(score.toString(), (int)(dimension.width / 2 + notAnaglyph * swap * y * size), (int)(dimension.height/2 + size));

	    }
	}

	graphics.drawImage(offImage, 0, 0, this);
    }
}