import { React, ReactDND, PropTypes, classnames } from '@gravityforms/libraries';
import { spacerClasses, sprintf, trigger } from '@gravityforms/utils';
import ItemTypes from './item-types';
import { ARROW_UP, ARROW_DOWN, ARROW_LEFT, ARROW_RIGHT, TAB } from '../../utils/keymap';
import Button from '../../elements/Button';

const { forwardRef, useRef, useState } = React;
const { useDrag, useDrop } = ReactDND;

const UP = 'up';
const DOWN = 'down';

/**
 * @module RepeaterItem
 * @description A RepeaterItem component to display a list of items that can be reordered with drag and drop or keyboard navigation.
 *
 * @since 4.7.0
 *
 * @param {object}                     props                        Component props.
 * @param {Array}                      props.children               Children for this item.
 * @param {string|Function|element}    props.content                The content for this item.
 * @param {object}                     props.customAttributes       Custom attributes for the component.
 * @param {string|Array|object}        props.customClasses          Custom classes for the component.
 * @param {object}                     props.deleteButtonAttributes Custom attributes for the delete button.
 * @param {string|Array|object}        props.deleteButtonClasses    Custom classes for the delete button.
 * @param {Function}                   props.deleteItem             Callback for deleting an item.
 * @param {number}                     props.depth                  Current depth.
 * @param {object}                     props.downButtonAttributes   Custom attributes for the down button.
 * @param {string|Array|object}        props.downButtonClasses      Custom classes for the down button.
 * @param {object}                     props.dragHandleAttributes   Custom attributes for the drag handle.
 * @param {string|Array|object}        props.dragHandleClasses      Custom classes for the drag handle.
 * @param {object}                     props.i18n                   The i18n object for speak functions and a11y.
 * @param {string}                     props.id                     The ID.
 * @param {boolean}                    props.isDraggable            Whether the item is draggable.
 * @param {boolean}                    props.isSortable             Whether the item is sortable.
 * @param {number}                     props.listIndex              The current list index.
 * @param {Function}                   props.moveItem               Callback to move items.
 * @param {Function}                   props.renderRepeater         Callback for rendering a repeater.
 * @param {boolean}                    props.showArrows             Whether to show navigation arrows.
 * @param {boolean}                    props.showDeleteButton       Whether to show the delete button.
 * @param {boolean}                    props.showDragHandle         Whether to show the drag handle.
 * @param {string|number|Array|object} props.spacing                The spacing for the component, as a string, number, array, or object.
 * @param {Function}                   props.speak                  Callback to make screen readers speak.
 * @param {number}                     props.totalItems             Total # of children.
 * @param {object}                     props.upButtonAttributes     Custom attributes for the up button.
 * @param {string|Array|object}        props.upButtonClasses        Custom classes for the up button.
 * @param {object}                     ref                          Ref to the component.
 *
 * @return {JSX.Element}
 *
 * @example
 * import RepeaterItem from '@gravityforms/components/react/admin/modules/RepeaterItem';
 *
 * return (
 *      <RepeaterItem
 *          spacing={ { '': 6, md: 8 } }
 *      />
 * );
 *
 */
const RepeaterItem = forwardRef( ( {
	children = false,
	content = null,
	customAttributes = {},
	customClasses = [],
	deleteButtonAttributes = {},
	deleteButtonClasses = [],
	deleteItem = () => {},
	depth = 0,
	downButtonAttributes = {},
	downButtonClasses = [],
	dragHandleAttributes = {},
	dragHandleClasses = [],
	i18n = {},
	id = '',
	isDraggable = false,
	isSortable = false,
	listIndex = 0,
	moveItem = () => {},
	// renderRepeater = () => {}, @todo: Implement when we need to render nested repeaters.
	showArrows = true,
	showDeleteButton = true,
	showDragHandle = true,
	// showDropZone = true, @todo: Implement when we need to render drop zone.
	spacing = '',
	speak = () => {},
	totalItems = 0,
	upButtonAttributes = {},
	upButtonClasses = [],
}, refProp ) => {
	const internalRef = useRef( null );
	const ref = refProp || internalRef;
	const [ keyboardDragActive, setKeyboardDragActive ] = useState( false );

	const hasChildren = Array.isArray( children );

	/**
	 * @function moveItemUp
	 * @description Move an item up in the list.
	 *
	 * @since 4.7.0
	 *
	 * @param {number} itemListIndex The list index of the item.
	 */
	const moveItemUp = ( itemListIndex ) => {
		speak( sprintf( i18n.moveItem, id, itemListIndex - 1 ) );
		moveItem( itemListIndex, ( itemListIndex - 1 ) );

		trigger( { event: 'gform/repeater/item_moved', native: false, data: {
			itemListIndex,
			itemId: id,
			direction: 'up',
		} } );
	};

	/**
	 * @function moveItemDown
	 * @description Move an item down in the list.
	 *
	 * @since 4.7.0
	 *
	 * @param {number} itemListIndex The list index of the item.
	 */
	const moveItemDown = ( itemListIndex ) => {
		speak( sprintf( i18n.moveItem, id, itemListIndex + 1 ) );
		moveItem( itemListIndex, ( itemListIndex + 1 ) );

		trigger( { event: 'gform/repeater/item_moved', native: false, data: {
			itemListIndex,
			itemId: id,
			direction: 'down',
		} } );
	};

	/**
	 * @function beginDragging
	 * @description Enter dragging mode for keyboard nav.
	 *
	 * @since 4.7.0
	 *
	 */
	const beginDragging = () => {
		speak( sprintf( i18n.beginDrag, id ) );
		setKeyboardDragActive( true );

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

	/**
	 * @function endDragging
	 * @description End dragging mode for keyboard nav.
	 *
	 * @since 4.7.0
	 *
	 */
	const endDragging = () => {
		speak( sprintf( i18n.endDrag, id ) );
		setKeyboardDragActive( false );

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

	/**
	 * @function toggleDragging
	 * @description Toggle dragging mode for keyboard nav.
	 *
	 * @since 4.7.0
	 */
	const toggleDragging = () => {
		if ( keyboardDragActive ) {
			endDragging();
		} else {
			beginDragging();
		}
	};

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

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

		if ( ! keyboardDragActive ) {
			return;
		}

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

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

	/**
	 * @function handleArrowPress
	 * @description Handle when the up/down arrows UI elements are interacted with.
	 *
	 * @since 4.7.0
	 *
	 * @param {Event}  event     The triggering event.
	 * @param {string} direction The direction being handled.
	 */
	const handleArrowPress = ( event, direction ) => {
		if ( direction === UP && listIndex > 0 ) {
			moveItemUp( listIndex );
			return;
		}

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

	/**
	 * @function renderChildren
	 * @description Render children for the item.
	 *
	 * @since 4.7.0
	 *
	 * @param {Array} itemChildren The children to render.
	 *
	 * @return {JSX.Element} The rendered nested repeater.
	 */
	// @todo: Implement when we need to render nested repeaters.
	// const renderChildren = ( itemChildren ) => {
	// 	trigger( { event: 'gform/repeater/before_render_children', native: false, data: {
	// 		itemChildren,
	// 		depth,
	// 		item: ref,
	// 	} } );
	// 	return renderRepeater( depth, itemChildren );
	// };

	/**
	 * @function renderContent
	 * @description Render the content for the item.
	 *
	 * @since 4.7.0
	 *
	 * @param {string|Function|element} itemContent The content to render.
	 *
	 * @return {JSX.Element} The rendered content.
	 */
	const renderContent = ( itemContent ) => {
		if ( typeof itemContent !== 'function' ) {
			return itemContent;
		}

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

		return itemContent( args, ref );
	};

	const [ { handlerId }, drop ] = useDrop( {
		accept: ItemTypes.REPEATER_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( sprintf( i18n.endDrop, content, item.listIndex + 1 ) );

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

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

	if ( isDraggable && isSortable ) {
		drag( drop( ref ) );
	}

	const componentAttributes = {
		className: classnames( {
			'gform-repeater-item': true,
			'gform-repeater-item--has-arrows': showArrows && isSortable,
			'gform-repeater-item--has-drag-handle': showDragHandle && isDraggable && isSortable,
			'gform-repeater-item--is-draggable': isDraggable && isSortable,
			'gform-repeater-item--is-sortable': isSortable,
			'gform-repeater-item--is-dragging': isDragging,
			'gform-repeater-item--is-keyboard-nav': keyboardDragActive,
			...spacerClasses( spacing ),
		}, customClasses ),
		'data-list-index': listIndex,
		'data-handler-id': handlerId,
		id,
		...customAttributes,
	};

	const dragHandleProps = {
		size: 'size-height-m',
		type: 'icon-white',
		icon: 'drag-indicator',
		label: i18n.dragLabel,
		customAttributes: {
			onKeyDown: handleKeyboardNav,
		},
		onClick: ( e ) => toggleDragging( e ),
		customClasses: classnames( [
			'gform-repeater-item__control',
			'gform-repeater-item__control--drag-toggle',
		], dragHandleClasses ),
		...dragHandleAttributes,
	};

	const upButtonProps = {
		size: 'size-height-m',
		type: 'icon-white',
		iconPrefix: 'gform-common-icon',
		icon: 'chevron-up',
		onClick: ( e ) => handleArrowPress( e, UP ),
		customClasses: classnames( [
			'gform-repeater-item__control',
			'gform-repeater-item__control--up',
		], upButtonClasses ),
		...upButtonAttributes,
	};

	const downButtonProps = {
		size: 'size-height-m',
		type: 'icon-white',
		iconPrefix: 'gform-common-icon',
		icon: 'chevron-down',
		onClick: ( e ) => handleArrowPress( e, DOWN ),
		customClasses: classnames( [
			'gform-repeater-item__control',
			'gform-repeater-item__control--down',
		], downButtonClasses ),
		...downButtonAttributes,
	};

	const deleteButtonProps = {
		size: 'size-height-m',
		type: 'icon-white',
		icon: 'trash',
		label: i18n.deleteLabel,
		onClick: () => deleteItem( id ),
		customClasses: classnames( [
			'gform-repeater-item__delete',
		], deleteButtonClasses ),
		...deleteButtonAttributes,
	};

	return (
		<div { ...componentAttributes } ref={ ref }>
			{ ( showArrows || showDragHandle ) &&
				<div className="gform-repeater-item__controls">
					{ showDragHandle && isDraggable && isSortable && <Button { ...dragHandleProps } /> }
					{ showArrows && isSortable && <Button { ...downButtonProps } /> }
					{ showArrows && isSortable && <Button { ...upButtonProps } /> }
				</div>
			}
			{ ! hasChildren && renderContent( content ) }
			{ /*{ hasChildren && renderChildren( children ) }*/ }
			{ showDeleteButton && <Button { ...deleteButtonProps } /> }
		</div>
	);
} );

RepeaterItem.propTypes = {
	children: PropTypes.oneOfType( [
		PropTypes.array,
		PropTypes.bool,
	] ),
	content: PropTypes.oneOfType( [
		PropTypes.element,
		PropTypes.func,
		PropTypes.string,
	] ),
	customAttributes: PropTypes.object,
	customClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	deleteButtonAttributes: PropTypes.object,
	deleteButtonClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	deleteItem: PropTypes.func,
	depth: PropTypes.number,
	downButtonAttributes: PropTypes.object,
	downButtonClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	dragHandleAttributes: PropTypes.object,
	dragHandleClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	id: PropTypes.string,
	isDraggable: PropTypes.bool,
	isSortable: PropTypes.bool,
	listIndex: PropTypes.number,
	moveItem: PropTypes.func,
	spacing: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.number,
		PropTypes.array,
		PropTypes.object,
	] ),
	renderRepeater: PropTypes.func,
	showArrows: PropTypes.bool,
	showDragHandle: PropTypes.bool,
	showDropZone: PropTypes.bool,
	speak: PropTypes.func,
	totalItems: PropTypes.number,
	upButtonAttributes: PropTypes.object,
	upButtonClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
};

RepeaterItem.displayName = 'RepeaterItem';

export default RepeaterItem;
