/**
 * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
 * For licensing, see LICENSE.md or http://ckeditor.com/license
 */

'use strict';

( function() {

	var template = '<img alt="" src="" />',
		templateBlock = '<figure class="caption">' +
				template +
				'<figcaption>Caption</figcaption>' +
			'</figure>',
		regexPercent = /^\s*(\d+\%)\s*$/i;

	CKEDITOR.plugins.add( 'image2', {
		lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
		requires: 'widget,dialog',
		icons: 'image',
		hidpi: true,

		onLoad: function( editor ) {
			CKEDITOR.addCss(
			'.cke_editable.cke_image_sw, .cke_editable.cke_image_sw *{cursor:sw-resize !important}' +
			'.cke_editable.cke_image_se, .cke_editable.cke_image_se *{cursor:se-resize !important}' +
			'.cke_image_resizer{' +
				'display:none;' +
				'position:absolute;' +
				'width:10px;' +
				'height:10px;' +
				'bottom:-5px;' +
				'right:-5px;' +
				'background:#000;' +
				'outline:1px solid #fff;' +
				// Prevent drag handler from being misplaced (#11207).
				'line-height:0;' +
				'cursor:se-resize;' +
			'}' +
			'.cke_image_resizer_wrapper{' +
				'position:relative;' +
				'display:inline-block;' +
				'line-height:0;' +
			'}' +
			// Bottom-left corner style of the resizer.
			'.cke_image_resizer.cke_image_resizer_left{' +
				'right:auto;' +
				'left:-5px;' +
				'cursor:sw-resize;' +
			'}' +
			'.cke_widget_wrapper:hover .cke_image_resizer,' +
			'.cke_image_resizer.cke_image_resizing{' +
				'display:block' +
			'}' );
		},

		init: function( editor ) {
			// Adapts configuration from original image plugin. Should be removed
			// when we'll rename image2 to image.
			var config = editor.config,
				lang = editor.lang.image2,
				image = widgetDef( editor );

			// Since filebrowser plugin discovers config properties by dialog (plugin?)
			// names (sic!), this hack will be necessary as long as Image2 is not named
			// Image. And since Image2 will never be Image, for sure some filebrowser logic
			// got to be refined.
			config.filebrowserImage2BrowseUrl = config.filebrowserImageBrowseUrl;
			config.filebrowserImage2UploadUrl = config.filebrowserImageUploadUrl;

			// Add custom elementspath names to widget definition.
			image.pathName = lang.pathName;
			image.editables.caption.pathName = lang.pathNameCaption;

			// Register the widget.
			editor.widgets.add( 'image', image );

			// Add toolbar button for this plugin.
			editor.ui.addButton && editor.ui.addButton( 'Image', {
				label: editor.lang.common.image,
				command: 'image',
				toolbar: 'insert,10'
			} );

			// Register context menu option for editing widget.
			if ( editor.contextMenu ) {
				editor.addMenuGroup( 'image', 10 );

				editor.addMenuItem( 'image', {
					label: lang.menu,
					command: 'image',
					group: 'image'
				} );
			}

			CKEDITOR.dialog.add( 'image2', this.path + 'dialogs/image2.js' );
		},

		afterInit: function( editor ) {
			var align = { left: 1, right: 1, center: 1, block: 1 },
				integrate = alignCommandIntegrator( editor );

			for ( var value in align )
				integrate( value );
		}
	} );

	// @param {CKEDITOR.editor}
	// @returns {Object}
	function widgetDef( editor ) {
		return {
			// Widget-specific rules for Allowed Content Filter.
			allowedContent: {
				// This widget may need <div> centering wrapper.
				div: {
					match: centerWrapperChecker( editor ),
					styles: 'text-align'
				},
				figcaption: true,
				figure: {
					classes: '!caption',
					styles: 'float,display'
				},
				img: {
					attributes: '!src,alt,width,height',
					styles: 'float'
				},
				// This widget may need <p> centering wrapper.
				p: {
					match: centerWrapperChecker( editor ),
					styles: 'text-align'
				}
			},

			requiredContent: 'img[src,alt]',

			// Note: The following may not cover all the possible cases since
			// requiredContent supports a single tag only.
			features: {
				dimension: {
					requiredContent: 'img[width,height]'
				},
				align: {
					requiredContent: 'img{float}'
				},
				caption: {
					requiredContent: 'figcaption'
				}
			},

			// This widget converts style-driven dimensions to attributes.
			contentTransformations: [
				[ 'img[width]: sizeToAttribute' ]
			],

			// This widget has an editable caption.
			editables: {
				caption: {
					selector: 'figcaption',
					allowedContent: 'br em strong sub sup u s; a[!href]'
				}
			},

			parts: {
				image: 'img',
				caption: 'figcaption'
			},

			// The name of this widget's dialog.
			dialog: 'image2',

			// Template of the widget: plain image.
			template: template,

			data: function() {
				var widget = this,
					editor = widget.editor,
					doc = editor.document,
					editable = editor.editable(),
					oldState = widget.oldData,
					newState = widget.data,
					features = this.features;

				// Image can't be captioned when figcaption is disallowed (#11004).
				if ( newState.hasCaption && !editor.filter.checkFeature( features.caption ) )
					newState.hasCaption = false;

				// Image can't be aligned when floating is disallowed (#11004).
				if ( newState.align != 'none' && !editor.filter.checkFeature( features.align ) )
					newState.align = 'none';

				// Convert the internal form of the widget from the old state to the new one.
				widget.shiftState( {
					element: widget.element,
					oldState: oldState,
					newState: newState,

					// Destroy the widget.
					destroy: function() {
						if ( this.destroyed )
							return;

						// Remember whether widget was focused before destroyed.
						if ( editor.widgets.focused == widget )
							this.focused = true;

						editor.widgets.destroy( widget );

						// Mark widget was destroyed.
						this.destroyed = true;
					},

					init: function( element ) {
						// Create a new widget. This widget will be either captioned
						// non-captioned, block or inline according to what is the
						// new state of the widget.
						if ( this.destroyed ) {
							widget = editor.widgets.initOn( element, 'image', widget.data );

							// Once widget was re-created, it may become an inline element without
							// block wrapper (i.e. when unaligned, end not captioned). Let's do some
							// sort of autoparagraphing here (#10853).
							if ( widget.inline && !( new CKEDITOR.dom.elementPath( widget.wrapper, editable ).block ) ) {
								var block = doc.createElement( editor.activeEnterMode == CKEDITOR.ENTER_P ? 'p' : 'div' );
								block.replace( widget.wrapper );
								widget.wrapper.move( block );
							}

							// The focus must be transferred from the old one (destroyed)
							// to the new one (just created).
							if ( this.focused ) {
								widget.focus();
								delete this.focused;
							}

							delete this.destroyed;
						}

						// If now widget was destroyed just update wrapper's alignment.
						// According to the new state.
						else
							setWrapperAlign( widget );

					}
				} );

				widget.parts.image.setAttributes( {
					src: widget.data.src,

					// This internal is required by the editor.
					'data-cke-saved-src': widget.data.src,

					alt: widget.data.alt
				} );

				// Set dimensions of the image according to gathered data.
				// Do it only when the attributes are allowed (#11004).
				if ( editor.filter.checkFeature( features.dimension ) )
					setDimensions( widget );

				// Cache current data.
				widget.oldData = CKEDITOR.tools.extend( {}, widget.data );
			},

			init: function() {
				var helpers = CKEDITOR.plugins.image2,
					image = this.parts.image,
					data = {
						hasCaption: !!this.parts.caption,
						src: image.getAttribute( 'src' ),
						alt: image.getAttribute( 'alt' ) || '',
						width: image.getAttribute( 'width' ) || '',
						height: image.getAttribute( 'height' ) || '',

						// Lock ratio is on by default (#10833).
						lock: this.ready ? helpers.checkHasNaturalRatio( image ) : true
					};

				// Read initial float style from figure/image and
				// then remove it. This style will be set on wrapper in #data listener.
				if ( !data.align ) {
					data.align = this.element.getStyle( 'float' ) || image.getStyle( 'float' ) || 'none';
					this.element.removeStyle( 'float' );
					image.removeStyle( 'float' );
				}

				// Get rid of extra vertical space when there's no caption.
				// It will improve the look of the resizer.
				if ( !data.hasCaption )
					this.wrapper.setStyle( 'line-height', '0' );

				this.setData( data );

				// Setup dynamic image resizing with mouse.
				// Don't initialize resizer when dimensions are disallowed (#11004).
				if ( editor.filter.checkFeature( this.features.dimension ) )
					setupResizer( this );

				this.shiftState = helpers.stateShifter( this.editor );

				// Add widget editing option to its context menu.
				this.on( 'contextMenu', function( evt ) {
					evt.data.image = CKEDITOR.TRISTATE_OFF;
				} );

				// Pass the reference to this widget to the dialog.
				this.on( 'dialog', function( evt ) {
					evt.data.widget = this;
				}, this );
			},

			upcast: upcastWidgetElement( editor ),
			downcast: downcastWidgetElement
		};
	}

	CKEDITOR.plugins.image2 = {
		stateShifter: function( editor ) {
			// Tag name used for centering non-captioned widgets.
			var doc = editor.document,
				editable = editor.editable(),

				// The order that stateActions get executed. It matters!
				shiftables = [ 'hasCaption', 'align' ],

				// Atomic procedures, one per state variable.
				stateActions = {
					align: function( data, oldValue, newValue ) {
						var hasCaptionAfter = data.newState.hasCaption,
							element = data.element;

						// Alignment changed.
						if ( changed( data, 'align' ) ) {
							// No caption in the new state.
							if ( !hasCaptionAfter ) {
								// Changed to "center" (non-captioned).
								if ( newValue == 'center' ) {
									data.destroy();
									data.element = wrapInCentering( editor, element );
								}

								// Changed to "non-center" from "center" while caption removed.
								if ( !changed( data, 'hasCaption' ) && oldValue == 'center' && newValue != 'center' ) {
									data.destroy();
									data.element = unwrapFromCentering( element );
								}
							}
						}

						// Alignment remains and "center" removed caption.
						else if ( newValue == 'center' && changed( data, 'hasCaption' ) && !hasCaptionAfter ) {
							data.destroy();
							data.element = wrapInCentering( editor, element );
						}

						// Finally set display for figure.
						if ( element.is( 'figure' ) ) {
							if ( newValue == 'center' )
								element.setStyle( 'display', 'inline-block' );
							else
								element.removeStyle( 'display' );
						}
					},

					hasCaption:	function( data, oldValue, newValue ) {
						// This action is for real state change only.
						if ( !changed( data, 'hasCaption' ) )
							return;

						var element = data.element,
							oldState = data.oldState,
							newState = data.newState,
							img;

						// Switching hasCaption always destroys the widget.
						data.destroy();

						// There was no caption, but the caption is to be added.
						if ( newValue ) {
							// Get <img> from element. As element may be either
							// <img> or centering <p>, consider it now.
							img = element.findOne( 'img' ) || element;

							// Create new <figure> from widget template.
							var figure = CKEDITOR.dom.element.createFromHtml( templateBlock, doc );

							// Replace element with <figure>.
							replaceSafely( figure, element );

							// Use old <img> instead of the one from the template,
							// so we won't lose additional attributes.
							img.replace( figure.findOne( 'img' ) );

							// Update widget's element.
							data.element = figure;
						}

						// The caption was present, but now it's to be removed.
						else {
							// Unwrap <img> from figure.
							img = element.findOne( 'img' );
							img.replace( element );

							// Update widget's element.
							data.element = img;
						}
					}
				};

			function getValue( state, name ) {
				return state && state[ name ] !== undefined ? state[ name ] : null;
			}

			function changed( data, name ) {
				if ( !data.oldState )
					return false;
				else
					return data.oldState[ name ] !== data.newState[ name ];
			}

			function wrapInCentering( editor, element ) {
				// When widget gets centered. Wrapper must be created.
				// Create new <p|div> with text-align:center.
				var center = doc.createElement( editor.activeEnterMode == CKEDITOR.ENTER_P ? 'p' : 'div', {
					styles: { 'text-align': 'center' }
				} );

				// Replace element with centering wrapper.
				replaceSafely( center, element );
				element.move( center );

				return center;
			}

			function unwrapFromCentering( element ) {
				var img = element.findOne( 'img' );

				img.replace( element );

				return img;
			}

			function replaceSafely( replacing, replaced ) {
				if ( replaced.getParent() ) {
					var range = editor.createRange();

					range.moveToPosition( replaced, CKEDITOR.POSITION_BEFORE_START );

					// Remove old element. Do it before insertion to avoid a case when
					// element is moved from 'replaced' element before it, what creates
					// a tricky case which insertElementIntorRange does not handle.
					replaced.remove();

					editable.insertElementIntoRange( replacing, range );
				}
				else
					replacing.replace( replaced );
			}

			return function( data ) {
				var oldState = data.oldState,
					newState = data.newState,
					name;

				// Iterate over possible state variables.
				for ( var i = 0; i < shiftables.length; i++ ) {
					name = shiftables[ i ];

					stateActions[ name ]( data,
						oldState ? oldState[ name ] : null,
						newState[ name ] );
				}

				data.init( data.element );
			};
		},

		// Checks whether current ratio of the image match the natural one.
		// by comparing dimensions.
		// @param {CKEDITOR.dom.element} image
		// @returns {Boolean}
		checkHasNaturalRatio: function( image ) {
			var $ = image.$,
				natural = this.getNatural( image );

			// The reason for two alternative comparisons is that the rounding can come from
			// both dimensions, e.g. there are two cases:
			// 	1. height is computed as a rounded relation of the real height and the value of width,
			//	2. width is computed as a rounded relation of the real width and the value of heigh.
			return Math.round( $.clientWidth / natural.width * natural.height ) == $.clientHeight ||
				Math.round( $.clientHeight / natural.height * natural.width ) == $.clientWidth;
		},

		// Returns natural dimensions of the image. For modern browsers
		// it uses natural(Width|Height) for old ones (IE8), creates
		// a new image and reads dimensions.
		// @param {CKEDITOR.dom.element} image
		// @returns {Object}
		getNatural: function( image ) {
			var dimensions;

			if ( image.$.naturalWidth ) {
				dimensions = {
					width: image.$.naturalWidth,
					height: image.$.naturalHeight
				};
			} else {
				var img = new Image();
				img.src = image.getAttribute( 'src' );

				dimensions = {
					width: img.width,
					height: img.height
				};
			}

			return dimensions;
		}
	};

	function setWrapperAlign( widget ) {
		var wrapper = widget.wrapper,
			align = widget.data.align;

		if ( align == 'center' ) {
			if ( !widget.inline )
				wrapper.setStyle( 'text-align', 'center' );

			wrapper.removeStyle( 'float' );
		} else {
			if ( !widget.inline )
				wrapper.removeStyle( 'text-align' );

			if ( align == 'none' )
				wrapper.removeStyle( 'float' );
			else
				wrapper.setStyle( 'float', align );
		}
	}

	// Returns a function that creates widgets from all <img> and
	// <figure class="{config.image2_captionedClass}"> elements.
	//
	// @param {CKEDITOR.editor} editor
	// @returns {Function}
	function upcastWidgetElement( editor ) {
		var isCenterWrapper = centerWrapperChecker( editor );

		// @param {CKEDITOR.htmlParser.element} el
		// @param {Object} data
		return function( el, data ) {
			var dimensions = { width: 1, height: 1 },
				name = el.name,
				image;

			// #11110 Don't initialize on pasted fake objects.
			if ( el.attributes[ 'data-cke-realelement' ] )
				return;

			// If a center wrapper is found, there are 3 possible cases:
			//
			// 1. <div style="text-align:center"><figure>...</figure></div>.
			//    In this case centering is done with a class set on widget.wrapper.
			//    Simply replace centering wrapper with figure (it's no longer necessary).
			//
			// 2. <p style="text-align:center"><img/></p>.
			//    Nothing to do here: <p> remains for styling purposes.
			//
			// 3. <div style="text-align:center"><img/></div>.
			//    Nothing to do here (2.) but that case is only possible in enterMode different
			//    than ENTER_P.
			if ( isCenterWrapper( el ) ) {
				if ( name == 'div' ) {
					var figure = el.getFirst( 'figure' );

					// Case #1.
					if ( figure ) {
						el.replaceWith( figure );
						el = figure;
					}
				}
				// Cases #2 and #3 (handled transparently)

				// If there's a centering wrapper, save it in data.
				data.align = 'center';

				image = el.getFirst( 'img' );
			}

			// No center wrapper has been found.
			else if ( name == 'figure' && el.hasClass( 'caption' ) )
				image = el.getFirst( 'img' );

			// Inline widget from plain img.
			else if ( name == 'img' )
				image = el;

			if ( !image )
				return;

			// If there's an image, then cool, we got a widget.
			// Now just remove dimension attributes expressed with %.
			for ( var d in dimensions ) {
				var dimension = image.attributes[ d ];

				if ( dimension && dimension.match( regexPercent ) )
					delete image.attributes[ d ];
			}

			return el;
		};
	}

	// Transforms the widget to the external format according to the current configuration.
	//
	// @param {CKEDITOR.htmlParser.element} el
	function downcastWidgetElement( el ) {
		var attrs = el.attributes,
			align = this.data.align;

		// De-wrap the image from resize handle wrapper.
		// Only block widgets have one.
		if ( !this.inline ) {
			var resizeWrapper = el.getFirst( 'span' ),
				img;

			if ( resizeWrapper ) {
				img = resizeWrapper.getFirst( 'img' );
				resizeWrapper.replaceWith( img );
			} else
				img = el.getFirst( 'img' );
		}

		if ( align && align != 'none' ) {
			var styles = CKEDITOR.tools.parseCssText( attrs.style || '' );

			// When the widget is captioned (<figure>) and internally centering is done
			// with widget's wrapper inline style, in the external data representation,
			// <figure> must be wrapped with an element holding an inline style:
			//
			//   <div style="text-align:center">
			//     <figure class="image" style="display:inline-block">
			//      <img alt="A" src="B" />
			//       <figcaption>C</figcaption>
			//     </figure>
			//   </div>
			if ( align == 'center' && el.name == 'figure' )
				el = el.wrapWith( new CKEDITOR.htmlParser.element( 'div', { style: 'text-align:center' } ) );

			// If left/right, add float style to the downcasted element.
			else if ( align in { left: 1, right: 1 } )
				styles[ 'float' ] = align;

			// Update element styles.
			if ( !CKEDITOR.tools.isEmpty( styles ) )
				attrs.style = CKEDITOR.tools.writeCssText( styles );
		}

		return el;
	}

	// Returns a function that checks if an element is a centering wrapper.
	//
	// @param {CKEDITOR.editor} editor
	// @returns {Function}
	function centerWrapperChecker( editor ) {
		return function( el ) {
			// Wrapper must be either <div> or <p>.
			if ( !( el.name in { div: 1, p: 1 } ) )
				return false;

			var children = el.children;

			// Centering wrapper can have only one child.
			if ( children.length !== 1 )
				return false;

			var child = children[ 0 ],
				childName = child.name;

			// Only <figure> or <img /> can be first (only) child of centering wrapper,
			// regardless of its type.
			if ( childName != 'figure' && childName != 'img' )
				return false;

			// If centering wrapper is <p>, only <img /> can be the child.
			//   <p style="text-align:center"><img /></p>
			if ( el.name == 'p' ) {
				if ( childName != 'img' )
					return false;
			}
			// Centering <div> can hold <img/> or <figure>, depending on enterMode.
			else {
				// If a <figure> is the first (only) child, it must have a class.
				//   <div style="text-align:center"><figure>...</figure><div>
				if ( childName == 'figure' && !child.hasClass( 'caption' ) )
					return false;

				// Centering <div> can hold <img /> only when enterMode is ENTER_(BR|DIV).
				//   <div style="text-align:center"><img /></div>
				if ( childName == 'img' && editor.enterMode == CKEDITOR.ENTER_P )
					return false;
			}

			var styles = CKEDITOR.tools.parseCssText( el.attributes.style || '', true );

			// Centering wrapper got to be... centering.
			if ( styles[ 'text-align' ] == 'center' )
				return true;

			return false;
		};
	}

	// Sets width and height of the widget image according to current widget data.
	//
	// @param {CKEDITOR.plugins.widget} widget
	function setDimensions( widget ) {
		var data = widget.data,
			dimensions = { width: data.width, height: data.height },
			image = widget.parts.image;

		for ( var d in dimensions ) {
			if ( dimensions[ d ] )
				image.setAttribute( d, dimensions[ d ] );
			else
				image.removeAttribute( d );
		}
	}

	// Defines all features related to drag-driven image resizing.
	//
	// @param {CKEDITOR.plugins.widget} widget
	function setupResizer( widget ) {
		var editor = widget.editor,
			editable = editor.editable(),
			doc = editor.document,

			// Store the resizer in a widget for testing (#11004).
			resizer = widget.resizer = doc.createElement( 'span' );

		resizer.addClass( 'cke_image_resizer' );
		resizer.setAttribute( 'title', editor.lang.image2.resizer );
		resizer.append( new CKEDITOR.dom.text( '\u200b', doc ) );

		// Inline widgets don't need a resizer wrapper as an image spans the entire widget.
		if ( !widget.inline ) {
			var oldResizeWrapper = widget.element.getFirst(),
				resizeWrapper = doc.createElement( 'span' );

			resizeWrapper.addClass( 'cke_image_resizer_wrapper' );
			resizeWrapper.append( widget.parts.image );
			resizeWrapper.append( resizer );
			widget.element.append( resizeWrapper, true );

			// Remove the old wrapper which could came from e.g. pasted HTML
			// and which could be corrupted (e.g. resizer span has been lost).
			if ( oldResizeWrapper.is( 'span' ) )
				oldResizeWrapper.remove();
		} else
			widget.wrapper.append( resizer );

		// Calculate values of size variables and mouse offsets.
		resizer.on( 'mousedown', function( evt ) {
			var image = widget.parts.image,

				// "factor" can be either 1 or -1. I.e.: For right-aligned images, we need to
				// subtract the difference to get proper width, etc. Without "factor",
				// resizer starts working the opposite way.
				factor = widget.data.align == 'right' ? -1 : 1,

				// The x-coordinate of the mouse relative to the screen
				// when button gets pressed.
				startX = evt.data.$.screenX,
				startY = evt.data.$.screenY,

				// The initial dimensions and aspect ratio of the image.
				startWidth = image.$.clientWidth,
				startHeight = image.$.clientHeight,
				ratio = startWidth / startHeight,

				listeners = [],

				// A class applied to editable during resizing.
				cursorClass = 'cke_image_s' + ( !~factor ? 'w' : 'e' ),

				nativeEvt, newWidth, newHeight, updateData,
				moveDiffX, moveDiffY, moveRatio;

			// Save the undo snapshot first: before resizing.
			editor.fire( 'saveSnapshot' );

			// Mousemove listeners are removed on mouseup.
			attachToDocuments( 'mousemove', onMouseMove, listeners );

			// Clean up the mousemove listener. Update widget data if valid.
			attachToDocuments( 'mouseup', onMouseUp, listeners );

			// The entire editable will have the special cursor while resizing goes on.
			editable.addClass( cursorClass );

			// This is to always keep the resizer element visible while resizing.
			resizer.addClass( 'cke_image_resizing' );

			// Attaches an event to a global document if inline editor.
			// Additionally, if classic (`iframe`-based) editor, also attaches the same event to `iframe`'s document.
			function attachToDocuments( name, callback, collection ) {
				var globalDoc = CKEDITOR.document,
					listeners = [];

				if ( !doc.equals( globalDoc ) )
					listeners.push( globalDoc.on( name, callback ) );

				listeners.push( doc.on( name, callback ) );

				if ( collection ) {
					for ( var i = listeners.length; i--; )
						collection.push( listeners.pop() );
				}
			}

			// Calculate with first, and then adjust height, preserving ratio.
			function adjustToX() {
				newWidth = startWidth + factor * moveDiffX;
				newHeight = Math.round( newWidth / ratio );
			}

			// Calculate height first, and then adjust width, preserving ratio.
			function adjustToY() {
				newHeight = startHeight - moveDiffY;
				newWidth = Math.round( newHeight * ratio );
			}

			// This is how variables refer to the geometry.
			// Note: x corresponds to moveOffset, this is the position of mouse
			// Note: o corresponds to [startX, startY].
			//
			// 	+--------------+--------------+
			// 	|              |              |
			// 	|      I       |      II      |
			// 	|              |              |
			// 	+------------- o -------------+ _ _ _
			// 	|              |              |      ^
			// 	|      VI      |     III      |      | moveDiffY
			// 	|              |         x _ _ _ _ _ v
			// 	+--------------+---------|----+
			// 	               |         |
			// 	                <------->
			// 	                moveDiffX
			function onMouseMove( evt ) {
				nativeEvt = evt.data.$;

				// This is how far the mouse is from the point the button was pressed.
				moveDiffX = nativeEvt.screenX - startX;
				moveDiffY = startY - nativeEvt.screenY;

				// This is the aspect ratio of the move difference.
				moveRatio = Math.abs( moveDiffX / moveDiffY );

				// Left, center or none-aligned widget.
				if ( factor == 1 ) {
					if ( moveDiffX <= 0 ) {
						// Case: IV.
						if ( moveDiffY <= 0 )
							adjustToX();

						// Case: I.
						else {
							if ( moveRatio >= ratio )
								adjustToX();
							else
								adjustToY();
						}
					} else {
						// Case: III.
						if ( moveDiffY <= 0 ) {
							if ( moveRatio >= ratio )
								adjustToY();
							else
								adjustToX();
						}

						// Case: II.
						else
							adjustToY();
					}
				}

				// Right-aligned widget. It mirrors behaviours, so I becomes II,
				// IV becomes III and vice-versa.
				else {
					if ( moveDiffX <= 0 ) {
						// Case: IV.
						if ( moveDiffY <= 0 ) {
							if ( moveRatio >= ratio )
								adjustToY();
							else
								adjustToX();
						}

						// Case: I.
						else
							adjustToY();
					} else {
						// Case: III.
						if ( moveDiffY <= 0 )
							adjustToX();

						// Case: II.
						else {
							if ( moveRatio >= ratio )
								adjustToX();
							else
								adjustToY();
						}
					}
				}

				// Don't update attributes if less than 10.
				// This is to prevent images to visually disappear.
				if ( newWidth >= 15 && newHeight >= 15 ) {
					image.setAttributes( { width: newWidth, height: newHeight } );
					updateData = true;
				} else
					updateData = false;
			}

			function onMouseUp( evt ) {
				var l;

				while ( ( l = listeners.pop() ) )
					l.removeListener();

				// Restore default cursor by removing special class.
				editable.removeClass( cursorClass );

				// This is to bring back the regular behaviour of the resizer.
				resizer.removeClass( 'cke_image_resizing' );

				if ( updateData ) {
					widget.setData( { width: newWidth, height: newHeight } );

					// Save another undo snapshot: after resizing.
					editor.fire( 'saveSnapshot' );
				}

				// Don't update data twice or more.
				updateData = false;
			}
		} );

		// Change the position of the widget resizer when data changes.
		widget.on( 'data', function() {
			resizer[ widget.data.align == 'right' ? 'addClass' : 'removeClass' ]( 'cke_image_resizer_left' );
		} );
	}

	// Integrates widget alignment setting with justify
	// plugin's commands (execution and refreshment).
	// @param {CKEDITOR.editor} editor
	// @param {String} value 'left', 'right', 'center' or 'block'
	function alignCommandIntegrator( editor ) {
		var execCallbacks = [],
			enabled;

		return function( value ) {
			var command = editor.getCommand( 'justify' + value );

			// Most likely, the justify plugin isn't loaded.
			if ( !command )
				return;

			// This command will be manually refreshed along with
			// other commands after exec.
			execCallbacks.push( function() {
				command.refresh( editor, editor.elementPath() );
			} );

			if ( value in { right: 1, left: 1, center: 1 } ) {
				command.on( 'exec', function( evt ) {
					var widget = getFocusedWidget( editor );

					if ( widget ) {
						widget.setData( 'align', value );

						// Once the widget changed its align, all the align commands
						// must be refreshed: the event is to be cancelled.
						for ( var i = execCallbacks.length; i--; )
							execCallbacks[ i ]();

						evt.cancel();
					}
				} );
			}

			command.on( 'refresh', function( evt ) {
				var widget = getFocusedWidget( editor ),
					allowed = { right: 1, left: 1, center: 1 };

				if ( !widget )
					return;

				// Cache "enabled" on first use. This is because filter#checkFeature may
				// not be available during plugin's afterInit in the future — a moment when
				// alignCommandIntegrator is called.
				if ( enabled == undefined )
					enabled = editor.filter.checkFeature( editor.widgets.registered.image.features.align );

				// Don't allow justify commands when widget alignment is disabled (#11004).
				if ( !enabled )
					this.setState( CKEDITOR.TRISTATE_DISABLED );
				else {
					this.setState(
						( widget.data.align == value ) ?
								CKEDITOR.TRISTATE_ON
							:
								( value in allowed ) ?
										CKEDITOR.TRISTATE_OFF
									:
										CKEDITOR.TRISTATE_DISABLED );
				}

				evt.cancel();
			} );
		};
	}

	// Returns the focused widget, if of the type specific for this plugin.
	// If no widget is focused, `null` is returned.
	//
	// @param {CKEDITOR.editor}
	// @returns {CKEDITOR.plugins.widget}
	function getFocusedWidget( editor ) {
		var widget = editor.widgets.focused;

		if ( widget && widget.name == 'image' )
			return widget;

		return null;
	}
} )();
