/**
 * jigsawpuzzle-rhill 3.0 (11-Jun-2009) (c) by Raymond Hill
 *
 * jigsawpuzzle-rhill licensed under a Creative Commons
 * Attribution-Noncommercial-Share Alike 2.5 Canada License.
 * Source: http://www.raymondhill.net/puzzle-rhill.php
 *
 * Sorry for the blend name, I didn't want to spend any time figuring
 * some unique, snappy name.
 *
 * I made this puzzle project simply because I was curious if
 * this could be done using the HTML5 <canvas> tag, and yet
 * still have a smooth and fluid in interface. As far as I can tell,
 * It can :)
 * 
 * The key to have responsive graphics interface is to redraw
 * *only* what is necessary. Thus, when moving a piece of puzzle,
 * it is necessary to redraw only the area intersecting the
 * former position, and the area intersecting the new position.
 * Seems trivial enough, but I couldn't find any canvas-based puzzle
 * which actually did that. Using minimal refresh allows huge amount
 * of puzzle piece: I tried 400 pieces, and except for the
 * initialization phase which takes longer to complete, the interface
 * stays smooth.
 *
 * What I learned:
 * - When using drawImage(), canvas to canvas operations are much
 *   faster than image to canvas operations. Thus it is a good idea
 *   to convert an image object into a canvas object if an image
 *   needs to be draw often onto a canvas object (this holds true
 *   for pattern objects.)
 * - The canvas element can handle subpixel positioning. However, there
 *   is a significant performance cost, and it should be avoided if
 *   wherever possible.
 *
 * Still a work in progress, as I wish to experiment further with
 * this little project.
 * 
 * Revisions:
 * 29-May-2009:
 *     - Added button to toggle on/off the showing of edge pieces only
 *     - Added shuffle button
 *     - When moving a piece, all other pieces fade
 *     - num_rows/num_cols respects more original image w/h ratio
 * 31-May-2009:
 *     - JSlint'ed
 *     - Gotten rid of cloneObject() as seen at
 *       http://www.andrewsellick.com/93/javascript-clone-object-function:
 *       The performance cost is way too high. Intelligent copy constructors
 *       are preferred
 * 04-Jun-2009:
 *     - Added rotation, this required rewrite of a good portion  of
 *       the code
 *     - Because of rotation, added code to correctly generate border
 *       shade for floating pieces
 *     - Added preview
 * 09-Jun-2009:
 *     - Pieces snap together rather than to the background (more like a real
 *       jigsaw puzzle.) This required polygons merging algorithm, including
 *       complex polygons -- polygons with holes in them.
 *     - <canvas>.isPointInPath() now replaces W. Randolph Franklin's PNPOLY.
 *     - up arrow (or 's') and down arrow (or 'd') sends a moved piece on top
 *       or behind others.
 */


/**
  Development notes

  Each PuzzlePiece object is decomposed in three type of tiles:

  Source tile descriptor:
    Information on where to find the tile in the source image:
    - Polygon describing the tile. Coords relative to source
      image = polySource

  Transient tile descriptor:
    Information about the extracted tile. Rotation is applied at
    this level:
    - Polygon describing the tile. Coords relative to the bounding box
      of the polygon = polyTransient
    - A copy of the pixels contained in the src polygon, after rotation.

  Display tile descriptor:
    Information about the location of the tile on screen:
    - Polygon describing the tile on screen = polyDisplay

  Each polygon object caches:
    - the bounding box containing the polygon
    - the centroid of the polygon

  If polySource is modified, the transient tile and onscreen tile
  descriptors must be recalculated.

  If polyTransient is modified (ex.: changing the rotation angle),
  the transient tile and onscreen tile descriptors must be recalculated

  If polyDisplay is changed, the display descriptor must be recalculated.
  However, this can be done efficiently when moving the object around, as
  this requires simply applying delta x and delta y to all cached coords.

  By convention, sTile = source tile descriptor,
  tTile = transient tile descriptor, dTile = display tile descriptor,
  or more generally s*, t*, d*
 */


/*global self*/
// Sound Manager initialization
if (self.soundManager){
	self.soundManager.debugMode=false; // disable debug output
	self.soundManager.onload=function(){
		self.soundManager.createSound({id:'snap1',url:'12842__schluppipuppie__klick_01.mp3',autoLoad:true});
		self.soundManager.createSound({id:'snap2',url:'12843__schluppipuppie__klick_02.mp3',autoLoad:true});
		self.soundManager.createSound({id:'applaud_low',url:'35102_m1rk0_applause_5sec.mp3',autoLoad:false});
		self.soundManager.createSound({id:'applaud_medium',url:'35104_m1rk0_applause_5sec.mp3',autoLoad:false});
		self.soundManager.createSound({id:'applaud_high',url:'60789_J.Zazvurek_Applause_9s.mp3',autoLoad:false});
		};
	}
else {
	self.soundManager={play:function(){}};
	}

var mthrnd = self.Math.round;
var mthceil = self.Math.ceil;
var mthrand = self.Math.random;
var mthsqrt = self.Math.sqrt;
var mthmx = self.Math.max;
var mthmn = self.Math.min;
var mthabs = self.Math.abs;
var mthatan = self.Math.atan2;
var debug_on = true;

// helper functions
function stdout(s,clear) {
	var consoleObj = self.document.getElementById('console');
	if ( consoleObj ) {
		if ( clear ) {
			consoleObj.innerHTML = "";
			}
		consoleObj.innerHTML += s + "<br>";
		}
	}
function stderr(s) {
	if ( debug_on ) {
		stdout(s);
		}
	}
// Point object
function Point(a,b,c) {
	// a=x,b=y, c=id
	if (b!==undefined) {
		this.x=a;
		this.y=b;
		this.id=(c!==undefined)?c:0;
		}
	// a=Point or {x:?,y:?,id:?}
	else if (a!==undefined&&a) {
		this.x=a.x;
		this.y=a.y;
		this.id=(a.id!==undefined)?a.id:0;		
		}
	// empty
	else {
		this.x=this.y=this.id=0;
		}
	}
Point.prototype.toString = function() {
	return "{x:"+this.x+",y:"+this.y+",id:"+this.id+"}";
	};
Point.prototype.toHashkey = function() {
	// We could use toString(), but I am concerned with
	// the performance of Polygon.merge(). As for now
	// I have no idea if its really that much of an
	// improvement, but I figure the shorter the string
	// used as a hash key, the better. This also reduce
	// the number of concatenations from 6 to 2. Ultimately,
	// I could cache the hash key..
	// Ah, there is this:
	//  http://www.softwaresecretweapons.com/jspwiki/javascriptstringconcatenation
	return this.x+"_"+this.y;
	};
Point.prototype.clone = function() {
	return new Point(this);
	};
Point.prototype.offset = function(dx,dy) {
	this.x+=dx; this.y+=dy;
	};
Point.prototype.set = function(a) {
	this.x=a.x;
	this.y=a.y;
	this.id=(a.id!==undefined)?a.id:0;
	};
Point.prototype.compare = function(other,strict) {
	return this.x == other.x && this.y == other.y && (!strict || this.id == other.id);
	};

// Segment object
function Segment(a,b) {
	this.ptA = new Point(a);
	this.ptB = new Point(b);
	}
Segment.prototype.toString = function() {
	return "["+this.ptA+","+this.ptB+"]";
	};
Segment.prototype.compare = function(other) {
	return (this.ptA.compare(other.ptA) && this.ptB.compare(other.ptB)) || (this.ptA.compare(other.ptB) && this.ptB.compare(other.ptA));
	};

// Bounding box object
function Bbox(a,b,c,d) {
	// a=x1,b=y1,c=x2,d=y2
	if (d!==undefined) {
		this.tl=new Point({x:a,y:b});
		this.br=new Point({x:c,y:d});
		}
	// a=Point or {x:?,y:?},b=Point or {x:?,y:?}
	else if (b!==undefined) {
		var mn=Math.min;
		var mx=Math.max;
		this.tl=new Point({x:mn(a.x,b.x),y:mn(a.y,b.y)});
		this.br=new Point({x:mx(a.x,b.x),y:mx(a.y,b.y)});
		}
	// a=Bbox or {tl:{x:?,y:?},br:{x:?,y:?}}
	else if (a) {
		this.tl=new Point(a.tl);
		this.br=new Point(a.br);
		}
	// empty
	else {
		this.tl=new Point();
		this.br=new Point();
		}
	}
Bbox.prototype.toString = function() {
	return "{tl:"+this.tl+",br:"+this.br+"}";
	};
Bbox.prototype.clone = function() {
	return new Bbox(this);
	};
Bbox.prototype.getTopleft = function() {
	return new Point(this.tl);
	};	
Bbox.prototype.getBottomright = function() {
	return new Point(this.br);
	};
Bbox.prototype.unionPoint = function(p) {
	var mn=Math.min;
	var mx=Math.max;
	this.tl.x=mn(this.tl.x,p.x);
	this.tl.y=mn(this.tl.y,p.y);
	this.br.x=mx(this.br.x,p.x);
	this.br.y=mx(this.br.y,p.y);
	};
Bbox.prototype.width = function() {
	return this.br.x-this.tl.x;
	};
Bbox.prototype.height = function() {
	return this.br.y-this.tl.y;
	};
Bbox.prototype.offset = function(dx,dy) {
	this.tl.offset(dx,dy);
	this.br.offset(dx,dy);
	};
Bbox.prototype.set = function(a) {
	// array of Points
	var mx = self.Math.max;
	var mn = self.Math.min;
	this.tl.x = this.br.x = a[0].x;
	this.tl.y = this.br.y = a[0].y;
	for ( var i=1; i<a.length; i++ ) {
		var p = a[i];
		this.tl.x = mn(this.tl.x,p.x);
		this.tl.y = mn(this.tl.y,p.y);
		this.br.x = mx(this.br.x,p.x);
		this.br.y = mx(this.br.y,p.y);
		}
	};
Bbox.prototype.pointIn = function(p) {
	return p.x > this.tl.x && p.x < this.br.x && p.y > this.tl.y && p.y < this.br.y;
	};
Bbox.prototype.doesIntersect = function(bb) {
	var mn = self.Math.min;
	var mx = self.Math.max;
	return (mn(bb.br.x,this.br.x)-mx(bb.tl.x,this.tl.x)) > 0 && (mn(bb.br.y,this.br.y)-mx(bb.tl.y,this.tl.y)) > 0;
	};
Bbox.prototype.union = function(other) {
	// this bbox is empty
	if ( this.isEmpty() ) {
		this.tl = new Point(other.tl);
		this.br = new Point(other.br);
		}
	// union only if other bbox is not empty
	else if ( !other.isEmpty() ) {
		var mn = self.Math.min;
		var mx = self.Math.max;
		this.tl.x = mn(this.tl.x,other.tl.x);
		this.tl.y = mn(this.tl.y,other.tl.y);
		this.br.x = mx(this.br.x,other.br.x);
		this.br.y = mx(this.br.y,other.br.y);
		}
	return this;
	};
Bbox.prototype.inflate = function(a) {
	this.tl.x-=a;
	this.tl.y-=a;
	this.br.x+=a;
	this.br.y+=a;
	};
Bbox.prototype.isEmpty = function() {
	return this.width() <= 0 || this.height() <= 0;
	};
Bbox.prototype.toCanvasPath = function(ctx) {
	ctx.rect(this.tl.x,this.tl.y,this.width(),this.height());
	};

// Region object (collection of [todo:non-overlapping] bounding boxes
function Region() {
	this.bboxes=[];
	}
Region.prototype.add = function(tl,br) {
	this.bboxes.push(new Bbox(tl,br));
	};
Region.prototype.fill = function(ctx,fillStyle,clip) {
	ctx.fillStyle = fillStyle;
	for ( var i=0; i<this.bboxes.length; i++ ) {
		var bbox = this.bboxes[i];
		if ( clip === undefined || !clip || bbox.doesIntersect(clip) ) {
			ctx.fillRect(bbox.tl.x,bbox.tl.y,bbox.width(),bbox.height());
			}
		}
	};

// Contour object, a collection of points forming a closed figure
// Clockwise = filled, counterclockwise = hollow
function Contour(a) {
	this.pts = []; // no points
	if (a) {
		var iPt; var nPts;
		if ( a instanceof Contour ) {
			var pts = a.pts;
			nPts = pts.length;
			for ( iPt=0; iPt<nPts; iPt++ ) {
				this.pts.push(pts[iPt].clone());
				}
			if ( a.bbox ) {
				this.bbox = a.bbox.clone();
				}
			this.area = a.area;
			this.hole = a.hole; // may sound funny..
			}
		else if ( a instanceof Array ) {
			nPts = a.length;
			for ( iPt=0; iPt<nPts; iPt++ ) {
				this.pts.push(a[iPt].clone());
				}
			}
		else {
			alert("Contour ctor: Unknown arg 'a'");
			}
		}
	}
Contour.prototype.clone = function() {
	return new Contour(this);
	};
Contour.prototype.addPoint = function(p) {
	this.pts.push(new Point(p));
	delete this.bbox;
	delete this.area;
	delete this.hole;
	};
Contour.prototype.getBbox = function() {
	if (this.bbox===undefined) {
		this.bbox = new Bbox();
		var pts = this.pts;
		var nPts = pts.length;
		// we need at least 3 points for a non-empty bbox
		if ( nPts > 2 ) {
			var bbox = new Bbox(pts[0],pts[1]);
			for ( var iPt=2; iPt<nPts; iPt++ ) {
				bbox.unionPoint(pts[iPt]);
				}
			this.bbox.union(bbox);
			}
		}
	return this.bbox.clone();
	};
Contour.prototype.offset = function(dx,dy) {
	var pts = this.pts;
	var nPts = pts.length;
	for ( var iPt=0; iPt<nPts; iPt++ ) {
		pts[iPt].offset(dx,dy);
		}
	if ( this.bbox ) {
		this.bbox.offset(dx,dy);
		}
	};
Contour.prototype.isHollow = function() {
	// A hole will have a negative surface area as per:
	// http://local.wasp.uwa.edu.au/~pbourke/geometry/polyarea/ by Paul Bourke
	// Since I started this project before I started to care about areas of polygons,
	// and that originally I described my contours with clockwise serie of points, filled
	// contour are currently represented with negative area, while hollow contour are
	// represented with positive area. Something to keep in mind.
	if (this.hole===undefined) {
		this.hole=(this.getArea()>0);
		}
	return this.hole;
	};
Contour.prototype.getArea = function() {
	// http://local.wasp.uwa.edu.au/~pbourke/geometry/polyarea/ by Paul Bourke
	// Quote: "for this technique to work is that the polygon must not be self intersecting"
	// Fine with us, that will never happen (unless there is a bug)
	// Quote: "the holes areas will be of opposite sign to the bounding polygon area"
	// This is great, just by calculating the area, we determine wether the contour
	// is hollow or filled. Moreover, by adding up the areas of all contours describing
	// a polygon, we find whether or not a polygon is mostly hollow or mostly filled,
	// useful to implement display performance enhancement strategies.
	if (this.area===undefined) {
		var area=0;
		var pts=this.pts;
		var nPts=pts.length;
		if (nPts > 2) {
			var j=nPts-1;
			var p1; var p2;
			for (var i=0;i<nPts;j=i++) {
				p1=pts[i];
				p2=pts[j];
				area+=p1.x*p2.y;
				area-=p1.y*p2.x;
				}
			this.area=area/2;
			}
		}
	return this.area;
	};
Contour.prototype.rotate = function(angle,x0,y0) {
	if ( !angle ) {return;}
	// http://www.webreference.com/js/tips/000712.html
	var cosang = self.Math.cos(angle);
	var sinang = self.Math.sin(angle);
	var rnd = self.Math.round;
	var pts = this.pts;
	var nPts = pts.length;
	var pt; var x; var y;
	for (var iPt=0; iPt<nPts; iPt++) {
		pt = pts[iPt];
		x = pt.x-x0;
		y = pt.y-y0;
		// http://www.topcoder.com/tc?module=Static&d1=tutorials&d2=geometry2
		pt.x = rnd(x*cosang-y*sinang)+x0;
		pt.y = rnd(x*sinang+y*cosang)+y0;
		}
	delete this.bbox; // no longer valid
	};

// Polygon object, a collection of Contour objects
function Polygon(a) {
	this.contours = []; // no contour
	if (a) {
		if (a instanceof Polygon) {
			var contours = a.contours;
			var nContours = contours.length;
			for ( var iContour=0; iContour<nContours; iContour++ ) {
				this.contours.push(new Contour(contours[iContour]));
				}
			if ( this.bbox ) {
				this.bbox = a.bbox.clone();
				}
			this.area = a.area;
			if ( this.centroid ) {
				this.centroid = a.centroid.clone();
				}
			this.mostlyHollow = a.mostlyHollow;
			}
		else if ( a instanceof Array ) {
			this.contours.push(new Contour(a));
			}
		else {
			alert("Polygon ctor: Unknown arg 'a'");
			}
		}
	}
Polygon.prototype.clone = function() {
	return new Polygon(this);
	};
Polygon.prototype.getBbox = function() {
	if (!this.bbox) {
		this.bbox = new Bbox();
		var contours = this.contours;
		var nContours = contours.length;
		for ( var iContour=0; iContour<nContours; iContour++ ) {
			this.bbox.union(contours[iContour].getBbox());
			}
		}
	return this.bbox.clone();
	};
Polygon.prototype.getArea = function() {
	// We addup the area of all our contours.
	// Contours representing holes will have a negative area.
	if (!this.area) {
		var area=0;
		var contours=this.contours;
		var nContours=contours.length;
		for (var iContour=0; iContour<nContours; iContour++) {
			area+=contours[iContour].getArea();
			}
		this.area=area;
		}
	return this.area;
	};
Polygon.prototype.getCentroid = function() {
	if (!this.centroid) {
		var contours=this.contours;
		var nContours=contours.length;
		var pts; var nPts;
		var x=0;
		var y=0;
		var f; var iPt; var jPt;
		var p1; var p2;
		for (var iContour=0; iContour<nContours; iContour++) {
			pts = contours[iContour].pts;
			nPts = pts.length;
			// http://local.wasp.uwa.edu.au/~pbourke/geometry/polyarea/ by Paul Bourke
			jPt=nPts-1;
			for (iPt=0; iPt<nPts; jPt=iPt++) {
				p1=pts[iPt];
				p2=pts[jPt];
				f=p1.x*p2.y-p2.x*p1.y;
				x+=(p1.x+p2.x)*f;
				y+=(p1.y+p2.y)*f;
				}
			}
		f=this.getArea()*6;
		// centroid relative to self bbox
		var origin=this.getBbox().getTopleft();
		this.centroid=new Point({x:mthrnd(x/f-origin.x),y:mthrnd(y/f-origin.y)});
		}
	return this.centroid.clone();
	};
Polygon.prototype.pointIn = function(p) {
	alert("Polygon.prototype.pointIn: No longer supported");
	};
Polygon.prototype.offset = function(dx,dy) {
	var contours = this.contours;
	var nContours = contours.length;
	for ( var iContour=0; iContour<nContours; iContour++ ) {
		contours[iContour].offset(dx,dy);
		}
	if ( this.bbox ) {
		this.bbox.offset(dx,dy);		
		}
	if ( this.centroid ) {
		this.centroid.offset(dx,dy);		
		}
	};
Polygon.prototype.moveto = function(x,y) {
	// position is centroid
	var centroid = this.getCentroid();
	var tl=this.getBbox().getTopLeft();
	this.offset(x-tl.x-centroid.x,y-tl.y-centroid.y);
	};
Polygon.prototype.rotate = function(angle,x0,y0) {
	if (!angle) {return;}
	// http://www.webreference.com/js/tips/000712.html
	var contours = this.contours;
	var nContours = contours.length;
	for (var iContour=0; iContour<nContours; iContour++) {
		contours[iContour].rotate(angle,x0,y0);
		}
	delete this.bbox; // no longer valid
	delete this.centroid; // no longer valid (since it's relative to self bbox
	};
Polygon.prototype.doesIntersect = function(bbox) {
	return this.getBbox().doesIntersect(bbox);
	};
Polygon.prototype.isMostlyHollow = function() {
	if (this.mostlyHollow===undefined) {
		// we add up all solid and hollow contours and
		// compare the result to determine whether this
		// polygon is mostly solid or hollow
		var areaSolid=0;
		var areaHollow=0;
		var contours=this.contours;
		var nContours=contours.length;
		var area;
		for (var iContour=0; iContour<nContours; iContour++) {
			area = contours[iContour].getArea();
			if (area < 0) {
				areaSolid+=area;
				}
			else {
				areaHollow+=area;
				}
			}
		this.mostlyHollow=(areaHollow>areaSolid);
		}
	return this.mostlyHollow;
	};
Polygon.prototype.getPoints = function() {
	var r=[];
	var contours=this.contours;
	var nContours=contours.length;
	var contour; var pts; var iPt; var nPts;
	for (var iContour=0; iContour<nContours; iContour++) {
		contour=contours[iContour];
		pts=contour.pts;
		nPts=pts.length;
		for (iPt=0; iPt<nPts; iPt++) {
			r.push(new Point(pts[iPt]));
			}
		}
	return r;
	};
Polygon.prototype.merge = function(other) {
	// Simply put, this algorithm XOR each segment of
	// a polygon with each segment of another polygon.
	// This means we delete any segment which appear an
	// even number of time. Whatever segments are left in the
	// collection are connected together to form one or more
	// contour.
	// Of course, this works because we know we are working
	// with polygons which are perfectly adjacent and never
	// overlapping.
	// A nice side-effect of the current algorithm is that
	// we do not need to know expressly which contours are full
	// and which are holes: The contours created will automatically
	// have a clockwise/counterclockwise direction such that they
	// fits exactly the non-zero winding number rule used by the
	// <canvas> element, thus suitable to be used as is for
	// clipping and complex polygon filling.
	// TODO: write an article to illustrate exactly how this work.
	// TODO: handle special cases here (ex. empty polygon, etc)

	// A Javascript object can be used as an associative array, but
	// they are not real associative array, as there is no way
	// to query the number of entries in the object. For this
	// reason, we maintain an element counter ourself.
	var segments={};
	var contours=this.contours;
	var nContours=contours.length;
	var pts; var nPts;
	var iPtA; var iPtB;
	var idA; var idB;
	for (var iContour=0; iContour<nContours; iContour++) {
		pts = contours[iContour].pts;
		nPts = pts.length;
		iPtA = nPts-1;
		for ( iPtB=0; iPtB<nPts; iPtA=iPtB++ ) {
			idA = pts[iPtA].toHashkey();
			idB = pts[iPtB].toHashkey();
			if (!segments[idA]) {
				segments[idA]={n:1,pts:{}};
				}
			else {
				segments[idA].n++;
				}
			segments[idA].pts[idB] = new Segment(pts[iPtA],pts[iPtB]);
			}
		}
	// enumerate segments in other's contours, eliminate duplicate
	contours = other.contours;
	nContours = contours.length;
	for ( iContour=0; iContour<nContours; iContour++ ) {
		pts = contours[iContour].pts;
		nPts = pts.length;
		iPtA=nPts-1;
		for (iPtB=0; iPtB<nPts; iPtA=iPtB++) {
			idA = pts[iPtA].toHashkey();
			idB = pts[iPtB].toHashkey();
			// duplicate (we eliminate same segment in reverse direction)
			if (segments[idB] && segments[idB].pts[idA]) {
				delete segments[idB].pts[idA];
				if (!--segments[idB].n) {
					delete segments[idB];
					}
				}
			// not a duplicate
			else {
				if (!segments[idA]) {
					segments[idA]={n:1,pts:{}};
					}
				else {
					segments[idA].n++;
					}
				segments[idA].pts[idB] = new Segment(pts[iPtA],pts[iPtB]);
				}
			}
		}
	// recreate and store new contours by jumping from one point to the next,
	// using the second point of the segment as hash key for next segment
	this.contours=[]; // regenerate new contours
	var contour; var segment;
	for (idA in segments) { // we need this to get a starting point for a new contour
		contour = new Contour();
		this.contours.push(contour);
		for (idB in segments[idA].pts) {break;}
		segment = segments[idA].pts[idB];
		while (segment) {
			contour.addPoint(segment.ptA);
			// remove from collection since it has now been used
			delete segments[idA].pts[idB];
			if (!--segments[idA].n) {
				delete segments[idA];
				}
			idA = segment.ptB.toHashkey();
			if (segments[idA]) {
				for (idB in segments[idA].pts) {break;} // any end point will do
				segment = segments[idA].pts[idB];
				}
			else {
				segment = null;
				}
			}
		}
	// invalidate cached values
	delete this.bbox;
	delete this.area;
	delete this.centroid;
	delete this.mostlyHollow;
	};
Polygon.prototype.toCanvasPath = function(ctx) {
	var contours=this.contours;
	var nContours=contours.length;
	var pts; var nPts; var iPt; var pt;
	for (var iContour=0; iContour<nContours; iContour++) {
		pts=contours[iContour].pts;
		nPts=pts.length;
		if (nPts > 1) {
			pt=pts[0];
			ctx.moveTo(pt.x,pt.y);
			for (iPt=1; iPt<nPts; iPt++) {
				pt=pts[iPt];
				ctx.lineTo(pt.x,pt.y);
				}
			ctx.closePath();
			}
		}
	};
Bbox.prototype.toPolygon = function() {
	return new Polygon([new Point(this.tl),new Point(this.br.x,this.tl.y),new Point(this.br),new Point(this.tl.x,this.br.y)]);
	};

/**
  Puzzle tile base class
 */
function PuzzleTile() {
	this.polygon = null;
	}
PuzzleTile.prototype.setPolygon = function(pts) {
	if ( !this.polygon ) {
		this.polygon = new Polygon(pts);
		}
	};
PuzzleTile.prototype.getBbox = function() {
	return this.polygon.getBbox();
	};
PuzzleTile.prototype.getCentroid = function() {
	return this.polygon.getCentroid();
	};
PuzzleTile.prototype.getPolygon = function() {
	return this.polygon.clone();
	};
PuzzleTile.prototype.getPolygonPoints = function() {
	return this.polygon.getPoints();
	};
PuzzleTile.prototype.merge = function(other) {
	this.polygon.merge(other.polygon);
	};


/**
  Source tile descriptor
 */
function PuzzleSourceTile(img) {
	PuzzleTile.call(this);
	this.sImage = img;
	}
PuzzleSourceTile.prototype = new PuzzleTile();
PuzzleSourceTile.prototype.getSourceImage = function() {
	return this.sImage;
	};

/**
  Transient tile descriptor
 */
function PuzzleTransientTile(sTile) {
	PuzzleTile.call(this);
	this.sTile = sTile;
	this.angleStep = 0;
	this.angleMaxSteps = 1; // default = no rotation
	this.angleRadians = 6.28318530718; // precalculated angle in radians for performance purpose
	}
PuzzleTransientTile.prototype = new PuzzleTile();
PuzzleTransientTile.prototype.getAngleStep = function() {
	return this.angleStep;
	};
PuzzleTransientTile.prototype.setAngleStep = function(step,maxSteps) {
	// quantize max steps into a factor of 360
	if ( maxSteps !== undefined ) {
		this.angleMaxSteps = 360/self.Math.round(360/self.Math.min(self.Math.max(maxSteps,1),90));
		}
	// ensure step is within range
	this.angleStep = step % this.angleMaxSteps;
	if (this.angleStep < 0) {
		this.angleStep += this.angleMaxSteps;
		}
	// precalculate angle in radian: 2PI*step/maxsteps
	this.angleRadians = 6.28318530718 * this.angleStep / this.angleMaxSteps;
	this.tImage = null; // invalidate internal state
	}; 
PuzzleTransientTile.prototype.sync = function() {
	if ( !this.tImage ) {
		this.tImage = self.document.createElement('canvas');
		// we need to find out the bbox of the rotated source
		// in order to know the required image size
		this.polygon = this.sTile.polygon.clone();
		var sBbox = this.sTile.getBbox();
		var sTopleft = sBbox.getTopleft();
		var sCentroid = this.sTile.getCentroid();
		// rotate
		this.polygon.rotate(this.angleRadians,sTopleft.x+sCentroid.x,sTopleft.y+sCentroid.y);
		// post-rotation bbox different from pre-
		var tBbox = this.polygon.getBbox();
		var tTopleft = tBbox.getTopleft();
		// set origin to self
		this.polygon.offset(-tTopleft.x,-tTopleft.y);
		var tCentroid = this.polygon.getCentroid();
		this.tImage.width = tBbox.width();
		this.tImage.height = tBbox.height();
		// transfer/rotate source tile image
		var ctx = this.tImage.getContext('2d');
		// first, set the clipping region as per contours
		ctx.save();
		ctx.beginPath();
		this.polygon.toCanvasPath(ctx);
		ctx.clip();
		// copy/rotate source image into local buffer
		ctx.save();
		if ( this.angleStep ) {
			ctx.translate(tCentroid.x,tCentroid.y);
			ctx.rotate(this.angleRadians);
			ctx.translate(-tCentroid.x,-tCentroid.y);
			}
		ctx.drawImage(this.sTile.getSourceImage(),sTopleft.x,sTopleft.y,sBbox.width(),sBbox.height(),tCentroid.x-sCentroid.x,tCentroid.y-sCentroid.y,sBbox.width(),sBbox.height());
		ctx.restore();
		// second, draw each segment (color of segment depends of the slope, for a 3d effect)
		var contours=this.polygon.contours;
		var nContours=contours.length; var iContour;
		var pts; var nPts; var iPt; var pt;
		for (iContour=0; iContour<nContours; iContour++) {
			pts=contours[iContour].pts;
			nPts=pts.length;
			if ( nPts>2 ) {
				var abs=mthabs;
				var rnd=mthrnd;
				var atan=mthatan;
				ctx.lineWidth = 2;
				var iPtA = nPts-1;
				var ptA; var ptB;
				var dx; var dy;
				var g; // gray level
				for (var iPtB=0; iPtB<nPts; iPtA=iPtB++) {
					ptA=pts[iPtA];
					ptB=pts[iPtB];
					// we rotate the segment backward 45 deg, this allow the light
					// source to be located top-left otherwise it would be located
					// left
					// we precalculate as much as we can for performance
					// cos(-PI/4)=0.70710678 and sin(-PI/4)=-0.70710678
					// dx,dy represent the rotated slope parameters as per
					// m = (y2-y1)/(x2-x1)
					// sin(PI/4) = cos(PI/4) = 0.70710678118654752440084436210485
					// 255/PI = 81.169020976866621242130719319982
					dx=(ptB.x-ptA.x)*0.70710678;
					dy=(ptB.y-ptA.y)*0.70710678;
					g=abs(rnd(atan(dx+dy,dy-dx)*81.16902098));
					g=(g<16)?"0"+g.toString(16):g.toString(16);
					ctx.strokeStyle = "#"+g+g+g;
					ctx.beginPath();
					ctx.moveTo(ptA.x,ptA.y);
					ctx.lineTo(ptB.x,ptB.y);
					ctx.stroke();
					}
				}
			}
		ctx.restore();
		}
	};
PuzzleTransientTile.prototype.getPolygon = function() {
	// we need to overload since our image is valid only when we are in sync with source tile
	if ( !this.tImage ) { this.sync(); }
	return PuzzleTile.prototype.getPolygon.call(this);
	};
PuzzleTransientTile.prototype.getPolygonPoints = function() {
	if ( !this.tImage ) { this.sync(); }
	return PuzzleTile.prototype.getPolygonPoints.call(this);
	};
PuzzleTransientTile.prototype.getTransientImage = function() {
	// we need to overload since our image is valid only when we are in sync with source tile
	if ( !this.tImage ) { this.sync(); }
	return this.tImage;
	};
PuzzleTransientTile.prototype.getBbox = function() {
	// we need to overload since our bbox is valid only when we are in sync with source tile
	if ( !this.tImage ) { this.sync(); }
	return PuzzleTile.prototype.getBbox.call(this);
	};
PuzzleTransientTile.prototype.getCentroid = function() {
	// we need to overload since our poly is valid only when we are in sync with source tile
	if ( !this.tImage ) { this.sync(); }
	return PuzzleTile.prototype.getCentroid.call(this);
	};
PuzzleTransientTile.prototype.pointIn = function(p) {
	if ( !this.tImage ) { this.sync(); }
	// HTML canvas has a nice function to test whether a point
	// lies inside a polygon, lets use it
	var r=false;
	var ctx=this.tImage.getContext('2d');
	ctx.save();
	ctx.beginPath();
	var contours=this.polygon.contours;
	var nContours=contours.length;
	var pts; var nPts; var iPt; var pt;
	for (var iContour=0; iContour<nContours; iContour++) {
		pts=contours[iContour].pts;
		nPts=pts.length;
		if ( nPts>2 ) {
			pt=pts[0];
			ctx.moveTo(pt.x,pt.y);
			for (iPt=1; iPt<nPts; iPt++) {
				pt=pts[iPt];
				ctx.lineTo(pt.x,pt.y);
				}
			ctx.closePath();
			}
		r=ctx.isPointInPath(p.x,p.y);
		}
	ctx.restore();
	return r;
	};
PuzzleTransientTile.prototype.merge = function(other) {
	this.sTile.merge(other.sTile);
	this.tImage = null;
	};

/**
  Display tile descriptor
 */
function PuzzleDisplayTile(tTile) {
	PuzzleTile.call(this);
	this.tTile = tTile;
	this.dPos = new Point();
	}
PuzzleDisplayTile.prototype = new PuzzleTile();
PuzzleDisplayTile.prototype.getDisplayPos = function() {
	return new Point(this.dPos);
	};
PuzzleDisplayTile.prototype.setDisplayPos = function(x,y) {
	var dx=x-this.dPos.x;
	var dy=y-this.dPos.y;
	this.dPos.set({x:x,y:y});
	if (this.polygon) {
		this.polygon.offset(dx,dy);
		}
	if (this.bbox) {
		this.bbox.offset(dx,dy);
		}
	};
PuzzleDisplayTile.prototype.getDisplayImage = function() {
	// the display image is the same as the transient image
	return this.tTile.getTransientImage();
	};
PuzzleDisplayTile.prototype.getAngleStep = function() {
	return this.tTile.getAngleStep();
	};
PuzzleDisplayTile.prototype.setAngleStep = function(step,maxSteps) {
	// pass call down to the transient tile and invalid self
	this.tTile.setAngleStep(step,maxSteps);
	delete this.polygon;
	delete this.bbox;
	};
PuzzleDisplayTile.prototype.sync = function() {
	if (!this.polygon) {
		this.polygon = this.tTile.getPolygon();
		// shift to screen position
		var tCentroid = this.tTile.getCentroid();
		this.polygon.offset(this.dPos.x-tCentroid.x,this.dPos.y-tCentroid.y);
		}
	};
PuzzleDisplayTile.prototype.getPolygonPoints = function() {
	if (!this.polygon) {this.sync();}
	return PuzzleTile.prototype.getPolygonPoints.call(this);
	};
PuzzleDisplayTile.prototype.getBbox = function() {
	return new Bbox(this.getBboxConst());
	};
PuzzleDisplayTile.prototype.getBboxConst = function() {
	// we need to overload since our bbox is valid only when we are in sync with transient tile
	if (!this.bbox) {
		if (!this.polygon) {this.sync();}
		this.bbox = PuzzleTile.prototype.getBbox.call(this);
		}
	return this.bbox;
	};
PuzzleDisplayTile.prototype.getCentroid = function() {
	// we need to overload since our poly is valid only when we are in sync with transient tile
	if (!this.polygon) {this.sync();}
	return PuzzleTile.prototype.getCentroid.call(this);
	};
PuzzleDisplayTile.prototype.pointIn = function(p) {
	// don't forget to convert point to transient tile coords
	// before propagating call
	var dPos = this.getBbox().getTopleft();
	return this.tTile.pointIn(new Point({x:p.x-dPos.x,y:p.y-dPos.y}));
	};
PuzzleDisplayTile.prototype.merge = function(other) {
	this.tTile.merge(other.tTile);
	delete this.polygon;
	delete this.bbox;
	};
PuzzleDisplayTile.prototype.isMostlyHollow = function() {
	if (!this.polygon) {this.sync();}
	return this.polygon.isMostlyHollow();
	};

/**
 Puzzle part
 */
function PuzzlePart() {
	}

// Puzzle piece
function PuzzlePiece(pts,img) {
	this.sTile = new PuzzleSourceTile(img);
	this.sTile.setPolygon(pts);
	this.tTile = new PuzzleTransientTile(this.sTile);
	this.dTile = new PuzzleDisplayTile(this.tTile);
	this.piece = true; // this is a puzzle piece
	this.correct = false; // whether the piece is correctly placed
	PuzzlePart.call(this);
	}
PuzzlePiece.prototype = new PuzzlePart();
PuzzlePiece.prototype.getAngleStep = function() {
	return this.dTile.getAngleStep();
	};
PuzzlePiece.prototype.setAngleStep = function(step,numsteps) {
	this.dTile.setAngleStep(step,numsteps);
	};
PuzzlePiece.prototype.setDisplayPos = function(x,y) {
	this.dTile.setDisplayPos(x,y);
	};
PuzzlePiece.prototype.getDisplayPos = function() {
	return this.dTile.getDisplayPos();
	};
PuzzlePiece.prototype.getDisplayBbox = function() {
	return this.dTile.getBbox();
	};
PuzzlePiece.prototype.getDisplayBboxConst = function() {
	return this.dTile.getBboxConst();
	};
PuzzlePiece.prototype.draw = function(ctx) {
	var dImage = this.dTile.getDisplayImage();
	var dTopleft = this.dTile.getBbox().getTopleft();
	ctx.drawImage(dImage,dTopleft.x,dTopleft.y);
	};
PuzzlePiece.prototype.pointIn = function(p) {
	return this.dTile.pointIn(p);
	};
PuzzlePiece.prototype.isEdge = function() {
	return this.edge;
	};
PuzzlePiece.prototype.doesIntersect = function(bbox) {
	return bbox.doesIntersect(this.getDisplayBbox());
	};
PuzzlePiece.prototype.merge = function(other) {
	// display angle steps must be the same
	if ( other.getAngleStep()!=this.getAngleStep() ) { return false; }
	// overall display bounding box, inflated as per tolerance, must intersect
	var dBbox = other.getDisplayBbox();
	dBbox.inflate(5); // within 5 pixels
	if ( !dBbox.doesIntersect(this.getDisplayBboxConst()) ) { return false; }
	// we test each quadri of this piece against each quadri of the other piece
	var abs=self.Math.abs;
	var thisPts = this.dTile.getPolygonPoints();
	var otherPts = other.dTile.getPolygonPoints();
	var nThisPts = thisPts.length;
	var nOtherPts = otherPts.length;
	var thisPt; var otherPt;
	var numMatches = 0;
	for (var iThisPt=0; iThisPt<nThisPts; iThisPt++) {
		thisPt=thisPts[iThisPt];
		for (var iOtherPt=0; iOtherPt<nOtherPts; iOtherPt++) {
			otherPt=otherPts[iOtherPt];
			if (otherPt.id == thisPt.id && abs(otherPt.x-thisPt.x)<5 && abs(otherPt.y-thisPt.y)<5 && (++numMatches > 1)) {
				// merge pieces
				var oldTopleft = this.getDisplayBbox().union(other.getDisplayBbox()).getTopleft();
				this.dTile.merge(other.dTile);
				this.edge = other.edge?other.edge:this.edge;
				var newTopleft = this.getDisplayBbox().getTopleft();
				var newPos = this.getDisplayPos();
				newPos.offset(oldTopleft.x-newTopleft.x,oldTopleft.y-newTopleft.y);
				this.setDisplayPos(newPos.x,newPos.y);
				return true;
				}
			}
		}
	return false;
	};
PuzzlePiece.prototype.isMostlyHollow = function() {
	return this.dTile.isMostlyHollow();
	};

// Puzzle preview box
function PuzzlePreview(img) {
	// preview is ?px max on either side
	this.shadow = 8;
	var imgw = 160;
	var imgh = 160;
	var ratio = img.width / img.height;
	if ( ratio >= 1 ) { imgh /= ratio; }
	else { imgw *= ratio; }
	// ensure no fraction, thus no subpixel operations on the canvas
	imgw = self.Math.round(imgw);
	imgh = self.Math.round(imgh);
	this.image = self.document.createElement('canvas');
	this.image.width = imgw+this.shadow;
	this.image.height = imgh+this.shadow;
	this.bbox = new Bbox(0,0,imgw+this.shadow,imgh+this.shadow);
	// transfer source tile image
	var ctx = this.image.getContext('2d');
	ctx.drawImage(img,0,0,imgw,imgh);
	// frame to highlight preview
	ctx.save();
	ctx.beginPath();
	ctx.rect(0,0,imgw,imgh);
	ctx.clip();
	ctx.strokeStyle = '#f00';
	ctx.lineWidth = 2;
	ctx.globalAlpha = 0.8;
	ctx.strokeRect(0,0,imgw,imgh);
	ctx.restore();
	// shadow simulation, I can't make the built-in work
	ctx.fillStyle = '#000';
	ctx.globalAlpha = 0.5;
	ctx.fillRect(imgw,this.shadow,this.shadow,imgh); // left shadow
	ctx.fillRect(this.shadow,imgh,imgw-this.shadow,this.shadow); // bottom shadow
	}
PuzzlePreview.prototype = new PuzzlePart();
PuzzlePreview.prototype.draw = function(ctx) {
	ctx.save();
	//ctx.shadowOffsetX = ctx.shadowOffsetY = this.shadow;
	//ctx.shadowColor = 'rgba(127, 127, 127, 0.33)';
	//ctx.shadowBlur = 2;
	ctx.drawImage(this.image,this.bbox.tl.x,this.bbox.tl.y);
	ctx.restore();
	};
PuzzlePreview.prototype.setDisplayPos = function(x,y) {
	var topleft = this.bbox.getTopleft();
	this.bbox.offset(x-topleft.x,y-topleft.y);
	};
PuzzlePreview.prototype.getDisplayPos = function() {
	return this.bbox.getTopleft();
	};
PuzzlePreview.prototype.getDisplayBbox = function() {
	return new Bbox(this.bbox);
	};
PuzzlePreview.prototype.getDisplayBboxConst = function() {
	return this.bbox;
	};
PuzzlePreview.prototype.pointIn = function(p) {
	var noShadowBbox = new Bbox(this.bbox);
	noShadowBbox.br.x -= this.shadow;
	noShadowBbox.br.y -= this.shadow;
	return noShadowBbox.pointIn(p);
	};
PuzzlePreview.prototype.doesIntersect = function(bbox) {
	return this.bbox.doesIntersect(bbox);
	};

// Puzzle bed area
function PuzzleBed(img,guide) {
	this.image = self.document.createElement('canvas');
	this.image.width = img.width;
	this.image.height = img.height;
	this.bbox = new Bbox(0,0,img.width,img.height);
	var ctx = this.image.getContext('2d');
	// load and apply checkerboard background
	var me = this;
	if ( !guide ) {
		ctx.fillStyle = '#fff';
		ctx.fillRect(0,0,img.width,img.height);
		var bedbgimg = new self.Image();
		bedbgimg.onload = function() {
			var ctx = me.image.getContext('2d');
			var bedbgpat = ctx.createPattern(this,'repeat');
			ctx.fillStyle = bedbgpat;
			ctx.fillRect(0,0,me.image.width,me.image.height);
			this.onload = me = null;
			};
		bedbgimg.src = 'checkerboard.png'; // Source: http://media.photobucket.com/image/transparent%20pattern%20background/bitwierd/Chequerboard.png
		}
	else {
		ctx.save();
		ctx.fillStyle = '#eee';
		ctx.fillRect(0,0,img.width,img.height);
		ctx.globalAlpha = 0.2;
		ctx.drawImage(img,0,0);
		ctx.restore();
		}
	}
PuzzleBed.prototype = new PuzzlePart();
PuzzleBed.prototype.draw = function(ctx) {
	ctx.drawImage(this.image,this.bbox.tl.x,this.bbox.tl.y);
	};
PuzzleBed.prototype.setDisplayPos = function(x,y) {
	var topleft = this.bbox.getTopleft();
	this.bbox.offset(x-topleft.x,y-topleft.y);
	};
PuzzleBed.prototype.getDisplayPos = function() {
	return this.bbox.getTopleft();
	};
PuzzleBed.prototype.getDisplayBbox = function() {
	return new Bbox(this.bbox);
	};
PuzzleBed.prototype.pointIn = function(p) {
	return false;
	};
PuzzleBed.prototype.doesIntersect = function(bbox) {
	return this.bbox.doesIntersect(bbox);
	};
PuzzleBed.prototype.drawPiece = function(dTile) {
	var dImg = dTile.getDisplayImage();
	var dBbox = dTile.getBbox();
	var ctx = this.image.getContext('2d');
	ctx.drawImage(dImg,dBbox.tl.x-this.bbox.tl.x,dBbox.tl.y-this.bbox.tl.y);
	};

// code needs cleaning below this line

// Puzzle object
function Puzzle(id,puzzleOptions) {
	var me = this;
	this.canvasObj = self.document.createElement('canvas');
	this.canvasObj.width = '1040';
	this.canvasObj.height = '840';
	if ( this.canvasObj.getContext === undefined ) { return; }
	var canvasParent=self.document.getElementById('canvasParent');
	canvasParent.style.backgroundColor='#777';
	canvasParent.style.width=this.canvasObj.width+'px';
	canvasParent.style.height=this.canvasObj.height+'px';
	canvasParent.innerHTML='';
	canvasParent.appendChild(this.canvasObj);
	this.imoved = -1; // the (drawing stack) index of the part being moved
	this.movedAnchor = new Point(); // the distance of the mouse position relative to the top-left corner of the piece being moved
	this.showEdges = false;
	this.numPieces = puzzleOptions.puzzlePieces?puzzleOptions.puzzlePieces:16;
	this.complexity = puzzleOptions.puzzleComplexity!==undefined?mthmx(mthmn(puzzleOptions.puzzleComplexity,9),0):4;
	this.angleMaxSteps = puzzleOptions.puzzleRotate!==undefined?self.Math.max(self.Math.min(puzzleOptions.puzzleRotate,90),1):24;
	this.puzzleGuide = (puzzleOptions.puzzleGuide!==undefined)?puzzleOptions.puzzleGuide:0;
	this.hideFloatingPieces = false;
	// handle user options
	self.document.getElementById("puzzleShowEdges").onclick = function() {
		me.showEdges = !me.showEdges;
		this.value = (me.showEdges)?"Show all pieces":"Show edge pieces only";
		me.draw();
		};
	// default drawing stack
	this.drawingStack=[];
	// default puzzle frame
	this.bedFrame = new Region();
	this.bedFrame.add({x:0,y:0},{x:this.canvasObj.width,y:this.canvasObj.height});
	// default puzzle bed
	// background image for puzzle frame
	var bgFrm = new self.Image();
	bgFrm.onload = function(){
		me.bgCanvas = self.document.createElement('canvas');
		me.bgCanvas.width = this.width;
		me.bgCanvas.height = this.height;
		me.bgCanvas.getContext('2d').drawImage(bgFrm,0,0,me.bgCanvas.width,me.bgCanvas.height);
		this.onload = null;
		me.draw(); // force redraw now that we have our background image ready
		};
	bgFrm.src = 'bg-b19marbles013.jpg'; // Source: http://www.imageafter.com/category.php?category=marbles
	// background image for puzzle bed

	this.shuffle = function() {
		for ( var ipart = 0; ipart < this.drawingStack.length; ipart++ ) {
			var partref = me.drawingStack[ipart];
			if ( !partref.piece ) {
				continue;
				}
			partref.setDisplayPos(mthrnd(mthrand()*(me.canvasObj.width-40)),mthrnd(mthrand()*(me.canvasObj.height-40)));
			partref.setAngleStep(mthrnd(mthrand()*me.angleMaxSteps),me.angleMaxSteps);
			partref.correct = false;
			}
		};
	this.draw = function(clip) {
		var ctx = me.canvasObj.getContext('2d');
		ctx.save();
		// comment out to verify minimal redrawing
		//ctx.clearRect(0,0,me.canvasObj.width,me.canvasObj.height);
		// draw only what intersect with clip
		if ( clip ) {
			ctx.beginPath();
			clip.toCanvasPath(ctx);
			ctx.clip();
			}
		// canvas area
		if ( !me.bgPat && me.bgCanvas!==undefined ) { 
			me.bgPat = ctx.createPattern(me.bgCanvas,'repeat');
			}
		me.bedFrame.fill(ctx,me.bgPat!==undefined?me.bgPat:self.document.getElementById('canvasParent').style.backgroundColor,clip);
		// puzzle parts
		ctx.globalCompositeOperation = "source-over";
		// if solved, display original picture
		// store locally often used properties for efficiency
		var imoved = me.imoved;
		var showedges = me.showEdges;
		var stack = me.drawingStack;
		// stack drawn from bottom to top
		var nparts = stack.length;
		for ( var ipart = 0; ipart < nparts; ipart++ ) {
			var partref = stack[ipart];
			if ( (!clip || partref.doesIntersect(clip)) && (!partref.piece || ipart==imoved || !showedges || partref.isEdge()) ) {
				partref.draw(ctx);
				}
			}
		ctx.restore();
		};
	this.init = function() {
		//stderr("Puzzle.init()");
		var img_width = me.img.width;
		var img_height = me.img.height;
		var img_ratio = img_width / img_height;
		// if image size < 80px, zoom in
		if ( img_width < 80 || img_height < 80 ) {
			if ( img_ratio >= 1.0 ) {
				img_width = 80;
				img_height = mthrnd(img_width/img_ratio);
				}
			else {
				img_height = 80;
				img_width = mthrnd(img_height*img_ratio);
				}
			}
		else if ( img_width > (me.canvasObj.width-40) || img_height > (me.canvasObj.height-40) ) {
			if ( img_ratio >= 1.0 ) {
				img_width = me.canvasObj.width-40;
				img_height = mthrnd(img_width/img_ratio);
				}
			else {
				img_height = me.canvasObj.height-40;
				img_width = mthrnd(img_height*img_ratio);
				}
			}
		if ( img_width < 80 || img_height < 80 || img_width > (me.canvasObj.width-40) || img_height > (me.canvasObj.height-40) ) {
			stdout("Because of its size, this image can't be used as a puzzle");
			return;
			}
		stdout("Original image size (w x h): "+me.img.width+"px &times; "+me.img.height+"px");
		if ( img_width != me.img.width || img_height != me.img.height ) {
			stdout("Resized to "+img_width+"px &times; "+img_height+"px");
			}
		// following will be used to generate puzzle pieces
		var num_cols = mthmn(mthmx(mthceil(mthsqrt(me.numPieces)*mthsqrt(img_ratio)),2),mthrnd(this.width/40)); // pieces can't be smaller than 40px
		var num_rows = mthmn(mthmx(mthceil(mthsqrt(me.numPieces)/mthsqrt(img_ratio)),2),mthrnd(this.height/40)); // pieces can't be smaller than 40px
		stdout("Actual number of pieces: "+num_cols*num_rows+" ("+num_cols+" &times; "+num_rows+" pieces)");
		// create the image that will be used as a basis for the puzzle
		me.imageObj = self.document.createElement('canvas');
		me.imageObj.width = img_width;
		me.imageObj.height = img_height;
		var ctx = me.imageObj.getContext('2d');
		ctx.drawImage(me.img,0,0,me.img.width,me.img.height,0,0,img_width,img_height);

		// build region for area surrounding puzzle bed
		me.bedFrame = new Region();
		me.bedFrame.add({x:0,y:0},{x:me.canvasObj.width,y:me.canvasObj.height}); // top
		// drawing stack
		me.drawingStack=[];
		// generate parts
		me.cut({width:img_width,height:img_height,numRows:num_rows,numCols:num_cols});
		// create preview
		me.drawingStack.push(new PuzzlePreview(me.img));
		me.shuffle();
		me.draw();
		};
	this.cut = function(parms) {
		var rnd=self.Math.round;
		var rand=self.Math.random;
		var min=self.Math.min;
		var max=self.Math.max;
		var imgWidth=parms.width;
		var imgHeight=parms.height;
		var numRows=parms.numRows;
		var numCols=parms.numCols;
		var partWidth=rnd(imgWidth/numCols); // rounded to avoid fractional pixels
		var partHeight=rnd(imgHeight/numRows); // rounded to avoid fractional pixels
		var partWidthVar=rnd(partWidth*max(min(me.complexity,9),0)*0.48/9);
		var partHeightVar=rnd(partHeight*max(min(me.complexity,9),0)*0.48/9);
		var randomX=function(iPart){return partWidth*iPart+rnd(partWidthVar*2*rand())-partWidthVar;};
		var randomY=function(iPart){return partHeight*iPart+rnd(partHeightVar*2*rand())-partHeightVar;};
		var pt1; var pt2; var pt3; var pt4;
		var ptId=1;
		var parts=[];
		var piece;
		for (var iRow=0; iRow<numRows; iRow++) {
			parts[iRow]=[];
			for (var iCol=0; iCol<numCols; iCol++) {
				pt1=new Point((iCol===0)?((iRow===0)?{x:0,y:0,id:ptId++}:parts[iRow-1][0][3]):parts[iRow][iCol-1][1]);
				pt2=new Point((iRow===0)?{x:(iCol==numCols-1)?imgWidth:randomX(iCol+1),y:0,id:ptId++}:parts[iRow-1][iCol][2]);
				pt3=new Point({x:(iCol==numCols-1)?imgWidth:randomX(iCol+1),y:(iRow==numRows-1)?imgHeight:randomY(iRow+1),id:ptId++});
				pt4=new Point((iCol===0)?{x:0,y:(iRow==numRows-1)?imgHeight:randomY(iRow+1),id:ptId++}:parts[iRow][iCol-1][2]);
				parts[iRow][iCol]=[pt1,pt2,pt3,pt4]; // we need to remember segments for later reference
				piece=new PuzzlePiece(parts[iRow][iCol],me.imageObj);
				piece.edge=(iRow===0)||(iCol===0)||(iRow==numRows-1)||(iCol==numCols-1);
				me.drawingStack.push(piece);
				}
			}
		};
	this.part_under_point = function(p) {
		for ( var ipart = this.drawingStack.length-1; ipart >= 0; ipart-- ) {
			var partref = me.drawingStack[ipart];
			if ( partref.pointIn(p) && (!partref.piece || (!partref.correct && (!me.showEdges || partref.isEdge()))) ) {
			    return ipart;
				}
			}
		return -1;
		};
	this.send_back = function(ipart) {
		if ( ipart >= 0 ) {
			var movedPart = me.drawingStack[ipart];
			me.drawingStack.splice(ipart,1);
			me.drawingStack.unshift(movedPart);
			}
		return 0;
		};
	this.send_top = function(ipart) {
		if ( ipart >= 0 && ipart < me.drawingStack.length-2 ) {
			var movedPart = me.drawingStack[ipart];
			me.drawingStack.splice(ipart,1);
			// insert below the preview part: todo: need to revisit for more solid programming
			ipart = me.drawingStack.length - 1;
			me.drawingStack.splice(ipart,0,movedPart);
			}
		return ipart;
		};
	// complete initialization of object
	// setup new image
	this.img = self.document.getElementById('puzzleImage');
	this.img.onload = me.init;
	// normalize event position
	// based on Quirksmode.org's Peter-Paul Koch
	// http://www.quirksmode.org/js/events_properties.html#position
	function normalizeEventPos(e) {
		if (!e) {
			e = self.event;
			}
		var pos = new Point();
		if (e.pageX || e.pageY) {
			pos.x = e.pageX;
			pos.y = e.pageY;
			}
		else if (e.clientX || e.clientY) {
			pos.x = e.clientX + self.document.body.scrollLeft + self.document.documentElement.scrollLeft;
			pos.y = e.clientY + self.document.body.scrollTop + self.document.documentElement.scrollTop;
			}
		pos.x -= me.canvasObj.offsetLeft;
		pos.y -= me.canvasObj.offsetTop;
		return pos;
		}
	// event handlers
	this.canvasObj.onclick = function(e) {
		var imoved = me.imoved;
		me.imoved = -1;
		if ( imoved >= 0 ) {
			// is this a piece of puzzle?
			if ( me.drawingStack[imoved].piece ) {
				me.snap_piece(imoved);
				}
			me.hideFloatingPieces = false;
			me.draw();
			}
		else {
			var pos = normalizeEventPos(e);
			var ipart = me.part_under_point(pos);
			if ( ipart >= 0 ) {
				// bring on top
				me.imoved = me.send_top(ipart);
				var moved = me.drawingStack[me.imoved];
				var dPos = moved.getDisplayPos();
				me.movedAnchor.x = pos.x - dPos.x;
				me.movedAnchor.y = pos.y - dPos.y;
				me.draw(moved.getDisplayBbox());
				}
			}
		};
	this.canvasObj.onmousemove = function(e) {
		if ( me.imoved < 0 ) { return; }
		var pos = normalizeEventPos(e);
		var moved = me.drawingStack[me.imoved];
		var draw_bbox = moved.getDisplayBbox();
		moved.setDisplayPos(pos.x-me.movedAnchor.x,pos.y-me.movedAnchor.y);
		draw_bbox.union(moved.getDisplayBboxConst());
		me.draw(draw_bbox);
		};
	self.onkeypress = function(e) {
		if ( me.imoved < 0 ) { return true; }
		var moved = me.drawingStack[me.imoved];
		if ( !moved.piece ) { return true; }
		if (!e) {e=self.window.event;}
		var code = e.keyCode?e.keyCode:(e.which?e.which:0);
		var draw_bbox;
		if ( code == 37 || code == 65 || code == 97 ) { // left arrow or 'a'/'D'
			if ( me.angleMaxSteps <= 1 ) { return true; }
			draw_bbox = moved.getDisplayBbox();
			moved.setAngleStep(moved.getAngleStep()-1);
			draw_bbox.union(moved.getDisplayBbox());
			me.draw(draw_bbox);
			}
		else if ( code == 39 || code == 68 || code == 100 ) { // right arrow or 'd'/'D'
			if ( me.angleMaxSteps <= 1 ) { return true; }
			draw_bbox = moved.getDisplayBbox();
			moved.setAngleStep(moved.getAngleStep()+1);
			draw_bbox.union(moved.getDisplayBbox());
			me.draw(draw_bbox);
			}
		else if ( code == 38 || code == 87 || code == 119 ) { // send top = up arrow or 'w'/'W'
			draw_bbox = moved.getDisplayBbox();
			me.imoved = me.send_top(me.imoved);
			draw_bbox.union(moved.getDisplayBbox());
			me.draw(draw_bbox);
			}
		else if ( code == 40 || code == 83 || code == 115 ) { // send back = dn arrow or 's'/'s'
			draw_bbox = moved.getDisplayBbox();
			me.imoved = me.send_back(me.imoved);
			draw_bbox.union(moved.getDisplayBbox());
			me.draw(draw_bbox);
			}
		else if ( code == 32 ) { // hide all pieces except the one being handled
			me.hideFloatingPieces = !me.hideFloatingPieces;
			me.draw();
			}
		return false;
		};
	// mouse wheel handling: http://adomas.org/javascript-mouse-wheel/
	self.onmousewheel = function(e) {
		if ( me.imoved < 0 || me.angleMaxSteps <= 1 ) { return true; }
		var d = 0;
		if (!e) {e=self.e;}
		if (e.wheelDelta) {
			d=e.wheelDelta;
			if (self.opera) {d=-d;}
			}
		else if (e.detail) {
			d = e.detail;
			}
		if (d) {
			var moved = me.drawingStack[me.imoved];
			if ( moved.piece ) {
				var draw_bbox = moved.getDisplayBbox();
				moved.setAngleStep(moved.getAngleStep()+(d>0?1:-1));
				draw_bbox.union(moved.getDisplayBbox());
				me.draw(draw_bbox);
				}
			}
		if (e.preventDefault) {e.preventDefault();}
		e.returnValue = false;
		return false;
		};
	/** DOMMouseScroll is for mozilla. */
	if (self.addEventListener) {self.addEventListener('DOMMouseScroll',self.onmousewheel,false);}

	// whether the puzzle is all solved
	this.isSolved = function() {
		return me.drawingStack.length <= 2;
		};

	// check whether a piece snaps onto another one
	this.snap_piece = function(iTarget) {
		var stack=me.drawingStack;
		var nParts=stack.length;
		var target=stack[iTarget];
		for (var iPart=0; iPart<nParts; iPart++) {
			// skip self
			if (iPart==iTarget) {continue;}
			// consider only puzzle piece (leaving other puzzle parts)
			var part=stack[iPart];
			if (!part.piece) {continue;}
			// test if it's a match
			if (part.merge(target)) {
				self.soundManager.play('snap'+(mthrnd(mthrand())+1));
				me.drawingStack.splice(iTarget,1);
				if ( me.isSolved() ) {
					self.soundManager.play(me.numPieces>=100?'applaud_high':(me.numPieces>=40?'applaud_medium':'applaud_low'));
					me.drawingStack.pop(); // remove preview, no longer needed
					}
				me.draw();
				break;
				}
			}
		};
	}
