<?php

// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;

// Register store.
acf_register_store( 'block-types' );
acf_register_store( 'block-cache' );
		
/**
 * acf_register_block_type
 *
 * Registers a block type.
 *
 * @date	18/2/19
 * @since	5.8.0
 *
 * @param	array $block The block settings.
 * @return	(array|false)
 */
function acf_register_block_type( $block ) {
	
	// Validate block type settings.
	$block = acf_validate_block_type( $block );
	
	/**
	 * Filters the arguments for registering a block type.
	 *
	 * @since	5.8.9
	 *
	 * @param	array $block The array of arguments for registering a block type.
	 */
    $block = apply_filters( 'acf/register_block_type_args', $block );
    
    // Require name.
    if( !$block['name'] ) {
	    $message = __( 'Block type name is required.', 'acf' );
	    _doing_it_wrong( __FUNCTION__, $message, '5.8.0' );
		return false;
    }
    
	// Bail early if already exists.
	if( acf_has_block_type($block['name']) ) {
		$message = sprintf( __( 'Block type "%s" is already registered.' ), $block['name'] );
		_doing_it_wrong( __FUNCTION__, $message, '5.8.0' );
		return false;
	}
	
	// Add to storage.
	acf_get_store( 'block-types' )->set( $block['name'], $block );
	
	// Register block type in WP.
	if( function_exists('register_block_type') ) {
		register_block_type($block['name'], array(
			'attributes'		=> acf_get_block_type_default_attributes( $block ),
			'render_callback'	=> 'acf_render_block_callback',
		));
	}
	
	// Register action.
	add_action( 'enqueue_block_editor_assets', 'acf_enqueue_block_assets' );
	
	// Return block.
	return $block;
}

/**
 * acf_register_block
 *
 * See acf_register_block_type().
 *
 * @date	18/2/19
 * @since	5.7.12
 *
 * @param	array $block The block settings.
 * @return	(array|false)
 */
function acf_register_block( $block ) {
	return acf_register_block_type( $block );
}

/**
 * acf_has_block_type
 *
 * Returns true if a block type exists for the given name.
 *
 * @date	18/2/19
 * @since	5.7.12
 *
 * @param	string $name The block type name.
 * @return	bool
 */
function acf_has_block_type( $name ) {
	return acf_get_store( 'block-types' )->has( $name );
}

/**
 * acf_get_block_types
 *
 * Returns an array of all registered block types.
 *
 * @date	18/2/19
 * @since	5.7.12
 *
 * @param	void
 * @return	array
 */
function acf_get_block_types() {
	return acf_get_store( 'block-types' )->get();
}

/**
 * acf_get_block_types
 *
 * Returns a block type for the given name.
 *
 * @date	18/2/19
 * @since	5.7.12
 *
 * @param	string $name The block type name.
 * @return	(array|null)
 */
function acf_get_block_type( $name ) {
	return acf_get_store( 'block-types' )->get( $name );
}

/**
 * acf_remove_block_type
 *
 * Removes a block type for the given name.
 *
 * @date	18/2/19
 * @since	5.7.12
 *
 * @param	string $name The block type name.
 * @return	void
 */
function acf_remove_block_type( $name ) {
	acf_get_store( 'block-types' )->remove( $name );
}

/**
 * acf_get_block_type_default_attributes
 *
 * Returns an array of default attribute settings for a block type.
 *
 * @date	19/11/18
 * @since	5.8.0
 *
 * @param	void
 * @return	array
 */
function acf_get_block_type_default_attributes( $block_type ) {
	$attributes = array(
		'id'		=> array(
			'type'		=> 'string',
			'default'	=> '',
		),
		'name'		=> array(
			'type'		=> 'string',
			'default'	=> '',
		),
		'data'		=> array(
			'type'		=> 'object',
			'default'	=> array(),
		),
		'align'		=> array(
			'type'		=> 'string',
			'default'	=> '',
		),
		'mode'		=> array(
			'type'		=> 'string',
			'default'	=> '',
		)
	);
	if( !empty( $block_type['supports']['align_text'] ) ) {
		$attributes['align_text'] = array(
			'type'		=> 'string',
			'default'	=> '',
		);
	}
	if( !empty( $block_type['supports']['align_content'] ) ) {
		$attributes['align_content'] = array(
			'type'		=> 'string',
			'default'	=> '',
		);
	}
	return $attributes;
}

/**
 * acf_validate_block_type
 *
 * Validates a block type ensuring all settings exist.
 *
 * @date	10/4/18
 * @since	5.8.0
 *
 * @param	array $block The block settings.
 * @return	array
 */
function acf_validate_block_type( $block ) {
	
	// Add default settings.
	$block = wp_parse_args($block, array(
		'name'				=> '',
		'title'				=> '',
		'description'		=> '',
		'category'			=> 'common',
		'icon'				=> '',
		'mode'				=> 'preview',
		'align'				=> '',
		'keywords'			=> array(),
		'supports'			=> array(),
		'post_types'		=> array(),
		'render_template'	=> false,
		'render_callback'	=> false,
		'enqueue_style'		=> false,
		'enqueue_script'	=> false,
		'enqueue_assets'	=> false,
	));
	
	// Restrict keywords to 3 max to avoid JS error in older versions.
	if( acf_version_compare('wp', '<', '5.2') ) {
		$block['keywords'] = array_slice($block['keywords'], 0, 3);
	}
	
	// Generate name with prefix.
	if( $block['name'] ) {
		$block['name'] = 'acf/' . acf_slugify($block['name']);
	}
	
	// Add default 'supports' settings.
	$block['supports'] = wp_parse_args($block['supports'], array(
		'align'		=> true,
		'html'		=> false,
		'mode'		=> true,
	));

	// Correct "Experimental" flags.
	if( isset($block['supports']['__experimental_jsx']) ) {
		$block['supports']['jsx'] = $block['supports']['__experimental_jsx'];
	}
	
	// Return block.
	return $block;
}

/**
 * acf_prepare_block
 *
 * Prepares a block for use in render_callback by merging in all settings and attributes.
 *
 * @date	19/11/18
 * @since	5.8.0
 *
 * @param	array $block The block props.
 * @return	array
 */
function acf_prepare_block( $block ) {
	
	// Bail early if no name.
	if( !isset($block['name']) ) {
		return false;
	}
	
	// Get block type and return false if doesn't exist.
	$block_type = acf_get_block_type( $block['name'] );
	if( !$block_type ) {
		return false;
	}
	
	// Generate default attributes.
	$attributes = array();
	foreach( acf_get_block_type_default_attributes($block_type) as $k => $v ) {
		$attributes[ $k ] = $v['default'];
	}
	
	// Merge together arrays in order of least to most specific.
	$block = array_merge($block_type, $attributes, $block);
	
	// Return block.
	return $block;
}

/**
 * The render callback for all ACF blocks.
 *
 * @date	28/10/20
 * @since	5.9.2
 *
 * @param	array  $attributes The block attributes.
 * @param	string $content The block content.
 * @param	WP_Block $wp_block The block instance (since WP 5.5).
 * @return	string The block HTML.
 */
function acf_render_block_callback( $attributes, $content = '', $wp_block = null ) {
	$is_preview = false;
	$post_id = get_the_ID();
	
	// Set preview flag to true when rendering for the block editor.
	if( is_admin() && acf_is_block_editor() ) {
		$is_preview = true;
	}
	
	// Return rendered block HTML.
	return acf_rendered_block( $attributes, $content, $is_preview, $post_id, $wp_block );
}

/**
 * Returns the rendered block HTML.
 *
 * @date	28/2/19
 * @since	5.7.13
 *
 * @param	array  $attributes The block attributes.
 * @param	string $content The block content.
 * @param	bool $is_preview Whether or not the block is being rendered for editing preview.
 * @param	int $post_id The current post being edited or viewed.
 * @param	WP_Block $wp_block The block instance (since WP 5.5).
 * @return	string The block HTML.
 */
function acf_rendered_block( $attributes, $content = '', $is_preview = false, $post_id = 0, $wp_block = null ) {
	
	// Capture block render output.
	ob_start();
	acf_render_block( $attributes, $content, $is_preview, $post_id, $wp_block );
	$html = ob_get_clean();

	// Replace <InnerBlocks /> placeholder on front-end.
	if( !$is_preview ) {
		// Escape "$" character to avoid "capture group" interpretation.
		$content = str_replace( '$', '\$', $content );
		$html = preg_replace( '/<InnerBlocks([\S\s]*?)\/>/', $content, $html );
	}
	
	// Store in cache for preloading.
	acf_get_store( 'block-cache' )->set( $attributes['id'], $html );
	return $html;
}

/**
 * Renders the block HTML.
 *
 * @date	19/2/19
 * @since	5.7.12
 *
 * @param	array  $attributes The block attributes.
 * @param	string $content The block content.
 * @param	bool $is_preview Whether or not the block is being rendered for editing preview.
 * @param	int $post_id The current post being edited or viewed.
 * @param	WP_Block $wp_block The block instance (since WP 5.5).
 * @return	void
 */
function acf_render_block( $attributes, $content = '', $is_preview = false, $post_id = 0, $wp_block = null ) {
	
	// Prepare block ensuring all settings and attributes exist.
	$block = acf_prepare_block( $attributes );
	if( !$block ) {
		return '';
	}
	
	// Find post_id if not defined.
	if( !$post_id ) {
		$post_id = get_the_ID();
	}
	
	// Enqueue block type assets.
	acf_enqueue_block_type_assets( $block );
	
	// Setup postdata allowing get_field() to work.
	acf_setup_meta( $block['data'], $block['id'], true );
	
	// Call render_callback.
	if( is_callable( $block['render_callback'] ) ) {
		call_user_func( $block['render_callback'], $block, $content, $is_preview, $post_id, $wp_block );
	
	// Or include template.
	} elseif( $block['render_template'] ) {
		
		// Locate template.
		if( file_exists($block['render_template']) ) {
			$path = $block['render_template'];
	    } else {
		    $path = locate_template( $block['render_template'] );
	    }
	    
	    // Include template.
	    if( file_exists($path) ) {
		    include( $path );
	    }
	}
	
	// Reset postdata.
	acf_reset_meta( $block['id'] );
}

/**
 * acf_get_block_fields
 *
 * Returns an array of all fields for the given block.
 *
 * @date	24/10/18
 * @since	5.8.0
 *
 * @param	array $block The block props.
 * @return	array
 */
function acf_get_block_fields( $block ) {
	
	// Vars.
	$fields = array();
	
	// Get field groups for this block.
	$field_groups = acf_get_field_groups( array(
		'block'	=> $block['name']
	));
			
	// Loop over results and append fields.
	if( $field_groups ) {
	foreach( $field_groups as $field_group ) {
		$fields = array_merge( $fields, acf_get_fields( $field_group ) );
	}}
	
	// Return fields.
	return $fields;
}

/**
 * acf_enqueue_block_assets
 *
 * Enqueues and localizes block scripts and styles.
 *
 * @date	28/2/19
 * @since	5.7.13
 *
 * @param	void
 * @return	void
 */
function acf_enqueue_block_assets() {
	
	// Localize text.
	acf_localize_text(array(
		'Switch to Edit'		=> __('Switch to Edit', 'acf'),
		'Switch to Preview'		=> __('Switch to Preview', 'acf'),
		'Change content alignment'	=> __('Change content alignment', 'acf'),

		/* translators: %s: Block type title */
		'%s settings'			=> __('%s settings', 'acf'),
	));
	
	// Get block types.
	$block_types = acf_get_block_types();
	
	// Localize data.
	acf_localize_data(array(
		'blockTypes'	=> array_values( $block_types ),
		
		// List of attributes to replace for HTML to JSX compatibility.
		// https://github.com/facebook/react/blob/master/packages/react-dom/src/shared/possibleStandardNames.js
		'jsxAttributes'	=> array(
			'cellpadding'	=> 'cellPadding',
			'cellspacing'	=> 'cellSpacing',
			'class'			=> 'className',
			'colspan'		=> 'colSpan',
			'datetime'		=> 'dateTime',
			'for'			=> 'htmlFor',
			'hreflang'		=> 'hrefLang',
			'readonly'		=> 'readOnly',
			'rowspan'		=> 'rowSpan',
			'srclang'		=> 'srcLang',
			'srcset'		=> 'srcSet',
			'allowedblocks'	=> 'allowedBlocks',
			'templatelock'	=> 'templateLock'
		),
		'postType'	=> get_post_type()
	));
	
	// Enqueue script.
	wp_enqueue_script( 'acf-blocks', acf_get_url("pro/assets/js/acf-pro-blocks.min.js"), array('acf-input', 'wp-blocks'), ACF_VERSION, true );
	
	// Enqueue block assets.
	array_map( 'acf_enqueue_block_type_assets', $block_types );
	
	// During the edit screen loading, WordPress renders all blocks in its own attempt to preload data.
	// Retrieve any cached block HTML and include this in the localized data.
	if( defined('ACF_EXPERIMENTAL_PRELOAD_BLOCKS') && ACF_EXPERIMENTAL_PRELOAD_BLOCKS ) {
		$preloaded_blocks = acf_get_store( 'block-cache' )->get_data();
		acf_localize_data(array(
			'preloadedBlocks' => $preloaded_blocks
		));
	}
}

/**
 * acf_enqueue_block_type_assets
 *
 * Enqueues scripts and styles for a specific block type.
 *
 * @date	28/2/19
 * @since	5.7.13
 *
 * @param	array $block_type The block type settings.
 * @return	void
 */
function acf_enqueue_block_type_assets( $block_type ) {
	
	// Generate handle from name.
	$handle = 'block-' . acf_slugify($block_type['name']);
	
	// Enqueue style.
	if( $block_type['enqueue_style'] ) {
		wp_enqueue_style( $handle, $block_type['enqueue_style'], array(), false, 'all' );
	}
	
	// Enqueue script.
	if( $block_type['enqueue_script'] ) {
		wp_enqueue_script( $handle, $block_type['enqueue_script'], array(), false, true );
	}
	
	// Enqueue assets callback.
	if( $block_type['enqueue_assets'] && is_callable($block_type['enqueue_assets']) ) {
		call_user_func( $block_type['enqueue_assets'], $block_type );
	}
}

/**
 * acf_ajax_fetch_block
 *
 * Handles the ajax request for block data.
 *
 * @date	28/2/19
 * @since	5.7.13
 *
 * @param	void
 * @return	void
 */
function acf_ajax_fetch_block() {
	
	// Validate ajax request.
	if( !acf_verify_ajax() ) {
		 wp_send_json_error();
	}
	
	// Get request args.
	extract(acf_request_args(array(
		'block'		=> false,
		'post_id'	=> 0,
		'query'		=> array(),
	)));
	
	// Bail ealry if no block.
	if( !$block ) {
		wp_send_json_error();
	}
	
	// Unslash and decode $_POST data.
	$block = wp_unslash($block);
	$block = json_decode($block, true);
	
	// Prepare block ensuring all settings and attributes exist.
	if( !$block = acf_prepare_block( $block ) ) {
		wp_send_json_error();
	}
	
	// Load field defaults when first previewing a block.
	if( !empty($query['preview']) && !$block['data'] ) {
		$fields = acf_get_block_fields( $block );
		foreach( $fields as $field ) {
		   	$block['data'][ "_{$field['name']}" ] = $field['key'];
   		}
	}
	
	// Setup postdata allowing form to load meta.
	acf_setup_meta( $block['data'], $block['id'], true );
   	
	// Vars.
	$response = array();
	
	// Query form.
	if( !empty($query['form']) ) {
		
		// Load fields for form.
		$fields = acf_get_block_fields( $block );
		
		// Prefix field inputs to avoid multiple blocks using the same name/id attributes.
		acf_prefix_fields( $fields, "acf-{$block['id']}" );
		
		// Start Capture.
		ob_start();
		
		// Render.
		echo '<div class="acf-block-fields acf-fields">';
   			acf_render_fields( $fields, $block['id'], 'div', 'field' );
		echo '</div>';
		
		// Store Capture.
		$response['form'] = ob_get_contents();
		ob_end_clean();
	}
	
	// Query preview.
	if( !empty($query['preview']) ) {
		
		// Render_callback vars.
   		$content = '';
   		$is_preview = true;
   		
		// Render.
		$html = '';
		$html .= '<div class="acf-block-preview">';
   		$html .= 	acf_rendered_block( $block, $content, $is_preview, $post_id );
		$html .= '</div>';
		
		// Store HTML.
		$response['preview'] = $html;
	}
	
	// Send repsonse.
	wp_send_json_success( $response );
}

// Register ajax action.
acf_register_ajax( 'fetch-block', 'acf_ajax_fetch_block' );

/**
 * acf_parse_save_blocks
 *
 * Parse content that may contain HTML block comments and saves ACF block meta.
 *
 * @date	27/2/19
 * @since	5.7.13
 *
 * @param	string $text Content that may contain HTML block comments.
 * @return	string
 */
function acf_parse_save_blocks( $text = '' ) {
	
	// Search text for dynamic blocks and modify attrs.
	return addslashes(
		preg_replace_callback(
			'/<!--\s+wp:(?P<name>[\S]+)\s+(?P<attrs>{[\S\s]+?})\s+(?P<void>\/)?-->/',
			'acf_parse_save_blocks_callback',
			stripslashes( $text )
		)
	);
}

// Hook into saving process.
add_filter( 'content_save_pre', 'acf_parse_save_blocks', 5, 1 );

/**
 * acf_parse_save_blocks_callback
 *
 * Callback used in preg_replace to modify ACF Block comment.
 *
 * @date	1/3/19
 * @since	5.7.13
 *
 * @param	array $matches The preg matches.
 * @return	string
 */
function acf_parse_save_blocks_callback( $matches ) {
	
	// Defaults
	$name = isset($matches['name']) ? $matches['name'] : '';
	$attrs = isset($matches['attrs']) ? json_decode( $matches['attrs'], true) : '';
	$void = isset($matches['void']) ? $matches['void'] : '';
	
	// Bail early if missing data or not an ACF Block.
	if( !$name || !$attrs || !acf_has_block_type($name) ) {
		return $matches[0];
	}
	
	// Convert "data" to "meta".
	// No need to check if already in meta format. Local Meta will do this for us.
	if( isset($attrs['data']) ) {
		$attrs['data'] = acf_setup_meta( $attrs['data'], $attrs['id'] );
	}
	
	// Prevent wp_targeted_link_rel from corrupting JSON.
	remove_filter( 'content_save_pre', 'wp_filter_post_kses' );
	remove_filter( 'content_save_pre', 'wp_targeted_link_rel' );
	remove_filter( 'content_save_pre', 'balanceTags', 50 );
	
	/**
	 * Filteres the block attributes before saving.
	 *
	 * @date	18/3/19
	 * @since	5.7.14
	 *
	 * @param	array $attrs The block attributes.
	 */
	$attrs = apply_filters( 'acf/pre_save_block', $attrs );
	
	// Return new comment
	return '<!-- wp:' . $name . ' ' . acf_json_encode($attrs) . ' ' . $void . '-->';
}
