/**
 * TinyMCE Advanced Image Resize Helper Plugin
 *
 * Forces images to maintain aspect ratio while scaling - also optionally enforces
 * min/max image dimensions, and appends width/height to the image URL for server-side
 * resizing
 *
 * @author     Marc Hodgins
 * @link       http://www.hodginsmedia.com Hodgins Media Ventures Inc.
 * @copyright  Copyright (C) 2008 Hodgins Media Ventures Inc., All right reserved.
 * @license    http://www.opensource.org/licenses/lgpl-3.0.html LGPLv3
 */
(function() {

	/**
	 * Stores pre-resize image dimensions
	 * @var {array} (w,h)
	 */
	var originalDimensions = new Array();
	
	/**
	 * Stores last dimensions before a resize
	 * @var {array} (w,h)
	 */
	var lastDimensions = new Array();
	
	/**
	 * Track mousedown status in editor
	 * @var {boolean}
	 */
	var edMouseDown = false;
	
	tinymce.create('tinymce.plugins.AdvImageScale', {
		/**
		 * Initializes the plugin, this will be executed after the plugin has been created.
		 *
		 * @param {tinymce.Editor} ed Editor instance that the plugin is initialized in.
		 * @param {string} url Absolute URL to where the plugin is located.
		 */
		init : function(ed, url) {
					
			// Watch for mousedown (as a fall through to ensure that prepareImage() definitely
			// got called on an image tag before mouseup).
			// 
			// Normally this should have happened via the onPreProcess/onSetContent listeners, but
			// for completeness we check once more here in case there are edge cases we've missed.
			ed.onMouseDown.add(function(ed, e) {
				var el = tinyMCE.activeEditor.selection.getNode();
				if (el.nodeName == 'IMG') {
					// prepare image for resizing
					prepareImage(ed, e.target);
				}
				return true;
			});
			
			// Watch for mouseup (catch image resizes)
			ed.onMouseUp.add(function(ed, e) {			
				var el = tinyMCE.activeEditor.selection.getNode();	
				if (el.nodeName == 'IMG') {
					// setTimeout is necessary to allow the browser to complete the resize so we have new dimensions
					setTimeout(function() {
						constrainSize(ed, el);
					}, 100);
				}
				return true;
			});

			/*****************************************************
			 * ENFORCE CONSTRAINTS ON CONTENT INSERTED INTO EDITOR
			 *****************************************************/

			// Catch editor.setContent() events via onPreProcess (because onPreProcess allows us to 
			// modify the DOM before it is inserted, unlike onSetContent)
			ed.onPreProcess.add(function(ed, o) {
				if (!o.set) return; // only 'set' operations let us modify the nodes
				
				// loop in each img node and run constrainSize	
				tinymce.each(ed.dom.select('img', o.node), function(currentNode) {
					constrainSize(ed, currentNode);
				});
			});

			// To be complete, we also need to watch for setContent() calls on the selection object so that
			// constraints are enforced (i.e. in case an <img> tag is inserted via mceInsertContent).
			// So, catch all insertions using the editor's selection object
			ed.onInit.add(function(ed) {
				// http://wiki.moxiecode.com/index.php/TinyMCE:API/tinymce.dom.Selection/onSetContent
				ed.selection.onSetContent.add(function(se, o) {
					// @todo This seems to grab the entire editor contents - it works but could
					//       perform poorly on large documents
					var currentNode = se.getNode();
					tinymce.each(ed.dom.select('img', currentNode), function (currentNode) {
						// IF condition required as tinyMCE inserts 24x24 placeholders uner some conditions
						if (currentNode.id != "__mce_tmp") 
							constrainSize(ed, currentNode);
					});
				});
			});

			/*****************************
			 * DISALLOW EXTERNAL IMAGE DRAG/DROPS
			 *****************************/
			// This is a hack.  Listening for drag events wasn't working.
			// 
			// Watches for mousedown and mouseup/dragdrop events within the editor.  If a mouseup or
			// dragdrop occurs in the editor without a preceeding mousedown, we assume it is an external
			// dragdrop that should be rejected.
			if (ed.getParam('advimagescale_reject_external_dragdrop', true)) {

				// catch mousedowns mouseups and dragdrops (which are basically mouseups too..)
				ed.onMouseDown.add(function(e) { edMouseDown = true; });
				ed.onMouseUp.add(function(e) { edMouseDown = false; });
				ed.onInit.add(function(ed, o) {
					tinymce.dom.Event.add(ed.getBody().parentNode, 'dragdrop', function(e) { edMouseDown = false; });
				});
	
				// watch for drag attempts
				var evt = (tinymce.isIE) ? 'dragenter' : 'dragover'; // IE allows dragdrop reject on dragenter (more efficient)
				ed.onInit.add(function(ed, o) {
					// use parentNode to go above editor content, to cover entire editor area
					tinymce.dom.Event.add(ed.getBody().parentNode, evt, function (e) {
						if (!edMouseDown) {
							// disallow drop
							return tinymce.dom.Event.cancel(e);
						}
					});
				});
				
			}
		},

		/**
		 * Returns information about the plugin as a name/value array.
		 * The current keys are longname, author, authorurl, infourl and version.
		 *
		 * @return {Object} Name/value array containing information about the plugin.
		 */
		getInfo : function() {
			return {
				longname  : 'Advanced Image Resize Helper',
				author    : 'Marc Hodgins',
				authorurl : 'http://www.hodginsmedia.com',
				infourl   : 'http://code.google.com/p/tinymce-plugin-advimagescale',
				version   : '1.1.2'
			};
		}
	});

	// Register plugin
	tinymce.PluginManager.add('advimagescale', tinymce.plugins.AdvImageScale);

	/**
	 * Store image dimensions, pre-resize
	 *
	 * @param {object} el HTMLDomNode
	 */
	function storeDimensions(ed, el) {
		var dom = ed.dom;
		var elId = dom.getAttrib(el, 'mce_advimageresize_id');

		// store original dimensions if this is the first resize of this element
		if (!originalDimensions[elId]) {
			originalDimensions[elId] = lastDimensions[elId] = {width: dom.getAttrib(el, 'width', el.width), height: dom.getAttrib(el, 'height', el.height)};
		}
		return true;
	}

	/**
	 * Prepare image for resizing
	 * Check to see if we've seen this IMG tag before; does tasks such as adding
	 * unique IDs to image tags, saving "original" image dimensions, etc.
	 * @param {object} e is optional
	 */
	function prepareImage(ed, el) {
		var dom = ed.dom;
		var elId= dom.getAttrib(el, 'mce_advimageresize_id');

		// is this the first time this image tag has been seen?
		if (!elId) {
			var elId = ed.dom.uniqueId();
			dom.setAttrib(el, 'mce_advimageresize_id', elId);
			storeDimensions(ed, el);
		}
		
		return elId;
	}

	/**
	 * Adjusts width and height to keep within min/max bounds and also maintain aspect ratio
	 * If mce_noresize attribute is set to image tag, then image resize is disallowed
	 */
	function constrainSize(ed, el, e) {
		var dom = ed.dom;
		var elId = prepareImage(ed, el); // also calls storeDimensions
		var resized = (dom.getAttrib(el, 'width') != lastDimensions[elId].width || dom.getAttrib(el, 'height') != lastDimensions[elId].height);

		if (!resized)
			return; // nothing to do

		// disallow image resize if mce_noresize or the noresize class is set on the image tag
		if (dom.getAttrib(el, 'mce_noresize') || dom.hasClass(el, ed.getParam('advimagescale_noresize_class', 'noresize')) || ed.getParam('advimagescale_noresize_all')) {
			dom.setAttrib(el, 'width', lastDimensions[elId].width);
			dom.setAttrib(el, 'height', lastDimensions[elId].height);
			if (tinymce.isGecko)
				fixGeckoHandles(ed);
			return;
		}

		// Both IE7 and Gecko (as of FF3.0.03) has a "expands image by border width" bug before doing anything else
		if (ed.getParam('advimagescale_fix_border_glitch', true /* default to true */)) {
			fixImageBorderGlitch(ed, el);
			storeDimensions(ed, el); // store adjusted dimensions
		}

		// filter by regexp so only some images get constrained
		var src_filter = ed.getParam('advimagescale_filter_src');
		if (src_filter) {
			var r = new RegExp(src_filter);
			if (!el.src.match(r)) {
				return; // skip this element
			}
		}
		
		// allow filtering by classname
		var class_filter = ed.getParam('advimagescale_filter_class');
		if (class_filter) {
			if (!dom.hasClass(el, class_filter)) {
				return; // skip this element, doesn't have the class we want
			}
		}

		// populate new dimensions object
		var newDimensions = { width: dom.getAttrib(el, 'width', el.width), height: dom.getAttrib(el, 'height', el.height) };

		// adjust w/h to maintain aspect ratio
		if (ed.getParam('advimagescale_maintain_aspect_ratio', true /* default to true */)) {
		        newDimensions = maintainAspect(ed, el, newDimensions.width, newDimensions.height);
		}
		
		// enforce minW/minH/maxW/maxH
		newDimensions = checkBoundaries(ed, el, newDimensions.width, newDimensions.height);

		// was an adjustment made?
		var adjusted      = (dom.getAttrib(el, 'width', el.width) != newDimensions.width || dom.getAttrib(el, 'height', el.height) != newDimensions.height);
		
		// apply new w/h
		if (adjusted) {
			dom.setAttrib(el, 'width',  newDimensions.width);
			dom.setAttrib(el, 'height', newDimensions.height);
			if (tinymce.isGecko) fixGeckoHandles(ed);
		}

		if (ed.getParam('advimagescale_append_to_url')) {
			appendToUri(ed, el, dom.getAttrib(el, 'width', el.width), dom.getAttrib(el, 'height', el.height));
		}

		// was the image resized?
		if (lastDimensions[elId].width != dom.getAttrib(el, 'width', el.width) || lastDimensions[elId].height != dom.getAttrib(el, 'height', el.height)) {
		        // call "image resized" callback (if set)
			if (ed.getParam('advimagescale_resize_callback')) {
				ed.getParam('advimagescale_resize_callback')(ed, el);
			}
		}

		// remember "last dimensions" for next time
	        lastDimensions[elId] = { width: dom.getAttrib(el, 'width', el.width), height: dom.getAttrib(el, 'height', el.height) };
	}

	/**
	 * Fixes IE7 and Gecko border width glitch
	 *
	 * Both "add" the border width to an image after the resize handles have been
	 * dropped.  This reverses it by looking at the "previous" known size and comparing
	 * to the current size.  If they don't match, then a resize has taken place and the browser
	 * has (probably) messed it up.  So, we reverse it.  Note, this will probably need to be
	 * wrapped in a conditional statement if/when each browser fixes this bug.
	 */
	function fixImageBorderGlitch(ed, el) {
		var dom = ed.dom;
		var elId = dom.getAttrib(el, 'mce_advimageresize_id');		
		var currentWidth = dom.getAttrib(el, 'width', el.width);
		var currentHeight= dom.getAttrib(el, 'height', el.height);
		var adjusted = false;
		
		// if current dimensions do not match what we last saw, then a resize has taken place
		if (currentWidth != lastDimensions[elId].width) {
			var adjustWidth = 0;

			// get computed border left/right widths
			adjustWidth += parseInt(dom.getStyle(el, 'borderLeftWidth', 'borderLeftWidth'));
			adjustWidth += parseInt(dom.getStyle(el, 'borderRightWidth', 'borderRightWidth'));
			
			// reset the width height to NOT include these amounts
			if (adjustWidth > 0) {
				dom.setAttrib(el, 'width', (currentWidth - adjustWidth));
				adjusted = true;
			}
		}
		if (currentHeight != lastDimensions[elId].height) {
			var adjustHeight = 0;

			// get computed border top/bottom widths
			adjustHeight += parseInt(dom.getStyle(el, 'borderTopWidth', 'borderTopWidth'));
			adjustHeight += parseInt(dom.getStyle(el, 'borderBottomWidth', 'borderBottomWidth'));

			if (adjustHeight > 0) {
				dom.setAttrib(el, 'height', (currentHeight - adjustHeight));
				adjusted = true;
			}
		}
		if (adjusted && tinymce.isGecko) fixGeckoHandles(ed);
	}

	/**
	 * Fix gecko resize handles glitch
	 */
	function fixGeckoHandles(ed) {
		ed.execCommand('mceRepaint', false);
	}

	/**
	 * Set image dimensions on into a uri as querystring params
	 */
	function appendToUri(ed, el, w, h) {
		var dom  = ed.dom;
		var uri  = dom.getAttrib(el, 'src');
		var wKey = ed.getParam('advimagescale_url_width_key', 'w');
		uri      = setQueryParam(uri, wKey, w);
		var hKey = ed.getParam('advimagescale_url_height_key', 'h');
		uri      = setQueryParam(uri, hKey, h);

		// no need to continue if URL didn't change
		if (uri == dom.getAttrib(el, 'src')) {
			return;
		}
			
		// trigger image loading callback (if set)
		if (ed.getParam('advimagescale_loading_callback')) {
			// call loading callback
			ed.getParam('advimagescale_loading_callback')(el);
		}
		// hook image load(ed) callback (if set)
		if (ed.getParam('advimagescale_loaded_callback')) {
			// hook load event on the image tag to call the loaded callback
			tinymce.dom.Event.add(el, 'load', imageLoadedCallback, {el: el, ed: ed});
		}

		// set new src
		dom.setAttrib(el, 'src', uri);
	}
	
	/**
	 * Callback event when an image is (re)loaded
	 * @param {object} e Event (use e.target or this.el to access element, this.ed to access editor instance)
	 */
	function imageLoadedCallback(e) {
		var el       = this.el; // image element
		var ed       = this.ed; // editor
		var callback = ed.getParam('advimagescale_loaded_callback'); // user specified callback

		// call callback, pass img as param
		callback(el);
		
		// remove callback event
		tinymce.dom.Event.remove(el, 'load', imageLoadedCallback);
	}

	/**
 	 * Sets URL querystring parameters by appending or replacing existing params of same name
	 */
	function setQueryParam(uri, key, value) {
		if (!uri.match(/\?/)) uri += '?';
		if (!uri.match(new RegExp('([\?&])' + key + '='))) {
			if (!uri.match(/[&\?]$/)) uri += '&';
			uri += key + '=' + escape(value);
		} else {
		        uri = uri.replace(new RegExp('([\?\&])' + key + '=[^&]*'), '$1' + key + '=' + escape(value));
		}
		return uri;
	}

	/**
	 * Returns w/h that maintain aspect ratio
	 */
	function maintainAspect(ed, el, w, h) {
		var elId = ed.dom.getAttrib(el, 'mce_advimageresize_id');

		// calculate aspect ratio of original so we can maintain it
	        var ratio = originalDimensions[elId].width / originalDimensions[elId].height;
	
		// decide which dimension changed more (percentage),  because that's the
	        // one we'll respect (the other we'll adjust to keep aspect ratio)
		var lastW  = lastDimensions[elId].width;
		var lastH  = lastDimensions[elId].height;
		var deltaW = Math.abs(lastW - w);       // absolute
		var deltaH = Math.abs(lastH - h);       // absolute
		var pctW   = Math.abs(deltaW / lastW);  // percentage
		var pctH   = Math.abs(deltaH / lastH);  // percentage

		if (deltaW || deltaH) {
			if (pctW > pctH) {
				// width changed more - use that as the locked point and adjust height
				return { width: w, height: Math.round(w / ratio) };
			} else {
				// height changed more - use that as the locked point and adjust width
				return { width: Math.round(h * ratio), height: h };
			}
		}
		
		// nothing changed
		return { width: w, height: h };
	}

	/**
	 * Enforce min/max boundaries
	 *
	 * Returns true if an adjustment was made
	 */
	function checkBoundaries(ed, el, w, h) {

		var elId  	   = ed.dom.getAttrib(el, 'mce_advimageresize_id');
		var maxW	   = ed.getParam('advimagescale_max_width');
		var maxH	   = ed.getParam('advimagescale_max_height');
		var minW	   = ed.getParam('advimagescale_min_width');
		var minH	   = ed.getParam('advimagescale_min_height');
	        var maintainAspect = ed.getParam('advimagescale_maintain_aspect_ratio', true);
	        var oW 		   = originalDimensions[elId].width;
	        var oH 		   = originalDimensions[elId].height;
	        var ratio 	   = oW/oH;

		// max
		if (maxW && w > maxW) {
			w = maxW;
			h = maintainAspect ? Math.round(w / ratio) : h;
                }
                if (maxH && h > maxH) {
                	h = maxH;
			w = maintainAspect ? Math.round(h * ratio) : w;
                }

                // min
                if (minW && w < minW) {
                	w = minW;
                	h = maintainAspect ? Math.round(w / ratio) : h;
                }
                if (minH && h < minH) {
                	h = minH;
                	w = maintainAspect ? Math.round(h * ratio) : h;
                }

		return { width: w, height:h };
	}

})();