/**
 * @author Alexey Gavrilov (alex@metalinkltd.com)
*/

/**
 * Vector class
 */
function V(x, y, z) {
	if (typeof x == "object") {
		this.x = x[0]; 
		this.y = x[1];
		this.z = x[2];		
	} else {
		this.x = x; 
		this.y = y;
		this.z = z;	
	}
}
V.prototype.add = function (v) {
	this.x += v.x;
	this.y += v.y;
	this.z += v.z;
	return this;
} 
V.prototype.subtract = function (v) {
	this.x -= v.x;
	this.y -= v.y;
	this.z -= v.z;
	return this;
}
V.prototype.clone = function() {
	return new V(this.x, this.y, this.z);
} 
V.prototype.module = function() {
	return Math.sqrt(this.x*this.x + this.y*this.y + this.z*this.z);	
}
V.prototype.mult = function(v) {
	return this.x*v.x + this.y*v.y + this.z*v.z;	
}
V.prototype.multV = function(v) {
	return 	new V(
				this.y*v.z - this.z*v.y,	
				-this.x*v.z + this.z*v.x,
				this.x*v.y - this.y*v.x
			);	
}
V.prototype.scale = function(t) {
	this.x *= t;
	this.y *= t;
	this.z *= t;
	return this;	
}

/**
 * The model of our "world in a box"
 */
var model = {
	"walls" : {"minx":-250, "miny":-250, "minz": -150, "maxx": 250, "maxy": 250, "maxz": 150,
				"shapes": Array(),
				"makeWalls": function() {},
				"renderWalls": function (front) {
					for (s in this.shapes) {
						if (front == this.shapes[s]._orientation) {
							this.shapes[s].render();
						}
					}	
				}
				},
	"elastity" : 1.0,
	"gravity" : -0.00,
	"ballRadius" : 26,
	"maxSpeed" : 5.0, 
	"setPOV" : function (angleZ, angleY, R) {
		this.camera.setPOV(angleZ, angleY, R);
		for (shape in this.walls.shapes) {
			this.walls.shapes[shape].updateView();
		}
	},
	"camera" : {
		"POV": {"x": -1000, "y": -1000, "z": 0}, 
		"FOV": 0.5, 
		"screen": {"wx": 500, "wy": 400},
		"matrix": {
			"xx": 1.0, "xy": 0.0, "xz": 0.0,
			"yx": 0.0, "yy": 1.0, "yz": 0.0, 
			"zx": 0.0, "zy": 0.0, "zz": 1.0
				},
		makeMatrix: function() {
			var L = 1.0*Math.sqrt(this.POV.x*this.POV.x + this.POV.y*this.POV.y + this.POV.z*this.POV.z);
			this.matrix.zx = this.POV.x/L;
			this.matrix.zy = this.POV.y/L;
			this.matrix.zz = this.POV.z/L;
			
			var n = Math.sqrt(this.matrix.zx*this.matrix.zx + this.matrix.zy*this.matrix.zy); 
			this.matrix.xx = this.matrix.zy/n;
			this.matrix.xy = -this.matrix.zx/n;			
			this.matrix.xz = 0.0;
			
			this.matrix.yx = /*this.matrix.zy*this.matrix.xz*/ - this.matrix.xy*this.matrix.zz;
			this.matrix.yy = /*-this.matrix.zx*this.matrix.xz*/ + this.matrix.xx*this.matrix.zz;			
			this.matrix.yz = this.matrix.zx*this.matrix.xy - this.matrix.xx*this.matrix.zy;			
		},
		getProjection: function (x, y, z) {
			x -= this.POV.x;
			y -= this.POV.y;
			z -= this.POV.z;
			var px = x*this.matrix.xx + y*this.matrix.xy + z*this.matrix.xz;
			var py = x*this.matrix.yx + y*this.matrix.yy + z*this.matrix.yz;
			var depth = -(x*this.matrix.zx + y*this.matrix.zy + z*this.matrix.zz);
			var scale = (1.0/depth)*this.screen.wx/this.FOV;
			px *= scale;
			py *= scale;	
			px += model.camera.screen.wx/2;
			py += model.camera.screen.wy/2;	
			return {"x": px, "y": py, "depth": depth, "scale": scale};
		},
		setPOV: function (angleZ, angleY, R) {
			this.POV.z = R*Math.sin(angleZ);
			this.POV.x = R*Math.cos(angleZ)*Math.cos(angleY);
			this.POV.y = R*Math.cos(angleZ)*Math.sin(angleY);
			this.makeMatrix();
		}
	}
};


// helper
var extend = function(subClass, baseClass)
{
	// Create a new class that has an empty constructor
	// with the members of the baseClass
	function inheritance() {};
	inheritance.prototype = baseClass.prototype;
	
	// set prototype to new instance of baseClass
	// _without_ the constructor
	subClass.prototype = new inheritance();
	subClass.prototype.constructor = subClass;
	subClass.baseConstructor = baseClass;
	
	// enable multiple inheritance
	if (baseClass.base)
	{
		baseClass.prototype.base = baseClass.base;
	}
	subClass.base = baseClass.prototype;
}
/**
 *	Represents flat 3D-face object
 * 
 * @param {Array} vertexes
 */
function Face(vertexes) {
	if (vertexes == undefined) {
		vertexes = [[100.0, 100.0, 0.0], [100.0,-100.0, 0.0], [-100.0, -100.0, 0.0], [-100.0, 100.0, 0.0]];
	}
	
	this._vertexA = vertexes;
	this.pA = new Array();
	this.updateView();
}

Face.prototype.getProjection = function() {
	for (var i=0; i< this._vertexA.length; i++) {
		this.pA[i] = model.camera.getProjection(this._vertexA[i][0], this._vertexA[i][1], this._vertexA[i][2]);
	}
}

Face.prototype.render = function(orientation) {
	if (this._orientation != orientation) 
		return false;
	else 
		return true;	
}

Face.prototype.getOrientation = function() {
	var v0 = new V(this._vertexA[0]);
	var v1 = new V(this._vertexA[1]);
	var v2 = new V(this._vertexA[2]);
	var pov = new V(model.camera.POV.x, model.camera.POV.y, model.camera.POV.z );
	v2.subtract(v1);
	v1.subtract(v0);
	this._orientation = ( (v1.multV(v2)).mult( pov.subtract(v0) ) > 0 );		
	return this._orientation;
}

Face.prototype.updateView = function() {
	this.getProjection();
	this.getOrientation();
}

function WPFEFace(host, parent, vertexes) {
	WPFEFace.baseConstructor.call(this, vertexes);
	this._host = host;
	this._elem = host.createFromXaml('<Polygon  />');
	this._parent = parent;
	// set default style values
	this._elem.stroke = "#FF0000EE";
	this._elem.opacity = 0.3;
	this._elem.fill = "#EEEEEEFF";
	this._elem.StrokeMiterLimit = 1.0; 
	this.render();
}
extend(WPFEFace, Face);

WPFEFace.prototype.render = function(front) {
	var path = "";
	for (point in this.pA) {
		path += this.pA[point].x + "," + this.pA[point].y + " ";
	}
	this._elem.points = path;
	if (front)
		this.bringToFront();
	else 		
		this.sendToBack();	
}

WPFEFace.prototype.sendToBack = function() {
	this._parent.children.remove(this._elem);
	this._parent.children.insert(0, this._elem);
}

WPFEFace.prototype.bringToFront = function() {
	this._parent.children.remove(this._elem);
	this._parent.children.insert(this._parent.children.count-1, this._elem);
}
/**
 * Base "platform-independent" class representing ball
 * 
 * @param {Object} x
 * @param {Object} y
 * @param {Object} vx
 * @param {Object} vy
 */
function Ball(x, y, z, vx, vy, vz) {
	// default provisioning
	if (x == undefined) {
		x = model.walls.minx + (model.walls.maxx - model.walls.minx - 2*model.ballRadius)*Math.random(); 
		y = model.walls.miny + (model.walls.maxy - model.walls.miny - 2*model.ballRadius)*Math.random(); 
		z = model.walls.minz + (model.walls.maxz - model.walls.minz - 2*model.ballRadius)*Math.random(); 		
		vx = 2*model.maxSpeed*Math.random() - model.maxSpeed;
		vy = 2*model.maxSpeed*Math.random() - model.maxSpeed;
		vz = 2*model.maxSpeed*Math.random() - model.maxSpeed;		

	}
	this._x = x;
	this._y = y;
	this._z = z;
	this._vx = vx;
	this._vy = vy;
	this._vz = vz;
	this._r = model.ballRadius; // d = 52 px
	this._d = 2*this._r;
	this._d2 = this._d*this._d;
}

Ball.prototype.move = function() {
	this._x += this._vx;
	this._y += this._vy;
	this._z += this._vz;
	
	this._vz += model.gravity;
	
	this.getProjection();
	
	// walls collisons
	
	if (this._x < model.walls.minx + this._r && this._vx<0) {
		this._vx = -this._vx*model.elastity;
	}
	if (this._y < model.walls.miny + this._r && this._vy<0) {
		this._vy = -this._vy*model.elastity;
	}
	if (this._z < model.walls.minz + this._r && this._vz<0) {
		this._vz = -this._vz*model.elastity;
	}	
	if (this._x > model.walls.maxx - this._r && this._vx>0) {
		this._vx = -this._vx*model.elastity;
	}
	if (this._y > model.walls.maxy - this._r && this._vy>0) {
		this._vy = -this._vy*model.elastity;
	}
	if (this._z > model.walls.maxz - this._r && this._vz>0) {
		this._vz = -this._vz*model.elastity;
	}	
}

Ball.prototype.getProjection = function() {
	this.p = model.camera.getProjection(this._x, this._y, this._z);
}

Ball.prototype.doCollide = function(b) {
	// calculate some vectors 
	var dx = this._x - b._x;
	var dy = this._y - b._y;
	var dz = this._z - b._z;	
	var dvx = this._vx - b._vx;
	var dvy = this._vy - b._vy;	
	var dvz = this._vz - b._vz;
			
	var distance2 = dx*dx + dy*dy + dz*dz;
		
	if (Math.abs(dx) > this._d || Math.abs(dy) > this._d || Math.abs(dz) > this._d) 
		return false;
	if (distance2 > this._d2)
		return false;
	
	// make absolutely elastic collision
	var mag = dvx*dx + dvy*dy + dvz*dz;
	
	// test that balls move towards each other	
	if (mag > 0) 
		return false;

	mag /= distance2;
	
	var delta_vx = dx*mag;
	var delta_vy = dy*mag;
	var delta_vz = dz*mag;	
	
	this._vx -= delta_vx;
	this._vy -= delta_vy;
	this._vz -= delta_vz;	
	
	b._vx += delta_vx;
	b._vy += delta_vy;
	b._vz += delta_vz;	
		
	return true;
}

/**
 * WPF/e-specific implementation
 * 
 * @param {Object} name
 * @param {Object} x
 * @param {Object} y
 * @param {Object} vx
 * @param {Object} vy
 */
function WPFEBall(host, name, x, y, vx, vy) {
	WPFEBall.baseConstructor.call(this, x, y, vx, vy);
	this._host = host;
	this._name = name;
	this._elem = host.findName(name);
	this._parent = this._elem.getParent();
	this.move();
}
extend(WPFEBall, Ball);
/*
WPFEBall.prototype.move = function(){
	WPFEBall.base.move.call(this);
}
*/
WPFEBall.prototype.render = function() {
	var scaleTransform = this._host.createFromXaml("<ScaleTransform />");
	scaleTransform.ScaleX = this.p.scale;
	scaleTransform.ScaleY = this.p.scale;
	
	this._elem.RenderTransform = scaleTransform;
	
	this._elem["canvas.left"] = this.p.x - model.ballRadius*this.p.scale;
	this._elem["canvas.top"] = this.p.y - model.ballRadius*this.p.scale;
	
	this.sendToBack();	
}

WPFEBall.prototype.sendToBack = function() {
	this._parent.children.remove(this._elem);
	this._parent.children.insert(0, this._elem);
}

WPFEBall.prototype.bringToFront = function() {
	this._parent.children.remove(this._elem);
	this._parent.children.insert(this._parent.children.count-1, this._elem);
}

WPFEBall.prototype.clone = function(newName, is_bmp) {
	// oops, wpf/e doesn't support objects cloning nor getting their XAML source!
	// it's just too bad -- I had to paste all XAML right here
	// 
	var newXAML = "";
	if (this._is_bmp && this._is_bmp != undefined) {
		newXAML = '<Canvas xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Name="' + newName + '" Width="54.6667" Height="54.6667" Canvas.Left="0" Canvas.Top="0"><Image Source="assets/ball.png"/></Canvas>';
	} else {
		newXAML = '<Canvas xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Name="' + newName + '" Width="52" Height="52" Canvas.Left="0" Canvas.Top="30"><Path Opacity="0.900000" StrokeThickness="2.000000" Stroke="#ffa6d000" StrokeMiterLimit="1.000000" Fill="#ffcbff00" Data="F1 M 51.000000,26.000000 C 51.000000,39.806641 39.807129,51.000000 26.000000,51.000000 C 12.192871,51.000000 1.000000,39.806641 1.000000,26.000000 C 1.000000,12.193359 12.192871,1.000000 26.000000,1.000000 C 39.807129,1.000000 51.000000,12.193359 51.000000,26.000000 Z"/><Path Opacity="0.740000" Data="F1 M 43.143066,13.087891 C 50.602051,22.888672 49.009766,36.642578 39.590332,43.812500 C 30.170898,50.980469 16.489258,48.842773 9.032715,39.042969 C 1.573242,29.240234 3.166016,15.486328 12.584961,8.316406 C 22.003906,1.149414 35.685547,3.285156 43.143066,13.087891 Z"><Path.Fill><RadialGradientBrush MappingMode="Absolute" GradientOrigin="156.791016,170.453125" Center="156.791016,170.453125" RadiusX="53.626404" RadiusY="53.626404"><RadialGradientBrush.GradientStops><GradientStop Offset="0.000000" Color="#ffffffff"/><GradientStop Offset="0.361685" Color="#fff5f7dd"/><GradientStop Offset="0.415730" Color="#ffebf0bc"/><GradientStop Offset="1.000000" Color="#ffcbff00"/></RadialGradientBrush.GradientStops><RadialGradientBrush.Transform><MatrixTransform Matrix="1.190000,0.165000,-0.165000,-1.281300,-113.414185,241.757843" /></RadialGradientBrush.Transform></RadialGradientBrush></Path.Fill></Path> <Path Fill="#ffffffff" Data="F1 M 23.100586,9.477539 C 24.741699,11.634766 23.116211,15.630859 19.470703,18.404297 C 15.825684,21.178711 11.540039,21.678711 9.899414,19.522461 C 8.258301,17.365234 9.883789,13.369141 13.529297,10.594727 C 17.174316,7.821289 21.459961,7.321289 23.100586,9.477539 Z"/></Canvas>';
	}
	
	var newNode = this._host.createFromXaml(newXAML);
	this._parent.children.add(newNode);
	return new WPFEBall(this._host, newName);
}

/**
 * Abstract test class
 * 
 * @param {Object} N
 */
function BallsTest(N) {
	this._N = N; // number of objects
	this._ballsO = new Array();
	this._isRunning = false;
	this._angleY = 0.0;
	this._angleZ = Math.PI/4;;
	this._distance = 1800;
	model.setPOV(this._angleZ, this._angleY, this._distance);
}

BallsTest.prototype._showFPS = null;

BallsTest.prototype.start = function(N) {
	if (this._isRunning) return false;
	this._isRunning = true;
	
	if (N != undefined) {
		this._N = N;
	}
	
	this._F = 0;  // frames counter for FPS
	this._lastF = 0;
	this._lastTime = new Date();
	var _this = this;
	
	var moveBalls = function() {
		if (_this._N > _this._ballsO.length) 
			return;
		_this._F++;
		
		// move balls
		for (var i=0; i<_this._N; i++) {
			_this._ballsO[i].move();
		}
		if (_this._moveBalls) 
			_this._moveBalls.call(_this);	
		// render front walls	
		model.walls.renderWalls(true);	
		// render balls
		for (var i=0; i<_this._N; i++) {
			_this._ballsO[i].render();
		}
		// render back walls	
		model.walls.renderWalls(false);		
		// process collisions
		for (i=0; i<_this._N; i++) {
			for (j=i+1; j<_this._N; j++) {
				_this._ballsO[i].doCollide(_this._ballsO[j]);
			}
		}
	}
	var showFps = function() {
		if (_this._F - _this._lastF < 10) return;
		var currTime = new Date();
		var delta_t = (currTime.getMinutes() - _this._lastTime.getMinutes())*60 + currTime.getSeconds() - _this._lastTime.getSeconds() + (currTime.getMilliseconds() - _this._lastTime.getMilliseconds())/1000.0;
		 
		var fps = (_this._F - _this._lastF)/delta_t;
		
		_this._lastF = _this._F;
		_this._lastTime = currTime;
		
		if (_this._showFPS) 
			_this._showFPS.call(_this, Math.round(fps));
	}

	this._int1 = setInterval(moveBalls, 5);
	this._int2 = setInterval(showFps, 3000);
	return true;
}
BallsTest.prototype.stop = function(){
	if (!this._isRunning) return false;
	this._isRunning = false;
	clearInterval(this._int1);
	clearInterval(this._int2);
	return true;
}


/**
 * DHTML-specific implementation
 * 
 * @param {Object} id
 * @param {Object} x
 * @param {Object} y
 * @param {Object} vx
 * @param {Object} vy
 */
function DHTMLBall(id, x, y, vx, vy) {
	DHTMLBall.baseConstructor.call(this, x, y, vx, vy);
	this._id = id;
	this._elem = document.getElementById(id);
	this._elem.style.position = "absolute"; 
	this._elem.style.display = "block";
	this.move();
}
extend(DHTMLBall, Ball);

DHTMLBall.prototype.render = function(){
	this._elem.style.width = 2*model.ballRadius*this.p.scale + "px";
	this._elem.style.height = 2*model.ballRadius*this.p.scale + "px";
	this._elem.style.zIndex = Math.floor(1000*this.p.scale);
	this._elem.style.left = (this.p.x - model.ballRadius*this.p.scale) + "px";
	this._elem.style.top = (this.p.y - model.ballRadius*this.p.scale) + "px";
}

DHTMLBall.prototype.clone = function(newId) {
	var newNode = this._elem.cloneNode(true);
	newNode.id = newId;
	newNode.style.zIndex = this._elem.style.zIndex + 1;
	this._elem.parentNode.appendChild(newNode);
	return new DHTMLBall(newId);
}

