/**
 * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @fileOverview The Balloon Panel plugin that provides an ability to create a floating, balloon-shaped
 * container capable of presenting content at a precise position in the document.
 */

( function() {
	'use strict';

	// This flag prevents appending stylesheet more than once.
	var stylesLoaded = false;

	CKEDITOR.plugins.add( 'balloonpanel', {
		init: function() {
			if ( !stylesLoaded ) {
				CKEDITOR.document.appendStyleSheet( this.path + 'skins/' + CKEDITOR.skin.name + '/balloonpanel.css' );
				stylesLoaded = true;
			}
		}
	} );

	/**
	 * A class that represents a floating, balloon-shaped panel capable of presenting defined
	 * content at a precise position in the document. It can be used to represent
	 * contextual data or forms i.e. related to an element in the editor's editable.
	 *
	 *		// Create an instance of the balloon panel.
	 *		var panel = new CKEDITOR.ui.balloonPanel( editor, {
	 *			title: 'My Panel',
	 *			content: '<p>This is my panel</p>'
	 *		} );
	 *
	 *		// Attach the panel to an element in DOM and show it immediately.
	 *		panel.attach( domElement );
	 *
	 * @class
	 * @since 4.6.0
	 * @param {CKEDITOR.editor} editor The editor instance for which the panel is created.
	 * @param {Object} definition An object containing the panel definition.
	 */
	CKEDITOR.ui.balloonPanel = function( editor, definition ) {
		/**
		 * The editor for this balloon panel.
		 */
		this.editor = editor;

		CKEDITOR.tools.extend( this, {
			/**
			 * The default width of the balloon panel.
			 */
			width: 360,

			/**
			 * The default height of the balloon panel.
			 */
			height: 'auto',

			/**
			 * The default width of the triangle that points to the element in the editable.
			 */
			triangleWidth: 20,

			/**
			 * The default height of the triangle that points to the element in the editable.
			 */
			triangleHeight: 20,

			/**
			 * The default distance between the triangle and the vertical edge of the panel.
			 */
			triangleMinDistance: 40
		}, definition, true );

		/**
		 * Templates for UI elements in this balloon panel.
		 * See {@link #templateDefinitions}, {@link #parts}.
		 */
		this.templates = {};

		for ( var t in this.templateDefinitions ) {
			this.templates[ t ] = new CKEDITOR.template( this.templateDefinitions[ t ] );
		}

		/**
		 * @property parts The UI elements of the balloon panel.
		 * @property {CKEDITOR.dom.element} parts.title The title bar of the panel.
		 * @property {CKEDITOR.dom.element} parts.close The Close button.
		 * @property {CKEDITOR.dom.element} parts.content The element that stores the content of the panel.
		 * @property {CKEDITOR.dom.element} parts.panel The undermost element that stores all other elements. The panel is positioned absolutely.
		 * @property {CKEDITOR.dom.element} parts.triangle The panel's triangle.
		 * @property {CKEDITOR.dom.element} parts.triangleOuter The outer element of the triangle.
		 * @property {CKEDITOR.dom.element} parts.triangleInner The inner element of the triangle.
		 */
		this.parts = {};

		/**
		 * Focusable elements in this balloon panel.
		 * See {@link #registerFocusable}, {@link #deregisterFocusable} and {@link CKEDITOR.focusManager}.
		 */
		this.focusables = {};

		/**
		 * Event listeners associated with this balloon panel, activated on panel show.
		 * See {@link #addShowListener}, {@link #activateShowListeners}, {@link #deactivateShowListeners}.
		 */
		this.showListeners = {};

		/**
		 * Event listeners associated with this balloon panel, active as long as the panel is shown.
		 * See {@link #addShowListener}, {@link #activateShowListeners}, {@link #deactivateShowListeners}.
		 */
		this.activeShowListeners = {};

		/**
		 * @property rect Contains panel properties as {@link #move}, {@link #resize},
		 * {@link #method-show} and {@link #method-hide} are called. It stores values and avoids unnecessary
		 * and expensive checks in the future.
		 *
		 * @property {Number} rect.width
		 * @property {Number} rect.height
		 * @property {Number} rect.top
		 * @property {Number} rect.left
		 * @property {Boolean} rect.visible
		 */
		this.rect = {
			visible: false
		};

		// Build the UI of the balloon panel.
		this.build();

		// Handle panel destruction.
		editor.on( 'destroy', function() {
			this.destroy();
		}, this );

		/**
		 * Event fired when the balloon panel is shown.
		 *
		 * @event show
		 */

		/**
		 * Event fired when the balloon panel is hidden.
		 *
		 * @event hide
		 */

		/**
		 * Event fired when the balloon panel is attached to an element.
		 *
		 * @event attach
		 */
	};

	CKEDITOR.ui.balloonPanel.prototype = {
		/**
		 * @property templateDefinitions Balloon panel templates. Automatically converted into a {@link CKEDITOR.template} in the panel constructor.
		 * @property {String} templateDefinitions.panel The template for the panel outermost container.
		 * @property {String} templateDefinitions.content The template for the panel content container.
		 * @property {String} templateDefinitions.title The template for the panel title bar.
		 * @property {String} templateDefinitions.close The template for the panel Close button.
		 * @property {String} templateDefinitions.triangleOuter The template for the panel outer triangle.
		 * @property {String} templateDefinitions.triangleInner The template for the panel inner triangle.
		 */
		templateDefinitions: {
			panel:
				'<div' +
					' class="cke {id} cke_reset_all cke_chrome cke_balloon cke_editor_{name} cke_{langDir} ' + CKEDITOR.env.cssClass + '"' +
					' dir="{langDir}"' +
					' title="' + ( CKEDITOR.env.gecko ? ' ' : '' ) + '"' +
					' lang="{langCode}"' +
					' role="dialog"' +
					' style="{style}"' +
					' tabindex="-1"' +	// Needed to make the panel focusable.
					' aria-labelledby="cke_{name}_arialbl"' +
				'></div>',

			content: '<div class="cke_balloon_content">{content}</div>',

			title: '<div class="cke_balloon_title" role="presentation">{title}</div>',

			close:
				'<a class="cke_balloon_close_button" href="javascript:void(0)" title="Close" role="button" tabindex="-1">' +
					'<span class="cke_label">X</span>' +
				'</a>',

			triangleOuter: '<span class="cke_balloon_triangle cke_balloon_triangle_outer"></span>',

			triangleInner: '<span class="cke_balloon_triangle cke_balloon_triangle_inner">&#8203;</span>'
		},

		/**
		 * Builds the UI of the balloon panel.
		 */
		build: function() {
			var editor = this.editor;

			this.parts = {
				title: CKEDITOR.dom.element.createFromHtml( this.templates.title.output( {
					title: this.title
				} ) ),

				close: CKEDITOR.dom.element.createFromHtml( this.templates.close.output() ),

				panel: CKEDITOR.dom.element.createFromHtml( this.templates.panel.output( {
					id: editor.id,
					langDir: editor.lang.dir,
					langCode: editor.langCode,
					name: editor.name,
					style: 'display:none;',
					voiceLabel: editor.lang.editorPanel + ', ' + editor.name
				} ) ),

				content: CKEDITOR.dom.element.createFromHtml( this.templates.content.output( {
					content: this.content || ''
				} ) ),

				triangleOuter: CKEDITOR.dom.element.createFromHtml( this.templates.triangleOuter.output() ),

				triangleInner: CKEDITOR.dom.element.createFromHtml( this.templates.triangleInner.output() )
			};

			// Append UI elements to create a panel.
			this.parts.panel.append( this.parts.title, 1 );
			this.parts.panel.append( this.parts.close, 1 );
			this.parts.panel.append( this.parts.triangleOuter );
			this.parts.panel.append( this.parts.content );
			this.parts.triangleOuter.append( this.parts.triangleInner );

			// Register panel children to focusManager (prevent from blurring the editor).
			this.registerFocusable( this.parts.panel );
			this.registerFocusable( this.parts.close );

			// Panel title and close button are not to be selected.
			this.parts.title.unselectable();
			this.parts.close.unselectable();

			// Append the panel to the global document.
			CKEDITOR.document.getBody().append( this.parts.panel );

			// Set default dimensions of the panel.
			this.resize( this.width, this.height );

			// Activates listeners on panel show.
			// All listeners will be deactivated on panel hide.
			this.on( 'show', this.activateShowListeners, this );

			// Deactivate all listeners on panel hide.
			this.on( 'hide', this.deactivateShowListeners, this );

			this.parts.close.on( 'click', function( evt ) {
				this.hide();
				evt.data.preventDefault();
			}, this );
		},

		/**
		 * Shows the balloon panel.
		 */
		show: function() {
			if ( this.rect.visible ) {
				return;
			}

			this.rect.visible = true;
			this.parts.panel.show();

			this.fire( 'show' );
		},

		/**
		 * Hides the balloon panel and moves the focus back to the editable.
		 */
		hide: function() {
			if ( !this.rect.visible ) {
				return;
			}

			this.rect.visible = false;
			this.parts.panel.hide();
			this.blur();

			this.fire( 'hide' );
		},

		/**
		 * Moves the focus back to the editor's editable.
		 *
		 * @method blur
		 * @member CKEDITOR.ui.balloonPanel
		 */
		blur: function() {
			this.editor.focus();
		},

		/**
		 * Moves the **upper-left** balloon panel corner to the specified absolute position.
		 *
		 * @param {Number} top
		 * @param {Number} left
		 */
		move: function( top, left ) {
			this.rect.left = left;
			this.rect.top = top;

			this.parts.panel.setStyles( {
				left: CKEDITOR.tools.cssLength( left ),
				top: CKEDITOR.tools.cssLength( top )
			} );
		},

		/**
		 * Places the balloon panel next to a specified element or a selection so the tip of the balloon's triangle
		 * touches that element or the center of the selection. Once the panel is attached it gains focus.
		 *
		 * @method attach
		 * @param {CKEDITOR.dom.element/CKEDITOR.dom.selection} elementOrSelection The element or selection to which the panel is attached.
		 * **Since 4.11.0** instead of an element it is possible to pass a selection {@link CKEDITOR.dom.selection}.
		 * @param {Object/CKEDITOR.dom.element/Boolean} [options] **Since 4.8.0** this parameter works as an `options` object.
		 *
		 * If a `{@link CKEDITOR.dom.element}/Boolean` instance is given, this parameter acts as an `options.focusElement`.
		 * @param {CKEDITOR.dom.element/Boolean} [options.focusElement] The element to be focused after the panel
		 * is attached. By default the `panel` property of {@link #parts} will be focused. You might specify the element
		 * to be focused by passing any {@link CKEDITOR.dom.element} instance.
		 * You can also prevent changing the focus at all by setting it to `false`.
		 * @param {Boolean} [options.show=true] Defines if the balloon panel should be shown after being attached.
		 */
		attach: ( function() {
			var winGlobal, frame, editable, isInline;

			function rectIntersectArea( rect1, rect2 ) {
				var hOverlap = Math.max( 0, Math.min( rect1.right, rect2.right ) - Math.max( rect1.left, rect2.left ) ),
					vOverlap = Math.max( 0, Math.min( rect1.bottom, rect2.bottom ) - Math.max( rect1.top, rect2.top ) );

				return hOverlap * vOverlap;
			}

			function newPanelRect( top, left, panelWidth, panelHeight ) {
				var newRect = {
					top: top,
					left: left
				};

				newRect.right = newRect.left + panelWidth;
				newRect.bottom = newRect.top + panelHeight;

				return newRect;
			}

			function createLineRect( first, last ) {
				var newRect = first;
				newRect.right = last.right;
				newRect.width = newRect.right - newRect.left;

				if ( last.y ) {
					newRect.y = last.y;
				}

				return newRect;
			}

			function getTopAndBottomRects( rectList ) {
				var topAlignedRects = getAlignedRects( rectList, true ),
					bottomAlignedRects = getAlignedRects( rectList ),
					first = createLineRect( topAlignedRects[ 0 ], topAlignedRects.pop() ),
					last = createLineRect( bottomAlignedRects[ 0 ], bottomAlignedRects.pop() );

				// Make height of both rects equal to height of whole selection, so panel won't cover selection unless it needs to.
				first.bottom = last.bottom;
				first.height = first.bottom - first.top;
				if ( last.y ) {
					first.y = last.y;
				}

				last.top = first.top;
				last.height = first.height;

				return [ first, last ];
			}

			function getAlignedRects( rectList, top ) {
				var edgeRect = top ? rectList [ 0 ] : rectList[ rectList.length - 1 ],
					alignment = top ? 'top' : 'bottom';

				return CKEDITOR.tools.array.filter( rectList, function( item ) {
					if ( item[ alignment ] === edgeRect[ alignment ] ) {
						return item;
					}
				} );
			}

			var triangleRelativePosition = {
				right: 'left',
				top: 'bottom',
				topLeft: 'bottomLeft',
				topRight: 'bottomRight',
				bottom: 'top',
				bottomLeft: 'topLeft',
				bottomRight: 'topRight',
				left: 'right'
			};

			return function( elementOrSelection, options ) {
				if ( elementOrSelection instanceof CKEDITOR.dom.selection ) {
					var ranges = elementOrSelection.getRanges(),
						rectList;

					// Handle fake selection within table.
					if ( elementOrSelection.isFake && elementOrSelection.isInTable() ) {
						rectList = CKEDITOR.tools.array.map( ranges, function( item ) {
							// With table selection the first rect represents `td` element rect. Lets use it in that case.
							return item.getClientRects( true )[ 0 ];
						} );
					} else {
						rectList = ranges[ ranges.length - 1 ].getClientRects( true );
					}

					// We need two rects, one representing the first selected line, and other representing the last selected line.
					var first = rectList[ 0 ],
						last = rectList[ rectList.length - 1 ],
						selectionRects;

					if ( first === last ) {
						selectionRects = [ first ];
					} else if ( first.top === last.top ) {
						selectionRects = [ createLineRect( first, last ) ];
					} else {
						selectionRects = getTopAndBottomRects( rectList );
					}
				}

				if ( options instanceof CKEDITOR.dom.element || !options ) {
					options = { focusElement: options };
				}

				options = CKEDITOR.tools.extend( options, {
					show: true
				} );

				if ( options.show === true ) {
					this.show();
				}

				this.fire( 'attach' );

				winGlobal = CKEDITOR.document.getWindow();
				frame = this.editor.window.getFrame();
				editable = this.editor.editable();
				isInline = editable.isInline();

				if ( !isInline && CKEDITOR.env.safari ) {
					// Overwrite frame with editor iframe closest parent, because iframe has wrong rect values in mobile Safari (#1076).
					frame = frame.getParent();
				}

				var panelWidth = this.getWidth(),
					panelHeight = this.getHeight(),

					// The area of the panel.
					panelArea = panelWidth * panelHeight,
					alignments, minDifferenceAlignment, alignmentRect, areaDifference,

					elementRect = elementOrSelection.getClientRect && elementOrSelection.getClientRect( true ),
					editorRect = isInline ? editable.getClientRect( true ) : frame.getClientRect( true ),

					viewPaneSize = winGlobal.getViewPaneSize(),
					winGlobalScroll = winGlobal.getScrollPosition(),

				// allowedRect is the rect into which the panel should fit to remain
				// both within the visible area of the editor and the viewport, i.e.
				// the rect area covered by "#":
				//
				// 	[Viewport]
				// 	+-------------------------------------+
				// 	|                        [Editor]     |
				// 	|                        +--------------------+
				// 	|                        |############|       |
				// 	|                        |############|       |
				// 	|                        |############|       |
				// 	|                        +--------------------+
				// 	|                                     |
				// 	+-------------------------------------+
					allowedRect = {
						top: Math.max( editorRect.top, winGlobalScroll.y ),
						left: Math.max( editorRect.left, winGlobalScroll.x ),
						right: Math.min( editorRect.right, viewPaneSize.width + winGlobalScroll.x ),
						bottom: Math.min( editorRect.bottom, viewPaneSize.height + winGlobalScroll.y )
					},
					alignmentKeys;

				// Position balloon on entire view port only when it's real inline mode (#1048).
				if ( isInline && this.editor.elementMode === CKEDITOR.ELEMENT_MODE_INLINE ) {
					// In inline we want to limit position within the window.
					allowedRect = this._getViewPaneRect( winGlobal );

					// We need also consider triangle.
					allowedRect.right += this.triangleWidth;
					allowedRect.bottom += this.triangleHeight;
				}

				// This method will modify elementRect if the element is outside of allowedRect / editorRect.
				// If it's outside then in
				if ( selectionRects ) {
					CKEDITOR.tools.array.forEach( selectionRects, function( item ) {
						this._adjustElementRect( item, isInline ? allowedRect : editorRect );
					}, this );

					alignments = this._getAlignments( selectionRects[ 0 ], panelWidth, panelHeight );

					if ( selectionRects.length > 1 ) {
						alignments[ 'bottom hcenter' ] = this._getAlignments( selectionRects[ 1 ], panelWidth, panelHeight )[ 'bottom hcenter' ];
					}

					alignmentKeys = {
						'top hcenter': true,
						'bottom hcenter': true
					};
				} else {
					this._adjustElementRect( elementRect, isInline ? allowedRect : editorRect );
					alignments = this._getAlignments( elementRect, panelWidth, panelHeight );
				}

				// Iterate over all possible alignments to find the optimal one.
				for ( var a in alignmentKeys || alignments ) {
					// Create a rect which would represent the panel in such alignment.
					alignmentRect = newPanelRect( alignments[ a ].top, alignments[ a ].left, panelWidth, panelHeight );

					// Calculate the difference between the area of the panel and intersection of allowed rect and alignment rect.
					// It is the area of the panel, which would be OUT of allowed rect if such alignment was used. Less is better.
					areaDifference = alignments[ a ].areaDifference = panelArea - rectIntersectArea( alignmentRect, allowedRect );

					// If the difference is 0, it means that the panel is fully within allowed rect. That's great!
					if ( areaDifference === 0 ) {
						minDifferenceAlignment = a;
						break;
					}

					// If there's no alignment of a minimal area difference, use the first available.
					if ( !minDifferenceAlignment ) {
						minDifferenceAlignment = a;
					}

					// Determine the alignment of a minimal area difference. It will be used as a fallback
					// if no alignment provides a perfect fit into allowed rect.
					if ( areaDifference < alignments[ minDifferenceAlignment ].areaDifference ) {
						minDifferenceAlignment = a;
					}
				}

				// For non-static parent elements we need to remove its margin offset from balloon panel (#1048).
				var parent = this.parts.panel.getAscendant( function( el ) {
						return el instanceof CKEDITOR.dom.document ? false : el.getComputedStyle( 'position' ) !== 'static';
					} ),
					parentMargin = {
						left: parent ? parseInt( parent.getComputedStyle( 'margin-left' ), 10 ) : 0,
						top: parent ? parseInt( parent.getComputedStyle( 'margin-top' ), 10 ) : 0
					};

				this.move( alignments[ minDifferenceAlignment ].top - parentMargin.top , alignments[ minDifferenceAlignment ].left - parentMargin.left );

				minDifferenceAlignment = minDifferenceAlignment.split( ' ' );
				this.setTriangle( triangleRelativePosition[ minDifferenceAlignment[ 0 ] ], minDifferenceAlignment[ 1 ] );

				// Set focus to proper element.
				if ( options.focusElement !== false ) {
					( options.focusElement || this.parts.panel ).focus();
				}
			};
		} )(),

		/**
		 * Resizes the balloon panel container to given dimensions. Use `'auto'` to
		 * make the dimensions of the panel flexible.
		 *
		 * @param {Number} width
		 * @param {Number} height
		 */
		resize: function( width, height ) {
			this.rect.width = width;
			this.rect.height = height;

			this.parts.panel.setStyles( {
				width: CKEDITOR.tools.cssLength( width ),
				height: CKEDITOR.tools.cssLength( height )
			} );
		},

		/**
		 * Returns the balloon panel width.
		 *
		 * @returns {Number}
		 */
		getWidth: function() {
			return this.rect.width === 'auto' ? this.parts.panel.getClientRect().width : this.rect.width;
		},

		/**
		 * Returns the balloon panel height.
		 *
		 * @returns {Number}
		 */
		getHeight: function() {
			return this.rect.height === 'auto' ? this.parts.panel.getClientRect().height : this.rect.height;
		},

		/**
		 * Changes the position of the balloon's triangle that points to the element in the editable.
		 *
		 * @param {String} side One of 'left', 'right', 'top' or 'bottom'.
		 */
		setTriangle: function( side, align ) {
			var outer = this.parts.triangleOuter,
				inner = this.parts.triangleInner;

			if ( this.triangleSide ) {
				outer.removeClass( 'cke_balloon_triangle_' + this.triangleSide );
				outer.removeClass( 'cke_balloon_triangle_align_' + this.triangleAlign );
				inner.removeClass( 'cke_balloon_triangle_' + this.triangleSide );
			}

			this.triangleSide = side;
			this.triangleAlign = align;

			outer.addClass( 'cke_balloon_triangle_' + side );
			outer.addClass( 'cke_balloon_triangle_align_' + align );
			inner.addClass( 'cke_balloon_triangle_' + side );
		},

		/**
		 * Registers a new focusable element in the editor's focus manager so the instance
		 * does not blur once the child of the balloon panel gains focus.
		 * See {@link #focusables}.
		 *
		 * @param {CKEDITOR.dom.element} element An element to be registered.
		 */
		registerFocusable: function( element ) {
			this.editor.focusManager.add( element );

			this.focusables[ element.getUniqueId() ] = element;
		},

		/**
		 * Unregisters an element from editor's focus manager.
		 * See {@link #focusables}.
		 *
		 * @param {CKEDITOR.dom.element} element An element to be registered.
		 */
		deregisterFocusable: function( element ) {
			this.editor.focusManager.remove( element );

			delete this.focusables[ element.getUniqueId() ];
		},

		/**
		 * Adds an event listener associated with this balloon panel. This listener
		 * will be activated on panel `show` and deactivated on panel `hide`.
		 * See {@link #showListeners}, {@link #activeShowListeners}, {@link #activateShowListeners},
		 * {@link #deactivateShowListeners}.
		 *
		 * @param {Function} listener A function that, if called, attaches the listener
		 * and returns the listener object.
		 * @returns {Object} An object containing the `removeListener` method that removes
		 * the listener from the collection.
		 */
		addShowListener: function( listener ) {
			var id = CKEDITOR.tools.getNextNumber();

			// Adds the listener to the register of on-show-activated listeners.
			this.showListeners[ id ] = listener;

			// Activate listener immediately if panel is already visible.
			if ( this.rect.visible ) {
				this.activateShowListener( id );
			}

			var that = this;

			return {
				removeListener: function() {
					that.removeShowListener( id );
				}
			};
		},

		/**
		 * Removes an event listener associated with this balloon panel visible state.
		 * See {@link #addShowListener}.
		 *
		 * @param {Number} id An ID of the listener.
		 */
		removeShowListener: function( id ) {
			this.deactivateShowListener( id );
			delete this.showListeners[ id ];
		},

		/**
		 * Activates an event listener associated with this balloon panel.
		 * See {@link #showListeners}, {@link #activeShowListeners}, {@link #deactivateShowListener},
		 * {@link #addShowListener}, {@link #removeShowListener}.
		 */
		activateShowListener: function( id ) {
			this.activeShowListeners[ id ] = this.showListeners[ id ].call( this );
		},

		/**
		 * Deactivates an event listener associated with this balloon panel.
		 * See {@link #activateShowListener}.
		 */
		deactivateShowListener: function( id ) {
			if ( this.activeShowListeners[ id ] ) {
				this.activeShowListeners[ id ].removeListener();
			}

			delete this.activeShowListeners[ id ];
		},

		/**
		 * Activates all event listeners associated with this balloon panel.
		 * See {@link #showListeners}, {@link #activeShowListeners}, {@link #deactivateShowListeners},
		 * {@link #addShowListener}, {@link #removeShowListener}.
		 */
		activateShowListeners: function() {
			for ( var id in this.showListeners ) {
				this.activateShowListener( id );
			}
		},

		/**
		 * Removes all event listeners associated with this balloon panel.
		 * See {@link #activateShowListeners}.
		 */
		deactivateShowListeners: function() {
			for ( var id in this.activeShowListeners ) {
				this.deactivateShowListener( id );
			}
		},

		/**
		 * Destroys the balloon panel by removing it from DOM and purging
		 * all associated event listeners.
		 */
		destroy: function() {
			this.deactivateShowListeners();
			this.parts.panel.remove();
		},

		/**
		 * Sets the balloon panel title.
		 *
		 * @param {String} title A new panel title.
		 */
		setTitle: function( title ) {
			this.parts.title.setHtml( title );
		},

		/**
		 * Returns a dictionary containing different alignment positions.
		 *
		 * Keys tell where the balloon is positioned relative to the element, e.g. this would be the result for "top center":
		 *
		 *		[Editor]
		 *		+-------------------------------------+
		 *		|         [Panel]                     |
		 *		|         +-----------------+         |
		 *		|         |                 |         |
		 *		|  [El.]  +--------v--------+         |
		 *		|  +-------------------------------+  |
		 *		|  |                               |  |
		 *		|  |                               |  |
		 *		+--+-------------------------------+--+
		 *
		 * Sample result:
		 *
		 *		{
		 *			"right vcenter": { top: 529.5, left: 175 },
		 *			"left vcenter": { top: 529.5, left: 10},
		 *			"top hcenter": { top: 402, left: 92.5},
		 *			"top left": { top: 402, left: 102.5},
		 *			"top right": { top: 402, left: 82.5},
		 *			"bottom hcenter": { top: 643, left: 92.5},
		 *			"bottom left": { top: 643, left: 102.5},
		 *			"bottom right": { top: 643, left: 82.5}
		 *		}
		 *
		 * @private
		 * @param elementRect
		 * @param {Number} panelWidth
		 * @param {Number} panelHeight
		 * @returns {Object}
		 */
		_getAlignments: function( elementRect, panelWidth, panelHeight ) {
			return {
				'right vcenter': {
					top: elementRect.top + elementRect.height / 2 - panelHeight / 2,
					left: elementRect.right + this.triangleWidth
				},
				'left vcenter': {
					top: elementRect.top + elementRect.height / 2 - panelHeight / 2,
					left: elementRect.left - panelWidth - this.triangleWidth
				},
				'top hcenter': {
					top: elementRect.top - panelHeight - this.triangleHeight,
					left: elementRect.left + elementRect.width / 2 - panelWidth / 2
				},
				'top left': {
					top: elementRect.top - panelHeight - this.triangleHeight,
					left: elementRect.left + elementRect.width / 2 - this.triangleMinDistance
				},
				'top right': {
					top: elementRect.top - panelHeight - this.triangleHeight,
					left: elementRect.right - elementRect.width / 2 - panelWidth + this.triangleMinDistance
				},
				'bottom hcenter': {
					top: elementRect.bottom + this.triangleHeight,
					left: elementRect.left + elementRect.width / 2 - panelWidth / 2
				},
				'bottom left': {
					top: elementRect.bottom + this.triangleHeight,
					left: elementRect.left + elementRect.width / 2 - this.triangleMinDistance
				},
				'bottom right': {
					top: elementRect.bottom + this.triangleHeight,
					left: elementRect.right - elementRect.width / 2 - panelWidth + this.triangleMinDistance
				}
			};
		},

		/**
		 * This method will modify `elementRect` if the element is outside of `editorRect`. If it is outside, it is
		 * going to change it into a rectangle that is within `editorRect`.
		 *
		 * For example here `elementRect` is going to be changed into a very narrow rectangle (with unmodified height)
		 * representation within `editorRect`.
		 *
		 *		+------------------------------------------+
		 *		|                                          |
		 *		|                                         #|          +----------+
		 *		|                                         #|          |          |
		 *		|                                         #|          |          |
		 *		|               editorRect                #|          |elmentRect|
		 *		|                                         #|          |          |
		 *		|                                         #|          |          |
		 *		|                                         #|          +----------+
		 *		|                                          |
		 *		+------------------------------------------+
		 *
		 * @private
		 * @param elementRect Rectangle object that should be contained within `editorRect`. **This object might be modified.**
		 * @param editorRect Reference container that should contain `elementRect`.
		 */
		_adjustElementRect: function( elementRect, editorRect ) {
			elementRect.left = numberInRange( editorRect.left, editorRect.right - 1, elementRect.left );
			elementRect.right = numberInRange( editorRect.left, editorRect.right, elementRect.right );
			elementRect.top = numberInRange( editorRect.top, editorRect.bottom - 1, elementRect.top );
			elementRect.bottom = numberInRange( editorRect.top, editorRect.bottom, elementRect.bottom );
		},

		/**
		 * @param {CKEDITOR.dom.window} window
		 * @returns {Object} Returns viewport position, taking scroll offset into account.
		 * @returns {Number} return.top
		 * @returns {Number} return.bottom
		 * @returns {Number} return.left
		 * @returns {Number} return.right
		 */
		_getViewPaneRect: function( window ) {
			var pos = window.getScrollPosition(),
				viewSize = window.getViewPaneSize();

			return {
				top: pos.y,
				bottom: pos.y + viewSize.height,
				left: pos.x,
				right: pos.x + viewSize.width
			};
		}
	};

	function numberInRange( min, max, num ) {
		return Math.max( min, Math.min( max, num ) );
	}

	CKEDITOR.event.implementOn( CKEDITOR.ui.balloonPanel.prototype );

	/**
	 * The definition of a balloon panel.
	 *
	 * This virtual class illustrates the properties that developers can use to define and create
	 * balloon panels.
	 *
	 *		CKEDITOR.ui.balloonPanel( editor, {
	 *			title: 'My Panel',
	 *			onShow: function() {
	 *				...
	 *			}
	 *		} );
	 *
	 * @class CKEDITOR.ui.balloonPanel.definition
	 */

	/**
	 * The title of the balloon panel.
	 *
	 * @member CKEDITOR.ui.balloonPanel.definition
	 * @property {String} title
	 */

	/**
	 * The static content of the balloon panel.
	 *
	 * @member CKEDITOR.ui.balloonPanel.definition
	 * @property {String} content
	 */
} )();
