/*
CSS Polygons - Use CSS to create polygons.
Copyright (c) 2004 Thomas Peri, http://www.tumuski.com/

Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The Software shall be used for Good, not Evil.

The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

// This file is only partly documented.  Much is needed.

Array.prototype.copy = function()
{
	var c = new Array();
	for (var i = 0; i < this.length; i++)
	{
		c[i] = this[i];
	}
	return c;
};

function squareFactory (/*number*/ side, color)
{
	return rectangleFactory(side,side,color);
}

function rectangleFactory(width, height, color)
{
	width /= 2;
	height /= 2;
	return new Polygon3d(
		new Vector(0,0,0),
		[
			new Vector(width, height, 0),
			new Vector(-width, height, 0),
			new Vector(-width, -height, 0),
			new Vector(width, -height, 0)
		],
		color
	);
}

function cubeFactory (/*number*/ side, color)
{
	var half = side/2;
	
	var s = new Array();
	
	s[0] = squareFactory(side, color);
	s[0].translate(new Vector(0,0,half),1);
	
	s[1] = s[0].copy();
	s[1].rotateY(degrees(90));

	s[2] = s[1].copy();
	s[2].rotateY(degrees(90));

	s[3] = s[2].copy();
	s[3].rotateY(degrees(90));

	s[4] = s[0].copy();
	s[4].rotateX(degrees(-90));

	s[5] = s[0].copy();
	s[5].rotateX(degrees(90));

	return new Figure(new Vector(0,0,0), s);
}

function Figure(/*Vector*/ center, /*Polygon3d[]*/ polygons)
{
	this.polygons = function()
	{
		return polygons.copy();
	};
	this.center = function()
	{
		return center;
	};
	
	this.addPolygon = function (poly)
	{
		polygons[polygons.length] = poly;
	};

	// fixme: should keep an array of elements, and have the scene sort them appropriately
	this.addFigure = function (figure)
	{
		var polys = figure.polygons();
		for (var i = 0; i < polys.length; i++)
		{
			polygons[polygons.length] = polys[i];
		}
	};

	function applyToAllPolygons(func,a,b,c)
	{
		center = center[func](a,b,c);
		for (var i = 0; i < polygons.length; i++)
		{
			polygons[i][func](a,b,c);
		};
	}
	
	this.rotateCustom = function(/*Vector*/ start, /*Vector*/ end, /*number*/ angle)
	{
		applyToAllPolygons('rotateCustom',start,end,angle);
	};
	
	this.translate = function (/*Vector*/ vector, /*number*/ scalar)
	{
		applyToAllPolygons('translate',vector,scalar);
	};
	
	this.rotateY = function (angle)
	{
		applyToAllPolygons('rotateY',angle);
	};

	this.rotateX = function (angle)
	{
		applyToAllPolygons('rotateX',angle);
	};

	this.rotateZ = function (angle)
	{
		applyToAllPolygons('rotateZ',angle);
	};
	
	this.copy = function()
	{
		var p = new Array();
		for (var i = 0; i < polygons.length; i++)
		{
			p[i] = polygons[i].copy();
		}
		return new Figure(center, p);
	};}

/**
 * These are mutable only in that they can be translated or rotated.
 */
function Polygon3d (/*Vector*/ center, /*Vector[]*/ points, /*RgbColor*/ color)
{
	this.toString = function()
	{
		return "Polygon3d ( "+center+", [ "+points+" ], "+color+" )";
	};
	
	this.color = function()
	{
		return color;
	};

	this.points = function()
	{
		return points.copy();
	};
	this.center = function()
	{
		return center;
	};
	
	function applyToAllPoints(func,a,b,c)
	{
		center = center[func](a,b,c);
		for (var i = 0; i < points.length; i++)
		{
			points[i] = points[i][func](a,b,c);
		};
	}
	
	this.reverseDirection = function()
	{
		points.reverse();
	};
	
	this.rotateCustom = function(/*Vector*/ start, /*Vector*/ end, /*number*/ angle)
	{
		applyToAllPoints('rotateCustom',start,end,angle);
	};
	
	this.translate = function (/*Vector*/ vector, /*number*/ scalar)
	{
		vector = vector.scale(scalar);
		applyToAllPoints('add',vector);
	};
	
	this.rotateY = function (angle)
	{
		applyToAllPoints('rotateY',angle);
	};

	this.rotateX = function (angle)
	{
		applyToAllPoints('rotateX',angle);
	};

	this.rotateZ = function (angle)
	{
		applyToAllPoints('rotateZ',angle);
	};
	
	this.copy = function()
	{
		return new Polygon3d(center,points.copy(),color);
	};
	
	/**
	 * The normal vector to the plane containing this polygon
	 */
	this.normal = function()
	{
		var B = points[0];
		var C = points[2];
		
		var minusA = points[1].scale(-1);
		
		var AB = B.add(minusA);
		var AC = C.add(minusA);
		
		return AC.cross(AB);
	};
}



function Scene(width,height,color)
{
	var /*Camera*/ camera;
	var /*number*/ diagonal;
	var /*Figure[]*/ figures;
	var /*CartesianPlaneElement*/ plane;
	
	var /*Vector*/ lightVector;
	var /*RgbColor*/ lightColor;
	
	construct();
	function construct()
	{
		// the distance from the center to any corner
		diagonal = Math.sqrt(width*width + height*height) / 2;
		
		figures = new Array();
	}
	
	this.setLightSource = setLightSource;
	function setLightSource(vector, color)
	{
		lightVector = vector;
		lightColor = color;
	}

	this.setCamera = setCamera;
	function setCamera(cam)
	{
		camera = cam;
	}
	
	this.addFigure = addFigure;
	function addFigure(figure)
	{
		figures[figures.length] = figure;
	}
	
	this.render = render;
	function render()
	{
		plane = new CartesianPlaneElement(width, height, color.cssColor());

		figures.sort(sortByDistance);
		
		for (var f = 0; f < figures.length; f++)
		{
			var polygons = figures[f].polygons();
			
			polygons.sort(sortByDistance);
			
			for (var i = 0; i < polygons.length; i++)
			{
				var points = polygons[i].points();
					
				var projectedPoints = new Array();
				var pointZs = new Array();
				
				for (var j = 0; j < points.length; j++)
				{
					projectedPoints[j] = camera.projectPoint(points[j],diagonal);
					pointZs[j] = camera.lastZ();
					
					// todo: check for points behind camera...
					
				}
				
				drawPolygon2d(projectedPoints, polygons[i]);
			}
		}
	}
	
	/*
	 * Either draw a certain polygon or not, depending on whether
	 * it faces the camera (its points are counterclockwise) or not.
	 */
	function drawPolygon2d(/*Point[]*/ points, /*Polygon3d*/ poly)
	{
		// does the polygon face the camera?
		// (this assumes the shape is convex, as it should be)

		// calculate the angle made by the first 3 points
		var A = positizeRadians(
			points[1].angleOfLineTo(points[2]) - 
			points[0].angleOfLineTo(points[1])
		);
		
		// if the angle is between 0 and 180 degrees, 
		// it's counter-clockwise, meaning it faces the camera
		if (0 < A && A < Math.PI)
		{
			// lighting
			
			var color = (poly.color().reflect(lightColor, poly.normal().angleTo(lightVector)).cssColor());
		
			var offsetPoints = new Array();
			
			// Pad the 2d polygon half a pixel all around,
			// to eliminate the space between them
			for (var k = 0; k < points.length; k++)
			{
				var temp = offset (
					points[k],
					points[ (k+1) % points.length],
					0.5
				);
				offsetPoints[offsetPoints.length] = temp.a;
				offsetPoints[offsetPoints.length] = temp.b;
			}
		
			var poly = new PolygonElement(offsetPoints,color);
			if (poly.isValid())
			{
				plane.addElement(poly);
			}
		}
	}
	
	/*
	 * Offset a pair of points defining a line segment, 
	 * dist units to the segment's right (moving from a to b).
	 */
	function offset(/*Point*/a, /*Point*/b, /*number*/dist)
	{
		var angle = a.angleOfLineTo(b) - (Math.PI / 2);
		var x = dist * Math.cos(angle);
		var y = dist * Math.sin(angle);
		return {
			a: new IntPoint(a.x()+x, a.y()+y),
			b: new IntPoint(b.x()+x, b.y()+y)
		};
	}
	
	this.div = div;
	function div()
	{
		return plane.div();
	}
	
	function sortByDistance(a,b)
	{
		var cam = camera.getLocation();
		return cam.distanceTo(b.center()) - cam.distanceTo(a.center());
	}
}

function Camera ()
{
	var /*Vector*/ location; 
	var /*number*/ azimuth;
	var /*number*/ elevation;
	var /*number*/ fov;
	
	var /*number*/ lastZ; // the distance of the last projected point
						// to the camera's viewplane

	var /*Vector*/ origin = new Vector(0,0,0);
	var /*number*/ halfPI = Math.PI/2;
	
	var distanceToViewingPlane;
	
	construct();
	function construct()
	{
		azimuth = 0;
		elevation = 0;
		location = origin;
		
		setFov(halfPI); // 90 degrees
	}
	
	this.getLocation = getLocation;
	function getLocation()
	{
		return location;
	}

	this.setLocation = setLocation;
	function setLocation(/*Point*/ newLoc)
	{
		location = newLoc;
	}
	
	this.setFov = setFov;
	function setFov(/*number*/ newFov)
	{
		fov = newFov;
		distanceToViewingPlane = 1 / Math.tan (fov / 2);
	}
	
	this.setElevation = setElevation;
	function setElevation(/*number*/ newEl)
	{
		elevation = newEl;
	}
	
	this.setAzimuth = setAzimuth;
	function setAzimuth(/*number*/ newAz)
	{
		azimuth = newAz;
	}
	
	this.projectPoint = projectPoint;
	function projectPoint(/*Vector*/ P, /*number*/ scale)
	{
		// Virtually move the camera and point together 
		// so that the camera is at the origin.
		P = P.add(location.scale(-1));
	
		// Virtually rotate the point about the origin in such a way 
		// that if the camera were rotated the opposite way, it would 
		// point along the z-axis.
		P = P.rotateY(halfPI-azimuth);
		P = P.rotateX(-elevation);

		lastZ = -P.z();
		
		var X = P.x() * distanceToViewingPlane / lastZ;
		var Y = P.y() * distanceToViewingPlane / lastZ;

		return new Point(X*scale, Y*scale);
	}
	
	this.lastZ = function()
	{
		return lastZ;
	};

}

function degrees(d)
{
	return d * Math.PI / 180;
}

Vector.prototype.toTrElement = function()
{
	var X = Math.round(this.x()*100)/100;
	var Y = Math.round(this.y()*100)/100;
	var Z = Math.round(this.z()*100)/100;

	var tr = document.createElement('tr');
	var tdx = document.createElement('td');
	var tdy = document.createElement('td');
	var tdz = document.createElement('td');
	
	tdx.style.border = '1px solid black';
	tdy.style.border = '1px solid black';
	tdz.style.border = '1px solid black';
	
	tdx.style.padding = '4px';
	tdy.style.padding = '4px';
	tdz.style.padding = '4px';
	
	tdx.appendChild(document.createTextNode(X));
	tdy.appendChild(document.createTextNode(Y));
	tdz.appendChild(document.createTextNode(Z));
	
	tr.appendChild(tdx);
	tr.appendChild(tdy);
	tr.appendChild(tdz);
	
	return tr;
};

function writeln(s)
{
	document.body.appendChild(document.createTextNode(s));
	document.body.appendChild(document.createElement('br'));
}

function Vector (x, y, z)
{
	this.x = function()
	{
		return x;
	};
	
	this.y = function()
	{
		return y;
	};
	
	this.z = function()
	{
		return z;
	};
	
	this.midpoint = function(/*Vector*/ vector)
	{
		return new Vector(
			(x + vector.x()) / 2,
			(y + vector.y()) / 2,
			(z + vector.z()) / 2
		);
	};
	
	/**
	 * Rotate this Vector about the y axis.
	 */
	this.rotateY = function (/*number*/ angle)
	{
		var X = (Math.sin(angle) * z) + (Math.cos(angle) * x);
		var Z = (Math.cos(angle) * z) - (Math.sin(angle) * x);
		
		return new Vector(X, y, Z)
	};

	/**
	 * Rotate this Vector about the x axis.
	 */
	this.rotateX = function (/*number*/ angle)
	{
		var Y = (Math.cos(angle) * y) - (Math.sin(angle) * z);
		var Z = (Math.sin(angle) * y) + (Math.cos(angle) * z);
		
		return new Vector(x, Y, Z)
	};
	
	/**
	 * Rotate this Vector about the z axis.
	 */
	this.rotateZ = function (/*number*/ angle)
	{
		var X = (Math.cos(angle) * x) - (Math.sin(angle) * y);
		var Y = (Math.sin(angle) * x) + (Math.cos(angle) * y);
		
		return new Vector(X, Y, z)
	};
	
	this.rotateCustom = function(/*Vector*/ start, /*Vector*/ end, /*number*/ angle)
	{	
		var point = this;
	
		// there are three important points here:
		// - the point at the end of this vector
		// - the start point of the line
		// - the end point of the line
		
		// translate all three points (actually just end and point, 
		// since once the start point is at the origin, it doens't 
		// matter any more -- plus we need to preserve it for 
		// translating back at the end) so that the start point is at
		// the origin
		
		var negStart = start.scale(-1);
		
		end = end.add(negStart);
		point = point.add(negStart);
		
		// rotate all three points about the y axis 
		// so that the end point is on the x-y plane
		
		var angleY = Math.atan2(end.z(), end.x());
		end = end.rotateY(angleY);
		point = point.rotateY(angleY);
		
		// rotate all three points (actually just the point, since 
		// start and end don't matter after this step) about the z axis
		// so that the end point is on the y axis

		var angleZ = Math.atan2(end.x(), end.y());
		point = point.rotateZ(angleZ);
		
		// rotate this point about the y axis
		
		point = point.rotateY(angle);
		
		// rotate all three points about the z axis 
		// to undo the z axis rotation above
		
		point = point.rotateZ(-angleZ);
		
		// rotate all three points about the y axis 
		// to undo the y axis rotation above

		point = point.rotateY(-angleY);
		
		// translate all three points 
		// to undo the translation above
		
		point = point.add(start);
			
			
		return point;
	};

	this.translate = function (/*Vector*/ vector, /*number*/ scalar)
	{
		return this.add(vector.scale(scalar));
	}
	
	this.add = function(/*Vector*/ vector)
	{
		return new Vector(
			x + vector.x(),
			y + vector.y(),
			z + vector.z()
		);
	};
	
	this.scale = function(/*number*/ s)
	{
		return new Vector(s * x, s * y, s * z);
	};
	
	this.magnitude = function()
	{
		return Math.sqrt(x*x + y*y + z*z);
	};
	
	this.distanceTo = function (/*Vector*/ vector)
	{
		return vector.add(this.scale(-1)).magnitude();
	};
	
	this.dot = function (/*Vector*/ vector)
	{
		return x*vector.x() + y*vector.y() + z*vector.z();
	};
	
	this.cross = function (/*Vector*/ vector)
	{
		return new Vector (
			y*vector.z() - z*vector.y(),
			z*vector.x() - x*vector.z(),
			x*vector.y() - y*vector.x()
		);
	};
	
	this.angleTo = function(/*Vector*/ vector)
	{
		return Math.acos(
			this.dot(vector) / 
			(this.magnitude() * vector.magnitude())
		);
	};
	
	this.equals = function(/*Vector*/ vector)
	{
		return (x == vector.x() && y == vector.y() && z == vector.z());
	};
	
	this.toString = function()
	{
		var X = Math.round(x*100)/100;
		var Y = Math.round(y*100)/100;
		var Z = Math.round(z*100)/100;
		return 'Vector ('+X+', '+Y+', '+Z+')';
	};
}

/**
 * A representation of how much light of each channel an object reflects.
 */
function RgbColor(red, green, blue)
{
	this.cssColor = function()
	{
		return "rgb("+limit(red)+","+limit(green)+","+limit(blue)+")";
	};
	
	this.red = function()
	{
		return red;
	};
	this.green = function()
	{
		return green;
	};
	this.blue = function()
	{
		return blue;
	};
	
	function limit(channel)
	{
		return Math.round(Math.max(0, Math.min(255, channel)));
	}
	
	/**
	 * The light reflected when light of the supplied color
	 * hits this color at a certain angle.
	 */
	this.reflect = function(/*RgbColor*/ color, /*number*/ angle)
	{
		// reflect a lower proportion of light from 
		// the side opposite the light source
		if (angle < Math.PI / 2)
		{
			var selfShadow = 1.5;
		}
		else if (angle == Math.PI / 2)
		{
			var selfShadow = 1.3;
		}
		else
		{
			var selfShadow = 1;
		}
		var reflected = (angle / Math.PI) / selfShadow;
		
		return new RgbColor(
			color.red() * reflected * (red / 255),
			color.green() * reflected * (green / 255),
			color.blue() * reflected * (blue / 255)
		);
	};
}
RgbColor.randomColor = function(c1, c2)
{
	c1 = c1?c1:new RgbColor(0,0,0);
	c2 = c2?c2:new RgbColor(255,255,255);
	
	r1 = c1.red();
	r2 = c2.red();

	g1 = c1.green();
	g2 = c2.green();

	b1 = c1.blue();
	b2 = c2.blue();

	return new RgbColor(
		Math.random()*(r2-r1)+r1,
		Math.random()*(g2-g1)+g1,
		Math.random()*(b2-b1)+b1
	);
};

function LightSource()
{
	var color, vector;
	
}