/*
 *
 *	SketchThing is Copyright Chris McCormick, 2009-2010.
 *	
 *	The source code is licensed under the terms of the AGPL.
 *	See the file COPYING.txt for details.
 *
 */

function sketchthing(surface, readonly) {
	// TODO: check for typeof surface is DIV
	// TODO: check for typeof surface is CANVAS
	
	//*** Initialise some variables ***//
	// whether we are modifiable or not
	var readonly = readonly;
	// last point in the line
	var lastPoint = false;
	// collection of all lines
	var lines = [];
	// current line being drawn
	var line = false;
	// reference to the HTML body
	var body = document.getElementsByTagName("BODY").item(0);
	// whether the pointer has been put on the paper or not
	var down = false;
	// callback for when this sketch changes (pen lifted after a line)
	this.onChanged = function () { };
	// so we can make closured callbacks work
	//var sketchthing = this;
	
	//*** Set up the canvas ***//
	var boss = document.getElementById(surface);
	var w = boss.offsetWidth - 2;
	var h = boss.offsetHeight - 2;
	boss.style.padding = "";
	//boss.style.overflow = "hidden";
	boss.innerHTML = "<canvas width='" + w + "' height='" + h + "' style='width: " + w + "; height: " + h + "; background: white;'></canvas>";
	// set the cursor to the pointer for IE to stop the flickering text cursor problem
	boss.style.cursor = "default";
	// get the actual canvas element
	var canvas = boss.childNodes[0];
        // apply IE fix
	if (typeof(G_vmlCanvasManager) != "undefined")
		G_vmlCanvasManager.initElement(canvas);
	// get the context to draw on
	var ctx = canvas.getContext('2d');
	// 2px wide black stroke
	ctx.lineWidth = Math.max(1, Math.min(Math.round(2 * boss.offsetWidth / 480), Math.round(2 * boss.offsetHeight / 480)));
	// stop the bug where lines on whole integers are blurred (processingjs fix)
	if (ctx.lineWidth < 2)
		ctx.translate(0.5, 0.5);
	ctx.strokeStyle = "black";
	
	//***
	//***	Data exchange functions (save/load)
	//***
	
	// gets the current drawing
	this.getData = function getData() {
		return {"size": [w, h], "lines": lines};
	}
	
	// get the data in JSON format
	this.getJSON = function getJSON() {
		jsonparts = [];
		for (var l = 0; l < lines.length; l++) {
			lineparts = [];
			for (var p = 0; p < lines[l].length; p++) {
				lineparts.push("[" + lines[l][p][0] + ", " + lines[l][p][1] + "]")
			}
			jsonparts.push("[" + lineparts.join(", ") + "]");
		}
		return '{"size": [' + w + ', ' + h + '], "lines": [' + jsonparts.join(", ") + ']}';
	}
	
	// set the data in this sketch
	this.setData = function setData(data) {
		// scale this sketch to the correct size
		var scale = Math.min(w / data['size'][0], h / data['size'][1]);
		// translate the sketch so the centres line up
		var translate = [0, 0];
		if (data['size'][0] * scale != w)
			translate[0] = Math.round((data['size'][0] * scale - w) / -2);
		else if (data['size'][1] * scale != h)
			translate[1] = Math.round((data['size'][1] * scale - h) / -2);
		// figure out the positions of our new lines
		lines = [];
		newlines = data['lines'];
		for (var l = 0; l < newlines.length; l++) {
			var newline = [];
			for (var p = 0; p < newlines[l].length; p++) {
				newline.push(translatepoint(scalepoint(newlines[l][p], scale), translate));
			}
			lines.push(newline);
		}
		// draw the lines
		redrawSketch();
	}
	
	//***
	//***	Helper functions
	//***
	
	// scale a point
	function scalepoint(point, scale) {
		return [Math.round(point[0] * scale), Math.round(point[1] * scale)];
	}
	
	// translate a point
	function translatepoint(point, translate) {
		return [point[0] + translate[0], point[1] + translate[1]];
	}
	
	// clear the sketch and start from scratch
	this.clear = function clear() {
		clearSketch();
		lines = [];
		line = false;
	}
	
	// destroy this sketch by removing the canvas element
	this.destroy = function destroy() {
		this.clear();
		boss.innerHTML = "";
	}
	
	// find the absolute position of elements
	// http://blog.firetree.net/2005/07/04/javascript-find-position/
	// also see 'quirks mode'
	function findPosX(obj) {
		var curleft = 0;
		if (obj.offsetParent) {
			while (1) {
				curleft += obj.offsetLeft;
				if(!obj.offsetParent)
					break;
				obj = obj.offsetParent;
			}
		} else if (obj.x) {
			curleft += obj.x;
		}
		return curleft;
	}
	
	function findPosY(obj) {
		var curtop = 0;
		if (obj.offsetParent) {
			while(1) {
				curtop += obj.offsetTop;
				if(!obj.offsetParent)
					break;
				obj = obj.offsetParent;
			}
		} else if(obj.y) {
			curtop += obj.y;
		}
		return curtop;
	}

	// cross platform way to find the position relative to the page
        function findPagePos(ev) {
		var docX=0;
		var docY=0;
		if (ev.pageX == null) {
			// IE case
			var d= (document.documentElement && 
			      document.documentElement.scrollLeft != null) ?
			     document.documentElement : document.body;
			docX = ev.clientX + d.scrollLeft;
			docY = ev.clientY + d.scrollTop;
		} else {
			// all other browsers
			docX = ev.pageX;
			docY = ev.pageY;
		}
		return [docX, docY];
	}

	// get the position of the triggered event
	function getPointerPosition(ev) {
		// Get the mouse position relative to the canvas element.
		if (ev.clientX || ev.clientX == 0) { // firefox and IE
			var newpos = findPagePos(ev);
			mouseX = newpos[0] - findPosX(canvas);
			mouseY = newpos[1] - findPosY(canvas);
		} else if (ev.touches) { // iSuck devices
			// Only deal with one finger
			if (ev.touches.length == 1) {
				// first finger
				var touch = ev.touches[0];
				mouseX = touch.pageX;
				mouseY = touch.pageY;
			}	
		}
		return [mouseX, mouseY];
	}
	
	// is a point in the canvas
	function onCanvas(point) {
		return point[0] >= 0 && point[0] <= w && point[1] >= 0 && point[1] <= h;
	}
	
	// test if two numbers have the same sign
	function sameSign(a, b) {
		return a * b >= 0;
	}
	
	// intersection between two lines
	// http://alienryderflex.com/intersect/
	function lineIntersection(p0, p1, p2, p3) {
		var x1 = p0[0];
		var y1 = p0[1];
		var x2 = p1[0];
		var y2 = p1[1];
		var x3 = p2[0];
		var y3 = p2[1];
		var x4 = p3[0];
		var y4 = p3[1];
		
		var offset, x, y;
		// Compute a1, b1, c1, where line joining points 1 and 2
		// is "a1 x + b1 y + c1 = 0".
		var a1 = y2 - y1;
		var b1 = x1 - x2;
		var c1 = (x2 * y1) - (x1 * y2);
		
		// Compute r3 and r4.
		var r3 = ((a1 * x3) + (b1 * y3) + c1);
		var r4 = ((a1 * x4) + (b1 * y4) + c1);
		
		// Check signs of r3 and r4. If both point 3 and point 4 lie on
		// same side of line 1, the line segments do not intersect.
		if ((r3 != 0) && (r4 != 0) && sameSign(r3, r4)) return false;
		
		// Compute a2, b2, c2
		var a2 = y4 - y3;
		var b2 = x3 - x4;
		var c2 = (x4 * y3) - (x3 * y4);
		
		// Compute r1 and r2
		var r1 = (a2 * x1) + (b2 * y1) + c2;
		var r2 = (a2 * x2) + (b2 * y2) + c2;
		
		// Check signs of r1 and r2. If both point 1 and point 2 lie
		// on same side of second line segment, the line segments do
		// not intersect.
		if ((r1 != 0) && (r2 != 0) && sameSign(r1, r2)) return false;
		
		// Line segments intersect: compute intersection point.
		var denom = (a1 * b2) - (a2 * b1);
		
		if (denom == 0)	return false;
		
		if (denom < 0) offset = -denom / 2;
		else offset = denom / 2;
		
		// The denom/2 is to get rounding instead of truncating. It
		// is added or subtracted to the numerator, depending upon the
		// sign of the numerator.
		var num = (b1 * c2) - (b2 * c1);
		if (num < 0) x = (num - offset) / denom;
		else x = (num + offset) / denom;
		
		var num = (a2 * c1) - (a1 * c2);
		if (num < 0) y = ( num - offset) / denom;
		else y = (num + offset) / denom;
		
		// lines_intersect
		return [Math.round(x), Math.round(y)];
	}
	
	//***
	//***	Drawing functions
	//***
	
	function drawAll() {
		for (var l = 0; l < lines.length; l++) {
			if (lines[l].length == 1) {
				drawPoint(lines[l][0]);
			} else {
				drawLines(lines[l]);
			}
		}
	}
	
	// clear the whole canvas
	function clearSketch() {
		ctx.clearRect(0, 0, w, h);
	}
	
	// draw the line
	function drawLines(l) {
		// otherwise draw the line
		ctx.beginPath();
		ctx.moveTo(l[0][0], l[0][1]);
		for (var p = 0; p < l.length; p++) {
			ctx.lineTo(l[p][0], l[p][1]);
		}
		//ctx.closePath();
		ctx.stroke();
	}
	
	// draw a point
	function drawPoint(point) {
		var oldWidth = ctx.lineWidth;
		ctx.lineWidth = 0;
		ctx.beginPath();
		if (oldWidth <= 1)
			ctx.rect(Math.round(point[0] - oldWidth / 2) - 0.5, Math.round(point[1] - oldWidth / 2) - 0.5, Math.round(oldWidth), Math.round(oldWidth));
		else
			ctx.arc(point[0], point[1], Math.max(1, oldWidth / 2), 0, Math.PI * 2, false);
		ctx.closePath();
		ctx.fill();
		ctx.lineWidth = oldWidth;
	}
	
	function redrawSketch() {
		clearSketch();
		drawAll();
	}
	
	//***
	//***	Line management methods
	//***
	
	// make part of a line
	function makeLine(oldPoint, newPoint) {
		if (oldPoint) {
			// if both points are outside the canvas, only keep the last one
			if (!onCanvas(oldPoint) && !onCanvas(newPoint)) {
				endLine();
				startLine(newPoint);
			} else {
				lastPoint = newPoint;
				line.push(newPoint);
			}
		}
	}
	
	// start a line on the canvas
	function startLine(point) {
		lastPoint = point;
		line = [lastPoint];
		lines.push(line);
	}
	
	var sides = [
		[[0, 0], [w, 0]],
		[[0, 0], [0, h]],
		[[w, 0], [w, h]],
		[[0, h], [w, h]],
	];
	// end a line
	function endLine() {
		// check if there is only one point and it's outside the canvas
		if (line.length == 1 && !onCanvas(line[0])) {
			line = false;
			lines.pop();
		} else {
			// check if the first or last point is outside the rectangle
			var lastline = lines[lines.length - 1];
			if (lastline) {
				var startend = [0, lastline.length - 1, lastline.length - 1, lastline.length - 2];
				for (var p=0; p<2; p++) {
					var m = startend[p * 2];
					var o = startend[p * 2 + 1];
					if (!onCanvas(lastline[m])) {
						for (var s in sides) {
							var intersect = lineIntersection(lastline[m], lastline[o], sides[s][0], sides[s][1]);
							if (intersect)
								lastline[m] = intersect;
						}
					}
				}
			}
		}
		lastPoint = false;
		sketchthing.onChanged();
	}
	
	//***
	//***	Actual events
	//***
	
	// pointer pressed event
	this.onmousedown = function onmousedown(ev) {
		var ev = (ev) ? ev : window.event;
		if (!ev.target || ev.target == canvas) {
			startLine(getPointerPosition(ev));
			redrawSketch();
			cancelEvent(ev);
			down = true;
			return false;
		}
	}
	attachEvent.call(this, "mousedown");
	
	// pointer released event
	this.onmouseup = function onmouseup(ev) {
		var ev = (ev) ? ev : window.event;
		endLine();
		down = false;
		cancelEvent(ev);
		return false;
	}
	attachEvent.call(this, "mouseup");
	
	// pointer moved event
	this.onmousemove = function onmousemove(ev) {
		var ev = (ev) ? ev : window.event;
		makeLine(lastPoint, getPointerPosition(ev));
		redrawSketch();
		cancelEvent(ev);
		return false;
	}
	attachEvent.call(this, "mousemove");
	
	// pointer has left the paper
	this.onmouseout = function onmouseout(ev) {
		var ev = (ev) ? ev : window.event;
		if (down) {
			makeLine(lastPoint, getPointerPosition(ev));
			endLine();
			redrawSketch();
		}
		cancelEvent(ev);
		return false;
	}
	attachEvent.call(this, "mouseout");
	
	// bindings for the iSuck devices
	this.ontouchstart = this.onmousedown;
	this.ontouchmove = this.onmousemove;
	this.ontouchend = this.onmouseup;
	attachEvent.call(this, "touchstart");
	attachEvent.call(this, "touchmove");
	attachEvent.call(this, "touchend");
	
	// pointer has entered the paper
	this.onmouseover = function onmouseover(ev) {
		var ev = (ev) ? ev : window.event;
		// if the pointer is down then start a new line
		if (down) {
			startLine(getPointerPosition(ev));
			redrawSketch();
		}
		cancelEvent(ev);
		return false;
	}
	attachEvent.call(this, "mouseover");
	
	// helper function to attach events in a cross platform way
	function attachEvent(name) {
		// for some reason key events don't work on the canvas in firefox
		//if (name.indexOf("key") == 0)
		//	boss = document;
		//what = canvas;
		if (!readonly) {
			// if we're on explorer, use the canvas
			if (typeof(G_vmlCanvasManager) != "undefined")
				var what = canvas;
			else
				var what = document;
			if ( what.addEventListener ) {
				what.addEventListener(name, this["on" + name], false);
			} else if ( what.attachEvent ) {
				what.attachEvent("on" + name, this["on" + name]);
			} else {
				what["on" + name] = this["on" + name];
			}
		}
	}
	
	// helper function to cancel an event
	function cancelEvent(ev) {
		if (ev.stopPropagation)
			ev.stopPropagation();
		if (ev.preventDefault)
			ev.preventDefault();
		// otherwise set the cancelBubble property of the original event to true (IE)
		ev.cancelBubble = true;
	}
}
