/* 3D drawing API */

Vectors = {
	normalise: function(v) {
		var mod = Math.sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2]);
		return [v[0] / mod, v[1] / mod, v[2] / mod];
	},
	dotProduct: function(a, b) {
		return (a[0]*b[0] + a[1]*b[1] + a[2]*b[2]);
	},
	crossProduct: function(a, b) {
		return [a[1]*b[2] - a[2]*b[1], a[2]*b[0] - a[0]*b[2], a[0]*b[1] - a[1]*b[0]];
	},
	isFrontFacing: function(sv0, sv1, sv2) {
		/* takes three sets of screen coordinates. Must be in anticlockwise order to return true */
		return ((sv1[0] - sv0[0]) * (sv2[1] - sv0[1]) - (sv2[0] - sv0[0]) * (sv1[1] - sv0[1]) < 0);
	},
	transformVector: function(v, m) {
		return [
			v[0]*m[0] + v[1]*m[1] + v[2]*m[2] + m[3],
			v[0]*m[4] + v[1]*m[5] + v[2]*m[6] + m[7],
			v[0]*m[8] + v[1]*m[9] + v[2]*m[10] + m[11]
		];
	},
	transformNormal: function(v, m) {
		return [
			v[0]*m[0] + v[1]*m[1] + v[2]*m[2],
			v[0]*m[4] + v[1]*m[5] + v[2]*m[6],
			v[0]*m[8] + v[1]*m[9] + v[2]*m[10]
		];
	}
};

function Stage(canvas1, canvas2) {
	this.canvas1 = this.canvas = canvas1;
	this.canvas2 = canvas2;
	this.canvas1.style.zIndex = 10;
	this.canvas2.style.zIndex = 11;
	this.ctx1 = this.ctx = canvas1.getContext('2d');
	this.ctx2 = canvas2.getContext('2d');
	this.width = this.canvas1.width;
	this.height = this.canvas1.height;
	this.backgroundColour = 'black';
	this.nearClipZ = 0.1;

	this.centreX = this.width / 2;
	this.centreY = this.height / 2;
	this.viewAngle = 1.5;
	this.verticalViewAngle = this.viewAngle * (this.height / this.width);
	this.projectionMultiplierX = (this.width / 2) / this.viewAngle;
	this.projectionMultiplierY = (this.height / 2) / this.verticalViewAngle;
}
Stage.prototype.startFrame = function() {
	this.ctx.fillStyle = this.backgroundColour;
	this.ctx.fillRect(0, 0, this.width, this.height);
	this.matrix = [
		1, 0, 0, 0,
		0, 1, 0, 0,
		0, 0, 1, 0
	];
	this.matrixStack = [];
	this.renderQueue = [];
}
Stage.prototype.popMatrix = function() {
	this.matrix = this.matrixStack.pop();
}
Stage.prototype.translate = function(p) {
	this.matrixStack.push(this.matrix);
	var m = this.matrix;
	this.matrix = [
		m[0], m[1], m[2], m[0]*p[0] + m[1]*p[1] + m[2]*p[2] + m[3],
		m[4], m[5], m[6], m[4]*p[0] + m[5]*p[1] + m[6]*p[2] + m[7],
		m[8], m[9], m[10], m[8]*p[0] + m[9]*p[1] + m[10]*p[2] + m[11]
	];
}
Stage.prototype.rotateX = function(r) {
	this.matrixStack.push(this.matrix);
	var m = this.matrix;
	var cos_a = Math.cos(r);
	var sin_a = Math.sin(r);
	this.matrix = [
		m[0], m[1]*cos_a+m[2]*sin_a, m[2]*cos_a-m[1]*sin_a, m[3],
		m[4], m[5]*cos_a+m[6]*sin_a, m[6]*cos_a-m[5]*sin_a, m[7],
		m[8], m[9]*cos_a+m[10]*sin_a, m[10]*cos_a-m[9]*sin_a, m[11]
	];
}
Stage.prototype.rotateY = function(r) {
	this.matrixStack.push(this.matrix);
	var m = this.matrix;
	var cos_a = Math.cos(r);
	var sin_a = Math.sin(r);
	this.matrix = [
		m[0]*cos_a-m[2]*sin_a, m[1], m[0]*sin_a+m[2]*cos_a, m[3],
		m[4]*cos_a-m[6]*sin_a, m[5], m[4]*sin_a+m[6]*cos_a, m[7],
		m[8]*cos_a-m[10]*sin_a, m[9], m[8]*sin_a+m[10]*cos_a, m[11]
	];
}
Stage.prototype.rotateZ = function(r) {
	this.matrixStack.push(this.matrix);
	var m = this.matrix;
	var cos_a = Math.cos(r);
	var sin_a = Math.sin(r);
	this.matrix = [
		m[0]*cos_a+m[1]*sin_a, m[1]*cos_a-m[0]*sin_a, m[2], m[3],
		m[4]*cos_a+m[5]*sin_a, m[5]*cos_a-m[4]*sin_a, m[6], m[7],
		m[8]*cos_a+m[9]*sin_a, m[9]*cos_a-m[8]*sin_a, m[10], m[11]
	];
}
Stage.prototype.enqueue = function(z, fn) {
	this.renderQueue.push({z: z, fn: fn});
}

Stage.prototype.paint = function() {
	/* invoke every paint function in the render queue */
	this.renderQueue.sort(function(a,b) {return (b.z - a.z)});
	for (var i = 0; i < this.renderQueue.length; i++) {
		this.renderQueue[i].fn.call(this);
	}
	if (this.canvas == this.canvas1) {
		this.canvas1.style.zIndex = 12;
		this.canvas = this.canvas2;
		this.ctx = this.ctx2;
	} else {
		this.canvas1.style.zIndex = 10;
		this.canvas = this.canvas1;
		this.ctx = this.ctx1;
	}
	//this.screenCtx.drawImage(this.canvas, 0, 0);
}
Stage.prototype.project = function(vt) {
	return [
		this.centreX + vt[0] * this.projectionMultiplierX / vt[2],
		this.centreY - vt[1] * this.projectionMultiplierY / vt[2]
	];
}
Stage.prototype.setCamera = function(pos, target, up) {
	if (!up) up = [0, 1, 0];
	var z = Vectors.normalise([target[0] - pos[0], target[1] - pos[1], target[2] - pos[2]]);
	var x = Vectors.normalise(Vectors.crossProduct(up, z));
	var y = Vectors.crossProduct(z, x);
	this.matrix = [
		x[0], x[1], x[2], -Vectors.dotProduct(x, pos),
		y[0], y[1], y[2], -Vectors.dotProduct(y, pos),
		z[0], z[1], z[2], -Vectors.dotProduct(z, pos)
	];
}
Stage.prototype.putSprite3d = function(img, v, width, height) {
	var vt = Vectors.transformVector(v, this.matrix);
	if (vt[2] >= this.nearClipZ) {
		var vp = this.project(vt);
		var screenWidth = width * this.projectionMultiplierX / vt[2];
		var screenHeight = height * this.projectionMultiplierY / vt[2];
		this.enqueueDrawImage(vt[2], img, vp[0] - screenWidth/2, vp[1] - screenHeight/2, screenWidth, screenHeight);
	}
}
Stage.prototype.enqueueDrawImage = function(z, img, x, y, w, h) {
	if (w == null) {
		this.enqueue(z, function() {this.ctx.drawImage(img, x, y)});
	} else {
		this.enqueue(z, function() {this.ctx.drawImage(img, x, y, w, h)});
	}
}
Stage.prototype.polygon = function(vertices) {
	var transformedVertices = [];
	var projectedVertices = [];
	var finalPoly = [];
	var maxZ = 0;

	for (var i0 = 0; i0 < vertices.length; i0++) {
		var i1 = (i0+1) % vertices.length;
		var vt0, vt1;
		if (transformedVertices[i0] == null) {
			vt0 = transformedVertices[i0] = Vectors.transformVector(vertices[i0], this.matrix);
		} else {
			vt0 = transformedVertices[i0];
		}
		if (transformedVertices[i1] == null) {
			vt1 = transformedVertices[i1] = Vectors.transformVector(vertices[i1], this.matrix);
		} else {
			vt1 = transformedVertices[i1];
		}
		if (vt0[2] < this.nearClipZ && vt1[2] < this.nearClipZ) {
			/* both vertices clipped; draw nothing */
		} else if (vt0[2] < this.nearClipZ) {
			/* vt0 clipped */
			var clipRatio = (vt1[2] - this.nearClipZ) / (vt1[2] - vt0[2]);
			vt0 = [
				vt0[0] * clipRatio + vt1[0] * (1 - clipRatio),
				vt0[1] * clipRatio + vt1[1] * (1 - clipRatio),
				this.nearClipZ
			];
			var vp0 = this.project(vt0);
			var vp1;
			if (projectedVertices[i1] == null) {
				vp1 = projectedVertices[i1] = this.project(vt1);
			} else {
				vp1 = projectedVertices[i1];
			}
			finalPoly.push(vp0);
			finalPoly.push(vp1);
		} else if (vt1[2] < this.nearClipZ) {			
			/* vt1 clipped */
			var clipRatio = (vt0[2] - this.nearClipZ) / (vt0[2] - vt1[2]);
			vt1 = [
				vt1[0] * clipRatio + vt0[0] * (1 - clipRatio),
				vt1[1] * clipRatio + vt0[1] * (1 - clipRatio),
				this.nearClipZ
			];
			var vp1 = this.project(vt1);
			if (finalPoly.length) {
				/* will have already plotted v0 in this case, so just move to (clipped) v1 */
				finalPoly.push(vp1);
			} else {
				var vp0;
				if (projectedVertices[i0] == null) {
					vp0 = projectedVertices[i0] = this.project(vt0);
				} else {
					vp0 = projectedVertices[i0];
				}
				finalPoly.push(vp0);
				finalPoly.push(vp1);
			}
		} else {
			/* neither clipped; draw normally */
			if (finalPoly.length) {
				/* will have already plotted v0 in this case, so just move to v1 */
				var vp1;
				if (projectedVertices[i1] == null) {
					vp1 = projectedVertices[i1] = this.project(vt1);
				} else {
					vp1 = projectedVertices[i1];
				}
				finalPoly.push(vp1);
			} else {
				var vp0, vp1;
				if (projectedVertices[i0] == null) {
					vp0 = projectedVertices[i0] = this.project(vt0);
				} else {
					vp0 = projectedVertices[i0];
				}
				if (projectedVertices[i1] == null) {
					vp1 = projectedVertices[i1] = this.project(vt1);
				} else {
					vp1 = projectedVertices[i1];
				}
				finalPoly.push(vp0);
				finalPoly.push(vp1);
			}
		}
		if (vt0[2] > maxZ) maxZ = vt0[2];
		if (vt1[2] > maxZ) maxZ = vt1[2];
	}
	finalPoly = this.clipPolygon2d(finalPoly);
	if (finalPoly.length) {
		this.enqueuePolygon(maxZ, finalPoly);
	}
}
Stage.prototype.enqueuePolygon = function(z, vertices) {
	var fillStyle = this.ctx.fillStyle;
	this.enqueue(z, function() {
		this.ctx.beginPath();
		this.ctx.moveTo(vertices[0][0], vertices[0][1]);
		for (var i = 1; i < vertices.length; i++) {
			this.ctx.lineTo(vertices[i][0], vertices[i][1]);
		}
		this.ctx.fillStyle = fillStyle;
		this.ctx.fill();
	});
}
Stage.prototype.strokedPolygon = function(vertices) {
	var transformedVertices = [];
	var projectedVertices = [];
	var finalPoly = [];
	var maxZ = 0;

	for (var i0 = 0; i0 < vertices.length; i0++) {
		var i1 = (i0+1) % vertices.length;
		var vt0, vt1;
		if (transformedVertices[i0] == null) {
			vt0 = transformedVertices[i0] = Vectors.transformVector(vertices[i0], this.matrix);
		} else {
			vt0 = transformedVertices[i0];
		}
		if (transformedVertices[i1] == null) {
			vt1 = transformedVertices[i1] = Vectors.transformVector(vertices[i1], this.matrix);
		} else {
			vt1 = transformedVertices[i1];
		}
		if (vt0[2] < this.nearClipZ && vt1[2] < this.nearClipZ) {
			/* both vertices clipped; draw nothing */
		} else if (vt0[2] < this.nearClipZ) {
			/* vt0 clipped */
			var clipRatio = (vt1[2] - this.nearClipZ) / (vt1[2] - vt0[2]);
			vt0 = [
				vt0[0] * clipRatio + vt1[0] * (1 - clipRatio),
				vt0[1] * clipRatio + vt1[1] * (1 - clipRatio),
				this.nearClipZ
			];
			var vp0 = this.project(vt0);
			var vp1;
			if (projectedVertices[i1] == null) {
				vp1 = projectedVertices[i1] = this.project(vt1);
			} else {
				vp1 = projectedVertices[i1];
			}
			finalPoly.push({line:false, v: vp0});
			finalPoly.push({line:true, v:vp1});
		} else if (vt1[2] < this.nearClipZ) {			
			/* vt1 clipped */
			var clipRatio = (vt0[2] - this.nearClipZ) / (vt0[2] - vt1[2]);
			vt1 = [
				vt1[0] * clipRatio + vt0[0] * (1 - clipRatio),
				vt1[1] * clipRatio + vt0[1] * (1 - clipRatio),
				this.nearClipZ
			];
			var vp1 = this.project(vt1);
			if (finalPoly.length) {
				/* will have already plotted v0 in this case, so just move to (clipped) v1 */
				finalPoly.push({line:true, v:vp1});
			} else {
				var vp0;
				if (projectedVertices[i0] == null) {
					vp0 = projectedVertices[i0] = this.project(vt0);
				} else {
					vp0 = projectedVertices[i0];
				}
				finalPoly.push({line:false, v:vp0});
				finalPoly.push({line:true, v:vp1});
			}
		} else {
			/* neither clipped; draw normally */
			if (finalPoly.length) {
				/* will have already plotted v0 in this case, so just move to v1 */
				var vp1;
				if (projectedVertices[i1] == null) {
					vp1 = projectedVertices[i1] = this.project(vt1);
				} else {
					vp1 = projectedVertices[i1];
				}
				finalPoly.push({line:true, v:vp1});
			} else {
				var vp0, vp1;
				if (projectedVertices[i0] == null) {
					vp0 = projectedVertices[i0] = this.project(vt0);
				} else {
					vp0 = projectedVertices[i0];
				}
				if (projectedVertices[i1] == null) {
					vp1 = projectedVertices[i1] = this.project(vt1);
				} else {
					vp1 = projectedVertices[i1];
				}
				finalPoly.push({line:false, v:vp0});
				finalPoly.push({line:true, v:vp1});
			}
		}
		if (vt0[2] > maxZ) maxZ = vt0[2];
		if (vt1[2] > maxZ) maxZ = vt1[2];
	}
	if (finalPoly.length) {
		this.enqueueStrokedPolygon(maxZ, finalPoly);
	}
}
Stage.prototype.enqueueStrokedPolygon = function(z, vertices) {
	var strokeStyle = this.ctx.strokeStyle;
	var lineWidth = this.ctx.lineWidth;
	this.enqueue(z, function() {
		this.ctx.beginPath();
		for (var i = 0; i < vertices.length; i++) {
			if (vertices[i].line) {
				this.ctx.lineTo(vertices[i].v[0], vertices[i].v[1]);
			} else {
				this.ctx.moveTo(vertices[i].v[0], vertices[i].v[1]);
			}
		}
		this.ctx.strokeStyle = strokeStyle;
		this.ctx.lineWidth = lineWidth;
		this.ctx.stroke();
	});
}
Stage.prototype.enqueueFillRect = function(z, x, y, w, h) {
	var fillStyle = this.ctx.fillStyle;
	this.enqueue(z, function() {
		this.ctx.fillStyle = fillStyle;
		this.ctx.fillRect(x, y, w, h)
	});
}
Stage.prototype.clipPolygon2d = function(poly) {
	/* clip against y=0 */
	var newPoly = [];
	for (var i = 0; i < poly.length; i++) {
		v0 = poly[i];
		v1 = poly[(i+1) % poly.length];
		if (v0[1] < 0 && v1[1] < 0) {
			/* entire line is clipped */
		} else if (v0[1] < 0) {
			/* v0 to boundary is clipped */
			var diff = [
				v1[0] - v0[0],
				v1[1] - v0[1]
			];
			var proportion = v1[1] / diff[1];
			var newX = v1[0] - proportion * diff[0];
			newPoly.push([newX, 0]);
		} else if (v1[1] < 0) {
			/* boundary to v1 is clipped */
			var diff = [
				v1[0] - v0[0],
				v1[1] - v0[1]
			];
			var proportion = -v0[1] / diff[1];
			var newX = v0[0] + proportion * diff[0];
			newPoly.push(v0);
			newPoly.push([newX, 0]);
		} else {
			/* no clipping */
			newPoly.push(v0);
		}
	}
	/* clip against x=0 */
	poly = newPoly;
	newPoly = [];
	for (var i = 0; i < poly.length; i++) {
		v0 = poly[i];
		v1 = poly[(i+1) % poly.length];
		if (v0[0] < 0 && v1[0] < 0) {
			/* entire line is clipped */
		} else if (v0[0] < 0) {
			/* v0 to boundary is clipped */
			var diff = [
				v1[0] - v0[0],
				v1[1] - v0[1]
			];
			var proportion = v1[0] / diff[0];
			var newY = v1[1] - proportion * diff[1];
			newPoly.push([0, newY]);
		} else if (v1[0] < 0) {
			/* boundary to v1 is clipped */
			var diff = [
				v1[0] - v0[0],
				v1[1] - v0[1]
			];
			var proportion = -v0[0] / diff[0];
			var newY = v0[1] + proportion * diff[1];
			newPoly.push(v0);
			newPoly.push([0, newY]);
		} else {
			/* no clipping */
			newPoly.push(v0);
		}
	}
	/* clip against y=height */
	var h = this.height;
	poly = newPoly;
	newPoly = [];
	for (var i = 0; i < poly.length; i++) {
		v0 = poly[i];
		v1 = poly[(i+1) % poly.length];
		if (v0[1] > h && v1[1] > h) {
			/* entire line is clipped */
		} else if (v0[1] > h) {
			/* v0 to boundary is clipped */
			var diff = [
				v1[0] - v0[0],
				v1[1] - v0[1]
			];
			var proportion = (v1[1] - h) / diff[1];
			var newX = v1[0] - proportion * diff[0];
			newPoly.push([newX, h]);
		} else if (v1[1] > h) {
			/* boundary to v1 is clipped */
			var diff = [
				v1[0] - v0[0],
				v1[1] - v0[1]
			];
			var proportion = (h-v0[1]) / diff[1];
			var newX = v0[0] + proportion * diff[0];
			newPoly.push(v0);
			newPoly.push([newX, h]);
		} else {
			/* no clipping */
			newPoly.push(v0);
		}
	}
	/* clip against x=width */
	var w = this.width;
	poly = newPoly;
	newPoly = [];
	for (var i = 0; i < poly.length; i++) {
		v0 = poly[i];
		v1 = poly[(i+1) % poly.length];
		if (v0[0] > w && v1[0] > w) {
			/* entire line is clipped */
		} else if (v0[0] > w) {
			/* v0 to boundary is clipped */
			var diff = [
				v1[0] - v0[0],
				v1[1] - v0[1]
			];
			var proportion = (v1[0] - w) / diff[0];
			var newY = v1[1] - proportion * diff[1];
			newPoly.push([w, newY]);
		} else if (v1[0] > w) {
			/* boundary to v1 is clipped */
			var diff = [
				v1[0] - v0[0],
				v1[1] - v0[1]
			];
			var proportion = (w-v0[0]) / diff[0];
			var newY = v0[1] + proportion * diff[1];
			newPoly.push(v0);
			newPoly.push([w, newY]);
		} else {
			/* no clipping */
			newPoly.push(v0);
		}
	}
	
	return newPoly;
}

var stage;
$(function() {
	var canvas = document.getElementById('canvas');
	var viewports = $('.viewport');
	if ($('.demo').length) {
		viewports.css({
			left: ($('body').innerWidth() - viewports.outerWidth()) / 2,
			top: (window.innerHeight - viewports.outerHeight()) / 2
		});
	}
	stage = new Stage(viewports.get(0), viewports.get(1));
})
