//<script type="text/javascript">

Graphics = JSX.Class.create({
	name: 'Graphics',
	constructor: function(canvas) {
		this.canvas = canvas;
		this.cache = new Array;
		this.shapes = new Object;
		this.nObject = 0;

		// defaults
		this.penColor = "black";
		this.zIndex = 0;
	},

	members: {
		plot: function(x,y, w,h, c) {

			if ( !c ) c = this.penColor;

			// detect canvas
			if ( !this.oCanvas ) {
				if ( this.canvas == undefined ) {
					this.oCanvas = document.body;
				} else if ( typeof(this.canvas) == 'string' ) {
					this.oCanvas = document.getElementById(this.canvas);
				} else {
					this.oCanvas = this.canvas;
				}
			}

			// retrieve DIV
			var oDiv;
			if ( this.cache.length ) {
				oDiv = this.cache.pop();
			} else {
				oDiv = document.createElement('div');
				this.oCanvas.appendChild(oDiv);

				oDiv.style.position = "absolute";
				oDiv.style.margin = "0px";
				oDiv.style.padding = "0px";
				oDiv.style.overflow = "hidden";
				oDiv.style.border = "0px";
			}

			// set attributes
			oDiv.style.zIndex = this.zIndex;
			oDiv.style.backgroundColor = c;
			
			// reset some other attributes
			oDiv.style.cursor = '';
			oDiv.onmousedown = null;
			oDiv.onmousemove = null;
			oDiv.onmouseup = null;
			
			// set position
			oDiv.style.left = x + 'px';
			oDiv.style.top = y + 'px';
			oDiv.style.width = w + "px";
			oDiv.style.height = h + "px";

			oDiv.style.visibility = "visible";
			
			return oDiv;
		},

		releasePlotElement: function(oDiv) {
			oDiv.style.visibility = "hidden";
			this.cache.push(oDiv);
		},

		addShape: function(shape) {
			shape.setCanvas(this);
			shape.graphicsID = this.nObject;
			this.shapes[this.nObject] = shape;
			this.nObject++;
			shape.draw();
			return shape;
		},

		removeShape: function(shape) {
			if ( (shape instanceof Object) && 
				 (this.shapes[shape.graphicsID] == shape) ) {
				
				shape.undraw();
				delete this.shapes[shape.graphicsID];
				delete shape.setCanvas(null);
			}
		},
		clear: function() {
			for ( var i in this.shapes ) {
				this.removeShape(this.shapes[i]);
			}
		},
		createBitmap: function() {
			var bitmap = new Graphics.Bitmap();
			bitmap.setCanvas(this);
			return bitmap;
		},
		drawPoint: function(x,y) {
			return this.addShape(new Graphics.Point(x,y))
		},
		drawLine: function(x1,y1,x2,y2) {
			return this.addShape(new Graphics.Line(x1,y1,x2,y2))
		},
		drawCircle: function(x,y,r) {
			return this.addShape(new Graphics.Circle(x,y,r))
		},
		drawEllipse: function(x,y, rx,ry) {
			return this.addShape(new Graphics.Ellipse(x,y, rx,ry));
		},
		drawBezierCurve: function(points) {
			return this.addShape(new Graphics.BezierCurve(points))
		},
		fillRectangle: function(x,y,w,h) {
			return this.addShape(new Graphics.FillRectangle(x,y,w,h))
		}
	}
});

//=============================================================================
// Shape - base class to all graphics shapes
Graphics.Shape = JSX.Class.create({
	name: 'Shape',
	constructor: function() {},
	members: {
		setCanvas: function(canvas) {
			if ( canvas ) this.canvas = canvas;
			else delete this.canvas;
		},

		addPlot: function(plot) {
			if ( !this.plots ) this.plots = new Array();
			this.plots.push(plot);
		},
		plot: function(x,y,wx,wy,c) {
			this.addPlot(this.canvas.plot(x, y, wx, wy, c));
		},

		draw: function() {
			this.bitmap = this.canvas.createBitmap();
			this.paint(this.bitmap);
			this.bitmap.draw();
		},
		paint: function(bitmap) {
		},
		undraw: function() {
			if ( this.plots ) {
				while ( this.plots.length ) {
					this.canvas.releasePlotElement(this.plots.pop());
				}
				delete this.plots;
			}
			if ( this.bitmap ) {
				this.bitmap.clear();
				this.bitmap.undraw();
				delete this.bitmap;
			}
		},

		setMouseDown: function(onMouseDown) {
			if ( this.plots ) {
				for ( var i = 0; i < this.plots.length; i++ ) {
					this.plots[i].onmousedown = onMouseDown;
				}
			}
			if ( this.bitmap ) {
				this.bitmap.setMouseDown(onMouseDown);
			}
		},
		setMouseMove: function(onMouseMove) {
			if ( this.plots ) {
				for ( var i = 0; i < this.plots.length; i++ ) {
					this.plots[i].onmousemove = onMouseMove;
				}
			}
			if ( this.bitmap ) {
				this.bitmap.setMouseMove(onMouseMove);
			}
		},
		setMouseUp: function(onMouseUp) {
			if ( this.plots ) {
				for ( var i = 0; i < this.plots.length; i++ ) {
					this.plots[i].onmouseup = onMouseUp;
				}
			}
			if ( this.bitmap ) {
				this.bitmap.setMouseUp(onMouseUp);
			}
		},
		setStyle: function(style, value) {
			if ( this.plots ) {
				for ( var i = 0; i < this.plots.length; i++ ) {
					this.plots[i].style[style] = value;
				}
			}
			if ( this.bitmap ) {
				this.bitmap.setStyle(style, value);
			}
		}
	}
});

//=============================================================================
// Bitmap - a hidden canvas to draw images on
Graphics.Bitmap = JSX.Class.create({
	name: 'Bitmap',
	base: Graphics.Shape,
	constructor: function() {
		// 2D sparse array of pixels
		this.lines = new Array();
		
		// linked list of pixels
		this.head = null;
	},
	statics: {
		pixelPool: null,
		allocPixel: function(x,y, c) {
			var Bitmap = Graphics.Bitmap; // a bit like a with statement.
			
			if ( Bitmap.pixelPool ) {
				var pixel = Bitmap.pixelPool;
				Bitmap.pixelPool = pixel.next;
				pixel.x = x;
				pixel.y = y;
				pixel.c = c;
				pixel.next = null;
			} else {
				var pixel = {
					x:x, y:y,
					c:c,
					next: null,
					mark: false
				};
			}
			
			return pixel;
		},
		freePixel: function(pixel) {
			var Bitmap = Graphics.Bitmap; // a bit like a with statement.

			pixel.next = Bitmap.pixelPool;
			Bitmap.pixelPool = pixel;
		}
	},
	members: {		
		draw: function() {
			// unmark all pixels
			var count = 0;
			for ( var pixel = this.head; pixel != null; pixel = pixel.next ) {
				count++;
				pixel.mark = false;
			}
			
			count = 0;
			// starting with the first pixel, try to find largest inclusive rect
			for ( pixel = this.head; pixel != null; pixel = pixel.next ) {
				// no need to redo marked pixels
				if ( pixel.mark ) continue;

				var area = new Graphics.Bitmap.Area(pixel, this);
				area.expand();		
				
				this.Shape.prototype.plot.call(this, area.x1, area.y1, 1+area.x2-area.x1, 1+area.y2-area.y1, pixel.c);
				area.mark();
				count++;
			}	
		},
		clear: function() {
			var Bitmap = Graphics.Bitmap;
			
			var pixel = this.head;
			this.head = null;
			while ( pixel ) {
				var next = pixel.next;
				Bitmap.freePixel(pixel);
				pixel = next;	
			}
		},

		// want shapes to draw onto bitmaps just like on graphics
		// shapes may even prefer to draw onto bitmaps to let bitmap draw onto graphics
		// shapes have algorithm to decide how they look (colour, pen size & shape, etc)
		// 
		getPixel: function(x,y) {
			var line = this.lines[y];
			if ( !line ) return null;
			return line[x];
		},

		plot: function(x,y, w,h, c) {
			if ( !w ) w = 1;
			if ( !h ) h = 1;
			if ( !c ) c = this.canvas.penColor;
			for ( var j = y; j < y + h; j++ ) {
				var line = this.lines[j];
				if ( !line ) line = this.lines[y] = new Array();
				for ( var i = x; i < x + w; i++ ) {
					var pixel = line[i];
					if ( pixel ) {
						pixel.c = c;
					} else {
						pixel = Graphics.Bitmap.allocPixel(i,j,c);

						// insert into 2D Array
						line[i] = pixel;
						
						// insert into list
						pixel.next = this.head;
						this.head = pixel;				
					}
				}
			}
		}
	}
});

Graphics.Bitmap.Area = JSX.Class.create({
	name: 'Area',
	constructor: function(pixel, bitmap) {
		this.bitmap = bitmap;
		this.x1 = this.x2 = pixel.x;
		this.y1 = this.y2 = pixel.y;
		this.c = pixel.c;
	},
	members: {
		destroy: function() {
			delete this.bitmap;
		},
		fCount: [
			function(a) { return {x1:a.x1,y1:a.y1-1,x2:a.x2,y2:a.y1-1,dx:1,dy:0}; },
			function(a) { return {x1:a.x2+1,y1:a.y1,x2:a.x2+1,y2:a.y2,dx:0,dy:1}; },
			function(a) { return {x1:a.x1,y1:a.y2+1,x2:a.x2,y2:a.y2+1,dx:1,dy:0}; },
			function(a) { return {x1:a.x1-1,y1:a.y1,x2:a.x1-1,y2:a.y2,dx:0,dy:1}; }
		],
		fExpand: [
			function(a) { return {x1:a.x1,y1:a.y1-1,x2:a.x2,y2:a.y2}; },
			function(a) { return {x1:a.x1,y1:a.y1,x2:a.x2+1,y2:a.y2}; },
			function(a) { return {x1:a.x1,y1:a.y1,x2:a.x2,y2:a.y2+1}; },
			function(a) { return {x1:a.x1-1,y1:a.y1,x2:a.x2,y2:a.y2}; }
		],
		expand: function() {
			while ( true ) {
				var index = -1;
				var count = -1;
				for ( var i = 0; i < 4; i++ ) {
					var pc = this.countPixels(this.fCount[i]);
					if ( pc.valid && (pc.count > count) ) {
						count = pc.count;
						index = i;
					}
				}
				if ( index >= 0 ) {
					var e = this.fExpand[index](this);
					this.x1 = e.x1;
					this.y1 = e.y1;
					this.x2 = e.x2;
					this.y2 = e.y2;
				} else {
					return;
				}
			}
		},
		countPixels: function(f) {
			var e = f(this);
			var count = 0;
			while ( (e.x1 != e.x2) || (e.y1 != e.y2) ) {
				var pixel = this.bitmap.getPixel(e.x1,e.y1);
				if ( !pixel || (pixel.c != this.c) ) return {valid:false};
				if ( !pixel.mark ) count++;
				e.x1 += e.dx;
				e.y1 += e.dy;
			}
			pixel = this.bitmap.getPixel(e.x1,e.y1);
			if ( !pixel || (pixel.c != this.c) ) return {valid:false};
			if ( !pixel.mark ) count++;
			return {valid:true,count:count};
		},
		mark: function() {
			for ( var i = this.x1; i <= this.x2; i++ ) {
				for ( var j = this.y1; j <= this.y2; j++ ) {
					this.bitmap.getPixel(i,j).mark = true;
				}
			}
		}
	}
});

//=============================================================================
// Point

Graphics.Point = JSX.Class.create({
	name: 'Point',
	base: Graphics.Shape,
	constructor: function(x,y) {
		this.Shape.call(this);

		this.x = x;
		this.y = y;
	},
	members: {
		paint: function(bitmap) {
			bitmap.plot(this.x,this.y,1,1);
		}
	}
});

//=============================================================================
// Line

Graphics.Line = JSX.Class.create({
	name: 'Line',
	base: Graphics.Shape,
	constructor: function(x1,y1,x2,y2) {
		this.Shape.call(this);

		this.x1 = x1;
		this.y1 = y1;
		this.x2 = x2;
		this.y2 = y2;
	},
	members: {
		paint: function(bitmap) {
			var dx = this.x2 - this.x1;
			var dy = this.y2 - this.y1;
			var x = this.x1;
			var y = this.y1;

			var n = Math.max(Math.abs(dx),Math.abs(dy));
			dx = dx / n;
			dy = dy / n;
			for ( i = 0; i <= n; i++ ) {
				bitmap.plot(Math.round(x),Math.round(y),1,1);

				x += dx;
				y += dy;
			}
		}
	}
});

//=============================================================================
// Circle

Graphics.Circle = JSX.Class.create({
	name: 'Circle',
	base: Graphics.Shape,
	constructor: function(x,y,r) {
		this.Shape.call(this);

		this.x = x;
		this.y = y;
		this.radius = r;
	},
	members: {
		paint: function(bitmap) {
			var r2 = this.radius * this.radius;
			var x = 0;
			var y = this.radius;

			while ( x <= y ) {
				bitmap.plot(Math.round(this.x + x), Math.round(this.y + y), 1, 1);
				bitmap.plot(Math.round(this.x - x), Math.round(this.y + y), 1, 1);
				bitmap.plot(Math.round(this.x + x), Math.round(this.y - y), 1, 1);
				bitmap.plot(Math.round(this.x - x), Math.round(this.y - y), 1, 1);
				bitmap.plot(Math.round(this.x + y), Math.round(this.y + x), 1, 1);
				bitmap.plot(Math.round(this.x + y), Math.round(this.y - x), 1, 1);
				bitmap.plot(Math.round(this.x - y), Math.round(this.y + x), 1, 1);
				bitmap.plot(Math.round(this.x - y), Math.round(this.y - x), 1, 1);

				x++;
				y = Math.round(Math.sqrt(r2 - x*x));
			}
		}
	}
});

//=============================================================================
// Ellipse

Graphics.Ellipse = JSX.Class.create({
	name: 'Ellipse',
	base: Graphics.Shape,
	constructor: function(x,y, rx,ry) {
		this.Shape.call(this);

		this.x = x;
		this.y = y;
		this.rx = rx;
		this.ry = ry;
	},
	members: {
		paint: function(bitmap) {

			var rx2 = this.rx * this.rx;
			var ry2 = this.ry * this.ry;
			var x = 0;
			var y = this.ry;
			
			// x*x/rx*rx + y*y/ry*ry = 1
			var x = 0;
			var y = this.ry;
			while ( (y > 0) && (x < this.rx) ) {
				bitmap.plot(Math.round(this.x + x), Math.round(this.y + y), 1, 1);
				bitmap.plot(Math.round(this.x - x), Math.round(this.y + y), 1, 1);
				bitmap.plot(Math.round(this.x + x), Math.round(this.y - y), 1, 1);
				bitmap.plot(Math.round(this.x - x), Math.round(this.y - y), 1, 1);
				
				x++;
				y2 = Math.sqrt((1 - x*x/rx2)*ry2);
				if ( y - y2 > 1 ) break;
				y = y2;
			}

			x = this.rx;
			y = 0;
			while ( (x > 0) && (y < this.ry) ) {
				bitmap.plot(Math.round(this.x + x), Math.round(this.y + y), 1, 1);
				bitmap.plot(Math.round(this.x - x), Math.round(this.y + y), 1, 1);
				bitmap.plot(Math.round(this.x + x), Math.round(this.y - y), 1, 1);
				bitmap.plot(Math.round(this.x - x), Math.round(this.y - y), 1, 1);
				
				y++;
				x2 = Math.sqrt((1 - y*y/ry2)*rx2);
				if ( x - x2 > 1 ) break;
				x = x2;
			}
		}
	}
});

//=============================================================================
// Bezier Curve

Graphics.BezierCurve = JSX.Class.create({
	name: 'BezierCurve',
	base: Graphics.Shape,
	constructor: function(points) {
		this.Shape.call(this);

		this.points = points;
	},
	members: {
		factorial: function(n) {
			if ( n == 1 ) { return 1; }
			if ( this.factorial[n] ) return this.factorial[n];
			return this.factorial[n] = n * this.factorial(n-1);
		},
		pascal: function(n, i) {
			// n! / ( i! * (n - i)!)
			return this.factorial(n) /
					(this.factorial(i) * this.factorial(n-i));
		},

		createBezierFunctor: function(points) {
			var n = points.length;
			
			var sEqn = new Array();
			
			sEqn.push('this.x =');
			sEqn.push(' ' + points[0].x + ' * Math.pow(1-t,' + (n-1) + ')');
			for ( var i = 1; i < n-1; i++ ) {
				sEqn.push(' + ' + this.pascal(n-1,i).toString());
				sEqn.push(' * ' + points[i].x + ' * Math.pow(t,' + i + ')');
				sEqn.push(' * Math.pow(1-t,' + (n-(i+1)) + ')');
			}
			sEqn.push(' + ' + points[n-1].x + ' * Math.pow(t,' + (n-1) + ');');
			
			sEqn.push('this.y =');
			sEqn.push(' ' + points[0].y + ' * Math.pow(1-t,' + (n-1) + ')');
			for ( var i = 1; i < n-1; i++ ) {
				sEqn.push(' + ' + this.pascal(n-1,i).toString());
				sEqn.push(' * ' + points[i].y + ' * Math.pow(t,' + i + ')');
				sEqn.push(' * Math.pow(1-t,' + (n-(i+1)) + ')');
			}
			sEqn.push(' + ' + points[n-1].y + ' * Math.pow(t,' + (n-1) + ');');
			
			// return a Functor - a function object.
			// call B to set values x and y based on t
			return { x:0, y:0, Fn:new Function('t',sEqn.join('')) };
		},

		paint: function(bitmap) {
			var n = this.points.length;

			// special case where there is one control point
			if ( n == 1 ) {
				bitmap.plot(Math.round(this.points[0].x), Math.round(this.points[0].y));
				return;
			}

			var B = this.createBezierFunctor(this.points);

			// calculate over-estimate for curve length
			var l = 0;
			for ( i = 0; i < n-1; i++ ) {
				l += Math.max(
					Math.abs(this.points[i+1].x-this.points[i].x), 
					Math.abs(this.points[i+1].y-this.points[i].y)
				);
			}
			var dt = 1/l;
				
			// now iterate from 0 to 1
			for ( var t = 0; t < 1; t += dt ) {
				B.Fn(t);
				bitmap.plot(Math.round(B.x), Math.round(B.y));
			}
			bitmap.plot(Math.round(this.points[n-1].x, this.points[n-1].y));
		}
	}
});


//=============================================================================
// FillRectangle

Graphics.FillRectangle = JSX.Class.create({
	name: 'FillRectangle',
	base: Graphics.Shape,
	constructor: function(x,y,w,h) {
		this.Shape.call(this);

		this.x = x;
		this.y = y;
		this.w = w;
		this.h = h;
	},
	members: {
		paint: function() {
			bitmap.plot(this.x,this.y,this.w,this.h);
		}
	}
});


//</script>