/**
 * WordPress dependencies
 */
import { renderToString } from '@wordpress/element';

/**
 * Internal dependencies
 */
import { convertLegacyBlockNameAndAttributes } from './parser/convert-legacy-block';
import { createBlock } from './factory';
import { getBlockType } from './registration';
import type { Block, BlockAttribute } from '../types';

type TemplateItem = [ string, Record< string, unknown >?, TemplateItem[]? ];

/**
 * Checks whether a list of blocks matches a template by comparing the block names.
 *
 * @param blocks   Block list.
 * @param template Block template.
 *
 * @return Whether the list of blocks matches a templates.
 */
export function doBlocksMatchTemplate(
	blocks: Block[] = [],
	template: TemplateItem[] = []
): boolean {
	return (
		blocks.length === template.length &&
		template.every( ( [ name, , innerBlocksTemplate ], index ) => {
			const block = blocks[ index ];
			return (
				name === block.name &&
				doBlocksMatchTemplate( block.innerBlocks, innerBlocksTemplate )
			);
		} )
	);
}

const isHTMLAttribute = (
	attributeDefinition: BlockAttribute | undefined
): boolean => attributeDefinition?.source === 'html';

const isQueryAttribute = (
	attributeDefinition: BlockAttribute | undefined
): boolean => attributeDefinition?.source === 'query';

function normalizeAttributes(
	schema: Record< string, BlockAttribute >,
	values: Record< string, unknown > | undefined
): Record< string, unknown > {
	if ( ! values ) {
		return {};
	}

	return Object.fromEntries(
		Object.entries( values ).map( ( [ key, value ] ) => [
			key,
			normalizeAttribute( schema[ key ], value ),
		] )
	);
}

function normalizeAttribute(
	definition: BlockAttribute | undefined,
	value: unknown
): unknown {
	if ( isHTMLAttribute( definition ) && Array.isArray( value ) ) {
		// Introduce a deprecated call at this point
		// When we're confident that "children" format should be removed from the templates.

		return renderToString( value );
	}

	if ( isQueryAttribute( definition ) && value ) {
		return ( value as Record< string, unknown >[] ).map(
			( subValues: Record< string, unknown > ) => {
				return normalizeAttributes( definition!.query!, subValues );
			}
		);
	}

	return value;
}

/**
 * Synchronize a block list with a block template.
 *
 * Synchronizing a block list with a block template means that we loop over the blocks
 * keep the block as is if it matches the block at the same position in the template
 * (If it has the same name) and if doesn't match, we create a new block based on the template.
 * Extra blocks not present in the template are removed.
 *
 * @param blocks   Block list.
 * @param template Block template.
 *
 * @return Updated Block list.
 */
export function synchronizeBlocksWithTemplate(
	blocks: Block[] = [],
	template?: TemplateItem[]
): Block[] {
	// If no template is provided, return blocks unmodified.
	if ( ! template ) {
		return blocks;
	}

	return template.map(
		( [ name, attributes, innerBlocksTemplate ], index ) => {
			const block = blocks[ index ];

			if ( block && block.name === name ) {
				const innerBlocks = synchronizeBlocksWithTemplate(
					block.innerBlocks,
					innerBlocksTemplate
				);
				return { ...block, innerBlocks };
			}

			// To support old templates that were using the "children" format
			// for the attributes using "html" strings now, we normalize the template attributes
			// before creating the blocks.

			const blockType = getBlockType( name );

			const normalizedAttributes = normalizeAttributes(
				blockType?.attributes ?? {},
				attributes
			);

			const [ blockName, blockAttributes ] =
				convertLegacyBlockNameAndAttributes(
					name,
					normalizedAttributes
				);

			return createBlock(
				blockName!,
				blockAttributes,
				synchronizeBlocksWithTemplate( [], innerBlocksTemplate )
			);
		}
	);
}
