/**
 * WordPress dependencies
 */
import { createHigherOrderComponent } from '@wordpress/compose';
import { forwardRef, memo } from '@wordpress/element';
import type { ForwardedRef } from 'react';

/**
 * Internal dependencies
 */
import useSelect from '../use-select';
import type { SelectFunction, DataRegistry } from '../../types';

/**
 * Higher-order component used to inject state-derived props using registered
 * selectors.
 *
 * @param mapSelectToProps Function called on every state change,
 *                         expected to return object of props to
 *                         merge with the component's own props.
 *
 * @example
 * ```js
 * import { withSelect } from '@wordpress/data';
 * import { store as myCustomStore } from 'my-custom-store';
 *
 * function PriceDisplay( { price, currency } ) {
 * 	return new Intl.NumberFormat( 'en-US', {
 * 		style: 'currency',
 * 		currency,
 * 	} ).format( price );
 * }
 *
 * const HammerPriceDisplay = withSelect( ( select, ownProps ) => {
 * 	const { getPrice } = select( myCustomStore );
 * 	const { currency } = ownProps;
 *
 * 	return {
 * 		price: getPrice( 'hammer', currency ),
 * 	};
 * } )( PriceDisplay );
 *
 * // Rendered in the application:
 * //
 * //  <HammerPriceDisplay currency="USD" />
 * ```
 * In the above example, when `HammerPriceDisplay` is rendered into an
 * application, it will pass the price into the underlying `PriceDisplay`
 * component and update automatically if the price of a hammer ever changes in
 * the store.
 *
 * @return Enhanced component with merged state data props.
 */
const withSelect = (
	mapSelectToProps: (
		select: SelectFunction,
		ownProps: Record< string, unknown >,
		registry: DataRegistry
	) => Record< string, unknown >
) =>
	createHigherOrderComponent(
		( WrappedComponent ) =>
			memo(
				forwardRef( function WithSelect(
					ownProps: Record< string, unknown >,
					ref: ForwardedRef< unknown >
				) {
					const mapSelect = (
						select: SelectFunction,
						registry: DataRegistry
					) => mapSelectToProps( select, ownProps, registry );
					const mergeProps = useSelect( mapSelect );
					return (
						<WrappedComponent
							ref={ ref }
							{ ...ownProps }
							{ ...mergeProps }
						/>
					);
				} )
			),
		'withSelect'
	);

export default withSelect;
