import { React, ReactDND, PropTypes, classnames } from '@gravityforms/libraries';
import { IdProvider, useIdContext } from '@gravityforms/react-utils';
import { spacerClasses, sprintf } from '@gravityforms/utils';
import KanbanColumn from './KanbanColumn';
import KanbanCard from './KanbanCard';
import KanbanControls from './KanbanControls';
import KanbanDragLayer from './KanbanDragLayer';
import KanbanEmpty from './KanbanEmpty';
import itemTypes from './item-types';
import { getModules, applyColumnMove, applyCardMove } from './utils';
import Button from '../../elements/Button';
import Grid from '../../elements/Grid';
import Heading from '../../elements/Heading';

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

const LEFT = 'left';
const RIGHT = 'right';
const ABOVE = 'above';
const BELOW = 'below';
const GRID_COLUMN = 'gridColumn';
const GRID_LEFT = 'gridLeft';
const GRID_RIGHT = 'gridRight';
const GRID_CARD = 'gridCard';
const COLUMN_TOP = 'columnTop';
const COLUMN_BOTTOM = 'columnBottom';

const NEEDS_I18N_LABEL = 'Needs i18n';

const KanbanComponent = forwardRef( ( props, ref ) => {
	const id = useIdContext();
	const [ screenReaderText, setScreenReaderText ] = useState( '' );
	const gridRef = useRef( null );

	const columnToIndexRef = useRef( null );
	const [ columnHoveredIndexState, setColumnHoveredIndexState ] = useState( null );
	const [ columnHoveredPositionState, setColumnHoveredPositionState ] = useState( null );
	const [ columnHoveredTargetState, setColumnHoveredTargetState ] = useState( null );

	const cardToIndexRef = useRef( null );
	const [ cardToColumnState, setCardToColumnState ] = useState( null );
	const [ cardHoveredIndexState, setCardHoveredIndexState ] = useState( null );
	const [ cardHoveredPositionState, setCardHoveredPositionState ] = useState( null );
	const [ cardHoveredTargetState, setCardHoveredTargetState ] = useState( null );

	const {
		addColumnButtonAttributes = {},
		addColumnButtonClasses = [],
		afterHeading = null,
		customAttributes = {},
		customClasses = [],
		data = [],
		height = 0,
		i18n = {},
		isLoading = false,
		modules = [],
		onCardMove = () => {},
		onChange = () => {},
		onColumnEdit = () => {},
		onColumnMove = async () => {},
		screenReaderAttributes = {},
		screenReaderClasses = [],
		titleAttributes = {},
		titleClasses = [],
		spacing = '',
	} = props;
	const columnItemType = `${ itemTypes.KANBAN_COLUMN }_${ id }`;

	const { ActiveFilters } = getModules( modules, [ 'ActiveFilters' ] );

	/**
	 * @function moveColumn
	 * @description Moves a column from one index to another.
	 *
	 * @since 5.8.4
	 *
	 * @async
	 *
	 * @param {number} fromIndex The index of the column being moved.
	 * @param {number} toIndex   The index to move the column to.
	 * @param {string} itemId    The ID of the column being moved.
	 */
	const moveColumn = async ( fromIndex, toIndex, itemId ) => {
		const moveResult = applyColumnMove( data, fromIndex, toIndex );

		if ( ! moveResult ) {
			return;
		}

		const {
			updatedData,
			movedColumn,
			oldNextColumn,
			newNextColumn,
		} = moveResult;

		setScreenReaderText( i18n?.moveColumn ? sprintf( i18n.moveColumn, itemId, toIndex ) : NEEDS_I18N_LABEL );
		await onColumnMove(
			data,
			updatedData,
			{
				movedColumn,
				oldNextColumn,
				newNextColumn,
			},
		);
		onChange( updatedData );
	};

	/**
	 * @function moveCard
	 * @description Moves a card from one column to another.
	 *
	 * @since 5.8.4
	 *
	 * @param {string} fromId    The id of the column the card is being moved from.
	 * @param {string} toId      The id of the column the card is being moved to.
	 * @param {number} fromIndex The index of the card being moved.
	 * @param {number} toIndex   The index to move the card to.
	 * @param {string} itemId    The ID of the card being moved.
	 */
	const moveCard = ( fromId, toId, fromIndex, toIndex, itemId ) => {
		const moveResult = applyCardMove( data, fromId, toId, fromIndex, toIndex, id );

		if ( ! moveResult ) {
			return;
		}

		const {
			updatedData,
			movedCard,
			oldNextCard,
			newNextCard,
			fromColumn,
			toColumn,
		} = moveResult;

		setScreenReaderText( i18n?.moveCard ? sprintf( i18n.moveCard, itemId, toId, toIndex ) : NEEDS_I18N_LABEL );
		onCardMove(
			data,
			updatedData,
			{
				movedCard,
				oldNextCard,
				newNextCard,
				fromColumn,
				toColumn,
			},
		);
		onChange( updatedData );
	};

	/**
	 * @function endColumnDrag
	 * @description Resets the column drag state.
	 *
	 * @since 5.8.4
	 *
	 */
	const endColumnDrag = () => {
		columnToIndexRef.current = null;
		setColumnHoveredIndexState( null );
		setColumnHoveredPositionState( null );
		setColumnHoveredTargetState( null );
	};

	/**
	 * @function onColumnHover
	 * @description Handles the hover state for a column.
	 *
	 * @since 5.8.4
	 *
	 * @param {object} item     The item being dragged.
	 * @param {object} monitor  The drag monitor.
	 * @param {string} columnId The ID of the column being hovered over.
	 */
	const onColumnHover = ( item, monitor, columnId ) => {
		const clientOffset = monitor.getClientOffset();
		if ( ! clientOffset ) {
			return;
		}

		// Get all card elements.
		const column = gridRef.current?.querySelector( `#${ columnId }` );
		const cardElements = column?.querySelectorAll( '.gform-kanban__card' ) || [];
		let hoveredId = null;
		let hoveredIndex = null;
		let hoveredPosition = null;
		let hoveredTarget = null;

		// Find which card element the mouse is over.
		for ( let i = 0; i < cardElements.length; i++ ) {
			const element = cardElements[ i ];
			const rect = element.getBoundingClientRect();
			if ( i === 0 && clientOffset.y < rect.top ) {
				hoveredTarget = COLUMN_TOP;
				break;
			}
			if ( clientOffset.y >= rect.top && clientOffset.y <= rect.bottom ) {
				hoveredId = element.id;
				hoveredIndex = i;
				hoveredTarget = GRID_CARD;
				const hoveredMiddleY = ( rect.bottom - rect.top ) / 2;
				const hoverClientY = clientOffset.y - rect.top;
				// Determine which side of the hovered card we are over
				hoveredPosition = hoverClientY < hoveredMiddleY ? ABOVE : BELOW;
				break;
			}
			if ( i === cardElements.length - 1 && clientOffset.y > rect.bottom ) {
				hoveredTarget = COLUMN_BOTTOM;
				break;
			}
		}
		if ( cardElements.length === 0 ) {
			hoveredTarget = COLUMN_TOP;
		}

		// Get the to index based on the hovered position.
		let toIndex = hoveredIndex;
		if ( hoveredId !== null ) {
			// If the card is in a different column and the hovered position is below, add 1 to the index.
			// If the hovered position is above, keep the index as is.
			if ( item.columnId !== columnId && hoveredPosition === BELOW ) {
				toIndex = hoveredIndex + 1;
			} else if ( item.columnId === columnId && item.index !== hoveredIndex ) {
				// Compute the final insertion index relative to the original drag index
				if ( hoveredPosition === ABOVE ) {
					toIndex = item.index > hoveredIndex ? hoveredIndex : hoveredIndex - 1;
				} else {
					// below
					toIndex = item.index >= hoveredIndex ? hoveredIndex + 1 : hoveredIndex;
				}
			}
		} else if ( hoveredTarget !== GRID_CARD ) {
			// Not over card.
			if ( hoveredTarget === COLUMN_TOP ) {
				// If hovering over the top of the column, set to index to 0.
				toIndex = 0;
			} else if ( columnId !== item.columnId ) {
				toIndex = cardElements.length;
			} else {
				toIndex = cardElements.length - 1;
			}
		}

		cardToIndexRef.current = toIndex;
		setCardToColumnState( columnId );
		setCardHoveredIndexState( hoveredIndex );
		setCardHoveredPositionState( hoveredPosition );
		setCardHoveredTargetState( hoveredTarget );
	};

	/**
	 * @function onColumnDrop
	 * @description Handles the drop event for a column.
	 *
	 * @since 5.8.4
	 *
	 * @param {object} item The item being dropped.
	 */
	const onColumnDrop = ( item ) => {
		const fromIndex = item.index;
		const fromId = item.columnId;
		const toIndex = cardToIndexRef.current;
		const toId = cardToColumnState;
		if (
			( typeof fromIndex === 'number' && typeof toIndex === 'number' ) &&
			( ( fromId === toId && fromIndex !== toIndex ) || fromId !== toId )
		) {
			moveCard( fromId, toId, fromIndex, toIndex, item.id );
		}
	};

	/**
	 * @function endCardDrag
	 * @description Resets the card drag state.
	 *
	 * @since 5.8.4
	 *
	 */
	const endCardDrag = () => {
		cardToIndexRef.current = null;
		setCardToColumnState( null );
		setCardHoveredIndexState( null );
		setCardHoveredPositionState( null );
		setCardHoveredTargetState( null );
	};

	/**
	 * @function updateColumnLabel
	 * @description Updates the label properties of a column.
	 *
	 * @since 5.8.4
	 *
	 * @async
	 *
	 * @param {string} columnId      The ID of the column to update.
	 * @param {object} newLabelProps The new label properties to set.
	 */
	const updateColumnLabel = async ( columnId, newLabelProps ) => {
		let column;
		const updatedData = data.map( ( col ) => {
			if ( `${ id }-${ col.id }` === columnId ) {
				column = {
					...col,
					props: {
						...col.props,
						columnLabelAttributes: {
							...col.props.columnLabelAttributes,
							...newLabelProps,
						},
					},
				};
				return column;
			}
			return col;
		} );
		await onColumnEdit( data, updatedData, { column, newLabelProps } );
		onChange( updatedData );
	};

	const [ , drop ] = useDrop( {
		accept: columnItemType,
		hover: ( item, monitor ) => {
			const clientOffset = monitor.getClientOffset();
			if ( ! clientOffset ) {
				return;
			}

			// Get all column elements.
			const columnElements = gridRef.current?.querySelectorAll( '.gform-kanban__grid-item' );
			let hoveredId = null;
			let hoveredIndex = null;
			let hoveredPosition = null;
			let hoveredTarget = null;

			// Find which column element the mouse is over.
			for ( let i = 0; i < columnElements.length; i++ ) {
				const element = columnElements[ i ];
				const rect = element.getBoundingClientRect();
				if ( i === 0 && clientOffset.x < rect.left ) {
					hoveredTarget = GRID_LEFT;
					break;
				}
				if ( clientOffset.x >= rect.left && clientOffset.x <= rect.right ) {
					hoveredId = data[ i ]?.id;
					hoveredIndex = i;
					hoveredTarget = GRID_COLUMN;
					const hoveredMiddleX = ( rect.right - rect.left ) / 2;
					const hoverClientX = clientOffset.x - rect.left;
					// Determine which side of the hovered column we are over
					hoveredPosition = hoverClientX < hoveredMiddleX ? LEFT : RIGHT;
					break;
				}
				if ( i === columnElements.length - 1 && clientOffset.x > rect.right ) {
					hoveredTarget = GRID_RIGHT;
					break;
				}
			}

			// Get the to index based on the hovered position.
			let toIndex = hoveredIndex;
			if ( hoveredId !== null && item.index !== hoveredIndex ) {
				// Compute the final insertion index relative to the original drag index
				if ( hoveredPosition === LEFT ) {
					toIndex = item.index > hoveredIndex ? hoveredIndex : hoveredIndex - 1;
				} else {
					// right
					toIndex = item.index >= hoveredIndex ? hoveredIndex + 1 : hoveredIndex;
				}
			} else if ( hoveredTarget !== GRID_COLUMN ) {
				// Not over column.
				toIndex = hoveredTarget === GRID_RIGHT ? columnElements.length - 1 : 0;
			}

			columnToIndexRef.current = toIndex;
			setColumnHoveredIndexState( hoveredIndex );
			setColumnHoveredPositionState( hoveredPosition );
			setColumnHoveredTargetState( hoveredTarget );
		},
		drop: ( item ) => {
			const fromIndex = item.index;
			const toIndex = columnToIndexRef.current;
			if ( typeof fromIndex === 'number' && typeof toIndex === 'number' && fromIndex !== toIndex ) {
				moveColumn( fromIndex, toIndex, item.id );
			}
		},
	} );

	const componentProps = {
		...customAttributes,
		className: classnames( {
			'gform-kanban': true,
			...spacerClasses( spacing ),
		}, customClasses ),
		style: {
			...( customAttributes.style || {} ),
			height: height > 0 ? `${ height }px` : 'auto',
		},
	};

	const titleProps = {
		size: 'text-lg',
		tagName: 'h3',
		weight: 'medium',
		...titleAttributes,
		customClasses: classnames( [ 'gform-kanban__header-title' ], titleClasses ),
	};

	const screenReaderProps = {
		id: `${ id }-kanban-screen-reader-text`,
		className: classnames( [
			'gform-kanban__screen-reader-text',
			'gform-visually-hidden',
		], screenReaderClasses ),
		'aria-live': 'polite',
		...screenReaderAttributes,
	};

	const gridProps = {
		alignItems: 'stretch',
		columnSpacing: 3,
		container: true,
		customClasses: [ 'gform-kanban__grid' ],
		elementType: 'div',
		justifyContent: 'flex-start',
	};

	const gridItemProps = {
		customClasses: [ 'gform-kanban__grid-item' ],
		elementType: 'div',
		item: true,
	};

	const addColumnButtonProps = {
		label: i18n?.addColumn || NEEDS_I18N_LABEL,
		icon: 'plus-regular',
		iconPosition: 'leading',
		size: 'size-height-s',
		type: 'white',
		...addColumnButtonAttributes,
		customClasses: classnames( [ 'gform-kanban__add-column-button' ], addColumnButtonClasses ),
	};

	const dragLayerProps = {
		...props,
		kanbanId: id,
	};

	const setRefs = useCallback( ( node ) => {
		if ( ref ) {
			ref.current = node;
		}
		drop( node );
	}, [ ref, drop ] );

	return (
		<div { ...componentProps } ref={ setRefs }>
			{ ( i18n?.heading || afterHeading ) && (
				<header className="gform-kanban__header">
					<Heading { ...titleProps }>{ i18n?.heading || NEEDS_I18N_LABEL }</Heading>
					{ afterHeading }
				</header>
			) }
			<KanbanControls { ...props } />
			{ ActiveFilters && <ActiveFilters.ActiveFilters { ...props } /> }
			<div { ...screenReaderProps }>
				{ screenReaderText }
			</div>
			<div className="gform-kanban__grid-container">
				<div className="gform-kanban__grid-inner">
					<Grid { ...gridProps } ref={ gridRef }>
						{ data.map( ( column, index ) => {
							const columnId = `${ id }-${ column.id }`;
							const cardHoveredWhenEmpty = column.cards.length === 0 && cardToColumnState === columnId && cardHoveredTargetState === COLUMN_TOP;
							const hoveredLeft = ( index === columnHoveredIndexState && columnHoveredPositionState === LEFT ) ||
								( columnHoveredTargetState === GRID_LEFT && index === 0 );
							const hoveredRight = ( index === columnHoveredIndexState && columnHoveredPositionState === RIGHT ) ||
								( columnHoveredTargetState === GRID_RIGHT && index === data.length - 1 );
							const columnProps = {
								...column.props,
								cardHoveredWhenEmpty,
								columnData: column,
								hoveredLeft,
								hoveredRight,
								i18n,
								id: columnId,
								index,
								isFirstColumn: index === 0,
								isLastColumn: index === data.length - 1,
								isLoading,
								kanbanId: id,
								moveColumn,
								onDragEnd: endColumnDrag,
								onDrop: onColumnDrop,
								onHover: onColumnHover,
								updateColumnLabel,
							};
							return (
								<Grid key={ columnId } { ...gridItemProps }>
									<KanbanColumn { ...columnProps }>
										{ column.cards.map( ( card, cardIndex ) => {
											if ( isLoading ) {
												return <KanbanCard key={ card?.id || cardIndex } isLoading={ true } />;
											}

											const CardComponent = card.component || KanbanCard;
											const hoveredAbove = cardToColumnState === columnId &&
												(
													( cardIndex === cardHoveredIndexState && cardHoveredPositionState === ABOVE ) ||
													( cardHoveredTargetState === COLUMN_TOP && cardIndex === 0 )
												);
											const hoveredBelow = cardToColumnState === columnId &&
												(
													( cardIndex === cardHoveredIndexState && cardHoveredPositionState === BELOW ) ||
													( cardHoveredTargetState === COLUMN_BOTTOM && cardIndex === column.cards.length - 1 )
												);
											const cardProps = {
												...card.props,
												columns: data.map( ( col ) => ( {
													id: `${ id }-${ col.id }`,
													label: col?.props?.columnLabelAttributes?.label || '',
													count: col.cards.length,
												} ) ),
												columnId,
												hoveredAbove,
												hoveredBelow,
												i18n,
												index: cardIndex,
												isFirstCard: cardIndex === 0,
												isLastCard: cardIndex === column.cards.length - 1,
												kanbanId: id,
												moveCard,
												onDragEnd: endCardDrag,
											};
											return <CardComponent { ...cardProps } key={ card.props.id } />;
										} ) }
									</KanbanColumn>
								</Grid>
							);
						} ) }
						<Grid { ...gridItemProps }>
							<Button { ...addColumnButtonProps } />
						</Grid>
					</Grid>
					<KanbanDragLayer { ...dragLayerProps } />
					<KanbanEmpty { ...props } />
				</div>
			</div>
		</div>
	);
} );

/**
 * @module Kanban
 * @description A Kanban board component that allows for drag-and-drop reordering of columns and cards.
 *
 * @since 5.8.4
 *
 * @param {object}                     props                           The properties for the Kanban component.
 * @param {object}                     props.addColumnButtonAttributes Attributes for the Add Column button.
 * @param {string|Array|object}        props.addColumnButtonClasses    Classes for the Add Column button.
 * @param {JSX.Element|null}           props.afterControlsLeft         Element to render after the left controls content.
 * @param {JSX.Element|null}           props.afterHeading              Content to display after the heading.
 * @param {JSX.Element|null}           props.controlsLeft              Content to display on the left side of the controls.
 * @param {object}                     props.customAttributes          Custom attributes for the Kanban component.
 * @param {string|Array|object}        props.customClasses             Custom classes for the Kanban component.
 * @param {Array}                      props.data                      The data for the Kanban columns and cards.
 * @param {JSX.Element|null}           props.EmptyImage                The image to display when there are no cards.
 * @param {number}                     props.height                    The height of the Kanban component.
 * @param {string}                     props.id                        The ID of the Kanban component.
 * @param {object}                     props.i18n                      I18n strings for the Kanban component.
 * @param {boolean}                    props.isLoading                 Whether the Kanban is in a loading state.
 * @param {Function}                   props.onCardMove                Callback function to handle card movement.
 * @param {Function}                   props.onChange                  Callback function to handle changes to the Kanban data.
 * @param {Function}                   props.onColumnEdit              Callback function to handle column edits.
 * @param {Function}                   props.onColumnMove              Callback function to handle column movement.
 * @param {Array}                      props.modules                   The modules to include in the Kanban component.
 * @param {object}                     props.moduleAttributes          Attributes for the modules.
 * @param {object}                     props.moduleState               The state of the modules.
 * @param {object}                     props.screenReaderAttributes    Attributes for the screen reader text.
 * @param {string|Array|object}        props.screenReaderClasses       Classes for the screen reader text.
 * @param {Function}                   props.setIsLoading              Function to set the loading state of the Kanban.
 * @param {string|number|Array|object} props.spacing                   Spacing for the Kanban component.
 * @param {object}                     props.titleAttributes           Attributes for the title.
 * @param {string|Array|object}        props.titleClasses              Classes for the title.
 * @param {Function}                   props.updateModuleState         Function to update the module state.
 * @param {boolean}                    props.useAjax                   Whether to use AJAX for data fetching.
 * @param {object}                     ref                             The ref to the Kanban component.
 *
 * @return {JSX.Element} The Kanban component.
 */
const Kanban = forwardRef( ( props, ref ) => {
	const defaultProps = {
		addColumnButtonAttributes: {},
		addColumnButtonClasses: [],
		afterControlsLeft: null,
		afterHeading: null,
		controlsLeft: null,
		customAttributes: {},
		customClasses: [],
		data: [],
		EmptyImage: null,
		height: 0,
		id: '',
		i18n: {},
		isLoading: false,
		modules: [],
		moduleAttributes: {},
		moduleState: {},
		onCardMove: () => {},
		onChange: () => {},
		onColumnEdit: () => {},
		onColumnMove: async () => {},
		screenReaderAttributes: {},
		screenReaderClasses: [],
		setIsLoading: () => {},
		spacing: '',
		titleAttributes: {},
		titleClasses: [],
		updateModuleState: () => {},
		useAjax: false,
	};
	const combinedProps = { ...defaultProps, ...props };
	const { id: idProp } = combinedProps;
	const idProviderProps = { id: idProp };

	return (
		<IdProvider { ...idProviderProps }>
			<KanbanComponent { ...combinedProps } ref={ ref } />
		</IdProvider>
	);
} );

Kanban.propTypes = {
	addColumnButtonAttributes: PropTypes.object,
	addColumnButtonClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	afterControlsLeft: PropTypes.element,
	afterHeading: PropTypes.element,
	controlsLeft: PropTypes.element,
	customAttributes: PropTypes.object,
	customClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	data: PropTypes.arrayOf( PropTypes.object ),
	EmptyImage: PropTypes.oneOfType( [
		PropTypes.element,
		PropTypes.func,
		PropTypes.object,
	] ),
	height: PropTypes.number,
	id: PropTypes.string,
	i18n: PropTypes.object,
	isLoading: PropTypes.bool,
	modules: PropTypes.array,
	moduleAttributes: PropTypes.object,
	moduleState: PropTypes.object,
	onCardMove: PropTypes.func,
	onChange: PropTypes.func,
	onColumnEdit: PropTypes.func,
	onColumnMove: PropTypes.func,
	screenReaderAttributes: PropTypes.object,
	screenReaderClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	setIsLoading: PropTypes.func,
	spacing: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.number,
		PropTypes.array,
		PropTypes.object,
	] ),
	titleAttributes: PropTypes.object,
	titleClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	updateModuleState: PropTypes.func,
	useAjax: PropTypes.bool,
};

Kanban.displayName = 'Kanban';

export default Kanban;
