function edge() {
	// create bezier nodes for drawing a puzzle piece edge
	sign = 2*Math.round(Math.random())-1;
	this.a1 = 0.054*(2+Math.random());
	this.a2 = 0.054*(2+Math.random());
	this.b1 = 0.21*(sign+0.2*(Math.random()-0.5));
	this.b2 = 0.21*(sign+0.2*(Math.random()-0.5));
}

function puzzle(name) {
	// initialize a new puzzle
	this.n = 0;
	this.name = name;
	this.piece = new Array();
	this.addpiece = function(id, imgsrc) { this.n++; this.piece.splice(Math.round((this.piece.length)*Math.random()), 0, new piece(id, imgsrc)); }
	this.draw = drawpuzzle;
}

function drawpuzzle(id, extra, edges, margin) {
	// check preloading of images
	loading = false;
	for (i in this.piece) {
		if (!this.piece[i]) loading = true;
		else if (!this.piece[i].image.complete) loading = true;
	}
	if (loading||(this.piece.length<this.n)) setTimeout(this.name+".draw(\""+id+"\", "+extra+", "+edges+", "+margin+")", 200);
	else {
		// id refers to html element on which the puzzle is to be drawn
		// extra is a multiplication factor for the minimum number of pieces to be created (float: >=1)
		// edges is a flag that sets flat edges for the whole puzzle (boolean: 1 is on, 0 is off)
		// margin is a part of the target height and width that should be treated as margin (float: <0.5)
		// get properties of the target area for the puzzle
		target = document.getElementById(id);
		width = target.clientWidth;
		height = target.clientHeight;
		ratio = width/height;
		// set target style properties
		target.style.overflow = "hidden";
		// determine puzzle grid dimensions
		this.nx = 1; while (this.nx*Math.round(this.nx/ratio) < extra*this.piece.length) this.nx++;
		this.ny = 1; while (this.ny*Math.round(this.ny*ratio) < extra*this.piece.length) this.ny++;
		if (this.nx*Math.round(this.nx/ratio) < this.ny*Math.round(this.ny*ratio)) this.ny = Math.round(this.nx/ratio); else this.nx = Math.round(this.ny*ratio);
		// create array of bezier nodes for horizontal and vertical puzzle piece edges
		this.hb = new Array(this.nx);
		this.vb = new Array(this.nx);
		for (x=0;x<this.nx+1;x++) {
			this.hb[x] = new Array(this.ny);
			this.vb[x] = new Array(this.ny);
		}
		for (x=0;x<this.nx+1;x++) for (y=0;y<this.ny+1;y++) {
			this.hb[x][y] = new edge();
			this.vb[x][y] = new edge();
		}
		// determine edge width
		stroke = Math.max(1,Math.max(height,width)/200);
		// create empty cells
		missing = this.nx*this.ny - this.piece.length;
		for (i=0;i<missing;i++) this.piece.splice(Math.round((this.piece.length)*Math.random()), 0, new piece(0,"empty.png"));
		// draw all non-empty pieces
		for (i in this.piece) if (this.piece[i].id) {
			// create canvas and set basic properties
			this.piece[i].canvas = document.createElement('canvas');
			this.piece[i].canvas.id = "piece" + i;
			this.piece[i].canvas.style.position = "absolute";
			w = (1 - 2*margin)*width/this.nx;
			h = (1 - 2*margin)*height/this.ny;
			this.piece[i].canvas.width = Math.round(w*5/3) - stroke + 2;
			this.piece[i].canvas.height = Math.round(h*5/3) - stroke + 2;
			this.piece[i].canvas.style.left = Math.round((i%this.nx)*w - w/3 + margin*width) + "px";
			this.piece[i].canvas.style.top = Math.round(parseInt(i/this.nx)*h - h/3 + margin*height) + "px";
			target.appendChild(this.piece[i].canvas);
			// set properties necessary for drawing routine
			this.piece[i].stroke = stroke;
			this.piece[i].left = (((i%this.nx)==0) && (edges==1)) ? 0 : this.vb[i%this.nx][parseInt(i/this.nx)+1];
			this.piece[i].right = (((i%this.nx)==this.nx-1) && (edges==1)) ? 0 : this.vb[i%this.nx+1][parseInt(i/this.nx)+1];
			this.piece[i].top = ((parseInt(i/this.nx)==0) && (edges==1)) ? 0 : this.hb[i%this.nx+1][parseInt(i/this.nx)];
			this.piece[i].bottom = ((parseInt(i/this.nx)==this.ny-1) && (edges==1)) ? 0 : this.hb[i%this.nx+1][parseInt(i/this.nx)+1];
			// draw the piece
			this.piece[i].draw();
		}
		// create trigger elements
		for (i in this.piece) if (this.piece[i].id) {
			this.piece[i].trigger = document.createElement('div');
			this.piece[i].trigger.id = "trigger" + i;
			this.piece[i].trigger.style.position = "absolute";
			this.piece[i].trigger.style.width = Math.round(w);
			this.piece[i].trigger.style.height = Math.round(h);
			this.piece[i].trigger.style.left = Math.round((i%this.nx)*w + margin*width - stroke + 2) + "px";
			this.piece[i].trigger.style.top = Math.round(parseInt(i/this.nx)*h + margin*height - stroke + 2) + "px";
			this.piece[i].trigger.onmouseover = new Function("document.getElementById(\"trigger"+i+"\").style.cursor = \"pointer\"");
			this.piece[i].trigger.onclick = new Function("window.location.href = window.location.pathname" + (this.piece[i].id==-1?"":" + \"?id=" + this.piece[i].id +"\""));
			// exploder backup
//			if (navigator.userAgent.indexOf("MSIE")>=0) this.piece[i].trigger.innerHTML = "<img src=\"images/" + this.piece[i].imgsrc + "\" style=\"width:" + w + "; height:" + h + ";\">";
			target.appendChild(this.piece[i].trigger);
		}
	}
}

function piece(id, imgsrc) {
	// create a new puzzle piece
	this.id = id;
	this.imgsrc = imgsrc;
	this.image = new Image();
	if (id) this.image.src = "images/" + imgsrc;
	this.draw = drawpiece;
}

function drawpiece() {
	// get basic drawing properties: width, height and 'context'
	dx = this.canvas.width;
	dy = this.canvas.height;
	ctx = this.canvas.getContext("2d");
	// fill canvas with transparent black
	ctx.fillStyle = "rgba(0,0,0,0)";
	ctx.fillRect(0,0,dx,dy);
	// set line properties
	ctx.strokeStyle = 'rgb(160,160,160)';
	ctx.lineWidth = this.stroke;
	// create a polynode path outlining the puzzle piece
	ctx.beginPath();
	ctx.moveTo(0.2*dx, 0.2*dy);
	// draw top side
	if (this.top!=0) {
		ctx.lineTo(0.44*dx, 0.2*dy);
		ctx.bezierCurveTo((0.44-this.top.a1)*dx, (0.2+this.top.b1)*dy, (0.56+this.top.a2)*dx, (0.2+this.top.b2)*dy, 0.56*dx, 0.2*dy);
	}
	ctx.lineTo(0.8*dx, 0.2*dy);
	// draw right side
	if (this.right!=0) {
		ctx.lineTo(0.8*dx, 0.44*dy);
		ctx.bezierCurveTo((0.8+this.right.b1)*dx, (0.44-this.right.a1)*dy, (0.8+this.right.b2)*dx, (0.56+this.right.a2)*dy, 0.8*dx, 0.56*dy);
	}
	ctx.lineTo(0.8*dx, 0.8*dy);
	// draw bottom side
	if (this.bottom!=0) {
		ctx.lineTo(0.56*dx, 0.8*dy);
		ctx.bezierCurveTo((0.56+this.bottom.a2)*dx, (0.8+this.bottom.b2)*dy, (0.44-this.bottom.a1)*dx, (0.8+this.bottom.b1)*dy, 0.44*dx, 0.8*dy);
	}
	ctx.lineTo(0.2*dx, 0.8*dy);
	// draw left side
	if (this.left!=0) {
		ctx.lineTo(0.2*dx, 0.56*dy);
		ctx.bezierCurveTo((0.2+this.left.b2)*dx, (0.56+this.left.a2)*dy, (0.2+this.left.b1)*dx, (0.44-this.left.a1)*dy, 0.2*dx, 0.44*dy);
	}
	ctx.lineTo(0.2*dx, 0.2*dy);
	ctx.closePath();
	// draw the outline and use the outline-path to clip the piece-image
	ctx.stroke();
	ctx.clip();
	ctx.drawImage(this.image, 0, 0, dx, dy);
}

