/**
 * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
 * For licensing, see LICENSE.md.
 */

/**
 * @module ui/panel/balloon/balloonpanelview
 */

import View from '../../view';
import { getOptimalPosition } from '@ckeditor/ckeditor5-utils/src/dom/position';
import isRange from '@ckeditor/ckeditor5-utils/src/dom/isrange';
import toUnit from '@ckeditor/ckeditor5-utils/src/dom/tounit';
import global from '@ckeditor/ckeditor5-utils/src/dom/global';
import { isElement } from 'lodash-es';

import '../../../theme/components/panel/balloonpanel.css';

const toPx = toUnit( 'px' );
const defaultLimiterElement = global.document.body;

/**
 * The balloon panel view class.
 *
 * A floating container which can
 * {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView#pin pin} to any
 * {@link module:utils/dom/position~Options#target target} in the DOM and remain in that position
 * e.g. when the web page is scrolled.
 *
 * The balloon panel can be used to display contextual, non-blocking UI like forms, toolbars and
 * the like in its {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView#content} view
 * collection.
 *
 * There is a number of {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions}
 * that the balloon can use, automatically switching from one to another when the viewport space becomes
 * scarce to keep the balloon visible to the user as long as it is possible. The balloon will also
 * accept any custom position set provided by the user compatible with the
 * {@link module:utils/dom/position~Options options}.
 *
 *		const panel = new BalloonPanelView( locale );
 *		const childView = new ChildView();
 *		const positions = BalloonPanelView.defaultPositions;
 *
 *		panel.render();
 *
 *		// Add a child view to the panel's content collection.
 *		panel.content.add( childView );
 *
 *		// Start pinning the panel to an element with the "target" id DOM.
 *		// The balloon will remain pinned until unpin() is called.
 *		panel.pin( {
 *			target: document.querySelector( '#target' ),
 *			positions: [
 *				positions.northArrowSouth,
 *				positions.southArrowNorth
 *			]
 *		} );
 *
 * @extends module:ui/view~View
 */
export default class BalloonPanelView extends View {
	/**
	 * @inheritDoc
	 */
	constructor( locale ) {
		super( locale );

		const bind = this.bindTemplate;

		/**
		 * The absolute top position of the balloon panel in pixels.
		 *
		 * @observable
		 * @default 0
		 * @member {Number} #top
		 */
		this.set( 'top', 0 );

		/**
		 * The absolute left position of the balloon panel in pixels.
		 *
		 * @observable
		 * @default 0
		 * @member {Number} #left
		 */
		this.set( 'left', 0 );

		/**
		 * The balloon panel's current position. The position name is reflected in the CSS class set
		 * to the balloon, i.e. `.ck-balloon-panel_arrow_nw` for the "arrow_nw" position. The class
		 * controls the minor aspects of the balloon's visual appearance like the placement
		 * of an {@link #withArrow arrow}. To support a new position, an additional CSS must be created.
		 *
		 * Default position names correspond with
		 * {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions}.
		 *
		 * See the {@link #attachTo} and {@link #pin} methods to learn about custom balloon positions.
		 *
		 * @observable
		 * @default 'arrow_nw'
		 * @member {'arrow_nw'|'arrow_ne'|'arrow_sw'|'arrow_se'} #position
		 */
		this.set( 'position', 'arrow_nw' );

		/**
		 * Controls whether the balloon panel is visible or not.
		 *
		 * @observable
		 * @default false
		 * @member {Boolean} #isVisible
		 */
		this.set( 'isVisible', false );

		/**
		 * Controls whether the balloon panel has an arrow. The presence of the arrow
		 * is reflected in the `ck-balloon-panel_with-arrow` CSS class.
		 *
		 * @observable
		 * @default true
		 * @member {Boolean} #withArrow
		 */
		this.set( 'withArrow', true );

		/**
		 * An additional CSS class added to the {@link #element}.
		 *
		 * @observable
		 * @member {String} #class
		 */
		this.set( 'class' );

		/**
		 * A callback that starts pinning the panel when {@link #isVisible} gets
		 * `true`. Used by {@link #pin}.
		 *
		 * @private
		 * @member {Function} #_pinWhenIsVisibleCallback
		 */

		/**
		 * A collection of the child views that creates the balloon panel contents.
		 *
		 * @readonly
		 * @member {module:ui/viewcollection~ViewCollection}
		 */
		this.content = this.createCollection();

		this.setTemplate( {
			tag: 'div',
			attributes: {
				class: [
					'ck',
					'ck-balloon-panel',
					bind.to( 'position', value => `ck-balloon-panel_${ value }` ),
					bind.if( 'isVisible', 'ck-balloon-panel_visible' ),
					bind.if( 'withArrow', 'ck-balloon-panel_with-arrow' ),
					bind.to( 'class' )
				],

				style: {
					top: bind.to( 'top', toPx ),
					left: bind.to( 'left', toPx )
				}
			},

			children: this.content
		} );
	}

	/**
	 * Shows the panel.
	 *
	 * See {@link #isVisible}.
	 */
	show() {
		this.isVisible = true;
	}

	/**
	 * Hides the panel.
	 *
	 * See {@link #isVisible}.
	 */
	hide() {
		this.isVisible = false;
	}

	/**
	 * Attaches the panel to a specified {@link module:utils/dom/position~Options#target} with a
	 * smart positioning heuristics that chooses from available positions to make sure the panel
	 * is visible to the user i.e. within the limits of the viewport.
	 *
	 * This method accepts configuration {@link module:utils/dom/position~Options options}
	 * to set the `target`, optional `limiter` and `positions` the balloon should choose from.
	 *
	 *		const panel = new BalloonPanelView( locale );
	 *		const positions = BalloonPanelView.defaultPositions;
	 *
	 *		panel.render();
	 *
	 *		// Attach the panel to an element with the "target" id DOM.
	 *		panel.attachTo( {
	 *			target: document.querySelector( '#target' ),
	 *			positions: [
	 *				positions.northArrowSouth,
	 *				positions.southArrowNorth
	 *			]
	 *		} );
	 *
	 * **Note**: Attaching the panel will also automatically {@link #show} it.
	 *
	 * **Note**: An attached panel will not follow its target when the window is scrolled or resized.
	 * See the {@link #pin} method for a more permanent positioning strategy.
	 *
	 * @param {module:utils/dom/position~Options} options Positioning options compatible with
	 * {@link module:utils/dom/position~getOptimalPosition}. Default `positions` array is
	 * {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions}.
	 */
	attachTo( options ) {
		this.show();

		const defaultPositions = BalloonPanelView.defaultPositions;
		const positionOptions = Object.assign( {}, {
			element: this.element,
			positions: [
				defaultPositions.southArrowNorth,
				defaultPositions.southArrowNorthMiddleWest,
				defaultPositions.southArrowNorthMiddleEast,
				defaultPositions.southArrowNorthWest,
				defaultPositions.southArrowNorthEast,
				defaultPositions.northArrowSouth,
				defaultPositions.northArrowSouthMiddleWest,
				defaultPositions.northArrowSouthMiddleEast,
				defaultPositions.northArrowSouthWest,
				defaultPositions.northArrowSouthEast
			],
			limiter: defaultLimiterElement,
			fitInViewport: true
		}, options );

		const optimalPosition = BalloonPanelView._getOptimalPosition( positionOptions );

		// Usually browsers make some problems with super accurate values like 104.345px
		// so it is better to use int values.
		const left = parseInt( optimalPosition.left );
		const top = parseInt( optimalPosition.top );
		const position = optimalPosition.name;

		Object.assign( this, { top, left, position } );
	}

	/**
	 * Works the same way as the {@link #attachTo} method except that the position of the panel is
	 * continuously updated when:
	 *
	 * * any ancestor of the {@link module:utils/dom/position~Options#target}
	 * or {@link module:utils/dom/position~Options#limiter} is scrolled,
	 * * the browser window gets resized or scrolled.
	 *
	 * Thanks to that, the panel always sticks to the {@link module:utils/dom/position~Options#target}
	 * and is immune to the changing environment.
	 *
	 *		const panel = new BalloonPanelView( locale );
	 *		const positions = BalloonPanelView.defaultPositions;
	 *
	 *		panel.render();
	 *
	 *		// Pin the panel to an element with the "target" id DOM.
	 *		panel.pin( {
	 *			target: document.querySelector( '#target' ),
	 *			positions: [
	 *				positions.northArrowSouth,
	 *				positions.southArrowNorth
	 *			]
	 *		} );
	 *
	 * To leave the pinned state, use the {@link #unpin} method.
	 *
	 * **Note**: Pinning the panel will also automatically {@link #show} it.
	 *
	 * @param {module:utils/dom/position~Options} options Positioning options compatible with
	 * {@link module:utils/dom/position~getOptimalPosition}. Default `positions` array is
	 * {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions}.
	 */
	pin( options ) {
		this.unpin();

		this._pinWhenIsVisibleCallback = () => {
			if ( this.isVisible ) {
				this._startPinning( options );
			} else {
				this._stopPinning();
			}
		};

		this._startPinning( options );

		// Control the state of the listeners depending on whether the panel is visible
		// or not.
		// TODO: Use on() (https://github.com/ckeditor/ckeditor5-utils/issues/144).
		this.listenTo( this, 'change:isVisible', this._pinWhenIsVisibleCallback );
	}

	/**
	 * Stops pinning the panel, as set up by {@link #pin}.
	 */
	unpin() {
		if ( this._pinWhenIsVisibleCallback ) {
			// Deactivate listeners attached by pin().
			this._stopPinning();

			// Deactivate the panel pin() control logic.
			// TODO: Use off() (https://github.com/ckeditor/ckeditor5-utils/issues/144).
			this.stopListening( this, 'change:isVisible', this._pinWhenIsVisibleCallback );

			this._pinWhenIsVisibleCallback = null;

			this.hide();
		}
	}

	/**
	 * Starts managing the pinned state of the panel. See {@link #pin}.
	 *
	 * @private
	 * @param {module:utils/dom/position~Options} options Positioning options compatible with
	 * {@link module:utils/dom/position~getOptimalPosition}.
	 */
	_startPinning( options ) {
		this.attachTo( options );

		const targetElement = getDomElement( options.target );
		const limiterElement = options.limiter ? getDomElement( options.limiter ) : defaultLimiterElement;

		// Then we need to listen on scroll event of eny element in the document.
		this.listenTo( global.document, 'scroll', ( evt, domEvt ) => {
			const scrollTarget = domEvt.target;

			// The position needs to be updated if the positioning target is within the scrolled element.
			const isWithinScrollTarget = targetElement && scrollTarget.contains( targetElement );

			// The position needs to be updated if the positioning limiter is within the scrolled element.
			const isLimiterWithinScrollTarget = limiterElement && scrollTarget.contains( limiterElement );

			// The positioning target and/or limiter can be a Rect, object etc..
			// There's no way to optimize the listener then.
			if ( isWithinScrollTarget || isLimiterWithinScrollTarget || !targetElement || !limiterElement ) {
				this.attachTo( options );
			}
		}, { useCapture: true } );

		// We need to listen on window resize event and update position.
		this.listenTo( global.window, 'resize', () => {
			this.attachTo( options );
		} );
	}

	/**
	 * Stops managing the pinned state of the panel. See {@link #pin}.
	 *
	 * @private
	 */
	_stopPinning() {
		this.stopListening( global.document, 'scroll' );
		this.stopListening( global.window, 'resize' );
	}
}

// Returns the DOM element for given object or null, if there is none,
// e.g. when the passed object is a Rect instance or so.
//
// @private
// @param {*} object
// @returns {HTMLElement|null}
function getDomElement( object ) {
	if ( isElement( object ) ) {
		return object;
	}

	if ( isRange( object ) ) {
		return object.commonAncestorContainer;
	}

	if ( typeof object == 'function' ) {
		return getDomElement( object() );
	}

	return null;
}

/**
 * A horizontal offset of the arrow tip from the edge of the balloon. Controlled by CSS.
 *
 *		 +-----|---------...
 *		 |     |
 *		 |     |
 *		 |     |
 *		 |     |
 *		 +--+  |  +------...
 *		     \ | /
 *		      \|/
 *	    >|-----|<---------------- horizontal offset
 *
 * @default 30
 * @member {Number} module:ui/panel/balloon/balloonpanelview~BalloonPanelView.arrowHorizontalOffset
 */
BalloonPanelView.arrowHorizontalOffset = 25;

/**
 * A vertical offset of the arrow from the edge of the balloon. Controlled by CSS.
 *
 *		 +-------------...
 *		 |
 *		 |
 *		 |                      /-- vertical offset
 *		 |                     V
 *		 +--+    +-----...    ---------
 *		     \  /              |
 *		      \/               |
 *		-------------------------------
 *		                       ^
 *
 * @default 15
 * @member {Number} module:ui/panel/balloon/balloonpanelview~BalloonPanelView.arrowVerticalOffset
 */
BalloonPanelView.arrowVerticalOffset = 10;

/**
 * Function used to calculate the optimal position for the balloon.
 *
 * @protected
 * @member {Function} module:ui/panel/balloon/balloonpanelview~BalloonPanelView._getOptimalPosition
 */
BalloonPanelView._getOptimalPosition = getOptimalPosition;

/**
 * A default set of positioning functions used by the balloon panel view
 * when attaching using the {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView#attachTo} method.
 *
 * The available positioning functions are as follows:
 *
 *
 *
 * **North west**
 *
 * * `northWestArrowSouthWest`
 *
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *		 V
 *		 [ Target ]
 *
 * * `northWestArrowSouthMiddleWest`
 *
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *		    V
 *		    [ Target ]
 *
 * * `northWestArrowSouth`
 *
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *		         V
 *		         [ Target ]
 *
 * * `northWestArrowSouthMiddleEast`
 *
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *		             V
 *		             [ Target ]
 *
 * * `northWestArrowSouthEast`
 *
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *		                 V
 *		                 [ Target ]
 *
 *
 *
 * **North**
 *
 * * `northArrowSouthWest`
 *
 *		    +-----------------+
 *		    |     Balloon     |
 *		    +-----------------+
 *		     V
 *		[ Target ]
 *
 * * `northArrowSouthMiddleWest`
 *
 *		 +-----------------+
 *		 |     Balloon     |
 *		 +-----------------+
 *		     V
 *		[ Target ]
 *
 * * `northArrowSouth`
 *
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *		         V
 *		    [ Target ]
 *
 * * `northArrowSouthMiddleEast`
 *
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *		             V
 *		        [ Target ]
 *
 * * `northArrowSouthEast`
 *
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *		                V
 *		           [ Target ]
 *
 * **North east**
 *
 * * `northEastArrowSouthWest`
 *
 *		        +-----------------+
 *		        |     Balloon     |
 *		        +-----------------+
 *		         V
 *		[ Target ]
 *
 *
 * * `northEastArrowSouthMiddleWest`
 *
 *		     +-----------------+
 *		     |     Balloon     |
 *		     +-----------------+
 *		         V
 *		[ Target ]
 *
 * * `northEastArrowSouth`
 *
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *		         V
 *		[ Target ]
 *
 * * `northEastArrowSouthMiddleEast`
 *
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *		             V
 *		    [ Target ]
 *
 * * `northEastArrowSouthEast`
 *
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *		                 V
 *		        [ Target ]
 *
 *
 *
 * **South**
 *
 *
 * * `southArrowNorthWest`
 *
 *		[ Target ]
 *		     ^
 *		    +-----------------+
 *		    |     Balloon     |
 *		    +-----------------+
 *
 * * `southArrowNorthMiddleWest`
 *
 *		   [ Target ]
 *		        ^
 *		    +-----------------+
 *		    |     Balloon     |
 *		    +-----------------+
 *
 * * `southArrowNorth`
 *
 *		    [ Target ]
 *		         ^
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *
 * * `southArrowNorthMiddleEast`
 *
 *		            [ Target ]
 *		                 ^
 *		   +-----------------+
 *		   |     Balloon     |
 *		   +-----------------+
 *
 * * `southArrowNorthEast`
 *
 *		            [ Target ]
 *		                 ^
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *
 *
 *
 * **South west**
 *
 * * `southWestArrowNorthWest`
 *
 *		 [ Target ]
 *		 ^
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *
 * * `southWestArrowNorthMiddleWest`
 *
 *		     [ Target ]
 *		     ^
 *		 +-----------------+
 *		 |     Balloon     |
 *		 +-----------------+
 *
 * * `southWestArrowNorth`
 *
 *		         [ Target ]
 *		         ^
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *
 * * `southWestArrowNorthMiddleEast`
 *
 *		              [ Target ]
 *		              ^
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *
 * * `southWestArrowNorthEast`
 *
 *		                 [ Target ]
 *		                 ^
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *
 *
 *
 * **South east**
 *
 * * `southEastArrowNorthWest`
 *
 *		[ Target ]
 *		         ^
 *		        +-----------------+
 *		        |     Balloon     |
 *		        +-----------------+
* * `southEastArrowNorthMiddleWest`
 *
 *		   [ Target ]
 *		            ^
 *		        +-----------------+
 *		        |     Balloon     |
 *		        +-----------------+
 *
 * * `southEastArrowNorth`
 *
 *		[ Target ]
 *		         ^
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *
 * * `southEastArrowNorthMiddleEast`
 *
 *		     [ Target ]
 *		              ^
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *
 * * `southEastArrowNorthEast`
 *
 *		        [ Target ]
 *		                 ^
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *
 *
 * See {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView#attachTo}.
 *
 * Positioning functions must be compatible with {@link module:utils/dom/position~Position}.
 *
 * The name that the position function returns will be reflected in the balloon panel's class that
 * controls the placement of the "arrow". See {@link #position} to learn more.
 *
 * @member {Object} module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions
 */
BalloonPanelView.defaultPositions = {

	// ------- North west

	northWestArrowSouthWest: ( targetRect, balloonRect ) => ( {
		top: getNorthTop( targetRect, balloonRect ),
		left: targetRect.left - BalloonPanelView.arrowHorizontalOffset,
		name: 'arrow_sw'
	} ),

	northWestArrowSouthMiddleWest: ( targetRect, balloonRect ) => ( {
		top: getNorthTop( targetRect, balloonRect ),
		left: targetRect.left - ( balloonRect.width * .25 ) - BalloonPanelView.arrowHorizontalOffset,
		name: 'arrow_smw'
	} ),

	northWestArrowSouth: ( targetRect, balloonRect ) => ( {
		top: getNorthTop( targetRect, balloonRect ),
		left: targetRect.left - balloonRect.width / 2,
		name: 'arrow_s'
	} ),

	northWestArrowSouthMiddleEast: ( targetRect, balloonRect ) => ( {
		top: getNorthTop( targetRect, balloonRect ),
		left: targetRect.left - ( balloonRect.width * .75 ) + BalloonPanelView.arrowHorizontalOffset,
		name: 'arrow_sme'
	} ),

	northWestArrowSouthEast: ( targetRect, balloonRect ) => ( {
		top: getNorthTop( targetRect, balloonRect ),
		left: targetRect.left - balloonRect.width + BalloonPanelView.arrowHorizontalOffset,
		name: 'arrow_se'
	} ),

	// ------- North

	northArrowSouthWest: ( targetRect, balloonRect ) => ( {
		top: getNorthTop( targetRect, balloonRect ),
		left: targetRect.left + targetRect.width / 2 - BalloonPanelView.arrowHorizontalOffset,
		name: 'arrow_sw'
	} ),

	northArrowSouthMiddleWest: ( targetRect, balloonRect ) => ( {
		top: getNorthTop( targetRect, balloonRect ),
		left: targetRect.left + targetRect.width / 2 - ( balloonRect.width * .25 ) - BalloonPanelView.arrowHorizontalOffset,
		name: 'arrow_smw'
	} ),

	northArrowSouth: ( targetRect, balloonRect ) => ( {
		top: getNorthTop( targetRect, balloonRect ),
		left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2,
		name: 'arrow_s'
	} ),

	northArrowSouthMiddleEast: ( targetRect, balloonRect ) => ( {
		top: getNorthTop( targetRect, balloonRect ),
		left: targetRect.left + targetRect.width / 2 - ( balloonRect.width * .75 ) + BalloonPanelView.arrowHorizontalOffset,
		name: 'arrow_sme'
	} ),

	northArrowSouthEast: ( targetRect, balloonRect ) => ( {
		top: getNorthTop( targetRect, balloonRect ),
		left: targetRect.left + targetRect.width / 2 - balloonRect.width + BalloonPanelView.arrowHorizontalOffset,
		name: 'arrow_se'
	} ),

	// ------- North east

	northEastArrowSouthWest: ( targetRect, balloonRect ) => ( {
		top: getNorthTop( targetRect, balloonRect ),
		left: targetRect.right - BalloonPanelView.arrowHorizontalOffset,
		name: 'arrow_sw'
	} ),

	northEastArrowSouthMiddleWest: ( targetRect, balloonRect ) => ( {
		top: getNorthTop( targetRect, balloonRect ),
		left: targetRect.right - ( balloonRect.width * .25 ) - BalloonPanelView.arrowHorizontalOffset,
		name: 'arrow_smw'
	} ),
	northEastArrowSouth: ( targetRect, balloonRect ) => ( {
		top: getNorthTop( targetRect, balloonRect ),
		left: targetRect.right - balloonRect.width / 2,
		name: 'arrow_s'
	} ),

	northEastArrowSouthMiddleEast: ( targetRect, balloonRect ) => ( {
		top: getNorthTop( targetRect, balloonRect ),
		left: targetRect.right - ( balloonRect.width * .75 ) + BalloonPanelView.arrowHorizontalOffset,
		name: 'arrow_sme'
	} ),

	northEastArrowSouthEast: ( targetRect, balloonRect ) => ( {
		top: getNorthTop( targetRect, balloonRect ),
		left: targetRect.right - balloonRect.width + BalloonPanelView.arrowHorizontalOffset,
		name: 'arrow_se'
	} ),
	// ------- South west

	southWestArrowNorthWest: ( targetRect, balloonRect ) => ( {
		top: getSouthTop( targetRect, balloonRect ),
		left: targetRect.left - BalloonPanelView.arrowHorizontalOffset,
		name: 'arrow_nw'
	} ),

	southWestArrowNorthMiddleWest: ( targetRect, balloonRect ) => ( {
		top: getSouthTop( targetRect, balloonRect ),
		left: targetRect.left - ( balloonRect.width * .25 ) - BalloonPanelView.arrowHorizontalOffset,
		name: 'arrow_nmw'
	} ),

	southWestArrowNorth: ( targetRect, balloonRect ) => ( {
		top: getSouthTop( targetRect, balloonRect ),
		left: targetRect.left - balloonRect.width / 2,
		name: 'arrow_n'
	} ),

	southWestArrowNorthMiddleEast: ( targetRect, balloonRect ) => ( {
		top: getSouthTop( targetRect, balloonRect ),
		left: targetRect.left - ( balloonRect.width * .75 ) + BalloonPanelView.arrowHorizontalOffset,
		name: 'arrow_nme'
	} ),

	southWestArrowNorthEast: ( targetRect, balloonRect ) => ( {
		top: getSouthTop( targetRect, balloonRect ),
		left: targetRect.left - balloonRect.width + BalloonPanelView.arrowHorizontalOffset,
		name: 'arrow_ne'
	} ),

	// ------- South

	southArrowNorthWest: ( targetRect, balloonRect ) => ( {
		top: getSouthTop( targetRect, balloonRect ),
		left: targetRect.left + targetRect.width / 2 - BalloonPanelView.arrowHorizontalOffset,
		name: 'arrow_nw'
	} ),
	southArrowNorthMiddleWest: ( targetRect, balloonRect ) => ( {
		top: getSouthTop( targetRect, balloonRect ),
		left: targetRect.left + targetRect.width / 2 - ( balloonRect.width * 0.25 ) - BalloonPanelView.arrowHorizontalOffset,
		name: 'arrow_nmw'
	} ),

	southArrowNorth: ( targetRect, balloonRect ) => ( {
		top: getSouthTop( targetRect, balloonRect ),
		left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2,
		name: 'arrow_n'
	} ),

	southArrowNorthMiddleEast: ( targetRect, balloonRect ) => ( {
		top: getSouthTop( targetRect, balloonRect ),
		left: targetRect.left + targetRect.width / 2 - ( balloonRect.width * 0.75 ) + BalloonPanelView.arrowHorizontalOffset,
		name: 'arrow_nme'
	} ),

	southArrowNorthEast: ( targetRect, balloonRect ) => ( {
		top: getSouthTop( targetRect, balloonRect ),
		left: targetRect.left + targetRect.width / 2 - balloonRect.width + BalloonPanelView.arrowHorizontalOffset,
		name: 'arrow_ne'
	} ),

	// ------- South east

	southEastArrowNorthWest: ( targetRect, balloonRect ) => ( {
		top: getSouthTop( targetRect, balloonRect ),
		left: targetRect.right - BalloonPanelView.arrowHorizontalOffset,
		name: 'arrow_nw'
	} ),

	southEastArrowNorthMiddleWest: ( targetRect, balloonRect ) => ( {
		top: getSouthTop( targetRect, balloonRect ),
		left: targetRect.right - ( balloonRect.width * .25 ) - BalloonPanelView.arrowHorizontalOffset,
		name: 'arrow_nmw'
	} ),

	southEastArrowNorth: ( targetRect, balloonRect ) => ( {
		top: getSouthTop( targetRect, balloonRect ),
		left: targetRect.right - balloonRect.width / 2,
		name: 'arrow_n'
	} ),

	southEastArrowNorthMiddleEast: ( targetRect, balloonRect ) => ( {
		top: getSouthTop( targetRect, balloonRect ),
		left: targetRect.right - ( balloonRect.width * .75 ) + BalloonPanelView.arrowHorizontalOffset,
		name: 'arrow_nme'
	} ),

	southEastArrowNorthEast: ( targetRect, balloonRect ) => ( {
		top: getSouthTop( targetRect, balloonRect ),
		left: targetRect.right - balloonRect.width + BalloonPanelView.arrowHorizontalOffset,
		name: 'arrow_ne'
	} )

};

// Returns the top coordinate for positions starting with `north*`.
//
// @private
// @param {utils/dom/rect~Rect} targetRect A rect of the target.
// @param {utils/dom/rect~Rect} elementRect A rect of the balloon.
// @returns {Number}
function getNorthTop( targetRect, balloonRect ) {
	return targetRect.top - balloonRect.height - BalloonPanelView.arrowVerticalOffset;
}

// Returns the top coordinate for positions starting with `south*`.
//
// @private
// @param {utils/dom/rect~Rect} targetRect A rect of the target.
// @param {utils/dom/rect~Rect} elementRect A rect of the balloon.
// @returns {Number}
function getSouthTop( targetRect ) {
	return targetRect.bottom + BalloonPanelView.arrowVerticalOffset;
}
