/**
 * 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 (modified to remove global variables: http://sourceforge.net/p/tinymce/plugins/186/)
 * @link       http://www.hodginsmedia.com Hodgins Media Ventures Inc.
 * @copyright  Copyright (C) 2008-2010 Hodgins Media Ventures Inc., All right reserved.
 * @license    http://www.opensource.org/licenses/lgpl-3.0.html LGPLv3
 */
(function() {

    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) {

            /**
             * Stores pre-resize image dimensions
             * @var {array} (w,h)
             */
            ed.originalDimensions = new Array();

            /**
             * Stores last dimensions before a resize
             * @var {array} (w,h)
             */
            ed.lastDimensions = new Array();

            /**
             * Track mousedown status in editor
             * @var {boolean}
             */
            ed.edMouseDown = false;

            // 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 != null && 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 != null && 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) { ed.edMouseDown = true; });
                ed.onMouseUp.add(function(e) { ed.edMouseDown = false; });
                ed.onInit.add(function(ed, o) {
                    tinymce.dom.Event.add(ed.getBody().parentNode, 'dragdrop', function(e) { ed.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 (!ed.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.3'
            };
        }
    });

    // 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 (!ed.originalDimensions[elId]) {
            ed.originalDimensions[elId] = ed.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.id + "_" + 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') != ed.lastDimensions[elId].width || dom.getAttrib(el, 'height') != ed.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', ed.lastDimensions[elId].width);
            dom.setAttrib(el, 'height', ed.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 (ed.lastDimensions[elId].width != dom.getAttrib(el, 'width', el.width) || ed.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
            ed.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 != ed.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 != ed.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 = ed.originalDimensions[elId].width / ed.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  = ed.lastDimensions[elId].width;
        var lastH  = ed.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             = ed.originalDimensions[elId].width;
        var oH             = ed.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 };
    }

})();