/**
 * Internal dependencies
 */
import { DEPRECATED_ENTRY_KEYS } from '../constants';
import { validateBlock } from '../validation';
import { getBlockAttributes } from './get-block-attributes';
import { applyBuiltInValidationFixes } from './apply-built-in-validation-fixes';
import { omit } from '../utils';
import type { Block, BlockDeprecation, BlockType, RawBlock } from '../../types';

/**
 * Function that takes no arguments and always returns false.
 *
 * @return Always returns false.
 */
function stubFalse(): boolean {
	return false;
}

/**
 * Given a block object, returns a new copy of the block with any applicable
 * deprecated migrations applied, or the original block if it was both valid
 * and no eligible migrations exist.
 *
 * @param block     Parsed and invalid block object.
 * @param rawBlock  Raw block object.
 * @param blockType Block type. This is normalize not necessary and
 *                  can be inferred from the block name,
 *                  but it's here for performance reasons.
 *
 * @return Migrated block object.
 */
export function applyBlockDeprecatedVersions(
	block: Block,
	rawBlock: RawBlock,
	blockType: BlockType
): Block {
	const parsedAttributes = rawBlock.attrs ?? {};
	const { deprecated: deprecatedDefinitions } = blockType;
	// Bail early if there are no registered deprecations to be handled.
	if ( ! deprecatedDefinitions || ! deprecatedDefinitions.length ) {
		return block;
	}

	// By design, blocks lack any sort of version tracking. Instead, to process
	// outdated content the system operates a queue out of all the defined
	// attribute shapes and tries each definition until the input produces a
	// valid result. This mechanism seeks to avoid polluting the user-space with
	// machine-specific code. An invalid block is thus a block that could not be
	// matched successfully with any of the registered deprecation definitions.
	for ( let i = 0; i < deprecatedDefinitions.length; i++ ) {
		// A block can opt into a migration even if the block is valid by
		// defining `isEligible` on its deprecation. If the block is both valid
		// and does not opt to migrate, skip.
		const { isEligible = stubFalse } = deprecatedDefinitions[ i ];
		if (
			block.isValid &&
			! ( isEligible as Function )( parsedAttributes, block.innerBlocks, {
				blockNode: rawBlock,
				block,
			} )
		) {
			continue;
		}

		// Block type properties which could impact either serialization or
		// parsing are not considered in the deprecated block type by default,
		// and must be explicitly provided.
		const deprecatedBlockType = Object.assign(
			omit(
				blockType as unknown as Record< string, unknown >,
				DEPRECATED_ENTRY_KEYS
			),
			deprecatedDefinitions[ i ]
		) as unknown as BlockType;

		let migratedBlock = {
			...block,
			attributes: getBlockAttributes(
				deprecatedBlockType,
				block.originalContent ?? '',
				parsedAttributes
			),
		};

		// Ignore the deprecation if it produces a block which is not valid.
		let [ isValid ] = validateBlock( migratedBlock, deprecatedBlockType );

		// If the migrated block is not valid initially, try the built-in fixes.
		if ( ! isValid ) {
			migratedBlock = applyBuiltInValidationFixes(
				migratedBlock,
				deprecatedBlockType
			);
			[ isValid ] = validateBlock( migratedBlock, deprecatedBlockType );
		}

		// An invalid block does not imply incorrect HTML but the fact block
		// source information could be lost on re-serialization.
		if ( ! isValid ) {
			continue;
		}

		let migratedInnerBlocks = migratedBlock.innerBlocks;
		let migratedAttributes = migratedBlock.attributes;

		// A block may provide custom behavior to assign new attributes and/or
		// inner blocks.
		const { migrate } = deprecatedBlockType as unknown as BlockDeprecation;
		if ( migrate ) {
			let migrated = migrate( migratedAttributes, block.innerBlocks );
			if ( ! Array.isArray( migrated ) ) {
				migrated = [ migrated ] as unknown as [
					Record< string, unknown >,
					Block[],
				];
			}

			[
				migratedAttributes = parsedAttributes,
				migratedInnerBlocks = block.innerBlocks,
			] = migrated;
		}

		block = {
			...block,
			attributes: migratedAttributes,
			innerBlocks: migratedInnerBlocks,
			isValid: true,
			validationIssues: [],
		};
	}

	return block;
}
