/**
 * External dependencies
 */
import {
	useLayoutEffect,
	useRef,
	useCallback,
	useMemo,
} from '@wordpress/element';
import { useSelect, useDispatch } from '@wordpress/data';
import {
	createBlock,
	getBlockType,
	Block,
	AttributeSource,
	createBlocksFromInnerBlocksTemplate,
	TemplateArray,
} from '@wordpress/blocks';
import { isEqual } from 'lodash';

const isBlockLocked = ( {
	attributes,
}: {
	attributes: Record< string, AttributeSource.Attribute >;
} ) => Boolean( attributes.lock?.remove || attributes.lock?.default?.remove );

/**
 * useForcedLayout hook
 *
 * Responsible for ensuring FORCED blocks exist in the inner block layout. Forced blocks cannot be removed.
 */
export const useForcedLayout = ( {
	clientId,
	registeredBlocks,
	defaultTemplate = [],
}: {
	// Client ID of the parent block.
	clientId: string;
	// An array of registered blocks that may be forced in this particular layout.
	registeredBlocks: Array< string >;
	// The default template for the inner blocks in this layout.
	defaultTemplate: TemplateArray;
} ): void => {
	const currentRegisteredBlocks = useRef( registeredBlocks );
	const currentDefaultTemplate = useRef( defaultTemplate );

	const { insertBlock, replaceInnerBlocks } = useDispatch(
		'core/block-editor'
	);

	const { innerBlocks, registeredBlockTypes } = useSelect(
		( select ) => {
			return {
				innerBlocks: select( 'core/block-editor' ).getBlocks(
					clientId
				),
				registeredBlockTypes: currentRegisteredBlocks.current.map(
					( blockName ) => getBlockType( blockName )
				),
			};
		},
		[ clientId, currentRegisteredBlocks.current ]
	);

	const appendBlock = useCallback(
		( block, position ) => {
			const newBlock = createBlock( block.name );
			insertBlock( newBlock, position, clientId, false );
		},
		[ clientId, insertBlock ]
	);

	const lockedBlockTypes = useMemo(
		() =>
			registeredBlockTypes.filter(
				( block: Block | undefined ) => block && isBlockLocked( block )
			),
		[ registeredBlockTypes ]
	) as Block[];

	/**
	 * If the current inner blocks differ from the registered blocks, push the differences.
	 */
	useLayoutEffect( () => {
		if ( ! clientId ) {
			return;
		}

		// If there are NO inner blocks, sync with the given template.
		if (
			innerBlocks.length === 0 &&
			currentDefaultTemplate.current.length > 0
		) {
			const nextBlocks = createBlocksFromInnerBlocksTemplate(
				currentDefaultTemplate.current
			);
			if ( ! isEqual( nextBlocks, innerBlocks ) ) {
				replaceInnerBlocks( clientId, nextBlocks );
				return;
			}
		}

		// Find registered locked blocks missing from Inner Blocks and append them.
		lockedBlockTypes.forEach( ( block ) => {
			// If the locked block type is already in the layout, we can skip this one.
			if (
				innerBlocks.find(
					( { name }: { name: string } ) => name === block.name
				)
			) {
				return;
			}

			// Is the forced block part of the default template, find it's original position.
			const defaultTemplatePosition = currentDefaultTemplate.current.findIndex(
				( [ blockName ] ) => blockName === block.name
			);

			switch ( defaultTemplatePosition ) {
				case -1:
					// The block is not part of the default template so we append it to the current layout.
					appendBlock( block, innerBlocks.length );
					break;
				case 0:
					// The block was the first block in the default layout, so prepend it to the current layout.
					appendBlock( block, 0 );
					break;
				default:
					// The new layout may have extra blocks compared to the default template, so rather than insert
					// at the default position, we should append it after another default block.
					const adjacentBlock =
						currentDefaultTemplate.current[
							defaultTemplatePosition - 1
						];
					const position = innerBlocks.findIndex(
						( { name: blockName } ) =>
							blockName === adjacentBlock[ 0 ]
					);
					appendBlock(
						block,
						position === -1 ? defaultTemplatePosition : position + 1
					);
					break;
			}
		} );
	}, [
		clientId,
		innerBlocks,
		lockedBlockTypes,
		replaceInnerBlocks,
		appendBlock,
	] );
};
