/*!
 * VisualEditor ContentEditable linear arrow key down handler
 *
 * @copyright See AUTHORS.txt
 */

/* istanbul ignore next */
/**
 * Arrow key down handler for linear selections.
 *
 * @class
 * @extends ve.ce.KeyDownHandler
 *
 * @constructor
 */
ve.ce.LinearArrowKeyDownHandler = function VeCeLinearArrowKeyDownHandler() {
	// Parent constructor - never called because class is fully static
	// ve.ui.LinearArrowKeyDownHandler.super.apply( this, arguments );
};

/* Inheritance */

OO.inheritClass( ve.ce.LinearArrowKeyDownHandler, ve.ce.KeyDownHandler );

/* Static properties */

ve.ce.LinearArrowKeyDownHandler.static.name = 'linearArrow';

ve.ce.LinearArrowKeyDownHandler.static.keys = [
	OO.ui.Keys.UP, OO.ui.Keys.DOWN, OO.ui.Keys.LEFT, OO.ui.Keys.RIGHT,
	OO.ui.Keys.HOME, OO.ui.Keys.END, OO.ui.Keys.PAGEUP, OO.ui.Keys.PAGEDOWN
];

ve.ce.LinearArrowKeyDownHandler.static.supportedSelections = [ 'linear' ];

/* Static methods */

/**
 * @inheritdoc
 */
ve.ce.LinearArrowKeyDownHandler.static.execute = function ( surface, e ) {
	const isBlockMove = e.keyCode === OO.ui.Keys.UP || e.keyCode === OO.ui.Keys.DOWN ||
			e.keyCode === OO.ui.Keys.PAGEUP || e.keyCode === OO.ui.Keys.PAGEDOWN ||
			e.keyCode === OO.ui.Keys.HOME || e.keyCode === OO.ui.Keys.END,
		keyBlockDirection = e.keyCode === OO.ui.Keys.DOWN || e.keyCode === OO.ui.Keys.PAGEDOWN || e.keyCode === OO.ui.Keys.END ? 1 : -1,
		activeNode = surface.getActiveNode();
	let range = surface.model.getSelection().getRange();

	// TODO: onDocumentKeyDown did this already
	surface.surfaceObserver.stopTimerLoop();
	// TODO: onDocumentKeyDown did this already
	surface.surfaceObserver.pollOnce();

	function moveOffFocusableNode( focusableRange, dir ) {
		return surface.model.getDocument().getRelativeRange(
			focusableRange,
			dir,
			'character',
			e.shiftKey,
			activeNode && ( e.shiftKey || activeNode.trapsCursor() ) ? activeNode.getRange() : null
		);
	}

	/**
	 * Determine the direction to move based on the key pressed and text directionality.
	 *
	 * @param {ve.ce.Node|HTMLElement} nodeForDirection
	 * @return {number} -1 for backwards, 1 for forwards
	 */
	function getDirection( nodeForDirection ) {
		if ( isBlockMove ) {
			return keyBlockDirection;
		}
		let directionality;
		if ( nodeForDirection === surface.focusedNode ) {
			directionality = surface.getFocusedNodeDirectionality();
		} else if ( nodeForDirection instanceof ve.ce.Node ) {
			directionality = $( nodeForDirection.$element[ 0 ] ).css( 'direction' );
		} else {
			directionality = $( nodeForDirection ).css( 'direction' );
		}
		// Left arrow in ltr, or right arrow in rtl
		// eslint-disable-next-line no-bitwise
		if ( e.keyCode === OO.ui.Keys.LEFT ^ directionality === 'rtl' ) {
			return -1;
		}
		// Left arrow in rtl, or right arrow in ltr
		return 1;
	}

	function handleMove( nodeForDirection ) {
		const direction = getDirection( nodeForDirection );
		// Block level selection, so directionality is just css directionality
		range = moveOffFocusableNode( range, direction );
		surface.model.setLinearSelection( range );
		e.preventDefault();
	}

	if ( surface.focusedBlockSlug ) {
		handleMove( surface.focusedBlockSlug );
		return true;
	}

	// Empty paragraphs have inline slugs which cause the cursor to get
	// stuck, so move off them immediately (T162916)
	if ( range.isCollapsed() && !isBlockMove ) {
		const node = surface.getDocument().getBranchNodeFromOffset( range.from );
		if ( node.canContainContent() && node.getModel().length === 0 ) {
			handleMove( node );
			return true;
		}
	}

	if ( surface.focusedNode ) {
		const direction = getDirection( surface.focusedNode );

		if ( e.shiftKey ) {
			// There is no DOM range to expand (because the selection is faked), so
			// use "collapse to focus - observe - expand". Define "focus" to be the
			// edge of the focusedNode in the direction of motion (so the selection
			// always grows). This means that clicking on the focusableNode then
			// modifying the selection will always include the node.
			// eslint-disable-next-line no-bitwise
			if ( direction === -1 ^ range.isBackwards() ) {
				range = range.flip();
			}
			let observationRange = new ve.Range( range.to );

			if ( !surface.focusedNode.isContent() ) {
				// Block focusable node: move the observation range to an adjacent content offset
				observationRange = moveOffFocusableNode( observationRange, direction );
				observationRange = new ve.Range( observationRange.to );
			}
			surface.model.setLinearSelection( observationRange );
		} else {
			range = moveOffFocusableNode( range, direction );
			// Move to start/end of node in the model in DM (and DOM)
			range = new ve.Range( direction === 1 ? range.end : range.start );
			surface.model.setLinearSelection( range );
			if ( !isBlockMove ) {
				// Un-shifted left/right: we've already moved so preventDefault
				e.preventDefault();
				return true;
			}
			// Else keep going with the cursor in the new place
		}
	}
	// Else keep DM range and DOM selection as-is

	let collapseNode, collapseOffset;
	if ( e.shiftKey && !ve.supportsSelectionExtend && range.isBackwards() ) {
		// If the browser doesn't support backwards selections, but the dm range
		// is backwards, then use "collapse to anchor - observe - expand".
		collapseNode = surface.nativeSelection.anchorNode;
		collapseOffset = surface.nativeSelection.anchorOffset;
	} else if ( e.shiftKey && !range.isCollapsed() && isBlockMove ) {
		// If selection is expanded and cursoring is up/down, use
		// "collapse to focus - observe - expand" to work round quirks.
		collapseNode = surface.nativeSelection.focusNode;
		collapseOffset = surface.nativeSelection.focusOffset;
	}
	// Else don't collapse the selection

	if ( collapseNode ) {
		const nativeRange = surface.getElementDocument().createRange();
		nativeRange.setStart( collapseNode, collapseOffset );
		nativeRange.setEnd( collapseNode, collapseOffset );
		surface.nativeSelection.removeAllRanges();
		surface.nativeSelection.addRange( nativeRange );
	}

	const startFocusNode = surface.nativeSelection.focusNode;
	const startFocusOffset = surface.nativeSelection.focusOffset;

	// Re-expand (or fixup) the selection after the native action, if necessary
	surface.eventSequencer.afterOne( { keydown: function () {
		// Support: Chrome
		// Chrome bug lets you cursor into a multi-line contentEditable=false with up/down…
		const viewNode = $( surface.nativeSelection.focusNode ).closest( '.ve-ce-leafNode,.ve-ce-branchNode' ).data( 'view' );
		if ( !viewNode ) {
			// Irrelevant selection (or none)
			return;
		}

		let newRange;
		if ( viewNode.isFocusable() ) {
			let afterDirection;
			// We've landed in a focusable node; fixup the range
			if ( isBlockMove ) {
				// The intended direction is clear, even if the cursor did not move
				// or did something completely preposterous
				afterDirection = keyBlockDirection;
			} else {
				// Observe which way the cursor moved
				afterDirection = ve.compareDocumentOrder(
					surface.nativeSelection.focusNode,
					surface.nativeSelection.focusOffset,
					startFocusNode,
					startFocusOffset
				);
			}
			newRange = (
				afterDirection > 0 ?
					viewNode.getOuterRange() :
					viewNode.getOuterRange().flip()
			);
		} else {
			// Check where the range has moved to
			surface.surfaceObserver.pollOnceNoCallback();
			newRange = new ve.Range( surface.surfaceObserver.getRange().to );
		}

		// Adjust range to use old anchor, if necessary
		if ( e.shiftKey ) {
			newRange = new ve.Range( range.from, newRange.to );
			surface.getModel().setLinearSelection( newRange );
		}
		surface.updateActiveAnnotations();
		surface.surfaceObserver.pollOnce();
	} } );

	return true;
};

/* Registration */

ve.ce.keyDownHandlerFactory.register( ve.ce.LinearArrowKeyDownHandler );
