<?php
/**
 * Handle dynamic content.
 *
 * @package Builder
 */

/**
 * Gets the dynamic content fields related to Product post type.
 *
 * @since 3.29
 *
 * @return array
 */
function et_builder_get_product_dynamic_content_fields() {
	return array(
		'product_breadcrumb'             => array(
			'label' => esc_html__( 'Product Breadcrumb', 'et_builder' ),
			'type'  => 'text',
		),
		'product_price'                  => array(
			'label' => esc_html__( 'Product Price', 'et_builder' ),
			'type'  => 'text',
		),
		'product_description'            => array(
			'label' => esc_html__( 'Product Description', 'et_builder' ),
			'type'  => 'text',
		),
		'product_short_description'      => array(
			'label' => esc_html__( 'Product Short Description', 'et_builder' ),
			'type'  => 'text',
		),
		'product_reviews_count'          => array(
			'label' => esc_html__( 'Product Reviews Count', 'et_builder' ),
			'type'  => 'text',
		),
		'product_sku'                    => array(
			'label' => esc_html__( 'Product SKU', 'et_builder' ),
			'type'  => 'text',
		),
		'product_reviews'                => array(
			'label' => esc_html__( 'Product Reviews', 'et_builder' ),
			'type'  => 'text',
		),
		'product_additional_information' => array(
			'label' => esc_html__( 'Product Additional Information', 'et_builder' ),
			'type'  => 'text',
		),
		'product_reviews_tab'            => array(
			'label' => esc_html__( 'Product Reviews', 'et_builder' ),
			'type'  => 'url',
		),
	);
}

/**
 * Get built-in dynamic content fields.
 *
 * @since 3.17.2
 *
 * @param integer $post_id Post Id.
 *
 * @return array[]
 */
function et_builder_get_built_in_dynamic_content_fields( $post_id ) {
	$cache_key = 'et_builder_get_built_in_dynamic_content_fields';

	if ( et_core_cache_has( $cache_key ) ) {
		return et_core_cache_get( $cache_key );
	}

	$post_type                = get_post_type( $post_id );
	$post_type                = $post_type ? $post_type : 'post';
	$post_type_object         = get_post_type_object( $post_type );
	$post_type_label          = $post_type_object->labels->singular_name;
	$post_taxonomy_types      = et_builder_get_taxonomy_types( $post_type );
	$tag_taxonomy_post_type   = $post_type;
	$fields                   = array();
	$before_after_field_types = array( 'text', 'any' );

	if ( et_theme_builder_is_layout_post_type( $post_type ) ) {
		$post_type_label        = esc_html__( 'Post', 'et_builder' );
		$tag_taxonomy_post_type = 'post';
		$public_post_types      = array_keys( et_builder_get_public_post_types() );

		foreach ( $public_post_types as $public_post_type ) {
			$post_taxonomy_types = array_merge(
				$post_taxonomy_types,
				et_builder_get_taxonomy_types( $public_post_type )
			);
		}
	}

	$default_category_type = 'post' === $post_type ? 'category' : "${post_type}_category";

	if ( ! isset( $post_taxonomy_types[ $default_category_type ] ) ) {
		$default_category_type = 'category';

		if ( ! empty( $post_taxonomy_types ) ) {
			// Use the 1st available taxonomy as the default value.
			$default_category_type = array_keys( $post_taxonomy_types );
			$default_category_type = $default_category_type[0];
		}
	}

	$date_format_options = array(
		'default' => et_builder_i18n( 'Default' ),
		'M j, Y'  => esc_html__( 'Aug 6, 1999 (M j, Y)', 'et_builder' ),
		'F d, Y'  => esc_html__( 'August 06, 1999 (F d, Y)', 'et_builder' ),
		'm/d/Y'   => esc_html__( '08/06/1999 (m/d/Y)', 'et_builder' ),
		'm.d.Y'   => esc_html__( '08.06.1999 (m.d.Y)', 'et_builder' ),
		'j M, Y'  => esc_html__( '6 Aug, 1999 (j M, Y)', 'et_builder' ),
		'l, M d'  => esc_html__( 'Tuesday, Aug 06 (l, M d)', 'et_builder' ),
		'custom'  => esc_html__( 'Custom', 'et_builder' ),
	);

	$fields['post_title'] = array(
		// Translators: %1$s: Post type name.
		'label' => esc_html( sprintf( __( '%1$s/Archive Title', 'et_builder' ), $post_type_label ) ),
		'type'  => 'text',
	);

	$fields['post_excerpt'] = array(
		// Translators: %1$s: Post type name.
		'label'  => esc_html( sprintf( __( '%1$s Excerpt', 'et_builder' ), $post_type_label ) ),
		'type'   => 'text',
		'fields' => array(
			'words'           => array(
				'label'   => esc_html__( 'Number of Words', 'et_builder' ),
				'type'    => 'text',
				'default' => '',
			),
			'read_more_label' => array(
				'label'   => esc_html__( 'Read More Text', 'et_builder' ),
				'type'    => 'text',
				'default' => '',
			),
		),
	);

	$fields['post_date'] = array(
		// Translators: %1$s: Post type name.
		'label'  => esc_html( sprintf( __( '%1$s Publish Date', 'et_builder' ), $post_type_label ) ),
		'type'   => 'text',
		'fields' => array(
			'date_format'        => array(
				'label'   => esc_html__( 'Date Format', 'et_builder' ),
				'type'    => 'select',
				'options' => $date_format_options,
				'default' => 'default',
			),
			'custom_date_format' => array(
				'label'   => esc_html__( 'Custom Date Format', 'et_builder' ),
				'type'    => 'text',
				'default' => '',
				'show_if' => array(
					'date_format' => 'custom',
				),
			),
		),
	);

	$fields['post_comment_count'] = array(
		// Translators: %1$s: Post type name.
		'label'  => esc_html( sprintf( __( '%1$s Comment Count', 'et_builder' ), $post_type_label ) ),
		'type'   => 'text',
		'fields' => array(
			'link_to_comments_page' => array(
				'label'   => esc_html__( 'Link to Comments Area', 'et_builder' ),
				'type'    => 'yes_no_button',
				'options' => array(
					'on'  => et_builder_i18n( 'Yes' ),
					'off' => et_builder_i18n( 'No' ),
				),
				'default' => 'on',
			),
		),
	);

	if ( ! empty( $post_taxonomy_types ) ) {
		$fields['post_categories'] = array(
			// Translators: %1$s: Post type name.
			'label'  => esc_html( sprintf( __( '%1$s Categories', 'et_builder' ), $post_type_label ) ),
			'type'   => 'text',
			'fields' => array(
				'link_to_term_page' => array(
					'label'   => esc_html__( 'Link to Category Index Pages', 'et_builder' ),
					'type'    => 'yes_no_button',
					'options' => array(
						'on'  => et_builder_i18n( 'Yes' ),
						'off' => et_builder_i18n( 'No' ),
					),
					'default' => 'on',
				),
				'separator'         => array(
					'label'   => esc_html__( 'Categories Separator', 'et_builder' ),
					'type'    => 'text',
					'default' => ' | ',
				),
				'category_type'     => array(
					'label'   => esc_html__( 'Category Type', 'et_builder' ),
					'type'    => 'select',
					'options' => $post_taxonomy_types,
					'default' => $default_category_type,
				),
			),
		);
	}

	// Fill in tag taxonomies.
	if ( isset( $post_taxonomy_types[ "{$tag_taxonomy_post_type}_tag" ] ) ) {
		$fields['post_tags'] = array(
			// Translators: %1$s: Post type name.
			'label'  => esc_html( sprintf( __( '%1$s Tags', 'et_builder' ), $post_type_label ) ),
			'type'   => 'text',
			'fields' => array(
				'link_to_term_page' => array(
					'label'   => esc_html__( 'Link to Tag Index Pages', 'et_builder' ),
					'type'    => 'yes_no_button',
					'options' => array(
						'on'  => et_builder_i18n( 'Yes' ),
						'off' => et_builder_i18n( 'No' ),
					),
					'default' => 'on',
				),
				'separator'         => array(
					'label'   => esc_html__( 'Tags Separator', 'et_builder' ),
					'type'    => 'text',
					'default' => ' | ',
				),
				'category_type'     => array(
					'label'   => esc_html__( 'Category Type', 'et_builder' ),
					'type'    => 'select',
					'options' => $post_taxonomy_types,
					'default' => "{$tag_taxonomy_post_type}_tag",
				),
			),
		);
	}

	$fields['post_link'] = array(
		// Translators: %1$s: Post type name.
		'label'  => esc_html( sprintf( __( '%1$s Link', 'et_builder' ), $post_type_label ) ),
		'type'   => 'text',
		'fields' => array(
			'text'        => array(
				'label'   => esc_html__( 'Link Text', 'et_builder' ),
				'type'    => 'select',
				'options' => array(
					// Translators: %1$s: Post type name.
					'post_title' => esc_html( sprintf( __( '%1$s Title', 'et_builder' ), $post_type_label ) ),
					'custom'     => esc_html__( 'Custom', 'et_builder' ),
				),
				'default' => 'post_title',
			),
			'custom_text' => array(
				'label'   => esc_html__( 'Custom Link Text', 'et_builder' ),
				'type'    => 'text',
				'default' => '',
				'show_if' => array(
					'text' => 'custom',
				),
			),
		),
	);

	$fields['post_author'] = array(
		// Translators: %1$s: Post type name.
		'label'  => esc_html( sprintf( __( '%1$s Author', 'et_builder' ), $post_type_label ) ),
		'type'   => 'text',
		'fields' => array(
			'name_format'      => array(
				'label'   => esc_html__( 'Name Format', 'et_builder' ),
				'type'    => 'select',
				'options' => array(
					'display_name'    => esc_html__( 'Public Display Name', 'et_builder' ),
					'first_last_name' => esc_html__( 'First & Last Name', 'et_builder' ),
					'last_first_name' => esc_html__( 'Last, First Name', 'et_builder' ),
					'first_name'      => esc_html__( 'First Name', 'et_builder' ),
					'last_name'       => esc_html__( 'Last Name', 'et_builder' ),
					'nickname'        => esc_html__( 'Nickname', 'et_builder' ),
					'username'        => esc_html__( 'Username', 'et_builder' ),
				),
				'default' => 'display_name',
			),
			'link'             => array(
				'label'   => esc_html__( 'Link Name', 'et_builder' ),
				'type'    => 'yes_no_button',
				'options' => array(
					'on'  => et_builder_i18n( 'Yes' ),
					'off' => et_builder_i18n( 'No' ),
				),
				'default' => 'off',
			),
			'link_destination' => array(
				'label'   => esc_html__( 'Link Destination', 'et_builder' ),
				'type'    => 'select',
				'options' => array(
					'author_archive' => esc_html__( 'Author Archive Page', 'et_builder' ),
					'author_website' => esc_html__( 'Author Website', 'et_builder' ),
				),
				'default' => 'author_archive',
				'show_if' => array(
					'link' => 'on',
				),
			),
		),
	);

	$fields['post_author_bio'] = array(
		'label' => esc_html__( 'Author Bio', 'et_builder' ),
		'type'  => 'text',
	);

	if ( et_builder_tb_enabled() ) {
		$fields['term_description'] = array(
			'label' => esc_html__( 'Category Description', 'et_builder' ),
			'type'  => 'text',
		);
	}

	$fields['site_title'] = array(
		'label' => esc_html__( 'Site Title', 'et_builder' ),
		'type'  => 'text',
	);

	$fields['site_tagline'] = array(
		'label' => esc_html__( 'Site Tagline', 'et_builder' ),
		'type'  => 'text',
	);

	$fields['current_date'] = array(
		'label'  => esc_html__( 'Current Date', 'et_builder' ),
		'type'   => 'text',
		'fields' => array(
			'date_format'        => array(
				'label'   => esc_html__( 'Date Format', 'et_builder' ),
				'type'    => 'select',
				'options' => $date_format_options,
				'default' => 'default',
			),
			'custom_date_format' => array(
				'label'   => esc_html__( 'Custom Date Format', 'et_builder' ),
				'type'    => 'text',
				'default' => '',
				'show_if' => array(
					'date_format' => 'custom',
				),
			),
		),
	);

	$fields['post_link_url'] = array(
		// Translators: %1$s: Post type name.
		'label' => esc_html( sprintf( __( 'Current %1$s Link', 'et_builder' ), $post_type_label ) ),
		'type'  => 'url',
	);

	$fields['post_author_url'] = array(
		'label' => esc_html__( 'Author Page Link', 'et_builder' ),
		'type'  => 'url',
	);

	$fields['home_url'] = array(
		'label' => esc_html__( 'Homepage Link', 'et_builder' ),
		'type'  => 'url',
	);

	// Fill in post type URL options.
	$post_types = et_builder_get_public_post_types();
	foreach ( $post_types as $public_post_type ) {
		$public_post_type_label = $public_post_type->labels->singular_name;
		$key                    = 'post_link_url_' . $public_post_type->name;

		$fields[ $key ] = array(
			// Translators: %1$s: Post type name.
			'label'  => esc_html( sprintf( __( '%1$s Link', 'et_builder' ), $public_post_type_label ) ),
			'type'   => 'url',
			'fields' => array(
				'post_id' => array(
					'label'     => $public_post_type_label,
					'type'      => 'select_post',
					'post_type' => $public_post_type->name,
					'default'   => '',
				),
			),
		);
	}

	$fields['post_featured_image'] = array(
		'label' => esc_html__( 'Featured Image', 'et_builder' ),
		'type'  => 'image',
	);

	$fields['post_author_profile_picture'] = array(
		// Translators: %1$s: Post type name.
		'label' => esc_html__( 'Author Profile Picture', 'et_builder' ),
		'type'  => 'image',
	);

	$fields['site_logo'] = array(
		'label' => esc_html__( 'Site Logo', 'et_builder' ),
		'type'  => 'image',
	);

	$fields['post_meta_key'] = array(
		'label'  => esc_html__( 'Manual Custom Field Name', 'et_builder' ),
		'type'   => 'any',
		'group'  => esc_html__( 'Custom Fields', 'et_builder' ),
		'fields' => array(
			'meta_key' => array(
				'label' => esc_html__( 'Field Name', 'et_builder' ),
				'type'  => 'text',
			),
		),
	);

	if ( current_user_can( 'unfiltered_html' ) ) {
		$fields['post_meta_key']['fields']['enable_html'] = array(
			'label'   => esc_html__( 'Enable raw HTML', 'et_builder' ),
			'type'    => 'yes_no_button',
			'options' => array(
				'on'  => et_builder_i18n( 'Yes' ),
				'off' => et_builder_i18n( 'No' ),
			),
			'default' => 'off',
			'show_on' => 'text',
		);
	}
	/*
	 * Include Product dynamic fields on Product post type.
	 *
	 * This is enforced based on the discussion at
	 *
	 * @see https://github.com/elegantthemes/Divi/issues/15921#issuecomment-512707471
	 */
	if ( et_is_woocommerce_plugin_active() && ( 'product' === $post_type || et_theme_builder_is_layout_post_type( $post_type ) ) ) {
		$fields = array_merge( $fields, et_builder_get_product_dynamic_content_fields() );
	}

	// Fill in boilerplate.
	foreach ( $fields as $key => $field ) {
		$fields[ $key ]['custom'] = false;
		$fields[ $key ]['group']  = et_()->array_get( $fields, "{$key}.group", 'Default' );

		if ( in_array( $field['type'], $before_after_field_types, true ) ) {
			$settings = isset( $field['fields'] ) ? $field['fields'] : array();
			$settings = array_merge(
				array(
					'before' => array(
						'label'   => et_builder_i18n( 'Before' ),
						'type'    => 'text',
						'default' => '',
					),
					'after'  => array(
						'label'   => et_builder_i18n( 'After' ),
						'type'    => 'text',
						'default' => '',
					),
				),
				$settings
			);

			$fields[ $key ]['fields'] = $settings;
		}
	}

	et_core_cache_add( $cache_key, $fields );

	return $fields;
}

/**
 * Clear dynamic content fields cache whenever a custom post type is registered.
 *
 * @since 3.26.7
 *
 * @return void
 */
function et_builder_clear_get_built_in_dynamic_content_fields_cache() {
	et_core_cache_delete( 'et_builder_get_built_in_dynamic_content_fields' );
}
add_action( 'registered_post_type', 'et_builder_clear_get_built_in_dynamic_content_fields_cache' );

/**
 * Get all public taxonomies associated with a given post type.
 *
 * @since 3.17.2
 *
 * @param string $post_type Post type.
 *
 * @return array
 */
function et_builder_get_taxonomy_types( $post_type ) {
	$taxonomies = get_object_taxonomies( $post_type, 'object' );
	$list       = array();

	if ( empty( $taxonomies ) ) {
		return $list;
	}

	foreach ( $taxonomies as $taxonomy ) {
		if ( ! empty( $taxonomy ) && $taxonomy->public && $taxonomy->show_ui ) {
			$list[ $taxonomy->name ] = $taxonomy->label;
		}
	}

	return $list;
}

/**
 * Get a user-friendly custom field label for the given meta key.
 *
 * @since 4.4.4
 *
 * @param string $key Post meta key.
 *
 * @return string
 */
function et_builder_get_dynamic_content_custom_field_label( $key ) {
	$label = str_replace( array( '_', '-' ), ' ', $key );
	$label = ucwords( $label );
	$label = trim( $label );
	return $label;
}

/**
 * Get all dynamic content fields in a given string.
 *
 * @since 4.4.4
 *
 * @param string $content Value content.
 *
 * @return array
 */
function et_builder_get_dynamic_contents( $content ) {
	$is_matched = preg_match_all( ET_THEME_BUILDER_DYNAMIC_CONTENT_REGEX, $content, $matches );

	if ( ! $is_matched ) {
		return array();
	}

	return $matches[0];
}

/**
 * Get all meta keys used as dynamic content in the content of a post.
 *
 * @param integer $post_id Post Id.
 *
 * @return array
 */
function et_builder_get_used_dynamic_content_meta_keys( $post_id ) {
	$transient      = 'et_builder_dynamic_content_used_meta_keys_' . $post_id;
	$used_meta_keys = get_transient( $transient );

	if ( false !== $used_meta_keys ) {
		return $used_meta_keys;
	}

	// The most used meta keys will change from time to time so we will also retrieve the used meta keys in the layout
	// content to make sure that the previously selected meta keys always stay in the list even if they are not in the
	// most used meta keys list anymore.
	$layout_post      = get_post( $post_id );
	$used_meta_keys   = array();
	$dynamic_contents = et_builder_get_dynamic_contents( $layout_post->post_content );

	foreach ( $dynamic_contents as $dynamic_content ) {
		$dynamic_content = et_builder_parse_dynamic_content( $dynamic_content );
		$key             = $dynamic_content->get_content();

		if ( et_()->starts_with( $key, 'custom_meta_' ) ) {
			$meta_key         = substr( $key, strlen( 'custom_meta_' ) );
			$used_meta_keys[] = $meta_key;
		}
	}

	set_transient( $transient, $used_meta_keys, 5 * MINUTE_IN_SECONDS );

	return $used_meta_keys;
}

/**
 * Get most used meta keys on public post types.
 *
 * @since 4.4.4
 *
 * @return string[]
 */
function et_builder_get_most_used_post_meta_keys() {
	global $wpdb;

	$most_used_meta_keys = get_transient( 'et_builder_most_used_meta_keys' );
	if ( false !== $most_used_meta_keys ) {
		return $most_used_meta_keys;
	}

	$public_post_types = array_keys( et_builder_get_public_post_types() );
	$post_types        = "'" . implode( "','", esc_sql( $public_post_types ) ) . "'";

	$sql = "SELECT DISTINCT pm.meta_key FROM {$wpdb->postmeta} pm
		INNER JOIN {$wpdb->posts} p ON ( p.ID = pm.post_id AND p.post_type IN ({$post_types}) )
		WHERE pm.meta_key NOT LIKE '\_%'
		GROUP BY pm.meta_key
		ORDER BY COUNT(pm.meta_key) DESC
		LIMIT 50";

	// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $sql query does not use users/visitor input
	$most_used_meta_keys = $wpdb->get_col( $sql );

	set_transient( 'et_builder_most_used_meta_keys', $most_used_meta_keys, 5 * MINUTE_IN_SECONDS );

	return $most_used_meta_keys;
}

/**
 * Get custom dynamic content fields.
 *
 * @since 3.17.2
 *
 * @param integer $post_id Post Id.
 *
 * @return array[]
 */
function et_builder_get_custom_dynamic_content_fields( $post_id ) {
	$raw_custom_fields = get_post_meta( $post_id );
	$raw_custom_fields = is_array( $raw_custom_fields ) ? $raw_custom_fields : array();
	$custom_fields     = array();

	/**
	 * Filter post meta accepted as custom field options in dynamic content.
	 * Post meta prefixed with `_` is considered hidden from dynamic content options by default
	 * due to its nature as "hidden meta keys". This filter allows third parties to
	 * circumvent this limitation.
	 *
	 * @since 3.17.2
	 *
	 * @param string[] $meta_keys
	 * @param integer $post_id
	 *
	 * @return string[]
	 */
	$display_hidden_meta_keys = apply_filters( 'et_builder_dynamic_content_display_hidden_meta_keys', array(), $post_id );

	// Custom dynamic fields to be displayed on the TB.
	if ( et_theme_builder_is_layout_post_type( get_post_type( $post_id ) ) ) {
		$raw_custom_fields = array_merge(
			$raw_custom_fields,
			array_flip( et_builder_get_most_used_post_meta_keys() ),
			array_flip( et_builder_get_used_dynamic_content_meta_keys( $post_id ) )
		);
	}

	foreach ( $raw_custom_fields as $key => $values ) {
		if ( substr( $key, 0, 1 ) === '_' && ! in_array( $key, $display_hidden_meta_keys, true ) ) {
			// Ignore hidden meta keys.
			continue;
		}

		if ( substr( $key, 0, 3 ) === 'et_' ) {
			// Ignore ET meta keys as they are not suitable for dynamic content use.
			continue;
		}

		$label = et_builder_get_dynamic_content_custom_field_label( $key );

		/**
		 * Filter the display label for a custom field.
		 *
		 * @since 3.17.2
		 *
		 * @param string $label
		 * @param string $meta_key
		 */
		$label = apply_filters( 'et_builder_dynamic_content_custom_field_label', $label, $key );

		$field = array(
			'label'    => $label,
			'type'     => 'any',
			'fields'   => array(
				'before' => array(
					'label'   => et_builder_i18n( 'Before' ),
					'type'    => 'text',
					'default' => '',
					'show_on' => 'text',
				),
				'after'  => array(
					'label'   => et_builder_i18n( 'After' ),
					'type'    => 'text',
					'default' => '',
					'show_on' => 'text',
				),
			),
			'meta_key' => $key,
			'custom'   => true,
			'group'    => __( 'Custom Fields', 'et_builder' ),
		);

		if ( current_user_can( 'unfiltered_html' ) ) {
			$field['fields']['enable_html'] = array(
				'label'   => esc_html__( 'Enable raw HTML', 'et_builder' ),
				'type'    => 'yes_no_button',
				'options' => array(
					'on'  => et_builder_i18n( 'Yes' ),
					'off' => et_builder_i18n( 'No' ),
				),
				'default' => 'off',
				'show_on' => 'text',
			);
		}

		$custom_fields[ "custom_meta_{$key}" ] = $field;
	}

	/**
	 * Filter available custom field options for dynamic content.
	 *
	 * @since 3.17.2
	 *
	 * @param array[] $custom_fields
	 * @param int     $post_id
	 * @param mixed[] $raw_custom_fields
	 *
	 * @return array[]
	 */
	$custom_fields = apply_filters( 'et_builder_custom_dynamic_content_fields', $custom_fields, $post_id, $raw_custom_fields );

	return $custom_fields;
}

/**
 * Get all dynamic content fields.
 *
 * @since 3.17.2
 *
 * @param integer $post_id Post Id.
 * @param string  $context Context e.g `edit`, `display`.
 *
 * @return array[]
 */
function et_builder_get_dynamic_content_fields( $post_id, $context ) {
	global $__et_dynamic_content_fields_index_map;

	$fields        = et_builder_get_built_in_dynamic_content_fields( $post_id );
	$custom_fields = array();

	if ( 'display' === $context || et_pb_is_allowed( 'read_dynamic_content_custom_fields' ) ) {
		$custom_fields = et_builder_get_custom_dynamic_content_fields( $post_id );
	}

	$all = array_merge( $fields, $custom_fields );

	foreach ( $all as $id => $field ) {
		$all[ $id ]['id'] = $id;
	}

	$__et_dynamic_content_fields_index_map = array_flip( array_keys( $all ) );
	uasort( $all, 'et_builder_sort_dynamic_content_fields' );
	$__et_dynamic_content_fields_index_map = array();

	return $all;
}

/**
 * Sort dynamic content fields.
 *
 * @since 4.0
 *
 * @param array $a First field.
 * @param array $b Second field.
 *
 * @return integer
 */
function et_builder_sort_dynamic_content_fields( $a, $b ) {
	global $__et_dynamic_content_fields_index_map;

	$top = array_flip(
		array(
			'Default',
			__( 'Custom Fields', 'et_builder' ),
		)
	);

	$a_group  = et_()->array_get( $a, 'group', 'Default' );
	$a_is_top = isset( $top[ $a_group ] );
	$b_group  = et_()->array_get( $b, 'group', 'Default' );
	$b_is_top = isset( $top[ $b_group ] );

	if ( $a_is_top && ! $b_is_top ) {
		return -1;
	}

	if ( ! $a_is_top && $b_is_top ) {
		return 1;
	}

	if ( $a_is_top && $b_is_top && $a_group !== $b_group ) {
		return $top[ $a_group ] - $top[ $b_group ];
	}

	$a_index = $__et_dynamic_content_fields_index_map[ $a['id'] ];
	$b_index = $__et_dynamic_content_fields_index_map[ $b['id'] ];

	return $a_index - $b_index;
}

/**
 * Get default value for a dynamic content field's setting.
 *
 * @since 3.17.2
 *
 * @param integer $post_id Post Id.
 * @param string  $field Custom field name.
 * @param string  $setting Array of dynamic content settings.
 *
 * @return string
 */
function et_builder_get_dynamic_attribute_field_default( $post_id, $field, $setting ) {
	$_      = ET_Core_Data_Utils::instance();
	$fields = et_builder_get_dynamic_content_fields( $post_id, 'edit' );

	return $_->array_get( $fields, "$field.fields.$setting.default", '' );
}

/**
 * Resolve dynamic content to a simple value.
 *
 * @param string  $name Custom field name.
 * @param array   $settings Array of dynamic content settings.
 * @param integer $post_id Post Id.
 * @param string  $context Context e.g `edit`, `display`.
 * @param array   $overrides An associative array of field_name => value to override field value.
 * @param bool    $is_content Whether dynamic content used in module's main_content field {@see et_builder_ajax_resolve_post_content()}.
 *
 * @return string
 * @since 3.17.2
 */
function et_builder_resolve_dynamic_content( $name, $settings, $post_id, $context, $overrides = array(), $is_content = false ) {
	/**
	 * Generic filter for content resolution based on a given field and post.
	 *
	 * @since 3.17.2
	 *
	 * @param string $content
	 * @param string $name
	 * @param array $settings
	 * @param integer $post_id
	 * @param string $context
	 * @param array $overrides
	 *
	 * @return string
	 */
	$content = apply_filters( 'et_builder_resolve_dynamic_content', '', $name, $settings, $post_id, $context, $overrides );

	/**
	 * Field-specific filter for content resolution based on a given field and post.
	 *
	 * @since 3.17.2
	 *
	 * @param string $content
	 * @param array $settings
	 * @param integer $post_id
	 * @param string $context
	 * @param array $overrides
	 *
	 * @return string
	 */
	$content = apply_filters( "et_builder_resolve_dynamic_content_{$name}", $content, $settings, $post_id, $context, $overrides );

	$content = et_maybe_enable_embed_shortcode( $content, $is_content );

	return $is_content ? do_shortcode( $content ) : $content;
}

/**
 * Wrap a dynamic content value with its before/after settings values.
 *
 * @since 3.17.2
 *
 * @param integer $post_id Post Id.
 * @param string  $name Custom field name.
 * @param string  $value Value content.
 * @param array   $settings Array of dynamic content settings.
 *
 * @return string
 */
function et_builder_wrap_dynamic_content( $post_id, $name, $value, $settings ) {
	$_           = ET_Core_Data_Utils::instance();
	$def         = 'et_builder_get_dynamic_attribute_field_default';
	$before      = $_->array_get( $settings, 'before', $def( $post_id, $name, 'before' ) );
	$after       = $_->array_get( $settings, 'after', $def( $post_id, $name, 'after' ) );
	$tb_post_id  = ET_Builder_Element::get_theme_builder_layout_id();
	$cap_post_id = $tb_post_id ? $tb_post_id : $post_id;
	$user_id     = get_post_field( 'post_author', $cap_post_id );

	if ( ! user_can( $user_id, 'unfiltered_html' ) ) {
		$allowlist = array_merge(
			wp_kses_allowed_html( '' ),
			array(
				'h1'   => array(),
				'h2'   => array(),
				'h3'   => array(),
				'h4'   => array(),
				'h5'   => array(),
				'h6'   => array(),
				'ol'   => array(),
				'ul'   => array(),
				'li'   => array(),
				'span' => array(),
				'p'    => array(),
			)
		);

		$before = wp_kses( $before, $allowlist );
		$after  = wp_kses( $after, $allowlist );
	}

	return $before . $value . $after;
}

/**
 * Resolve built-in dynamic content fields.
 *
 * @param string  $content Value content.
 * @param string  $name Custom field name.
 * @param array   $settings Array of dynamic content settings.
 * @param integer $post_id Post Id.
 * @param string  $context Context e.g `edit`, `display`.
 * @param array   $overrides An associative array of field_name => value to override field value.
 *
 * @return string
 * @since 3.17.2
 */
function et_builder_filter_resolve_default_dynamic_content( $content, $name, $settings, $post_id, $context, $overrides ) {
	global $shortname, $wp_query;

	$_       = ET_Core_Data_Utils::instance();
	$def     = 'et_builder_get_dynamic_attribute_field_default';
	$post    = get_post( $post_id );
	$author  = null;
	$wrapped = false;
	$is_woo  = false;

	if ( $post ) {
		$author = get_userdata( $post->post_author );
	} elseif ( is_author() ) {
		$author = get_queried_object();
	}

	switch ( $name ) {
		case 'product_title': // Intentional fallthrough.
		case 'post_title':
			if ( isset( $overrides[ $name ] ) ) {
				$content = $overrides[ $name ];
			} else {
				$content = et_builder_get_current_title( $post_id );
			}

			$content = et_core_intentionally_unescaped( $content, 'cap_based_sanitized' );
			break;

		case 'post_excerpt':
			if ( ! $post ) {
				break;
			}

			$words     = (int) $_->array_get( $settings, 'words', $def( $post_id, $name, 'words' ) );
			$read_more = $_->array_get( $settings, 'read_more_label', $def( $post_id, $name, 'read_more_label' ) );
			$content   = isset( $overrides[ $name ] ) ? $overrides[ $name ] : get_the_excerpt( $post_id );

			if ( $words > 0 ) {
				$content = wp_trim_words( $content, $words );
			}

			if ( ! empty( $read_more ) ) {
				$content .= sprintf(
					' <a href="%1$s">%2$s</a>',
					esc_url( get_permalink( $post_id ) ),
					esc_html( $read_more )
				);
			}
			break;

		case 'post_date':
			if ( ! $post ) {
				break;
			}

			$format        = $_->array_get( $settings, 'date_format', $def( $post_id, $name, 'date_format' ) );
			$custom_format = $_->array_get( $settings, 'custom_date_format', $def( $post_id, $name, 'custom_date_format' ) );

			if ( 'default' === $format ) {
				$format = '';
			}

			if ( 'custom' === $format ) {
				$format = $custom_format;
			}

			$content = esc_html( get_the_date( $format, $post_id ) );
			break;

		case 'post_comment_count':
			if ( ! $post ) {
				break;
			}

			$link    = $_->array_get( $settings, 'link_to_comments_page', $def( $post_id, $name, 'link_to_comments_page' ) );
			$link    = 'on' === $link;
			$content = esc_html( get_comments_number( $post_id ) );

			if ( $link ) {
				$content = sprintf(
					'<a href="%1$s">%2$s</a>',
					esc_url( get_comments_link( $post_id ) ),
					et_core_esc_previously( et_builder_wrap_dynamic_content( $post_id, $name, $content, $settings ) )
				);
				$wrapped = true;
			}
			break;

		case 'post_categories': // Intentional fallthrough.
		case 'post_tags':
			if ( ! $post ) {
				break;
			}

			$overrides_map   = array(
				'category' => 'post_categories',
				'post_tag' => 'post_tags',
			);
			$post_taxonomies = et_builder_get_taxonomy_types( get_post_type( $post_id ) );
			$taxonomy        = $_->array_get( $settings, 'category_type', '' );

			if ( in_array( $taxonomy, array( 'et_header_layout_category', 'et_body_layout_category', 'et_footer_layout_category' ), true ) ) {
				// TB layouts were storing an invalid taxonomy in <= 4.0.3 so we have to correct it:.
				$taxonomy = $def( $post_id, $name, 'category_type' );
			}

			if ( ! isset( $post_taxonomies[ $taxonomy ] ) ) {
				break;
			}

			$link      = $_->array_get( $settings, 'link_to_term_page', $def( $post_id, $name, 'link_to_category_page' ) );
			$link      = 'on' === $link;
			$separator = $_->array_get( $settings, 'separator', $def( $post_id, $name, 'separator' ) );
			$separator = ! empty( $separator ) ? $separator : $def( $post_id, $name, 'separator' );
			$ids_key   = isset( $overrides_map[ $taxonomy ] ) ? $overrides_map[ $taxonomy ] : '';
			$ids       = isset( $overrides[ $ids_key ] ) ? array_filter( array_map( 'intval', explode( ',', $overrides[ $ids_key ] ) ) ) : array();
			$terms     = ! empty( $ids ) ? get_terms(
				array(
					'taxonomy' => $taxonomy,
					'include'  => $ids,
				)
			) : get_the_terms( $post_id, $taxonomy );
			if ( is_array( $terms ) ) {
				$content = et_builder_list_terms( $terms, $link, $separator );
			} else {
				$content = '';
			}
			break;

		case 'post_link':
			if ( ! $post ) {
				break;
			}

			$text        = $_->array_get( $settings, 'text', $def( $post_id, $name, 'text' ) );
			$custom_text = $_->array_get( $settings, 'custom_text', $def( $post_id, $name, 'custom_text' ) );
			$label       = 'custom' === $text ? $custom_text : get_the_title( $post_id );
			$content     = sprintf(
				'<a href="%1$s">%2$s</a>',
				esc_url( get_permalink( $post_id ) ),
				esc_html( $label )
			);
			break;

		case 'post_author':
			$name_format      = $_->array_get( $settings, 'name_format', $def( $post_id, $name, 'name_format' ) );
			$link             = $_->array_get( $settings, 'link', $def( $post_id, $name, 'link' ) );
			$link             = 'on' === $link;
			$link_destination = $_->array_get( $settings, 'link_destination', $def( $post_id, $name, 'link_destination' ) );
			$link_target      = 'author_archive' === $link_destination ? '_self' : '_blank';
			$label            = '';
			$url              = '';

			if ( ! $author ) {
				$content = '';
				break;
			}

			switch ( $name_format ) {
				case 'display_name':
					$label = $author->display_name;
					break;
				case 'first_last_name':
					$label = $author->first_name . ' ' . $author->last_name;
					break;
				case 'last_first_name':
					$label = $author->last_name . ', ' . $author->first_name;
					break;
				case 'first_name':
					$label = $author->first_name;
					break;
				case 'last_name':
					$label = $author->last_name;
					break;
				case 'nickname':
					$label = $author->nickname;
					break;
				case 'username':
					$label = $author->user_login;
					break;
			}

			switch ( $link_destination ) {
				case 'author_archive':
					$url = get_author_posts_url( $author->ID );
					break;
				case 'author_website':
					$url = $author->user_url;
					break;
			}

			$content = esc_html( $label );

			if ( $link && ! empty( $url ) ) {
				$content = sprintf(
					'<a href="%1$s" target="%2$s">%3$s</a>',
					esc_url( $url ),
					esc_attr( $link_target ),
					et_core_esc_previously( $content )
				);
			}
			break;

		case 'post_author_bio':
			if ( ! $author ) {
				break;
			}

			$content = et_core_intentionally_unescaped( $author->description, 'cap_based_sanitized' );
			break;

		case 'term_description':
			$content = et_core_intentionally_unescaped( term_description(), 'cap_based_sanitized' );
			break;

		case 'site_title':
			$content = esc_html( get_bloginfo( 'name' ) );
			break;

		case 'site_tagline':
			$content = esc_html( get_bloginfo( 'description' ) );
			break;

		case 'current_date':
			$format        = $_->array_get( $settings, 'date_format', $def( $post_id, $name, 'date_format' ) );
			$custom_format = $_->array_get( $settings, 'custom_date_format', $def( $post_id, $name, 'custom_date_format' ) );

			if ( 'default' === $format ) {
				$format = get_option( 'date_format' );
			}

			if ( 'custom' === $format ) {
				$format = $custom_format;
			}

			$content = esc_html( date_i18n( $format ) );
			break;

		case 'post_link_url':
			if ( ! $post ) {
				break;
			}

			$content = esc_url( get_permalink( $post_id ) );
			break;

		case 'post_author_url':
			if ( ! $author ) {
				break;
			}

			$content = esc_url( get_author_posts_url( $author->ID ) );
			break;

		case 'home_url':
			$content = esc_url( home_url( '/' ) );
			break;

		case 'any_post_link_url':
			$selected_post_id = $_->array_get( $settings, 'post_id', $def( $post_id, $name, 'post_id' ) );
			$content          = esc_url( get_permalink( $selected_post_id ) );
			break;

		case 'product_reviews_tab':
			$content = '#product_reviews_tab';
			break;

		case 'post_featured_image':
			$is_blog_query = isset( $wp_query->et_pb_blog_query ) && $wp_query->et_pb_blog_query;

			if ( isset( $overrides[ $name ] ) ) {
				$id      = (int) $overrides[ $name ];
				$content = wp_get_attachment_image_url( $id, 'full' );
				break;
			}

			if ( ! $is_blog_query && ( is_category() || is_tag() || is_tax() ) ) {
				$term_id       = (int) get_queried_object_id();
				$attachment_id = (int) get_term_meta( $term_id, 'thumbnail_id', true );
				$url           = wp_get_attachment_image_url( $attachment_id, 'full' );
				$content       = $url ? esc_url( $url ) : '';
				break;
			}

			if ( $post ) {
				$url     = get_the_post_thumbnail_url( $post_id, 'full' );
				$content = $url ? esc_url( $url ) : '';
				break;
			}

			break;

		case 'post_featured_image_alt_text':
			$is_blog_query = isset( $wp_query->et_pb_blog_query ) && $wp_query->et_pb_blog_query;

			if ( isset( $overrides[ $name ] ) ) {
				$id      = (int) $overrides[ $name ];
				$img_alt = $id ? get_post_meta( $id, '_wp_attachment_image_alt', true ) : '';
				$content = $img_alt ? esc_attr( $img_alt ) : '';
				break;
			}

			if ( ! $is_blog_query && ( is_category() || is_tag() || is_tax() ) ) {
				$term_id       = (int) get_queried_object_id();
				$attachment_id = (int) get_term_meta( $term_id, 'thumbnail_id', true );
				$img_alt       = $attachment_id ? get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) : '';
				$content       = $img_alt ? esc_attr( $img_alt ) : '';
				break;
			}

			if ( $post ) {
				$img_alt = get_post_thumbnail_id() ? get_post_meta( get_post_thumbnail_id(), '_wp_attachment_image_alt', true ) : '';
				$content = $img_alt ? esc_attr( $img_alt ) : '';
				break;
			}

			break;

		case 'post_featured_image_title_text':
			$is_blog_query = isset( $wp_query->et_pb_blog_query ) && $wp_query->et_pb_blog_query;

			if ( isset( $overrides[ $name ] ) ) {
				$id        = (int) $overrides[ $name ];
				$img_title = $id ? get_the_title( $id ) : '';
				$content   = $img_title ? esc_attr( $img_title ) : '';
				break;
			}

			if ( ! $is_blog_query && ( is_category() || is_tag() || is_tax() ) ) {
				$term_id       = (int) get_queried_object_id();
				$attachment_id = (int) get_term_meta( $term_id, 'thumbnail_id', true );
				$img_title     = $attachment_id ? get_the_title( $attachment_id ) : '';
				$content       = $img_title ? esc_attr( $img_title ) : '';
				break;
			}

			if ( $post ) {
				$img_title = get_post_thumbnail_id() ? get_the_title( get_post_thumbnail_id() ) : '';
				$content   = $img_title ? esc_attr( $img_title ) : '';
				break;
			}

			break;

		case 'post_author_profile_picture':
			if ( ! $author ) {
				break;
			}

			$content = get_avatar_url( $author->ID );
			break;

		case 'site_logo':
			$logo    = et_get_option( $shortname . '_logo' );
			$content = '';

			if ( ! empty( $logo ) ) {
				$content = esc_url( $logo );
			}

			break;

		case 'product_breadcrumb':
			if ( ! $post ) {
				break;
			}

			$dynamic_product = ET_Builder_Module_Helper_Woocommerce_Modules::get_product( $post_id );

			if ( $dynamic_product ) {
				$is_woo  = true;
				$content = ET_Builder_Module_Woocommerce_Breadcrumb::get_breadcrumb(
					array(
						'product' => $dynamic_product->get_id(),
					)
				);
			} else {
				$content = '';
			}
			break;

		case 'product_price':
			if ( ! $post ) {
				break;
			}

			$dynamic_product = ET_Builder_Module_Helper_Woocommerce_Modules::get_product( $post_id );

			if ( $dynamic_product ) {
				$is_woo  = true;
				$content = ET_Builder_Module_Woocommerce_Price::get_price(
					array(
						'product' => $dynamic_product->get_id(),
					)
				);
			} else {
				$content = '';
			}
			break;

		case 'product_description':
			if ( ! $post ) {
				break;
			}

			$dynamic_product = ET_Builder_Module_Helper_Woocommerce_Modules::get_product( $post_id );

			if ( $dynamic_product ) {
				$is_woo  = true;
				$content = ET_Builder_Module_Woocommerce_Description::get_description(
					array(
						'product'          => $dynamic_product->get_id(),
						'description_type' => 'description',
					)
				);
			} else {
				$content = '';
			}
			break;

		case 'product_short_description':
			if ( ! $post ) {
				break;
			}

			$dynamic_product = ET_Builder_Module_Helper_Woocommerce_Modules::get_product( $post_id );

			if ( $dynamic_product ) {
				$is_woo  = true;
				$content = ET_Builder_Module_Woocommerce_Description::get_description(
					array(
						'product'          => $dynamic_product->get_id(),
						'description_type' => 'short_description',
					)
				);
			} else {
				$content = '';
			}
			break;

		case 'product_reviews_count':
			if ( ! $post ) {
				break;
			}

			$dynamic_product = ET_Builder_Module_Helper_Woocommerce_Modules::get_product( $post_id );

			if ( $dynamic_product ) {
				$is_woo  = true;
				$content = $dynamic_product->get_review_count();
			} else {
				$content = 0;
			}
			break;

		case 'product_sku':
			if ( ! $post ) {
				break;
			}

			$dynamic_product = ET_Builder_Module_Helper_Woocommerce_Modules::get_product( $post_id );

			if ( $dynamic_product ) {
				$is_woo  = true;
				$content = $dynamic_product->get_sku();
			} else {
				$content = '';
			}
			break;

		case 'product_reviews':
			if ( ! $post ) {
				break;
			}

			$dynamic_product = ET_Builder_Module_Helper_Woocommerce_Modules::get_product( $post_id );

			if ( ! $dynamic_product ) {
				$content = '';
				break;
			}

			// Return early if comments are closed.
			if ( ! comments_open( $dynamic_product->get_id() ) ) {
				$content = '';
				break;
			}

			$is_woo = true;

			// Product description refers to Product short description.
			// Product short description is nothing but post excerpt.
			$args        = array( 'post_id' => $dynamic_product->get_id() );
			$comments    = get_comments( $args );
			$total_pages = get_comment_pages_count( $comments );
			$content     = wp_list_comments(
				array(
					'callback' => 'woocommerce_comments',
					'echo'     => false,
				),
				$comments
			);

			// Pass $dynamic_product, $reviews to unify the flow of data.
			$reviews_title        = ET_Builder_Module_Helper_Woocommerce_Modules::get_reviews_title( $dynamic_product );
			$reviews_comment_form = ET_Builder_Module_Helper_Woocommerce_Modules::get_reviews_comment_form( $dynamic_product, $comments );
			$no_reviews_text      = sprintf(
				'<p class="woocommerce-noreviews">%s</p>',
				esc_html__( 'There are no reviews yet.', 'et_builder' )
			);

			$no_reviews = is_array( $comments ) && count( $comments ) > 0 ? '' : $no_reviews_text;

			if ( wp_doing_ajax() ) {
				$page = get_query_var( 'cpage' );
				if ( ! $page ) {
					$page = 1;
				}
				$args = array(
					'base'         => add_query_arg( 'cpage', '%#%' ),
					'format'       => '',
					'total'        => $total_pages,
					'current'      => $page,
					'echo'         => false,
					'add_fragment' => '#comments',
					'type'         => 'list',
				);
				global $wp_rewrite;
				if ( $wp_rewrite->using_permalinks() ) {
					$args['base'] = user_trailingslashit( trailingslashit( get_permalink() ) . $wp_rewrite->comments_pagination_base . '-%#%', 'commentpaged' );
				}

				$pagination = paginate_links( $args );
			} else {
				$pagination = paginate_comments_links(
					array(
						'echo'  => false,
						'type'  => 'list',
						'total' => $total_pages,
					)
				);
			}

			$content = sprintf(
				'
						<div id="reviews" class="woocommerce-Reviews">
								<h2 class="woocommerce-Reviews-title">
									%1$s
								</h2>
							<div id="comments">
								<ol class="commentlist">
								%2$s
								</ol>
								<nav class="woocommerce-pagination">
									%5$s
								</nav>
								%4$s
							</div>
							<div id="review_form_wrapper">
								%3$s
							</div>
						</div>
						',
				et_core_esc_previously( $reviews_title ),
				et_core_esc_previously( $content ),
				et_core_esc_previously( $reviews_comment_form ),
				et_core_esc_previously( $no_reviews ),
				et_core_esc_previously( $pagination )
			);
			$wrapped = true;
			break;

		case 'product_additional_information':
			if ( ! $post ) {
				break;
			}

			$dynamic_product = ET_Builder_Module_Helper_Woocommerce_Modules::get_product( $post_id );

			if ( $dynamic_product ) {
				$is_woo  = true;
				$content = ET_Builder_Module_Woocommerce_Additional_Info::get_additional_info(
					array(
						'product' => $dynamic_product->get_id(),
					)
				);
			} else {
				$content = '';
			}
			break;

		case 'post_meta_key':
			$meta_key = $_->array_get( $settings, 'meta_key' );
			$content  = get_post_meta( $post_id, $meta_key, true );
			$is_fe    = 'fe' === et_builder_get_current_builder_type() ? true : false;

			if ( ( $is_fe && empty( $content ) ) || empty( $meta_key ) ) {
				$content = '';
				break;
			}

			if ( empty( $content ) ) {
				$content = et_builder_get_dynamic_content_custom_field_label( $meta_key );
			} else {
				$enable_html = $_->array_get( $settings, 'enable_html' );

				if ( 'on' !== $enable_html ) {
					$content = esc_html( $content );
				}
			}
			break;
	}

	// Handle in post type URL options.
	$post_types = et_builder_get_public_post_types();
	foreach ( $post_types as $public_post_type ) {
		$key = 'post_link_url_' . $public_post_type->name;

		if ( $key !== $name ) {
			continue;
		}

		$selected_post_id = $_->array_get( $settings, 'post_id', $def( $post_id, $name, 'post_id' ) );
		$content          = esc_url( get_permalink( $selected_post_id ) );
		break;
	}

	// Wrap non plain text woo data to add custom selector for styling inheritance.
	// It works by checking is the content has HTML tag.
	if ( $is_woo && $content && preg_match( '/<\s?[^\>]*\/?\s?>/i', $content ) ) {
		$content = sprintf( '<div class="woocommerce et-dynamic-content-woo et-dynamic-content-woo--%2$s">%1$s</div>', $content, $name );
	}

	if ( ! $wrapped ) {
		$content = et_builder_wrap_dynamic_content( $post_id, $name, $content, $settings );
		$wrapped = true;
	}

	return $content;
}
add_filter( 'et_builder_resolve_dynamic_content', 'et_builder_filter_resolve_default_dynamic_content', 10, 6 );

/**
 * Resolve custom field dynamic content fields.
 *
 * @param string  $content Value content.
 * @param string  $name Custom field name.
 * @param array   $settings Array of dynamic content settings.
 * @param integer $post_id Post Id.
 * @param string  $context Context e.g `edit`, `display`.
 * @param array   $overrides  An associative array of field_name => value to override field value.
 *
 * @return string
 * @since 3.17.2
 */
function et_builder_filter_resolve_custom_field_dynamic_content( $content, $name, $settings, $post_id, $context, $overrides ) {
	$post   = get_post( $post_id );
	$fields = et_builder_get_dynamic_content_fields( $post_id, $context );

	if ( empty( $fields[ $name ]['meta_key'] ) ) {
		return $content;
	}

	if ( 'edit' === $context && ! et_pb_is_allowed( 'read_dynamic_content_custom_fields' ) ) {
		if ( 'text' === $fields[ $name ]['type'] ) {
			return esc_html__( 'You don\'t have sufficient permissions to access this content.', 'et_builder' );
		}
		return '';
	}

	$_           = ET_Core_Data_Utils::instance();
	$def         = 'et_builder_get_dynamic_attribute_field_default';
	$enable_html = $_->array_get( $settings, 'enable_html', $def( $post_id, $name, 'enable_html' ) );

	if ( $post ) {
		$content = get_post_meta( $post_id, $fields[ $name ]['meta_key'], true );
	}

	/**
	 * Provide a hook for third party compatibility purposes of formatting meta values.
	 *
	 * @since 3.17.2
	 *
	 * @param string $meta_value
	 * @param string $meta_key
	 * @param integer $post_id
	 */
	$content = apply_filters( 'et_builder_dynamic_content_meta_value', $content, $fields[ $name ]['meta_key'], $post_id );

	// Sanitize HTML contents.
	$content = wp_kses_post( $content );

	if ( 'on' !== $enable_html ) {
		$content = esc_html( $content );
	}

	$content = et_builder_wrap_dynamic_content( $post_id, $name, $content, $settings );

	return $content;
}
add_filter( 'et_builder_resolve_dynamic_content', 'et_builder_filter_resolve_custom_field_dynamic_content', 10, 6 );

/**
 * Resolve a dynamic group post content field for use during editing.
 *
 * @since 3.17.2
 *
 * @param string  $field Custom field name.
 * @param array   $settings Array of dynamic content settings.
 * @param integer $post_id Post Id.
 * @param array   $overrides An associative array of field_name => value to override field value.
 * @param boolean $is_content Whether dynamic content used in module's main_content field {@see et_builder_ajax_resolve_post_content()}.
 *
 * @return string
 */
function et_builder_filter_resolve_dynamic_post_content_field( $field, $settings, $post_id, $overrides = array(), $is_content = false ) {
	return et_builder_resolve_dynamic_content( $field, $settings, $post_id, 'edit', $overrides, $is_content );
}
add_action( 'et_builder_resolve_dynamic_post_content_field', 'et_builder_filter_resolve_dynamic_post_content_field', 10, 5 );

/**
 * Clean potential dynamic content from filter artifacts.
 *
 * @since 3.20.2
 *
 * @param string $value Content.
 *
 * @return string
 */
function et_builder_clean_dynamic_content( $value ) {
	// Strip wrapping <p></p> tag as it appears in shortcode content in certain cases (e.g. BB preview).
	$value = preg_replace( '/^<p>(.*)<\/p>$/i', '$1', trim( $value ) );
	return $value;
}

/**
 * Parse a JSON-encoded string into an ET_Builder_Value instance or null on failure.
 *
 * @since 3.20.2
 *
 * @param string $json JSON-encoded string.
 *
 * @return ET_Builder_Value|null
 */
function et_builder_parse_dynamic_content_json( $json ) {
	// phpcs:disable WordPress.Security.NonceVerification -- This function does not change any stats, hence CSRF ok.
	$post_types         = array_keys( et_builder_get_public_post_types() );
	$dynamic_content    = json_decode( $json, true );
	$is_dynamic_content = is_array( $dynamic_content ) && isset( $dynamic_content['dynamic'] ) && (bool) $dynamic_content['dynamic'];
	$has_content        = is_array( $dynamic_content ) && isset( $dynamic_content['content'] ) && is_string( $dynamic_content['content'] );
	$has_settings       = is_array( $dynamic_content ) && isset( $dynamic_content['settings'] ) && is_array( $dynamic_content['settings'] );
	$has_category_type  = is_array( $dynamic_content ) && isset( $dynamic_content['settings'] ) && isset( $dynamic_content['settings']['category_type'] );

	// When adding a section from library get_post_type() will not work, and post type has to be fetched from $_POST.
	$is_added_from_library = isset( $_POST['et_post_type'] );

	if ( ! $is_dynamic_content || ! $has_content || ! $has_settings ) {
		return null;
	}

	// Replaces layout_category with proper category_type depending on the post type on which the layout is added.
	if ( $has_category_type && 'post_categories' === $dynamic_content['content'] && ! 0 === substr_compare( $dynamic_content['settings']['category_type'], '_tag', - 4 ) ) {
		if ( $is_added_from_library ) {
			$correct_post_type = sanitize_text_field( $_POST['et_post_type'] );
			$correct_post_type = in_array( $correct_post_type, $post_types, true ) ? $correct_post_type : 'post';
		} else {
			$correct_post_type = get_post_type();
			$correct_post_type = in_array( $correct_post_type, $post_types, true ) ? $correct_post_type : 'post';
		}

		if ( 'post' === $correct_post_type ) {
			$dynamic_content['settings']['category_type'] = 'category';
		} else {
			$dynamic_content['settings']['category_type'] = $correct_post_type . '_category';
		}
	}

	return new ET_Builder_Value(
		(bool) $dynamic_content['dynamic'],
		sanitize_text_field( $dynamic_content['content'] ),
		array_map( 'wp_kses_post', $dynamic_content['settings'] )
	);
	// phpcs:enable
}

/**
 * Convert a value to an ET_Builder_Value representation.
 *
 * @since 3.17.2
 *
 * @param string $content Value content.
 *
 * @return ET_Builder_Value
 */
function et_builder_parse_dynamic_content( $content ) {
	$json            = et_builder_clean_dynamic_content( $content );
	$json            = preg_replace( '/^@ET-DC@(.*?)@$/', '$1', $json );
	$dynamic_content = et_builder_parse_dynamic_content_json( $json );

	if ( null === $dynamic_content ) {
		$json            = base64_decode( $json ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- `base64_decode` is used to unserialize dynamic content.
		$dynamic_content = et_builder_parse_dynamic_content_json( $json );
	}

	if ( null === $dynamic_content ) {
		return new ET_Builder_Value( false, wp_kses_post( $content ), array() );
	}

	return $dynamic_content;
}

/**
 * Serialize dynamic content.
 *
 * @since 3.20.2
 *
 * @param bool    $dynamic Whether the value is static or dynamic.
 * @param string  $content Value content. Represents the dynamic content type when dynamic.
 * @param mixed[] $settings Array of dynamic content settings.
 *
 * @return string
 */
function et_builder_serialize_dynamic_content( $dynamic, $content, $settings ) {
	// JSON_UNESCAPED_SLASHES is only supported from 5.4.
	$options = defined( 'JSON_UNESCAPED_SLASHES' ) ? JSON_UNESCAPED_SLASHES : 0;
	$result  = wp_json_encode(
		array(
			'dynamic'  => $dynamic,
			'content'  => $content,
			// Force object type for keyed arrays as empty arrays will be encoded to
			// javascript arrays instead of empty objects.
			'settings' => (object) $settings,
		),
		$options
	);

	// Use fallback if needed.
	$result = 0 === $options ? str_replace( '\/', '/', $result ) : $result;

	return '@ET-DC@' . base64_encode( $result ) . '@'; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- `base64_encode` is used to serialize dynamic content.
}

/**
 * Strip dynamic content.
 *
 * @since 4.0.9
 *
 * @param string $content Post Content.
 *
 * @return string
 */
function et_builder_strip_dynamic_content( $content ) {
	return preg_replace( '/@ET-DC@(.*?)@/', '', $content );
}

/**
 * Reencode legacy dynamic content in post excerpts.
 *
 * @since 3.20.2
 *
 * @param string  $post_excerpt Post Excerpt.
 * @param integer $post_id Post Id.
 *
 * @return string
 */
function et_builder_reencode_legacy_dynamic_content_in_excerpt( $post_excerpt, $post_id ) {
	$json = '/
		\{              # { character
			(?:         # non-capturing group
				[^{}]   # anything that is not a { or }
				|       # OR
				(?R)    # recurse the entire pattern
			)*          # previous group zero or more times
		\}              # } character
	/x';

	return preg_replace_callback( $json, 'et_builder_reencode_legacy_dynamic_content_in_excerpt_callback', $post_excerpt );
}
add_filter( 'et_truncate_post', 'et_builder_reencode_legacy_dynamic_content_in_excerpt', 10, 2 );

/**
 * Callback to reencode legacy dynamic content for preg_replace_callback.
 *
 * @since 3.20.2
 *
 * @param array $matches PCRE match.
 *
 * @return string
 */
function et_builder_reencode_legacy_dynamic_content_in_excerpt_callback( $matches ) {
	$value = et_builder_parse_dynamic_content_json( $matches[0] );
	return null === $value ? $matches[0] : $value->serialize();
}

/**
 * Resolve dynamic content in post excerpts instead of showing raw JSON.
 *
 * @since 3.17.2
 *
 * @param string  $post_excerpt Post excerpt.
 * @param integer $post_id Post Id.
 *
 * @return string
 */
function et_builder_resolve_dynamic_content_in_excerpt( $post_excerpt, $post_id ) {
	// Use an obscure acronym named global variable instead of an anonymous function as we are
	// targeting PHP 5.2.
	global $_et_brdcie_post_id;

	$_et_brdcie_post_id = $post_id;
	$post_excerpt       = preg_replace_callback( '/@ET-DC@.*?@/', 'et_builder_resolve_dynamic_content_in_excerpt_callback', $post_excerpt );
	$_et_brdcie_post_id = 0;

	return $post_excerpt;
}
add_filter( 'et_truncate_post', 'et_builder_resolve_dynamic_content_in_excerpt', 10, 2 );

/**
 * Callback to resolve dynamic content for preg_replace_callback.
 *
 * @since 3.17.2
 *
 * @param array $matches PCRE match.
 *
 * @return string
 */
function et_builder_resolve_dynamic_content_in_excerpt_callback( $matches ) {
	global $_et_brdcie_post_id;
	return et_builder_parse_dynamic_content( $matches[0] )->resolve( $_et_brdcie_post_id );
}
