import { React, PropTypes, classnames } from '@gravityforms/libraries';
import { spacerClasses, trigger } from '@gravityforms/utils';
import { useDrop, useDrag } from 'react-dnd';
import { UP, DOWN } from './constants';
import { ARROW_UP, ARROW_DOWN, ARROW_LEFT, ARROW_RIGHT, TAB, SPACE } from '../../utils/keymap';
import itemTypes from './item-types';
import Button from '../../elements/Button';

const { useRef, useState } = React;

/**
 * @module SortableItem
 * @description A SortableItem component to display a list of items that can be reordered with drag and drop or keyboard navigation.
 *
 * @since 3.3.6
 *
 * @param {object}                      props                    Component props.
 * @param {string | Function | element} props.contents           The contents for this item.
 * @param {Function}                    props.moveItem           Callback to move items.
 * @param {Function}                    props.speak              Callback to make screen readers speak.
 * @param {string}                      props.id                 The ID.
 * @param {number}                      props.listIndex          The current list index.
 * @param {number}                      props.totalItems         Total # of children.
 * @param {Array | boolean}             props.children           Children for this item.
 * @param {number}                      props.depth              Current depth.
 * @param {Function}                    props.deleteItem         Callback for deleting an item.
 * @param {Function}                    props.renderSortableList Callback for rendering a sortable list.
 * @param {boolean}                     props.showDragHandle     Whether to show the drag handle.
 * @param {boolean}                     props.showArrows         Whether to show navigation arrows.
 * @param {object}                      props.customAttributes   Custom attributes for the component.
 * @param {string|Array|object}         props.customClasses      Custom classes for the component.
 * @param {string|number|Array|object}  props.spacing            The spacing for the component, as a string, number, array, or object.
 * @param {boolean}                     props.isNew              Whether the item is new.
 *
 * @return {JSX.Element}
 *
 * @example
 * import SortableItem from '@gravityforms/components/react/admin/modules/SortableItem';
 *
 * return (
 *      <SortableItem
 * 	        spacing={ { '': 6, md: 8 } }
 * 	    />
 * );
 *
 */
const SortableItem = ( {
	customAttributes = {},
	customClasses = [],
	spacing = '',
	contents = '',
	moveItem = () => {},
	speak = () => {},
	id = 0,
	listIndex = 0,
	totalItems = 0,
	children = false,
	depth = 0,
	deleteItem = () => {},
	renderSortableList = () => {},
	showDragHandle = true,
	showDropZone = true,
	showArrows = true,
	isNew = false,
} ) => {
	const ref = useRef( null );

	const [ keyboardDragActive, setKeyboardDragActive ] = useState( false );

	/**
	 * @function moveItemUp
	 * @description Move an item up in the list.
	 *
	 * @since 1.0.0
	 *
	 * @param {number} itemListIndex The list index of the item.
	 * @param {string} itemId        The ID of the item.
	 *
	 * @return void
	 */
	const moveItemUp = ( itemListIndex, itemId ) => {
		speak( `Moving item ${ itemId } to ${ itemListIndex - 1 }` );
		moveItem( itemListIndex, ( itemListIndex - 1 ) );

		trigger( { event: 'gform/sortable_list/item_moved', native: false, data: {
			itemListIndex,
			itemId,
			direction: UP,
		} } );
	};

	/**
	 * @function moveItemDown
	 * @description Move an item down in the list.
	 *
	 * @since 1.0.0
	 *
	 * @param {number} itemListIndex The list index of the item.
	 * @param {string} itemId        The ID of the item.
	 *
	 * @return void
	 */
	const moveItemDown = ( itemListIndex, itemId ) => {
		speak( `Moving item ${ itemId } to ${ itemListIndex + 1 }` );
		moveItem( itemListIndex, ( itemListIndex + 1 ) );

		trigger( { event: 'gform/sortable_list/item_moved', native: false, data: {
			itemListIndex,
			itemId,
			direction: DOWN,
		} } );
	};

	/**
	 * @function beginDragging
	 * @description Enter dragging mode for keyboard nav.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	const beginDragging = () => {
		speak( `Entering drag and drop.` );
		setKeyboardDragActive( true );

		trigger( { event: 'gform/sortable_list/begin_drag', native: false, data: {
			item: ref,
		} } );
	};

	/**
	 * @function endDragging
	 * @description End dragging mode for keyboard nav.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	const endDragging = () => {
		speak( `Exiting drag and drop.` );
		setKeyboardDragActive( false );

		trigger( { event: 'gform/sortable_list/end_drag', native: false, data: {
			item: ref,
		} } );
	};

	/**
	 * @function handleKeyboardNav
	 * @description Handle navigation via the keyboard events.
	 *
	 * @since 1.0.0
	 *
	 * @param {event} e The triggering keyboard event.
	 *
	 * @return void
	 */
	const handleKeyboardNav = ( e ) => {
		e.stopPropagation();

		if ( e.key === TAB && keyboardDragActive ) {
			e.preventDefault();
			return;
		}

		if ( e.key === SPACE && keyboardDragActive ) {
			endDragging();
			return;
		}

		if ( e.key === SPACE && ! keyboardDragActive ) {
			beginDragging();
			return;
		}

		if ( ! keyboardDragActive ) {
			return;
		}

		if ( ( e.key === ARROW_UP || e.key === ARROW_LEFT ) && listIndex > 0 ) {
			moveItemUp( listIndex, id );
			return;
		}

		if ( ( e.key === ARROW_DOWN || e.key === ARROW_RIGHT ) && listIndex < ( totalItems - 1 ) ) {
			moveItemDown( listIndex, id );
		}
	};

	/**
	 * @function handleArrowPresses
	 * @description Handle when the up/down arrows UI elements are interacted with.
	 *
	 * @since 1.0.0
	 *
	 * @param {event}  e         The triggering event.
	 * @param {string} direction The direction being handled.
	 *
	 * @return void
	 */
	const handleArrowPresses = ( e, direction ) => {
		// if ( e.key !== SPACE ) {
		// 	return;
		// }

		if ( direction === UP && listIndex > 0 ) {
			moveItemUp( listIndex, id );
			return;
		}

		if ( direction === DOWN && listIndex < ( totalItems - 1 ) ) {
			moveItemDown( listIndex, id );
		}
	};

	/**
	 * @function renderChildren
	 * @description Render children for the item.
	 *
	 * @param         itemChildren
	 *
	 * @since 1.0.0
	 *
	 * @param {Array} The          item children to render.
	 *
	 * @return {*}
	 */
	const renderChildren = ( itemChildren ) => {
		trigger( { event: 'gform/sortable_list/before_render_children', native: false, data: {
			itemChildren,
			depth,
			item: ref,
		} } );
		return renderSortableList( depth, itemChildren );
	};

	/**
	 * @function renderContents
	 * @description Render the contents for the item.
	 *
	 * @param                               itemContents
	 *
	 * @since 1.0.0
	 *
	 * @param {string | Function | element} The          content to render.
	 *
	 * @return {*}
	 */
	const renderContents = ( itemContents ) => {
		if ( typeof ( itemContents ) !== 'function' ) {
			return itemContents;
		}

		const args = {
			moveItem,
			speak,
			id,
			listIndex,
			totalItems,
			children,
			depth,
			deleteItem,
		};

		return itemContents( args, ref );
	};

	const [ { handlerId }, drop ] = useDrop( {
		accept: itemTypes.SORTABLE_ITEM,
		collect( monitor ) {
			return {
				handlerId: monitor.getHandlerId(),
				isOver: monitor.isOver(),
			};
		},
		hover( item, monitor ) {
			if ( ! ref.current ) {
				return;
			}

			// eslint-disable-next-line eqeqeq
			if ( ref.current.parentElement != item.itemRef.current.parentElement ) {
				return;
			}

			const dragIndex = item.listIndex;
			const hoverIndex = listIndex;

			if ( dragIndex === hoverIndex ) {
				return;
			}

			const hoverBoundingRect = ref.current?.getBoundingClientRect();
			const hoverMiddleY = ( hoverBoundingRect.bottom - hoverBoundingRect.top ) / 2;
			const clientOffset = monitor.getClientOffset();
			const hoverClientY = clientOffset.y - hoverBoundingRect.top;

			if ( dragIndex < hoverIndex && hoverClientY < hoverMiddleY ) {
				return;
			}

			if ( dragIndex > hoverIndex && hoverClientY > hoverMiddleY ) {
				return;
			}

			moveItem( dragIndex, hoverIndex );

			item.listIndex = hoverIndex;
		},
		drop( item ) {
			speak( `${ contents } moved to position ${ item.listIndex + 1 }` );

			trigger( { event: 'gform/sortable_list/item_dropped', native: false, data: {
				item,
			} } );
		},
	} );

	const [ { isDragging }, drag ] = useDrag( {
		type: itemTypes.SORTABLE_ITEM,
		item: () => {
			return { listIndex, children, itemRef: ref };
		},
		collect: ( monitor ) => ( {
			isDragging: monitor.isDragging(),
		} ),
	} );

	drag( drop( ref ) );

	const componentAttributes = {
		className: classnames( {
			'gform-sortable-list-item': true,
			...spacerClasses( spacing ),
		}, customClasses ),
		'data-list-index': listIndex,
		...customAttributes,
	};

	const dragAttrs = {
		type: 'icon-grey',
		icon: 'drag-indicator',
		customAttributes: {
			onKeyDown: handleKeyboardNav,
			onKeyPress: handleKeyboardNav,
		},
		customClasses: [
			'gform-sortable-list-item__control',
			'gform-sortable-list-item__control--first',
		],
	};

	const upArgs = {
		type: 'icon-grey',
		iconPrefix: 'gform-common-icon',
		icon: 'chevron-up',
		// customAttributes: {
		// 	onKeyDown: ( e ) => handleArrowPresses( e, UP ),
		// 	onKeyPress: ( e ) => handleArrowPresses( e, UP ),
		// },
		onClick: ( e ) => handleArrowPresses( e, UP ),
		customClasses: [
			'gform-sortable-list-item__control',
			'gform-sortable-list-item__control--stacked',
		],
	};

	const downArgs = {
		type: 'icon-grey',
		iconPrefix: 'gform-common-icon',
		icon: 'chevron-down',
		// customAttributes: {
		// 	onKeyDown: ( e ) => handleArrowPresses( e, DOWN ),
		// 	onKeyPress: ( e ) => handleArrowPresses( e, DOWN ),
		// },
		onClick: ( e ) => handleArrowPresses( e, DOWN ),
		customClasses: [
			'gform-sortable-list-item__control',
			'gform-sortable-list-item__control--stacked',
		],
	};

	const className = classnames( {
		'gform-sortable-list-item-wrapper': true,
		'is-dragging': isDragging,
		'is-keyboard-nav': keyboardDragActive,
		'floating-nav': children !== false,
		'no-top-margin': children !== false && children.length === 0 && ! showDropZone,
		'is-new': isNew,
	} );

	return (
		<div className={ className } ref={ ref } data-handler-id={ handlerId }>
			<div { ...componentAttributes }>
				{ ( showArrows || showDragHandle ) &&
					<div className={ 'gform-sortable-list-item__controls' } >
						{ showDragHandle && <Button { ...dragAttrs } /> }
						{ showArrows && <Button { ...upArgs } /> }
						{ showArrows && <Button { ...downArgs } /> }
					</div>
				}
				{ children === false && renderContents( contents ) }
				{ renderChildren( children ) }
			</div>
		</div>
	);
};

SortableItem.propTypes = {
	contents: PropTypes.oneOfType( [
		PropTypes.element,
		PropTypes.func,
		PropTypes.string,
	] ),
	customAttributes: PropTypes.object,
	customClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	deleteItem: PropTypes.func,
	depth: PropTypes.number,
	id: PropTypes.string,
	listIndex: PropTypes.number,
	listItems: PropTypes.array,
	moveItem: PropTypes.func,
	spacing: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.number,
		PropTypes.array,
		PropTypes.object,
	] ),
	renderSortableList: PropTypes.func,
	shoArrows: PropTypes.bool,
	showDragHandle: PropTypes.bool,
	showDropZone: PropTypes.bool,
	speak: PropTypes.func,
	totalItems: PropTypes.number,
	children: PropTypes.oneOfType( [
		PropTypes.array,
		PropTypes.bool,
	] ),
	isNew: PropTypes.bool,
};

SortableItem.displayName = 'SortableItem';

export default SortableItem;
