/*
		Copyright (c) 2008, Ernest Delgado
		Code licensed under the BSD License:
		http://www.ernestdelgado.com/public-tests/canvasphoto/src/license.txt
		version: 1.0.0
		*/
		
		var Canvas = window.Canvas || {};

		(function () {

			/**
			 * Canvas Element Class
			 *
			 * @namespace Canvas.Element
			 * @class Element
			 * @constructor
			 * @param el {HTMLElement | String} Container element for the canvas.
			 */
			Canvas.Element = function(el, oConfig) {
				this._initElement(el);
				this._initConfig(oConfig);
				this._createCanvasBackground();
				this._createContainer();
				this._initEvents();
				this._initCustomEvents();
			};

			function getVerifiedElement() {

			}

			/**
			 * From YUI (2.4.0?)
		     * We want to be able to use getElementsByTagName as a collection
		     * to attach a group of events to.  Unfortunately, different 
		     * browsers return different types of collections.  This function
		     * tests to determine if the object is array-like.  It will also 
		     * fail if the object is an array, but is empty.
		     * @method _isValidCollection
		     * @param o the object to test
		     * @return {boolean} true if the object is array-like and populated
		     * @static
		     * @private
		     */
		    function isValidCollection(o) {
		        try {
		            return ( o                     && // o is something
		                     typeof o !== "string" && // o is not a string
		                     o.length              && // o is indexed
		                     !o.tagName            && // o is not an HTML element
		                     !o.alert              && // o is not a window
		                     typeof o[0] !== "undefined" );
		        } catch(ex) {
		            return false;
		        }
		    }

			/**
			 * Constant for the default CSS class name that represents a Canvas
		     * @property Canvas.Element.CSS_CANVAS
		     * @static
		     * @final
		     * @type String
		     */
		    /// Canvas.Element.CSS_CANVAS = "canvas-module";

			Canvas.Element.prototype.fillBackground = true;
			Canvas.Element.prototype.showcorners = false;
			Canvas.Element.prototype.photoborder = true;
			Canvas.Element.prototype.polaroid = false;
			Canvas.Element.prototype._backgroundImg = null;

			/**
		     * The object literal containing mouse position if clicked in an empty area (no image)
		     * @property _groupSelector
		     * @type object
		     */
			Canvas.Element.prototype._groupSelector = null;

			/**
		     * The array element that contains all the images of the canvas
		     * @property _aImages
		     * @type object
		     */
			Canvas.Element.prototype._aImages = null;

			/**
		     * The element that references the canvas interface implementation
		     * @property _oContext
		     * @type object
		     */
			Canvas.Element.prototype._oContext = null;

			/**
		     * The main element that contains the canvas
		     * @property _oElement
		     * @type object
		     */
			Canvas.Element.prototype._oElement = null;

			/**
		     * The object literal containing config parameters
		     * @property _oConfig
		     * @type object
		     */
			Canvas.Element.prototype._oConfig = null;

			/**
		     * The object literal containing the current x,y params of the transformation
		     * @property _currentTransform
		     * @type object
		     */
			Canvas.Element.prototype._currentTransform = null;

			// 

			/**
		     * The Canvas class's initialization method. This method is automatically 
		     * called by the constructor, and sets up all DOM references for 
		     * pre-existing markup, and creates required markup if it is not 
		     * already present.
		     * @method _initElement
		     * @param el {HTMLElement | String} el The element representing the Canvas
		     */
			Canvas.Element.prototype._initElement = function(el) {
				if(YAHOO.util.Dom.inDocument(el)) {
					if(YAHOO.lang.isString(el)) {
						this._oElement = document.getElementById(el);
					} else {
						this._oElement = el;
					}
					/// YAHOO.util.Dom.addClass(this._oElement, Canvas.Element.CSS_CANVAS);
				}
				else {
					var canvasEl = document.createElement('canvas');
					canvasEl.id = el + '';
					var oCanvas = document.body.insertBefore(canvasEl, document.body.firstChild);
					/// oCanvas.setAttribute('width', this._oConfig.width);
				    /// oCanvas.setAttribute('height', this._oConfig.height);
					this._oElement = document.getElementById(el + '');
				}

				// it contains the active image and the listeners
				this._oContextTop = this._oElement.getContext('2d');
			};

			/**
		     * The custom events initialization method. 
		     * @method _initCustomEvents
		     */
			Canvas.Element.prototype._initCustomEvents = function() {
				this.beforeRenderEvent = new YAHOO.util.CustomEvent('beforeRenderEvent', this, true);	
			}

			/**
		     * For now we use an object literal without methods to store the config params
		     * @method _initConfig
		     * @param oConfig {Object} userConfig The configuration Object literal 
		     * containing the configuration that should be set for this module. 
		     * See configuration documentation for more details.
		     */
			Canvas.Element.prototype._initConfig = function(oConfig) {
				this._oConfig = oConfig;
				this._oElement.setAttribute('width', this._oConfig.width);
			    this._oElement.setAttribute('height', this._oConfig.height);
			};

			/**
		     * Adds main mouse listeners to the whole canvas
		     * @method _initEvents
		     * See configuration documentation for more details.
		     */
			Canvas.Element.prototype._initEvents = function() {
				YAHOO.util.Event.on(this._oElement, 'mousedown', this.onMouseDown, this, true);
				YAHOO.util.Event.on(this._oElement, 'mouseup', this.onMouseUp, this, true);
				YAHOO.util.Event.on(this._oElement, 'mousemove', this.onMouseMove, this, true);
				// Canvas.Element.addEventListener("mousedown", function(evt) { startTransform(evt); }, false);
			};

			/**
		     * It creates a secondary canvas to contain all the images are not being translated/rotated/scaled
		     * @method _createContainer
		     */
			Canvas.Element.prototype._createContainer = function() {
				var canvasEl = document.createElement('canvas');
				canvasEl.id = 'canvas-container';
				var oContainer = this._oElement.parentNode.insertBefore(canvasEl, this._oElement);
				oContainer.setAttribute('width', this._oConfig.width);
			    oContainer.setAttribute('height', this._oConfig.height);
				// this will contain all images that are not on the top
				this._oContextContainer = oContainer.getContext('2d'); 
			};

			Canvas.Element.prototype._createCanvasBackground = function() {
				var canvasEl = document.createElement('canvas');
				canvasEl.id = 'canvas-background';
				var oBackground = this._oElement.parentNode.insertBefore(canvasEl, this._oElement);
				oBackground.setAttribute('width', this._oConfig.width);
			    oBackground.setAttribute('height', this._oConfig.height);
				// this will contain the background
				this._oContextBackground = oBackground.getContext('2d'); 
			};

			Canvas.Element.prototype.setCanvasBackground = function(oImg) {
				this._backgroundImg = oImg;
				var originalImgSize = oImg.getOriginalSize();

				this._oContextBackground.drawImage(oImg._oElement, 0, 0, originalImgSize.width, originalImgSize.height);
			};

			/**
		     * Method that defines the actions when mouse is released on canvas.
			 * The method resets the currentTransform parameters, store the image corner
			 * position in the image object and render the canvas on top.
		     * @method onMouseUp
		     * @param e {Event} Event object fired on mouseup
		     */
			Canvas.Element.prototype.onMouseUp = function(e) {
				if (this._currentTransform) {
					// determine the new coords everytime the image changes its position
					this._currentTransform.target.setImageCoords();
				}
				this._currentTransform = null;
				this._groupSelector = null;

				// this is to clear the selector box
				this.renderTop();
			};

			/**
		     * Method that defines the actions when mouse is clicked on canvas.
			 * The method inits the currentTransform parameters and renders all the
			 * canvas so the current image can be placed on the top canvas and the rest
			 * in on the container one.
		     * @method onMouseDown
		     * @param e {Event} Event object fired on mousedown
		     */
			Canvas.Element.prototype.onMouseDown = function(e) {
				// ignore if something else is already going on
				if (this._currentTransform != null) {
					return;
				}

				// determine whether we clicked the image
				var oImg = this.findTargetImage(e, false);
				if (!oImg) {
					this._groupSelector = { ex: e.clientX, ey: e.clientY,
						 					top: 0, left: 0 };
				}
				else { 
					// determine if it's a drag or rotate case
					// rotate and scale will happen at the same time
					var action = (!this.findTargetCorner(e, oImg)) ? 'drag' : 'rotate';

					this._currentTransform = { 	target: oImg,
											action: action,
											scalex: oImg.scalex,
											offsetX: e.clientX - oImg.left,
					 						offsetY: e.clientY - oImg.top,
					 						ex: e.clientX, ey: e.clientY,
											left: oImg.left, top: oImg.top,
											theta: oImg.theta };

					// we must render all so the active image is placed in the canvastop
					this.renderAll(false);
				}
				// alert(this._oElement.className);
			};

			/**
		     * Method that defines the actions when mouse is hovering the canvas.
			 * The currentTransform parameter will definde whether the user is rotating/scaling/translating
			 * an image or neither of them (only hovering). A group selection is also possible and would cancel
			 * all any other type of action.
			 * In case of an image transformation only the top canvas will be rendered.
		     * @method onMouseMove
		     * @param e {Event} Event object fired on mousemove
		     */
			Canvas.Element.prototype.onMouseMove = function(e) {
				if (this._groupSelector != null) {
					// We initially clicked in an empty area, so we draw a box for multiple selection.
					this._groupSelector.left = e.clientX - this._groupSelector.ex;
					this._groupSelector.top = e.clientY - this._groupSelector.ey;
					this.renderTop();
				}
				else if (this._currentTransform == null) {
					// Here we are hovering the canvas then we will determine
					// what part of the pictures we are hovering to change the caret symbol.
					// We won't do that while dragging or rotating in order to improve the
					// performance.
					var targetImg = this.findTargetImage(e, true);

					// set mouse image
					this.setCursor(e, targetImg);
				}
				else {
					if (this._currentTransform.action == 'rotate') {
						this.rotateImage(e);
						this.scaleImage(e);			
					}		
					else {
						this.translateImage(e);
					}
					// only commit here. when we are actually moving the pictures
					this.renderTop();
				}		
			};

			/**
		     * Translate image
		     * @method translateImage
		     * @param e {Event} the mouse event
		     */	
			Canvas.Element.prototype.translateImage = function(e) {
				this._currentTransform.target.left = e.clientX - this._currentTransform.offsetX;
				this._currentTransform.target.top = e.clientY - this._currentTransform.offsetY;
			};

			/**
		     * Scale image
		     * @method scaleImage
		     * @param e {Event} the mouse event
		     */	
			Canvas.Element.prototype.scaleImage = function(e) {
				var lastLen = Math.sqrt(Math.pow(this._currentTransform.ey - this._currentTransform.top, 2) +
			                            Math.pow(this._currentTransform.ex - this._currentTransform.left, 2));
			    var curLen = Math.sqrt(Math.pow(e.clientY - this._currentTransform.top, 2) +
			                           Math.pow(e.clientX - this._currentTransform.left, 2));

			    this._currentTransform.target.scalex = this._currentTransform.scalex * (curLen / lastLen);
			    this._currentTransform.target.scaley = this._currentTransform.target.scalex;
		    };

			/**
		     * Rotate image
		     * @method rotateImage
		     * @param e {Event} the mouse event
		     */	
			Canvas.Element.prototype.rotateImage = function(e) {
				var lastAngle = Math.atan2(this._currentTransform.ey - this._currentTransform.top,
				                           this._currentTransform.ex - this._currentTransform.left);
				var curAngle = Math.atan2(e.clientY - this._currentTransform.top,
				                          e.clientX - this._currentTransform.left);

				this._currentTransform.target.theta = (curAngle - lastAngle) + this._currentTransform.theta;
			};

			/**
		     * Method to set the cursor image depending on where the user is hovering.
		 	 * Note: very buggy in Opera
		     * @method setCursor
		     * @param e {Event} the mouse event
		     * @param targetImg {Object} image that the mouse is hovering, if so.
		     */
			Canvas.Element.prototype.setCursor = function(e, targetImg) {
				if (!targetImg) {
					this._oElement.style.cursor = 'default';
				}
				else { 
					var corner = this.findTargetCorner(e, targetImg);
					if (!corner) {
						this._oElement.style.cursor = 'move';
					}
					else {
						if(corner == 'tr') {
							this._oElement.style.cursor = 'ne-resize';
						}
						else if(corner == 'br') {
							this._oElement.style.cursor = 'se-resize';
						}
						else if(corner == 'bl') {
							this._oElement.style.cursor = 'sw-resize';
						}
						else if(corner == 'tl') {
							this._oElement.style.cursor = 'nw-resize';
						}									
						else {
							this._oElement.style.cursor = 'default';
						}
					}
				}
			};

			/**
		     * Method to add an image to the canvas.
		 	 * It actually only pushes the images in an array that will be rendered later in the canvas.
		     * @method addImage
		     * @param oImg {Object} Image elment to attach
		     */
			Canvas.Element.prototype.addImage = function(oImg) {
				// this._aImages[this._aImages.length] = oImg;
				if(YAHOO.lang.isNull(this._aImages)) {
					this._aImages = [];
				}
				this._aImages.push(oImg);
				this.renderAll(false);

			};

			/**
		     * Method to render both the top canvas and the secondary container canvas.
		     * @method renderAll
		     * @param allOnTop {Boolean} Whether we want to force all images to be rendered on the top canvas
		     */	
			Canvas.Element.prototype.renderAll = function(allOnTop) {
				// when allOnTop equals true all images will be rendered in the top canvas.
				// This is used for actions like toDataUrl that needs to take some actions on a unique canvas.
				var containerCanvas = (allOnTop) ? this._oContextTop : this._oContextContainer;

				this._oContextTop.clearRect(0,0,parseInt(this._oConfig.width), parseInt(this._oConfig.height));
				containerCanvas.clearRect(0,0,parseInt(this._oConfig.width), parseInt(this._oConfig.height));

				if (allOnTop) {
					var originalImgSize = this._backgroundImg.getOriginalSize();
					this._oContextTop.drawImage(this._backgroundImg._oElement, 0, 0, originalImgSize.width, originalImgSize.height);
				}

				// we render the rest of images
				for (var i = 0, l = this._aImages.length-1; i < l; i += 1) {
					this.drawImageElement(containerCanvas, this._aImages[i]);			
				}
				// we render the top context
				this.drawImageElement(this._oContextTop, this._aImages[this._aImages.length-1]);
			};

			/**
		     * Method to render only the top canvas.
			 * Also used to render the group selection box.
		     * @method renderTop
		     */
			Canvas.Element.prototype.renderTop = function() {
				// this.beforeRenderEvent.fire(); // placeholder
				this._oContextTop.clearRect(0,0,parseInt(this._oConfig.width), parseInt(this._oConfig.height));

				// we render the top context
				this.drawImageElement(this._oContextTop, this._aImages[this._aImages.length-1]);

				if (this._groupSelector != null) {
					this._oContextTop.fillStyle = "rgba(0, 0, 200, 0.5)";
					this._oContextTop.fillRect(this._groupSelector.ex - ((this._groupSelector.left > 0) ? 0 : - this._groupSelector.left), this._groupSelector.ey - ((this._groupSelector.top > 0) ? 0 : - this._groupSelector.top), Math.abs(this._groupSelector.left), Math.abs(this._groupSelector.top));
					this._oContextTop.strokeRect(this._groupSelector.ex - ((this._groupSelector.left > 0) ? 0 : Math.abs(this._groupSelector.left)), this._groupSelector.ey - ((this._groupSelector.top > 0) ? 0 : Math.abs(this._groupSelector.top)), Math.abs(this._groupSelector.left), Math.abs(this._groupSelector.top));
				}
			};

			/**
		     * Method that finally uses the canvas function to render the image
		     * @method drawImageElement
		     * @param context {Object} canvas context where the image must be rendered
		     * @param oImg {Object} the image object
		     */
			Canvas.Element.prototype.drawImageElement = function(context, oImg) {
				var offsetY = oImg.height / 2;
				var offsetX = oImg.width / 2;

				context.save();
				context.translate(oImg.left, oImg.top);
				context.rotate(oImg.theta);
				context.scale(oImg.scalex, oImg.scaley);

				this.drawBorder(context, oImg, offsetX, offsetY);

				var originalImgSize = oImg.getOriginalSize();

				// drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh)
				// A = oImg.width - oImg._oElement.width = oImg.borderwidth (if any)
				// B = oImg.height - oImg._oElement.height = oImg.borderwidth + oImg.polaroidheight
				// B - A = oImg.polaroidheight
				var polaroidHeight = ((oImg.height - originalImgSize.height) - (oImg.width - originalImgSize.width))/2;

				context.drawImage(oImg._oElement, 
							- originalImgSize.width/2,  
							(- originalImgSize.height)/2 - polaroidHeight, 
							originalImgSize.width, 
							originalImgSize.height);

				if (oImg.cornervisibility) {
					this.drawCorners(context, oImg, offsetX, offsetY);
				}
				context.restore();
			};

			/**
		     * Method that returns an object with the image lines in it given the coordinates of the corners
		     * @method _getImageLines
		     * @param oCoords {Object} coordinates of the image corners
		     */
			Canvas.Element.prototype._getImageLines = function(oCoords) {
				return {
					topline: { 
						o: oCoords.tl,
						d: oCoords.tr 
					},
					rightline: { 
						o: oCoords.tr,
						d: oCoords.br 
					},
					bottomline: { 
						o: oCoords.br,
						d: oCoords.bl 
					},
					leftline: { 
						o: oCoords.bl,
						d: oCoords.tl 
					}
				}
			};

			/**
		     * Method that determines what picture are we clicking on
		     * Applied one implementation of 'point inside polygon' algorithm
		     * @method findTargetImage
		     * @param e {Event} the mouse event
		     * @param hovering {Boolean} whether or not we have the mouse button pressed
		     */	
			Canvas.Element.prototype.findTargetImage = function(e, hovering) {
				// http://www.geog.ubc.ca/courses/klink/gis.notes/ncgia/u32.html
				// http://idav.ucdavis.edu/~okreylos/TAship/Spring2000/PointInPolygon.html
				var ex = e.clientX;
				var ey = e.clientY;
				for (var i = this._aImages.length-1; i >= 0; i -= 1) {
					// we iterate through each image. If target found then return target
					var iLines = this._getImageLines(this._aImages[i].oCoords);
					var xpoints = this._findCrossPoints(ex, ey, iLines);

					// if xcount is odd then we clicked inside the image
					// For the specific case of square images xcount == 1 in all true cases
					if (xpoints % 2 == 1 && xpoints != 0) {
						var target = this._aImages[i];
						//reorder array
						if (!hovering) {
							this._aImages.splice(i, 1);
							this._aImages.push(target);
						}
						return target;
					}
				}
				return false;
			};

			/**
		     * Helper method to determine how many cross points are between the 4 image edges
		     * and the horizontal line determined by the position of our mouse when clicked on canvas
		     * @method _findCrossPoints
		     * @param ex {Number} x coordinate of the mouse
		     * @param ey {Number} y coordinate of the mouse
		     * @param oCoords {Object} Coordinates of the image being evaluated
		     */		
			Canvas.Element.prototype._findCrossPoints = function(ex, ey, oCoords) {
				var b1, b2, a1, a2, xi, yi;
				var xcount = 0;
				var iLine = null;
				for (lineKey in oCoords) {
					iLine = oCoords[lineKey];
					// optimisation 1: line below dot. no cross
					if ((iLine.o.y < ey) && (iLine.d.y < ey)) {
						continue;
					}
					// optimisation 2: line above dot. no cross
					if ((iLine.o.y >= ey) && (iLine.d.y >= ey)) {
						continue;
					}
					// optimisation 3: vertical line case
					if ((iLine.o.x == iLine.d.x) && (iLine.o.x >= ex)) { 
						xi = iLine.o.x;
						yi = ey;
					}
					// calculate the intersection point
					else {
						b1 = 0; //(y2-ey)/(x2-ex); 
						b2 = (iLine.d.y-iLine.o.y)/(iLine.d.x-iLine.o.x); 
						a1 = ey-b1*ex;
						a2 = iLine.o.y-b2*iLine.o.x;

						xi = - (a1-a2)/(b1-b2); 
						yi = a1+b1*xi; 
					}

					// dont count xi < ex cases
					if (xi >= ex) { 
						xcount += 1;
					}
					// optimisation 4: specific for square images
					if (xcount == 2) {
						break;
					}
				}
				return xcount;
			};

			/**
		     * Determine which one of the four corners has been clicked
		     * @method findTargetCorner
		     * @param e {Event} the mouse event
		     * @param oImg {Object} the image object
		     */
			Canvas.Element.prototype.findTargetCorner = function(e, oImg) {
				var ex = e.clientX;
				var ey = e.clientY;
				var xpoints = null;

				var corners = ['tl','tr','br','bl'];
				for (var i in oImg.oCoords) {
					xpoints = this._findCrossPoints(ex, ey, this._getImageLines(oImg.oCoords[i].corner));
					if (xpoints % 2 == 1 && xpoints != 0) {
						return i;
					}		
				}
				return false;
			};

			/**
		     * Draw image border, if any. That includes both normal border and polaroid border
		     * @method drawBorder
		     * @param context {Object} context (layer) where the border will be drawn
		     * @param oImg {Object} the Image object
		     * @param offsetX {Number} The horizontal offset applied from the (0,0) of the canvas axis
		     * @param offsetY {Number} The vertical offset applied from the (0,0) of the canvas axis
		     */	
			Canvas.Element.prototype.drawBorder = function(context, oImg, offsetX, offsetY) {
				var outlinewidth = 2;
				context.fillStyle = 'rgba(0, 0, 0, .3)';
				context.fillRect(-2 - offsetX, -2 - offsetY, oImg.width + (2 * outlinewidth), oImg.height + (2 * outlinewidth));
				context.fillStyle = '#fff';
				context.fillRect(-offsetX, -offsetY, oImg.width, oImg.height);
			};

			/**
		     * Draw image corners to help visual understanding of the UI (if required)
		     * @method drawCorners
		     * @param context {Object} context (layer) where the corners will be drawn
		     * @param oImg {Object} the Image object
		     * @param offsetX {Number} The horizontal offset applied from the (0,0) of the canvas axis
		     * @param offsetY {Number} The vertical offset applied from the (0,0) of the canvas axis
		     */	
			Canvas.Element.prototype.drawCorners = function(context, oImg, offsetX, offsetY) {
				context.fillStyle = "rgba(0, 200, 50, 0.5)";
				context.fillRect(-offsetX, -offsetY, oImg.cornersize, oImg.cornersize);
				context.fillRect(oImg.width - offsetX - oImg.cornersize, -offsetY, oImg.cornersize, oImg.cornersize);
				context.fillRect(-offsetX, oImg.height - offsetY - oImg.cornersize, oImg.cornersize, oImg.cornersize);
				context.fillRect(oImg.width - offsetX - oImg.cornersize, oImg.height - offsetY - oImg.cornersize, oImg.cornersize, oImg.cornersize);
			};

			/**
		     * Export the specific canvas element to an Image. Created and rendered on the browser.
		     * Beware of crossbrowser support.
		     * @method canvasTo
		     * @param format {String} the format of the output image. Either jpeg or png.
		     */	
			Canvas.Element.prototype.canvasTo = function(format) {
				this.renderAll(true);
				if (format == 'jpeg' || format == 'png') {
					return this._oElement.toDataURL('image/'+format);
				}
			};


		}());