var CanvasPlot = new Class({
	Implements: Options,
	
	formulas: [],
	resultCache: {},
	resultInProgrees: {},
	
	mouseX: null,
	mouseFrozen: false,
	
	options: {
		/* Origin Position */
		// To have crisp axis lines, see https://developer.mozilla.org/en/Canvas_tutorial/Applying_styles_and_colors#Line_styles
		xOffset: null,
		yOffset: null,
		/* Axis Scale */
		xScale: 20,
		yScale: 20,
		
		/* Background colour */
		backgroundStyle: 'white', //'transparent',
		
		/* Axis Styles */
		axisStyle: 'rgb(0,0,0)',
		axisWidth: 1,
		axisNotchLength: 2,
		axisDrawLabels: true,
		axisLabelStyle: 'rgb(0,0,0)',
		axisLabelHeight: 10, // This must be in this variable so the number can be used in calculations
		axisLabelFont: 'monospace',
		
		mouseLineStyle: 'black',
		
		/* Grid Styles */
		gridLineStyle: 'rgba(0,0,0, .5)',
		gridLineWidth: .25,
		
		/* Graph plot Styles */
		graphLineStyle: 'rgb(0,0,0)',
		graphLineWidth: 1,
		
		/* Max times to recurse */
		maxRecurse: 100
	},
	initialize: function(canvas, formulas, options) {
		this.elem = $(canvas);
		formulas = formulas || [];
		
		this.cellChangeBind = this.cellChange.bindWithEvent(this);
		this.keypressBind = this.cellKeypress.bindWithEvent(this);
		
		for (var i = 0; i < formulas.length; i++) {
			this.addFormula(formulas[i]);
		}
		
		this.setOptions(options);
		
		if (this.options.xOffset == null)
			this.options.xOffset = this.elem.width /2;
		if (this.options.yOffset == null)
			this.options.yOffset = this.elem.height /2;
		
		this.mouseEventBind = this.mouseEvent.bindWithEvent(this);
		this.elem.addEvent('mousemove', this.mouseEventBind);
		this.elem.addEvent('mouseover', this.mouseEventBind);
		this.elem.addEvent('mouseout', this.mouseEventBind);
		this.elem.addEvent('click', this.mouseEventBind);
		
		this.paint();
	},
	addFormula: function(formula) {
		formula.f.addEvent('change',this.cellChangeBind);
		formula.f.addEvent('keypress',this.keypressBind);
		formula.c.addEvent('change',this.cellChangeBind);
		formula.c.addEvent('keypress',this.keypressBind);
		
		this.formulas.push(formula);
	},	
	cellChange: function(event){
		var index = event.target.id;
		if (index.search('colour') == -1) {
			index = index.substr(index.indexOf('-') +1);
			delete this.resultCache[index];
		}
		
		this.paint();
	},
	cellKeypress: function(event){
		if (event.key == 'enter')
			this.cellChange(event);
	},
	mouseEvent: function(event){
		var oldMouseX = this.mouseX;
		var wasMouseFrozen = this.mouseFrozen;
		
		if (event.type == 'click' && this.mouseFrozen){
			this.mouseFrozen = false;
		} 
		
		if (this.mouseFrozen){
			return;
		} else {
			if (event.type == 'mouseout') {
				this.mouseX = null;
			} else {
				var pos = event.client;
				var ePos = this.elem.getPosition();
				var borders = this.elem.getStyle('border-width')
				borders = borders.replace('px','','g').split(' ').map(function(val){ return parseInt(val)});
				pos.x -= ePos.x + borders[3];
				
				var x = pos.x;
				var canvasWidth = this.elem.width;
				var pixelWidth = this.elem.getSize().x - borders[1] - borders[3];
				
				x -= (canvasWidth % 2) /2;
				if (x < 0 || x > pixelWidth){
					this.mouseX = null
				} else {
					if (pixelWidth == canvasWidth)
						this.mouseX = x;
					else
						this.mouseX = (x / pixelWidth) * canvasWidth;
				}
				
				if (event.type == 'click' && ! wasMouseFrozen) {
					this.mouseFrozen = true;
				}
			}
		}
		if (oldMouseX == this.mouseX)
			return;
			
		this.paint();
		
		if (this.mouseX == null) {
			$('value-x').set('html','');
			for (var i = 0; i < this.formulas.length; i++)
				this.formulas[i].v.set('html','');
		} else {
			var x = this.getFakeX(this.mouseX)
			$('value-x').set('html', x);

			for (var i = 0; i < this.formulas.length; i++) {
				var id = this.formulas[i].f.id;
				id = id.substr(id.indexOf('-') +1);
				
				if (this.resultCache[id] != null) {
					if (this.resultCache[id][this.mouseX] != null)
						this.formulas[i].v.set('html',this.resultCache[id][this.mouseX].y);
					else
						this.formulas[i].v.set('html',this.computeValueAt(x, this.formulas[i].f.value));
				} else {
					this.formulas[i].v.set('html','');
				}
			}
		}
	},
	paint: function() {
		var ctx = this.elem.getContext("2d");
		ctx.clearRect(0,0,this.elem.width,this.elem.height);
		
		ctx.fillStyle = this.options.backgroundStyle;
		ctx.fillRect(0,0,this.elem.width,this.elem.height);
		
		this.paintGrid(ctx);
		this.paintAxis(ctx);
		this.plotGraphs();
		this.drawMouseLine(ctx);
	},
	paintAxis: function(ctx) {
		var width = this.elem.width, height = this.elem.height;
		var offX = this.options.xOffset, offY = this.options.yOffset;
		var perX = this.options.xScale, perY = this.options.yScale;
		var l = this.options.axisNotchLength;
		
		ctx.lineWidth = this.options.axisWidth;
		ctx.strokeStyle = this.options.axisStyle;
		
		ctx.beginPath();
		if (offY >= 0 && offY <= height) {
			ctx.moveTo(0,     offY);
			ctx.lineTo(width, offY);
		}
		if (offX >= 0 && offX <= width) {
			ctx.moveTo(offX,  0);
			ctx.lineTo(offX,  height);
		}
		ctx.stroke();
		
		if (ctx.strokeText) {
			ctx.font = this.options.axisLabelHeight + 'px ' + this.options.axisLabelFont;
		} else if (ctx.mozDrawText) {
			ctx.mozTextStyle = this.options.axisLabelHeight + 'px ' + this.options.axisLabelFont;
		}
		
		ctx.beginPath();
		for (var i = offX % perX; i < width; i += perX){
			if (i == offX)
				continue;
				
			ctx.moveTo(i, offY - l);
			ctx.lineTo(i, offY + l);
			
			if (this.options.axisDrawLabels) {
				var text = this.getFakeX(i);
				if (ctx.strokeText) {
					ctx.textAlign = 'center';
					ctx.textBaseline = 'top';
					
					ctx.strokeText(text, i, offY + l);
					
				} else if (ctx.mozDrawText) {
					ctx.save();
					var w = ctx.mozMeasureText(text);
					
					ctx.translate(i - (w/2), offY + this.options.axisLabelHeight + l);
					
					ctx.fillStyle = this.options.axisLabelStyle;
					ctx.mozDrawText(text);
					ctx.restore();
				}
			}
		}
		ctx.stroke();
		
		ctx.beginPath();
		for (var j = offY % perY; j < height; j += perY){
			if (j == offY)
				continue;
				
			ctx.moveTo(offX - l, j);
			ctx.lineTo(offX + l, j);
			
			if (this.options.axisDrawLabels) {
				var text = this.getFakeY(j);
				if (ctx.strokeText) {
					ctx.textAlign = 'right';
					ctx.textBaseline = 'middle';
					ctx.strokeText(text, offX - l - 2, j - 1);

				} else if (ctx.mozDrawText) {
					ctx.save();
					var w = ctx.mozMeasureText(text);
					
					ctx.translate(offX - l - w -2, j + (this.options.axisLabelHeight /4));
					
					ctx.fillStyle = this.options.axisLabelStyle;
					ctx.mozDrawText(text);
					ctx.restore();
				}
			}
		}
		ctx.stroke();
	},
	paintGrid: function(ctx) {
		var width = this.elem.width, height = this.elem.height;
		var offX = this.options.xOffset, offY = this.options.yOffset;
		var perX = this.options.xScale, perY = this.options.yScale;
		
		ctx.lineWidth = this.options.gridLineWidth;
		ctx.strokeStyle = this.options.gridLineStyle;
		
		ctx.beginPath();
		for (var i = offX % perX; i < width; i += perX){
			if (i == offX)
				continue;
				
			ctx.moveTo(i, 0);
			ctx.lineTo(i, height);
		}
		ctx.stroke();
		ctx.beginPath();
		for (var j = offY % perY; j < height; j += perY){
			if (j == offY)
				continue;
				
			ctx.moveTo(0,     j);
			ctx.lineTo(width, j);
		}
		ctx.stroke();
	},
	drawMouseLine: function(ctx){
		if (this.mouseX == null)
			return;
			
		ctx.strokeStyle = this.options.mouseLineStyle;
		
		ctx.beginPath();
		ctx.moveTo(this.mouseX, 0);
		ctx.lineTo(this.mouseX, this.elem.height);
		ctx.stroke();
	},
	plotGraphs: function() {
		
		for (var i = 0; i < this.formulas.length; i++){
			var formula = this.formulas[i].f.value;
			var style = this.formulas[i].c.value;
			
			if (formula == '')
				continue;
				
			if (style == '')
				style = null;
			
			var id = this.formulas[i].f.id;
			id = id.substr(id.indexOf('-') +1);
			
			this.plotGraph(id, formula, this.options.graphLineStyle, style);
		}
	},
	plotGraph: function(id, formula, defaultStyle, style) {
		
		if (this.resultCache[id] != null) {
			this.plotGraphAsync(id, formula, defaultStyle, style, this.elem.width + 2, 2, this.elem.width, this.resultCache[id]);
		} else {
			var x = 0;
			var y;
			try {
				eval('y = ' + formula +';');
			} catch (e) {
				// TODO error message
				return;
			}
			
			this.plotGraphAsync(id, formula, defaultStyle, style, 0.5, 2, this.elem.width);
		}
	},
	plotGraphAsync: function(id, formula, defaultStyle, style, xVal, xStep, xMax, path, recurse){
		var maxRecurse = this.options.maxRecurse;
		if (recurse == null)
			recurse = 0;
		
		if (path == null) {
			if (this.resultInProgrees[id] != null) {
				return;
			} else {
				this.resultInProgrees[id] = true;
				path = {};
			}
		}
		
		if(xVal > xMax) { /* Done computing */
			var ctx = this.elem.getContext("2d");
			
			ctx.lineWidth = this.options.graphLineWidth;
			ctx.strokeStyle = defaultStyle;
			ctx.strokeStyle = style;
			
			ctx.beginPath();
			var jump = true;
			for (var i in path){
				if (path[i].y == Infinity)
					path[i].y = this.getFakeY(0);
				else if (path[i].y == -Infinity)
					path[i].y = this.getFakeY(this.elem.height);
					
				if (isNaN(path[i].y)) {
					jump = true;
				} else if (jump){
					ctx.moveTo(path[i].x,this.getCanvasY(path[i].y));
					jump = false;
				} else {
					ctx.lineTo(path[i].x, this.getCanvasY(path[i].y));
				}
			}
			ctx.stroke();
			
			this.resultCache[id] = path;
			delete this.resultInProgrees[id];
		} else {
			x = this.getFakeX(xVal);
			var y = this.computeValueAt(x, formula);
			
			path[xVal] = {x: xVal, y: y};
			xVal += xStep;
			
			if (recurse < maxRecurse) 
				this.plotGraphAsync(id, formula, defaultStyle, style, xVal, xStep, xMax, path, recurse +1);
			else
				this.plotGraphAsync.delay(1, this, [id, formula, defaultStyle, style, xVal, xStep, xMax, path]);
		}
	},
	computeValueAt: function(x, formula) {
		var y;
		eval('y = ' + formula +';');
		
		return y;
	},
	getCanvasX: function(xPoint) {
		var offX = this.options.xOffset;
		var perX = this.options.xScale;
		
		return offX + xPoint * perX;
	},
	getCanvasY: function(yPoint) {
		var offY = this.options.yOffset;
		var perY = this.options.yScale;
		
		return offY - yPoint * perY;
	},
	getFakeX: function(xPoint) {
		var offX = this.options.xOffset;
		var perX = this.options.xScale;
		
		return (xPoint - offX) / perX;
	},
	getFakeY: function(yPoint) {
		var offY = this.options.yOffset;
		var perY = this.options.yScale;
		
		return (offY - yPoint) / perY;
	}
});
