/*!
 * VisualEditor ContentEditable DragDropHandler class.
 *
 * @copyright See AUTHORS.txt
 */

/**
 * Drag and drop handler
 *
 * @param {ve.ce.Surface} surface
 */
ve.ce.DragDropHandler = function VeCeDragDropHandler( surface ) {
	this.surface = surface;

	this.$dropMarker = $( '<div>' ).addClass( 've-ce-surface-dropMarker oo-ui-element-hidden' );
	this.$lastDropTarget = null;
	this.lastDropPosition = null;
	this.relocatingSelection = null;
	this.relocatingNode = null;
	this.allowedFile = null;

	this.getSurface().$element.on( {
		dragstart: this.onDocumentDragStart.bind( this ),
		dragover: this.onDocumentDragOver.bind( this ),
		dragleave: this.onDocumentDragLeave.bind( this ),
		drop: this.onDocumentDrop.bind( this )
	} );
};

/* Methods */

/**
 * Get the handled surface
 *
 * @return {ve.ce.Surface}
 */
ve.ce.DragDropHandler.prototype.getSurface = function () {
	return this.surface;
};

/**
 * Handle document drag start events.
 *
 * @param {jQuery.Event} e Drag start event
 * @fires ve.ce.Surface#relocationStart
 */
ve.ce.DragDropHandler.prototype.onDocumentDragStart = function ( e ) {
	this.getSurface().getClipboardHandler().onCopy( e );
	this.startRelocation();
};

/**
 * Handle document drag over events.
 *
 * @param {jQuery.Event} e Drag over event
 */
ve.ce.DragDropHandler.prototype.onDocumentDragOver = function ( e ) {
	const dataTransferHandlerFactory = this.getSurface().getSurface().dataTransferHandlerFactory,
		dataTransfer = e.originalEvent.dataTransfer;
	let isContent = true;

	if ( this.getSurface().isReadOnly() ) {
		return;
	}

	let nodeType;
	if ( this.relocatingNode ) {
		isContent = this.relocatingNode.isContent();
		nodeType = this.relocatingNode.getType();
	} else {
		if ( this.allowedFile === null ) {
			this.allowedFile = false;
			// If we can get file metadata, check if there is a DataTransferHandler registered
			// to handle it.
			if ( dataTransfer.items ) {
				for ( let i = 0, l = dataTransfer.items.length; i < l; i++ ) {
					const item = dataTransfer.items[ i ];
					if ( item.kind !== 'string' ) {
						const fakeItem = new ve.ui.DataTransferItem( item.kind, item.type );
						if ( dataTransferHandlerFactory.getHandlerNameForItem( fakeItem ) ) {
							this.allowedFile = true;
							break;
						}
					}
				}
			} else if ( dataTransfer.files && dataTransfer.files.length ) {
				for ( let i = 0, l = dataTransfer.files.length; i < l; i++ ) {
					const item = dataTransfer.items[ i ];
					const fakeItem = new ve.ui.DataTransferItem( item.kind, item.type );
					if ( dataTransferHandlerFactory.getHandlerNameForItem( fakeItem ) ) {
						this.allowedFile = true;
						break;
					}
				}
			} else if ( Array.prototype.indexOf.call( dataTransfer.types || [], 'Files' ) !== -1 ) {
				// Support: Firefox
				// If we have no metadata (e.g. in Firefox) assume it is droppable
				this.allowedFile = true;
			}
		}
		// this.allowedFile is cached until the next dragleave event
		if ( this.allowedFile ) {
			isContent = false;
			nodeType = 'alienBlock';
		}
	}

	function getNearestDropTarget( node ) {
		while ( node.parent && !node.parent.isAllowedChildNodeType( nodeType ) ) {
			node = node.parent;
		}
		if ( node.parent ) {
			node.parent.traverseUpstream( ( n ) => {
				if ( n.shouldIgnoreChildren() ) {
					node = null;
					return false;
				}
			} );
			return node;
		}
	}

	if ( !isContent ) {
		e.preventDefault();
		const $target = $( e.target ).closest( '.ve-ce-branchNode, .ve-ce-leafNode' );
		let $dropTarget;
		let dropPosition;
		if ( $target.length ) {
			// Find the nearest node which will accept this node type
			let dropTargetNode = getNearestDropTarget( $target.data( 'view' ) );
			if ( dropTargetNode ) {
				$dropTarget = dropTargetNode.$element;
				dropPosition = e.originalEvent.pageY - $dropTarget.offset().top > $dropTarget.outerHeight() / 2 ? 'bottom' : 'top';
			} else {
				const targetOffset = this.getSurface().getOffsetFromEventCoords( e.originalEvent );
				if ( targetOffset !== -1 ) {
					dropTargetNode = getNearestDropTarget( this.getSurface().getDocument().getBranchNodeFromOffset( targetOffset ) );
					if ( dropTargetNode ) {
						$dropTarget = dropTargetNode.$element;
						dropPosition = 'top';
					}
				}
				if ( !$dropTarget ) {
					$dropTarget = this.$lastDropTarget;
					dropPosition = this.lastDropPosition;
				}
			}
		}
		if ( this.$lastDropTarget && (
			!this.$lastDropTarget.is( $dropTarget ) || dropPosition !== this.lastDropPosition
		) ) {
			this.$dropMarker.addClass( 'oo-ui-element-hidden' );
			$dropTarget = null;
		}
		if ( $dropTarget && (
			!$dropTarget.is( this.$lastDropTarget ) || dropPosition !== this.lastDropPosition
		) ) {
			const targetPosition = $dropTarget.position();
			// Go beyond margins as they can overlap
			let top = targetPosition.top + parseFloat( $dropTarget.css( 'margin-top' ) );
			const left = targetPosition.left + parseFloat( $dropTarget.css( 'margin-left' ) );
			if ( dropPosition === 'bottom' ) {
				top += $dropTarget.outerHeight();
			}
			this.$dropMarker
				.css( {
					top: top,
					left: left
				} )
				.width( $dropTarget.outerWidth() )
				.removeClass( 'oo-ui-element-hidden' );
		}
		if ( $dropTarget !== undefined ) {
			this.$lastDropTarget = $dropTarget;
			this.lastDropPosition = dropPosition;
		}
	}
};

/**
 * Handle document drag leave events.
 *
 * @param {jQuery.Event} e Drag leave event
 */
ve.ce.DragDropHandler.prototype.onDocumentDragLeave = function () {
	this.allowedFile = null;
	if ( this.$lastDropTarget ) {
		this.$dropMarker.addClass( 'oo-ui-element-hidden' );
		this.$lastDropTarget = null;
		this.lastDropPosition = null;
	}
};

/**
 * Handle document drop events.
 *
 * Limits native drag and drop behaviour.
 *
 * @param {jQuery.Event} e Drop event
 * @fires ve.ce.Surface#relocationEnd
 */
ve.ce.DragDropHandler.prototype.onDocumentDrop = function ( e ) {
	// Properties may be nullified by other events, so cache before setTimeout
	const surfaceModel = this.getSurface().getModel(),
		$dropTarget = this.$lastDropTarget,
		dropPosition = this.lastDropPosition,
		platformKey = ve.getSystemPlatform() === 'mac' ? 'mac' : 'pc';

	// Prevent native drop event from modifying view
	e.preventDefault();

	if ( this.getSurface().isReadOnly() ) {
		return;
	}

	let targetOffset;
	// Determine drop position
	if ( $dropTarget ) {
		// Block level drag and drop: use the lastDropTarget to get the targetOffset
		if ( $dropTarget ) {
			const targetRange = $dropTarget.data( 'view' ).getModel().getOuterRange();
			if ( dropPosition === 'top' ) {
				targetOffset = targetRange.start;
			} else {
				targetOffset = targetRange.end;
			}
		} else {
			return;
		}
	} else {
		targetOffset = this.getSurface().getOffsetFromEventCoords( e.originalEvent );
		if ( targetOffset === -1 ) {
			return;
		}
	}
	const targetFragment = surfaceModel.getLinearFragment( new ve.Range( targetOffset ) );

	const targetViewNode = this.getSurface().getDocument().getBranchNodeFromOffset(
		targetFragment.getSelection().getCoveringRange().from
	);
	// TODO: Support sanitized drop on a single line node (removing line breaks)
	const isMultiline = targetViewNode.isMultiline();

	// Internal drop
	if ( this.relocatingSelection ) {
		// Get a fragment and data of the node being dragged
		const originFragment = surfaceModel.getFragment( this.relocatingSelection );
		let originData;
		if ( !isMultiline ) {
			// Data needs to be balanced to be sanitized
			const slice = this.getSurface().getModel().getDocument().shallowCloneFromRange( originFragment.getSelection().getCoveringRange() );
			const linearData = new ve.dm.ElementLinearData(
				originFragment.getDocument().getStore(),
				slice.getBalancedData()
			);
			linearData.sanitize( { singleLine: true } );
			originData = linearData.data;
			// Unwrap CBN
			if ( originData[ 0 ].type && ve.dm.nodeFactory.canNodeContainContent( originData[ 0 ].type ) ) {
				originData = originData.slice( 1, originData.length - 1 );
			}
		} else {
			originData = originFragment.getData();
		}

		// Start staging so we can abort in the catch later
		surfaceModel.pushStaging();

		// Dragging performs cut-and-paste by default (remove content from old location).
		// If Ctrl on PC, or Opt (alt) on Mac, is held, it performs copy-and-paste instead.
		if ( ( platformKey === 'pc' && !e.ctrlKey ) || ( platformKey === 'mac' && !e.altKey ) ) {
			originFragment.removeContent();
		}

		try {
			// Re-insert data at new location
			targetFragment.insertContent( originData );
			surfaceModel.applyStaging();
		} catch ( error ) {
			// Insert content may throw an exception if it can't find a way
			// to fixup the insertion sensibly
			surfaceModel.popStaging();
		}

	} else {
		// External drop
		// TODO: Support sanitized external drop into single line contexts
		if ( isMultiline ) {
			this.getSurface().getClipboardHandler().onPaste( e );
		}
	}
	this.endRelocation();
};

/* Relocation */

/**
 * Start a relocation action.
 *
 * @fires ve.ce.Surface#relocationStart
 */
ve.ce.DragDropHandler.prototype.startRelocation = function () {
	// Cache the selection and selectedNode when the drag starts, to
	// avoid having to recompute them while dragging.
	this.relocatingSelection = this.getSurface().getModel().getSelection();
	this.relocatingNode = this.getSurface().getModel().getSelectedNode();
	this.getSurface().emit( 'relocationStart' );
};

/**
 * Complete a relocation action.
 *
 * @fires ve.ce.Surface#relocationEnd
 */
ve.ce.DragDropHandler.prototype.endRelocation = function () {
	this.relocatingSelection = null;
	this.relocatingNode = null;
	// Trigger a drag leave event to clear markers
	this.onDocumentDragLeave();
	this.getSurface().emit( 'relocationEnd' );
};