/*
UvumiTools Crop V1.0.0 http://uvumi.com/tools/crop.html

Copyright (c) 2008 Uvumi LLC

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

-------------------------------------------------------

Class: Cropper

Note:
	Originally written to allow user to generate custom thumbnails after uploading an image to the server.
	User selects a zone on the picture, and then the coordinates can be sent to the server where the actual image file will be processed.
	Since most of you out there love PHP, you can use imagecopyresampled() from GD library to achieve that. Check it out on php.net
	Sorry other people, I'm sure you can find similar tools for asp or whatever you're using.
	I thought it would also be usefull to create highlight zones, like on flickr.
	The mask, overlayed on the image, was originally optional, but we decided to make it a standard feature because : a) it really helps readability, b) save a lot of testing.
	If you really don't want the mask, just set its opacity to 0, or recode the whole thing, it's open source.
	Tested in Firefox2, Opera, IE6, IE7 and Safari for windows.
	IE (of course) requires a CSS trick to work. Not really a hack, since we don't use special condition or browser specific syntax,
	but in order to drag the selection zone, it needs a "solid" background, otherwise, it doesn't catch the mouse down event, it just goes through.
	So we just set a fully transparent GIF as a background image, it does the job.
	

Arguments:
	target - the picture to crop. Can be the picture element or the picture's id
	options - see Options bellow

Options:
	maskOpacity - to avoid ugly CSS for cross-brower compatibility, we set the mask opacity  with mootools. range from 0 to 1. Default is 0.5
	maskClassName - Mask color really on CSS file. Define the CSS class for the mask here. Default is 'cropperMask'.
	resizerClassName - Here again, to avoid hard-coded styling, the selector style is define with css. Correspond to the outter area of the selector, the one that let you resize.
	This class is important to define the selection border (this dashed red one in the demo) Default is 'copper'.
	moverClassName - Same here, correspond to the inner area of the selector. in the demo it's only used to set a 'move' cursor.
	mini - {x,y} minimum size of the selection in pixels. Selection will be initialised with this values. Default are 80 pixels wide and high. This are also the values used to generate the preview. They must match the size of the tumbnail/image your going to generate on the serverside.
	onComplete - Event function fired when when you stop dragging/resizing. recieves four parameters: top,left,width,height of the selection.
	you can use this function and values to update a form that contains one hidden input for each value (or one input with a json string, it's up to you) Then by submitting this form to a server script, you should be able to cropped your image (eg : with GD library in PHP)
	resizerWidth - Size of the resizing zone, on left and bottom of selection. Default is 8px 
	resizable - if the selection can be resized. Default is true
	keepRatio - if aspect ratio must be kept when resizing. Default is true
	preview - if set as a target element/id, a preview will be rendered in the element. Default is just set to false, no preview

Exemple:
	var myCropper = new Cropper('myImage', {
		mini: {x:160, y:120},
		onComplete:function(top,left,width,height){
			$('resize_coords').value="top:"+ top +"px, left:"+ left +"px, width:"+ width +"px, height:"+ height +"px";
		}
	});
*/

var uvumiCropper = new Class({
	
	Implements : [Events, Options],
	
	options : {
		maskOpacity:0.5,
		maskClassName:'cropperMask',
		moverClassName:'cropperMove',
		resizerClassName:'cropperResize',
		wrapperClassName:'cropperWrapper',
		mini:{x:100,y:100},
		onComplete:$empty,
		resizerWidth:8,
		resizable:true,
		keepRatio:true,
		preview:false
	},

	initialize : function(target,options){
		window.addEvent('domready',function(){
			//target is our image
			this.target=$(target);
			
			//just an idiot check before anything else, if target is not an image, just exits.
			if(this.target.get('tag'!='img')){
				return false;
			}
					
			this.setOptions(options);
			
			//Just creating a SHORTER variable name, because this one comes back often
			this.mini={};
			this.mini.x=this.options.mini.x.toInt();
			this.mini.y=this.options.mini.y.toInt();
			
			if(!this.options.resizable){
				this.options.resizerWidth=0;
			}
			
			this.runonce = false;
			//we preload the image before doing anything, to make sure we get the right dimensions.
			new Asset.image(this.target.get('src'),{
				onload:function(){
					if(this.runonce){
						return;
					}
					this.runonce=true;
					//Generating the new elements. see the functions for more details
					this.buildCropper();
					this.buildMask();
					
					//initialize the dragging, using the target element as container, and the selection's dragger as handle
					//On complete we fire the optional event function, providing the top, left, width and height of the selection a parameters.
					//Those coordinates are also stored in the object, so you can access them from an external function,
					//while going against object oriented programming rules which state that object variables are supposed to be private
					//On drag we update the mask
					this.drag = new Drag.Move(this.resizer,{
						container:this.target,
						handle:this.dragger,
						snap:5,
						onComplete:function(){
							this.fireEvent('onComplete',[this.top,this.left,this.width,this.height]);
						}.bind(this),
						onDrag:function(){
							this.updateMask();
							this.top = ((this.rezr_coord.top-this.target_coord.top)*this.scaleRatio).toInt();
							this.left = ((this.rezr_coord.left-this.target_coord.left)*this.scaleRatio).toInt();
							this.width = Math.max((this.rezr_coord.width*this.scaleRatio).toInt(),this.mini.x);
							this.height = Math.max((this.rezr_coord.height*this.scaleRatio).toInt(),this.mini.y);
							
							this.fireEvent('onPreview',[this.top,this.left,this.width,this.height]);
						}.bind(this)
					});
					
					//We initializse the resizing object if option resizable is set to true
					if(this.options.resizable){
						//if keepRation is set to true, we initialize a ratio object variable, so we don't have to calculate on every drag event
						//it's note going to make a big differende, but I like optimization
						if(this.options.keepRatio){
							this.ratio = this.mini.x/this.mini.y;
						}
						this.resize = this.resizer.makeResizable({
							snap:5,
							limit:{
								x:[(this.mini.x/this.scaleRatio).toInt()-this.margin],
								y:[(this.mini.y/this.scaleRatio).toInt()-this.margin]
							},
							onComplete:function(){
								//Does the same thing as drag onComplete
								this.drag.fireEvent('onComplete');
							}.bind(this),
							onDrag:function(){
								//This is the tricky part. It works, but I got some bugs when stress testing it on the bottom and right borders of the element.
								//I'm sure there is a better way to do that, so feel free to adjust it.
								this.rezr_coord=this.resizer.getCoordinates(this.wrapper);
								if(this.options.keepRatio){
									this.resizer.setStyle('width',(this.rezr_coord.height*this.ratio-this.margin).toInt()+'px');
									this.rezr_coord=this.resizer.getCoordinates(this.wrapper);
									if(this.rezr_coord.bottom>this.target_coord.bottom){
										var bound = this.target_coord.bottom-this.rezr_coord.top;
										this.resizer.setStyles({'width':(bound*this.ratio).toInt()-this.margin+'px','height':bound-this.margin+'px'});
										this.rezr_coord=this.resizer.getCoordinates(this.wrapper);
									}
									if(this.rezr_coord.right>this.target_coord.right){
										var bound = this.target_coord.right-this.rezr_coord.left;
										this.resizer.setStyles({'width':bound-this.margin+'px','height':(bound/this.ratio).toInt()-this.margin+'px'});
									}
								}else{
									if(this.rezr_coord.right>this.target_coord.right){
										var bound = this.target_coord.right-this.rezr_coord.left-this.margin+'px';
										this.resizer.setStyle('width',bound);
										this.rezr_coord=this.resizer.getCoordinates(this.wrapper);
									}
									if(this.rezr_coord.bottom>this.target_coord.bottom){
										var bound = this.target_coord.bottom-this.rezr_coord.top-this.margin+'px';
										this.resizer.setStyle('height',bound);
									}
								}
								//to update the mask and preview
								this.drag.fireEvent('onDrag');
							}.bind(this)
						});
					}
					//because preview is only refreshed when the selection is beeing moved/resized, we fire the drag event to generate the initial preview, before the user has done anything.
					this.drag.fireEvent('onDrag');
					this.drag.fireEvent('onComplete');
					this.show();
				}.bind(this)
			});
		}.bind(this));	
	},

	buildCropper : function(){
		//a wrapper element is created. it adopts the target element
		//using a wrapper is important because every positionning can be done relatively to it.
		//If you resize the window of the the document layout is modified, the mask and selection will stay aligned onto the picture.
		//It wouldn't be the case if we were just working directly in the documen body.
		//You may have to edit the wrapper's CSS to keep the original look of your page after it is injected around the image
		//(if image was floateed, had a margin ore a border, you'll have to set the same properties to the wrapper)
		//That's why we assign it a css class.
		this.wrapper = new Element('div',{
			'class':this.options.wrapperClassName,
			'styles':{
				'position':'relative',
				'width' : this.target.getSize().x,
				'height' :this.target.getSize().y,
				'overflow':'hidden'
			}
		}).wraps(this.target);
		this.border = this.target.getStyle('border-left');
		//just in case, to avoid bad results because of browser default styling
		this.target.setStyles({
			'margin':0,
			'border':0,
			'float':'none'
		});
		
		//get the target element coordinates, will be used a lot. We suppose the element position doesn't change
		this.target_coord=this.target.getCoordinates(this.wrapper);
		
		//Here we test if the image has been resized on the output, with the width/height attributes, or with style properties.	
		new Asset.image(this.target.get('src'),{
			onload:function(image){
				this.scaleRatio = image.get('width')/this.target_coord.width;
				if(this.resize){
					if(this.scaleRatio<1){
						this.resizer.setStyles({
							width:(this.mini.x/this.scaleRatio).toInt()-this.margin,
							height:(this.mini.y/this.scaleRatio).toInt()-this.margin
						});
					}
					this.resize.options.limit = {
						x:[(this.mini.x/this.scaleRatio).toInt()-this.margin],
						y:[(this.mini.y/this.scaleRatio).toInt()-this.margin]
					};
					this.drag.fireEvent('onDrag');
					this.drag.fireEvent('onComplete');
				}
			}.bind(this)
		});	
		
		//We might modify the original mimimum values in the next tests,
		//but wew are still going to need them for the preview, so we make copies
		this.previewSize = {
			x:this.mini.x,
			y:this.mini.y
		};
		
		//This is just for extrem cases, if you have an image smaller than the minimum required, or a resized image with weird proportions (super tall or super wide)
		if(this.target_coord.width<this.mini.x){
			this.mini.y = this.target_coord.width*this.mini.y/this.mini.x;
			this.mini.x = this.target_coord.width;
		}
		if(this.target_coord.height<this.mini.y){
			this.mini.x = this.target_coord.height*this.mini.x/this.mini.y;
			this.mini.y = this.target_coord.height;
		}
		
		//the main selection element,  which will be draggable and resizable, generated from the options. It is centered on the target image
		//We assign it a CSS class, because it's important to give it a border, especially if you disable the mask.
		//Also, use it to set a blank GIF background image, otherwise the mousedown event is not fired in IE if the element doesn't have a "solid" background
		//The critical preoperties, like position and dimension are hardcoded by safety.
		//Another trick is to set a background color and a very low opacity, like 0.01, but in this case the boder disapears too.
		this.resizer = new Element('div',{
			'class':this.options.resizerClassName,
			'styles':{
				'position':'absolute',
				'display':'block',
				'margin':0,
				'opacity':0,
				'width':this.mini.x,
				'height':this.mini.y,
				'left':(this.target_coord.left+(this.target_coord.width/2)-(this.mini.x/2)).toInt(),
				'top':(this.target_coord.top+(this.target_coord.height/2)-(this.mini.y/2)).toInt(),
				'padding':'0 ' + this.options.resizerWidth.toInt() + 'px ' + this.options.resizerWidth.toInt()+ 'px 0',
				'z-index':5
			}
		}).inject(this.target,'after');
		
		//In our case, it is important that the selection has exactly the dimension we want it to have, because we want to use its coordinates
		//So, because the selection element has a margin and a padding(requiered), we must substract thos values everytime we set the selection width or height
		//we set it once in the object, so we doesn't have to calculate it everytime
		this.margin=2*this.resizer.getStyle('border-width').toInt() + this.options.resizerWidth.toInt();
		
		this.resizer.setStyles({
			width:this.mini.x - this.margin + "px",
			height:this.mini.y - this.margin + "px"
		});
				
		//The dragger is an element injected inside the selection element. it will be used as  a handle for the Drag.Move object
		//no much styling is required except the one hard-coded here. You can just set the cursor style in css.
		//it is important to stop the mouse event, or the selection would be moving and growing at the same time.
		this.dragger = new Element('div',{
			'class':this.options.moverClassName,
			'styles':{
				'display':'block',
				'position':'relative',
				'width':'100%',
				'height':'100%',
				'margin':0,
				'font-size':0,
				'line-height':0
			},
			'events':{
				'mousedown':function(e){
					e.stop();
				}
			}
		}).inject(this.resizer);
		
		//if a string or element has been set for preview option, and if the element exists,
		//we generate the preview picture
		if(this.options.preview && $(this.options.preview)){
			this.preview = $(this.options.preview);
			
			//we put the preview in a wrapper div with a fixed height, because preview height and width may change.
			this.previewWrapper = new Element('div',{
				'styles':{
					'height':this.previewSize.y+2*this.preview.getStyle('border-width').toInt()+this.preview.getStyle('margin-top').toInt()+this.preview.getStyle('margin-bottom').toInt()
				}
			}).wraps(this.preview);
			
			//Setting the preview container
			this.preview.setStyles({
				'display':'block',
				'position':'relative',
				'width':this.previewSize.x,
				'height':this.previewSize.y,
				'overflow':'hidden',
				'margin':'auto',
				'font-size':0,
				'line-height':0,
				'opacity':0
			});
			
			//cloning the original image for the preview
			this.previewImage = this.target.clone();
			this.previewImage.removeProperties('width','height').setStyles({
				'position':'absolute'
			}).inject(this.preview);
			
			//because preview generation is very differnt depending on if the aspect ratio must be kept or not, we have a separate function for each case
			if(this.options.keepRatio){
				this.addEvent('onPreview',this.updatePreviewKeepRatio.bind(this));
			}else{
				this.addEvent('onPreview',this.updatePreview.bind(this));
			}
		}
	},

	buildMask : function(){
		this.innermask=this.target.clone();
		this.innermask.setStyles({
			'position':'absolute',
			'padding':0,
			'margin':0,
			'top':0,
			'left':0,
			'z-index':4,
			'opacity':0
		}).inject(this.wrapper);
		this.outtermask = new Element('div',{
			'class':this.options.maskClassName,
			'styles':{
				'position':'absolute',
				'padding':0,
				'margin':0,
				'top':0,
				'left':0,
				'width':'100%',
				'height':this.target_coord.height,
				'z-index':3,
				'opacity':0
			},
			events:{
				'click':this.moveToClick.bind(this)
			}
		}).inject(this.wrapper);
		this.slide = new Fx.Elements($$(this.resizer,this.innermask,this.previewImage),{
			onComplete:function(){
				this.updateMask();
				this.drag.fireEvent('onDrag');
				this.drag.fireEvent('onComplete');
			}.bind(this)
		});
	},

	updatePreviewKeepRatio : function(top,left,width,height){
		//Function to upadte the preview when aspect ration is on
		this.previewImage.setStyles({
			'width':(this.target_coord.width*this.previewSize.x*this.scaleRatio/width).toInt(),
			'top':-(top*this.previewSize.y/height).toInt(),
			'left':-(left*this.previewSize.x/width).toInt()
		});
	},
	
	updatePreview: function(top,left,width,height){
		//Function to update the preview when aspect ratio is off
		if(height*this.previewSize.x/width<this.previewSize.y){
			this.preview.setStyles({
				'width':this.previewSize.x,
				'height':(this.previewSize.x*height/width).toInt()		
			});
			this.previewImage.setStyles({
				'width':(this.target_coord.width*this.previewSize.x*this.scaleRatio/width).toInt(),
				'height':'auto',
				'top':-(top*this.previewSize.x/width).toInt(),
				'left':-(left*this.previewSize.x/width).toInt()
			});
		}else{
			this.preview.setStyles({
				'height':this.previewSize.y,
				'width':(this.previewSize.y*width/height).toInt()
			});
			this.previewImage.setStyles({
				'height':(this.target_coord.height*this.previewSize.y*this.scaleRatio/height).toInt(),
				'width':'auto',
				'top':-(top*this.previewSize.y/height).toInt(),
				'left':-(left*this.previewSize.y/height).toInt()
			});
		}
	},
	
	updateMask : function(){
		//update the mask position
		this.rezr_coord = this.resizer.getCoordinates(this.wrapper);
		this.innermask.setStyle('clip','rect('+this.rezr_coord.top+'px '+this.rezr_coord.right+'px '+this.rezr_coord.bottom+'px '+this.rezr_coord.left+'px)');
	},
	
	moveToClick : function(e){
		var mouseX = e.page.x;
		var mouseY = e.page.y;
		var wrap_coord = this.wrapper.getPosition();
		var localX = mouseX-wrap_coord.x;
		var localY = mouseY-wrap_coord.y;
		var top = (localY-(this.rezr_coord.height/2).toInt()).limit(0,this.target_coord.height-this.rezr_coord.height);
		var left = (localX-(this.rezr_coord.width/2).toInt()).limit(0,this.target_coord.width-this.rezr_coord.width);
		var right = left+this.rezr_coord.width;
		var bottom = top+this.rezr_coord.height;
		var effect = {
			0:{
				'top':top,
				'left':left
			},
			1:{
				'clip':[[this.rezr_coord.top,this.rezr_coord.right,this.rezr_coord.bottom,this.rezr_coord.left],[top,right,bottom,left]]
			}
		};
		if(this.preview){
			var prevSize = this.preview.getSize();
			effect[2]={
				'top':-(top*prevSize.y/(bottom-top)).toInt(),
				'left':-(left*prevSize.x/(right-left)).toInt()
			}
		}
		this.slide.start(effect);
	},
	
	hide:function(){
		$$(this.resizer,this.innermask,this.outtermask,this.preview).fade('out');
	},
	
	show:function(){
		$$(this.resizer,this.innermask,this.preview).fade('in');
		this.outtermask.fade(this.options.maskOpacity);
	},
	
	toggle:function(){
		if(this.resizer.getStyle('opacity')==1){
			this.hide();
		}else{
			this.show();
		}
	},
	
	destroy:function(){
		//if you need to remove the cropper when you're done
		this.hide();
		this.target.setStyle('border',this.border);
		(function(){
			this.target.replaces(this.wrapper);
			if(this.preview){
				this.preview.empty().setStyles({
					height:0,
					width:0
				});
			}
		}).delay(600,this);
	}
});