<?php

/**
 * Views conditional output shortcode manager.
 * Uses WPToolset_Types and WPV_Handle_Users_Functions,
 * we should remove those dependencis so we do not need to load TC->toolset_forms.
 *
 * @since unknown
 */
class WPV_Views_Conditional {

	const SHORTCODE_NAME = 'wpv-conditional';

	/**
	 * Helper to resolve attributes to get data for related posts.
	 *
	 * @var Toolset_Shortcode_Attr_Item_M2M
	 */
	private $attr_item_chain = null;

	/**
	 * Helper to resolve attributes to get data for related posts.
	 *
	 * @var Toolset_Common_Bootstrap
	 */
	private $toolset_common_bootstrap = null;

	/**
	 * Instantiate the class.
	 *
	 * @param \Toolset_Shortcode_Attr_Item_M2M $attr_item_chain
	 * @param \Toolset_Common_Bootstrap $toolset_common_bootstrap
	 * @since 2.7.3
	 */
	public function __construct(
		\Toolset_Shortcode_Attr_Item_M2M $attr_item_chain,
		\Toolset_Common_Bootstrap $toolset_common_bootstrap
	) {
		$this->attr_item_chain = $attr_item_chain;
		$this->toolset_common_bootstrap = $toolset_common_bootstrap;
	}

	/**
	 * Initialize the conditionals:
	 * - get dependencies loaded.
	 * - register shortcodes.
	 * - add an API filter to resolve shortcodes early.
	 * - register the editor buttons.
	 *
	 * @return void
	 */
	public function initialize() {
		$toolset_common_sections = array(
			'toolset_parser',
			'toolset_forms'// Load WPToolset_Types and WPV_Handle_Users_Functions
		);
		$this->toolset_common_bootstrap->load_sections( $toolset_common_sections );

		add_shortcode( self::SHORTCODE_NAME, array( $this, 'resolve_shortcode' ) );
		add_filter( 'wpv_process_conditional_shortcodes', array( $this, 'process_conditional_shortcodes' ) );

		add_action( 'wp_loaded', array( $this, 'register_editor_buttons' ) );

	}

	/**
	 * Resolve the shortcode.
	 *
	 * @param array $attr
	 * @param string $content
	 * @return string
	 */
	public function resolve_shortcode( $attr, $content = '' ) {
		global $post;
		$has_post = true;
		$id = '';
		if ( empty( $post->ID ) ) {
			// Will not execute any condition that involves custom fields
			$has_post = false;
		} else {
			$id = $post->ID;
		}

		if (
			empty( $attr['if'] )
			|| (
				empty( $content )
				&& $content !== '0'
			)
		) {
			return ''; // ignore
		}

		extract(
			shortcode_atts(
				array(
					'evaluate' => 'true',
					'debug' => false,
					'if' => true
				),
				$attr
			)
		);


		$out = '';
		$evaluate = ( $evaluate == 'true' || $evaluate === true ) ? true : false;
		$debug = ( $debug == 'true' || $debug === true ) ? true : false;

		$attr['if'] = str_replace( " NEQ ", " ne ", $attr['if'] );
		$attr['if'] = str_replace( " neq ", " ne ", $attr['if'] );
		$attr['if'] = str_replace( " EQ ", " = ", $attr['if'] );
		$attr['if'] = str_replace( " eq ", " = ", $attr['if'] );
		$attr['if'] = str_replace( " NE ", " ne ", $attr['if'] );
		$attr['if'] = str_replace( " != ", " ne ", $attr['if'] );

		$attr['if'] = str_replace( " LT ", " < ", $attr['if'] );
		$attr['if'] = str_replace( " lt ", " < ", $attr['if'] );
		$attr['if'] = str_replace( " LTE ", " <= ", $attr['if'] );
		$attr['if'] = str_replace( " lte ", " <= ", $attr['if'] );
		$attr['if'] = str_replace( " GT ", " > ", $attr['if'] );
		$attr['if'] = str_replace( " gt ", " > ", $attr['if'] );
		$attr['if'] = str_replace( " GTE ", " >= ", $attr['if'] );
		$attr['if'] = str_replace( " gte ", " >= ", $attr['if'] );

		if ( strpos( $content, 'wpv-b64-' ) === 0 ) {
			$content = substr( $content, 7 );
			$content = base64_decode( $content );
		}

		$evaluation_result = $this->parse_conditional( $post, $attr['if'], $debug, $attr, $id, $has_post );

		if (
			(
				$evaluate
				&& $evaluation_result['passed']
			) || (
				!$evaluate
				&& !$evaluation_result['passed']
			)
		) {
			$out = $content;
		}

		if (
			$debug
			&& current_user_can( 'manage_options' )
		) {
			$out .= '<pre>' . $evaluation_result['debug'] . '</pre>';
		}

		apply_filters( 'wpv_shortcode_debug', self::SHORTCODE_NAME, wp_json_encode( $attr ), '', 'Data received from cache', $out );

		return $out;
	}

	/**
	 * Register the conditional output editor buttons both on MCE and quicktags.
	 * Note that this needs to run after init so third parties have initialized their own pages.
	 *
	 * @since 2.6.1
	 */
	public function register_editor_buttons() {
		/**
		 * Filter to disable the conditional output quicktag on demand on all editors of the current page.
		 *
		 * @param bool
		 * @return bool
		 * @since 2.6.1
		 */
		if ( apply_filters( 'wpv_filter_wpv_disable_conditional_output_quicktag', false ) ) {
			return;
		}

		add_filter( "mce_external_plugins", array( $this, "wpv_add_views_conditional_button_scripts" ), 9 );
		add_filter( "mce_buttons", array( $this, "register_buttons_editor" ), 9 );
		add_action( 'admin_print_footer_scripts', array( $this, 'add_quicktags' ), 99 );
		add_action( 'wp_print_footer_scripts', array( $this, 'add_quicktags' ), 99 );
	}

	/**
	 * Register the MCE button scripts.
	 *
	 * @param array $plugin_array
	 * @return array
	 */
	public function wpv_add_views_conditional_button_scripts( $plugin_array ) {
		if ( wp_script_is( 'views-shortcodes-gui-script' ) ) {
			// Enqueue TinyMCE plugin script with its ID.
			$plugin_array['wpv_add_views_conditional_button'] = WPV_URL_EMBEDDED . '/res/js/views_conditional_button_plugin.js?ver=' . WPV_VERSION;
		}

		return $plugin_array;
	}

	/**
	 * Register the MCE button.
	 *
	 * @param array $buttons
	 * @return array
	 */
	public function register_buttons_editor( $buttons ) {
		if ( wp_script_is( 'views-shortcodes-gui-script' ) ) {
			// Register buttons with their id.
			array_push( $buttons, 'wpv_conditional_output' );
		}

		return $buttons;
	}

	/**
	 * Register the quicktag button.
	 */
	public function add_quicktags() {
		if ( wp_script_is( 'views-shortcodes-gui-script' ) ) {
			?>
			<script type="text/javascript">
				QTags.addButton('wpv_conditional', '<?php echo esc_js( __( 'conditional output', 'wpv-views' ) ); ?>', wpv_add_conditional_quicktag_function, '', 'c', '<?php echo esc_js( __( 'Views conditional output', 'wpv-views' ) ); ?>', 121, '', {
					ariaLabel: '<?php echo esc_js( __( 'Views conditional output', 'wpv-views' ) ); ?>',
					ariaLabelClose: '<?php echo esc_js( __( 'Close Views conditional output', 'wpv-views' ) ); ?>'
				});
			</script>
			<?php
		}
	}

	/**
	 * Parse the shortcode condition.
	 *
	 * @param WP_Post|null $post
	 * @param string $condition
	 * @param bool $debug
	 * @param array $attr
	 * @param int|string $id
	 * @param bool $has_post
	 * @return array (
	 *     @type string debug The debug log string if crafted
	 *     @type bool passed Whether the condition evaluates to TRUE or FALSE
	 * )
	 */
	private function parse_conditional( $post, $condition, $debug = false, $attr, $id, $has_post ) {

		$logging_string = "####################\nwpv-conditional attributes\n####################\n"
			. print_r( $attr, true )
			. "\n####################\nDebug information\n####################"
			. "\n--------------------\nOriginal expression: "
			. $condition
			. "\n--------------------";

		// Resolve getting custom fields values from legacy relationships parents,
		// using the following syntax (double quotes in place of single also apply):
		//
		// $(field-slug).id(parent-slug), $('field-slug').id(parent-slug)
		// $(field-slug).id($parent-slug), $('field-slug').id($parent-slug)
		// $(field-slug).id('parent-slug'), $('field-slug').id('parent-slug')
		// $(field-slug).id('$parent-slug'), $('field-slug').id('$parent-slug')
		if ( strpos( $condition, '.id(' ) !== false ) {
			preg_match_all( '/[$]\(([^\)]+)\)\.id\(([^\)]+)\)/Uim', $condition, $matches );
			$matches_count = count( $matches[0] );
			if ( $matches_count > 0 ) {
				for ( $i = 0; $i < $matches_count; $i++ ) {
					$parent_name = str_replace( array( '"', "'" ), '', $matches[2][ $i ] );
					if ( strpos( $parent_name, '$' ) !== 0 ) {
						$parent_name = '$' . $parent_name;
					}
					if ( ! $post_id = $this->attr_item_chain->get( array( 'id' => $parent_name ) ) ) {
						// no valid item
						continue;
					}

					$field_name = str_replace( array( '"', "'" ), '', $matches[1][ $i ] );
					$temp_condition = $matches[0][ $i ];

					$condition = $this->extract_fields_in_condition( $condition, $temp_condition, $post_id, $field_name );
				}
			}
		}

		// Resolve getting values from m2m relationships related posts,
		// using the following syntax (double quotes in place of single also apply):
		//
		// $(field-slug).item(parent-slug), $('field-slug').item(parent-slug)
		// $(field-slug).item($parent-slug), $('field-slug').item($parent-slug)
		// $(field-slug).item('parent-slug'), $('field-slug').item('parent-slug')
		// $(field-slug).item('$parent-slug'), $('field-slug').item('$parent-slug')
		//
		// #(taxonomy-slug).item(parent-slug)
		// #(taxonomy-slug).item($parent-slug)
		// #(taxonomy-slug).item('parent-slug')
		// #(taxonomy-slug).item('$parent-slug')
		if ( strpos( $condition, '.item(' ) !== false ) {
			preg_match_all( '/[$]\(([^\)]+)\)\.item\(([^\)]+)\)/Uim', $condition, $matches );
			$matches_count = count( $matches[0] );
			if ( $matches_count > 0 ) {
				for ( $i = 0; $i < $matches_count; $i++ ) {
					$related_reference = str_replace( array( '"', "'" ), '', $matches[2][ $i ] );
					if ( ! $post_id = $this->attr_item_chain->get( array( 'item' => $related_reference ) ) ) {
						// no valid item
						continue;
					}

					$field_name = str_replace( array( '"', "'" ), '', $matches[1][ $i ] );
					$temp_condition = $matches[0][ $i ];

					$condition = $this->extract_fields_in_condition( $condition, $temp_condition, $post_id, $field_name );
				}
			}

			preg_match_all( '/[#]\(([^\)]+)\)\.item\(([^\)]+)\)/Uim', $condition, $matches );
			$matches_count = count( $matches[0] );
			if ( $matches_count > 0 ) {
				for ( $i = 0; $i < $matches_count; $i++ ) {
					$related_reference = str_replace( array( '"', "'" ), '', $matches[2][ $i ] );
					if ( ! $post_id = $this->attr_item_chain->get( array( 'item' => $related_reference ) ) ) {
						// no valid item
						continue;
					}

					$temp_condition = $matches[0][ $i ];
					$taxonomy_slug = $matches[1][ $i ];

					$condition = $this->extract_taxonomies_in_condition( $condition, $temp_condition, $post_id, $taxonomy_slug );
				}
			}
		}

		// Resolve getting term values,
		// using the following syntax (double quotes in place of single also apply):
		//
		// #(taxonomy-slug)
		preg_match_all( '/\#\(([^()]+)\)/', $condition, $matches );
		$matches_count = count( $matches[0] );
		if ( $matches_count > 0 ) {
			for ( $i = 0; $i < $matches_count; $i++ ) {
				$temp_condition = $matches[0][ $i ];
				$taxonomy_slug = $matches[1][ $i ];

				$condition = $this->extract_taxonomies_in_condition( $condition, $temp_condition, $id, $taxonomy_slug );
			}
		}

		/* Resolve parent*/

		$data = WPToolset_Types::getCustomConditional( $condition, '', WPToolset_Types::getConditionalValues( $id ) );

		$evaluate = $data['custom'];
		$values = $data['values'];

		if ( strpos( $evaluate, "REGEX" ) === false ) {
			$evaluate = trim( stripslashes( $evaluate ) );
			// Check dates
			$evaluate = wpv_filter_parse_date( $evaluate );
			$evaluate = $this->handle_user_function( $evaluate );
		}

		$fields = $this->extract_fields( $evaluate );

		$evaluate = apply_filters( 'wpv-extra-condition-filters', $evaluate );
		$temp = $this->extract_variables( $evaluate, $attr, $has_post, $id );
		$evaluate = $temp['evaluate'];
		$logging_string .= $temp['log'];
		if (
			empty( $fields )
			&& empty( $values )
		) {
			$passed = $this->evaluate_custom( $evaluate );
		} else {
			$evaluate = $this->update_values_in_expression( $evaluate, $fields, $values, $id );
			$logging_string .= "\n--------------------\nConverted expression: "
				. $evaluate
				. "\n--------------------";
			$passed = $this->evaluate_custom( $evaluate );
		}

		return array( 'debug' => $logging_string, 'passed' => $passed );

	}

	/**
	 * Adjust the syntax of a condition based on a field of a given post ID,
	 *
	 * @param string $condition_string
	 * @param string $condition_match
	 * @param int $post_id
	 * @param string $field_slug
	 * @return string
	 */
	private function extract_fields_in_condition( $condition_string, $condition_match, $post_id, $field_slug ) {
		$data = WPToolset_Types::getCustomConditional( $condition_match, '', WPToolset_Types::getConditionalValues( $post_id ) );
		if ( isset( $data['values'][ $field_slug ] ) ) {
			if ( is_string( $data['values'][ $field_slug ] ) ) {
				$data['values'][ $field_slug ] = "'" . $data['values'][ $field_slug ] . "'";
			}
			$condition_string = str_replace( $condition_match, $data['values'][ $field_slug ], $condition_string );
		} else {
			$condition_string = str_replace( $condition_match, "''", $condition_string );
		}

		return $condition_string;
	}

	/**
	 * Adjust the syntax of a condition based on a taxonomy of a given post ID,
	 *
	 * @param string $condition_string
	 * @param string $condition_match
	 * @param int $post_id
	 * @param string $taxonomy_slug
	 * @return string
	 */
	private function extract_taxonomies_in_condition( $condition_string, $condition_match, $post_id, $taxonomy_slug ) {
		$post_terms = wp_get_post_terms( $post_id, $taxonomy_slug );

		if (
			is_wp_error( $post_terms )
			|| empty( $post_terms )
		) {
			$condition_string = str_replace( $condition_match, 'ARRAY()', $condition_string );
		} else {
			$replace_condition = 'ARRAY(';
			$replace_condition_list = array();
			foreach ( $post_terms as $term ) {
				$replace_condition_list[] = '\'' . $term->slug . '\'';
				$replace_condition_list[] = '\'' . $term->term_id . '\'';
				$replace_condition_list[] = '\'' . str_replace( "'", "", $term->name ) . '\'';
			}
			$replace_condition .= implode( ',', $replace_condition_list ) . ')';
			$condition_string = str_replace( $condition_match, $replace_condition, $condition_string );
		}

		return $condition_string;
	}

	/**
	 * Extract the named variables from a condition if they match any given attribute.
	 *
	 * @param string $evaluate
	 * @param array $atts
	 * @param bool $has_post
	 * @param int $id
	 * @return array (
	 *     @type string evaluate String to evaluate
	 *     @type string log Logging string, if generated
	 * )
	 */
	private function extract_variables( $evaluate, $atts, $has_post, $id ) {
		$logging_string = '';
		// Evaluate quoted variables that are to be used as strings
		// '$f1' will replace $f1 with the custom field value

		$strings_count = preg_match_all( '/(\'[\$\w^\']*\')/', $evaluate, $matches );
		if (
			$strings_count
			&& $strings_count > 0
		) {
			for( $i = 0; $i < $strings_count; $i++ ) {
				$string = $matches[1][ $i ];
				// remove single quotes from string literals to get value only
				$string = ( strpos( $string, '\'' ) === 0 ) ? substr( $string, 1, strlen( $string ) - 2 ) : $string;
				if ( strpos( $string, '$' ) === 0 ) {
					$quoted_variables_logging_extra = '';
					$variable_name = substr( $string, 1 ); // omit dollar sign
					if ( isset( $atts[ $variable_name ] ) ) {
						$string = get_post_meta( $id, $atts[ $variable_name ], true );
						$evaluate = str_replace( $matches[1][ $i ], "'" . $string . "'", $evaluate );
					} else {
						$evaluate = str_replace( $matches[1][ $i ], "", $evaluate );
						$quoted_variables_logging_extra = "\n\tERROR: Key " . $matches[1][ $i ] . " does not point to a valid attribute in the wpv-if shortcode: expect parsing errors";
					}
					$logging_string .= "\nAfter replacing " . ( $i + 1 ) . " quoted variables: " . $evaluate . $quoted_variables_logging_extra;
				}
			}
		}

		// Evaluate non-quoted variables, by de-quoting the quoted ones if needed


		$strings_count = preg_match_all(
			'/((\$\w+)|(\'[^\']*\'))\s*([\!<>\=|lt|lte|eq|ne|gt|gte]+)\s*((\$\w+)|(\'[^\']*\'))/',
			$evaluate, $matches
		);

		// get all string comparisons - with variables and/or literals
		if (
			$strings_count
			&& $strings_count > 0
		) {
			for( $i = 0; $i < $strings_count; $i++ ) {

				// get both sides and sign
				$first_string = $matches[1][ $i ];
				$second_string = $matches[5][ $i ];
				$math_sign = $matches[4][ $i ];

				$general_variables_logging_extra = '';

				// remove single quotes from string literals to get value only
				$first_string = ( strpos( $first_string, '\'' ) === 0 ) ? substr( $first_string, 1, strlen( $first_string ) - 2 ) : $first_string;
				$second_string = ( strpos( $second_string, '\'' ) === 0 ) ? substr( $second_string, 1, strlen( $second_string ) - 2 ) : $second_string;
				$general_variables_logging_extra .= "\n\tComparing " . $first_string . " to " . $second_string;

				// replace variables with text representation
				if (
					strpos( $first_string, '$' ) === 0
					&& $has_post
				) {
					$variable_name = substr( $first_string, 1 ); // omit dollar sign
					if ( isset( $atts[ $variable_name ] ) ) {
						$first_string = get_post_meta( $id, $atts[ $variable_name ], true );
					} else {
						$first_string = '';
						$general_variables_logging_extra .= "\n\tERROR: Key " . $variable_name . " does not point to a valid attribute in the wpv-if shortcode";
					}
				}
				if ( strpos( $second_string, '$' ) === 0 && $has_post ) {
					$variable_name = substr( $second_string, 1 );
					if ( isset( $atts[ $variable_name ] ) ) {
						$second_string = get_post_meta( $id, $atts[ $variable_name ], true );
					} else {
						$second_string = '';
						$general_variables_logging_extra .= "\n\tERROR: Key " . $variable_name . " does not point to a valid attribute in the wpv-if shortcode";
					}
				}


				$evaluate = ( is_numeric( $first_string ) ? str_replace( $matches[1][ $i ], $first_string, $evaluate ) : str_replace( $matches[1][ $i ], "'$first_string'", $evaluate ) );
				$evaluate = ( is_numeric( $second_string ) ? str_replace( $matches[5][ $i ], $second_string, $evaluate ) : str_replace( $matches[5][ $i ], "'$second_string'", $evaluate ) );
				$logging_string .= "\nAfter replacing " . ( $i + 1 ) . " general variables and comparing strings: " . $evaluate . $general_variables_logging_extra;
			}
		}
		// Evaluate comparisons when at least one of them is numeric
		$strings_count = preg_match_all( '/(\'[^\']*\')/', $evaluate, $matches );
		if (
			$strings_count
			&& $strings_count > 0
		) {
			for( $i = 0; $i < $strings_count; $i++ ) {
				$string = $matches[1][ $i ];
				// remove single quotes from string literals to get value only
				$string = ( strpos( $string, '\'' ) === 0 ) ? substr( $string, 1, strlen( $string ) - 2 ) : $string;
				if ( is_numeric( $string ) ) {
					$evaluate = str_replace( $matches[1][ $i ], $string, $evaluate );
					$logging_string .= "\nAfter matching " . ( $i + 1 ) . " numeric strings into real numbers: " . $evaluate;
					$logging_string .= "\n\tMatched " . $matches[1][ $i ] . " to " . $string;
				}
			}
		}

		// Evaluate all remaining variables
		if ( $has_post ) {
			$count = preg_match_all( '/\$(\w+)/', $evaluate, $matches );

			// replace all variables with their values listed as shortcode parameters
			if (
				$count
				&& $count > 0
			) {
				$logging_string .= "\nRemaining variables: " . var_export( $matches[1], true );
				// sort array by length desc, fix str_replace incorrect replacement
				// wpv_sort_matches_by_length belongs to common/functions.php
				$matches[1] = wpv_sort_matches_by_length( $matches[1] );

				foreach( $matches[1] as $match ) {
					if ( isset( $atts[ $match ] ) ) {
						$meta = get_post_meta( $id, $atts[ $match ], true );
						if (
							empty( $meta )
							&& ! is_numeric( $meta )
						) {
							$meta = "''";
						}
					} else {
						$meta = "0";
					}
					$evaluate = str_replace( '$' . $match, $meta, $evaluate );
					$logging_string .= "\nAfter replacing remaining variables: " . $evaluate;
				}
			}
		}

		return array(
			'evaluate' => $evaluate,
			'log' => $logging_string,
		);
	}

	/**
	 * Evaluates conditions using custom conditional statement.
	 *
	 * @uses wpv_condition()
	 *
	 * @param type $post
	 * @param type $evaluate
	 * @return boolean
	 */
	private function evaluate_custom( $evaluate ) {
		$check = false;
		try {
			$parser = new Toolset_Parser( $evaluate );
			$parser->parse();
			$check = $parser->evaluate();
		} catch( Exception $e ) {
			$check = false;
		}

		return $check;
	}

	/**
	 * Get the remaining custom fields used in the condition.
	 *
	 * @param string $evaluate
	 * @return array
	 */
	private function extract_fields( $evaluate ) {
		//###############################################################################################
		//https://icanlocalize.basecamphq.com/projects/7393061-toolset/todo_items/193583580/comments
		//Fix REGEX conditions that contains \ that is stripped out
		if ( strpos( $evaluate, 'REGEX' ) === false ) {
			$evaluate = trim( stripslashes( $evaluate ) );
			// Check dates
			$evaluate = wpv_filter_parse_date( $evaluate );
			$evaluate = $this->handle_user_function( $evaluate );
		}

		// Add quotes = > < >= <= === <> !==
		$strings_count = preg_match_all( '/[=|==|===|<=|<==|<===|>=|>==|>===|\!===|\!==|\!=|<>]\s(?!\$)(\w*)[\)|\$|\W]/', $evaluate, $matches );

		if ( ! empty( $matches[1] ) ) {
			foreach ( $matches[1] as $temp_match ) {
				$temp_replace = is_numeric( $temp_match ) ? $temp_match : '\'' . $temp_match . '\'';
				$evaluate = str_replace( ' ' . $temp_match . ')', ' ' . $temp_replace . ')', $evaluate );
			}
		}
		// if new version $(field-value) use this regex
		if ( preg_match( '/\$\(([^()]+)\)/', $evaluate ) ) {
			preg_match_all( '/\$\(([^()]+)\)/', $evaluate, $matches );
		} // if old version $field-value use this other
		else {
			preg_match_all( '/\$([^\s]*)/', $evaluate, $matches );
		}


		$fields = array();
		if ( ! empty( $matches ) ) {
			foreach ( $matches[1] as $field_name ) {
				$fields[ trim( $field_name, '()' ) ] = trim( $field_name, '()' );
			}
		}

		return $fields;
	}

	/**
	 * Callback to sort items by length.
	 *
	 * @param string $a
	 * @param string $b
	 * @return int
	 */
	public function sort_by_length( $a, $b ) {
		return strlen( $b ) - strlen( $a );
	}

	/**
	 * Replace field references with their values in the condition.
	 *
	 * @param string $evaluate
	 * @param array $fields
	 * @param array $values
	 * @param int $id
	 * @return string
	 */
	private function update_values_in_expression( $evaluate, $fields, $values, $id ) {

		// use string replace to replace any fields with their values.
		// Sort by length just in case a field name contians a shorter version of another field name.
		// eg.  $my-field and $my-field-2

		$keys = array_keys( $fields );
		usort( $keys, array( $this, 'sort_by_length' ) );

		foreach ( $keys as $key ) {
			$is_numeric = false;
			$is_array = false;
			$value = isset( $values[ $fields[ $key ] ] ) ? $values[ $fields[ $key ] ] : '';

			if ( ! empty( $id ) ) {
				/**
				 * Maybe filter the postmeta value based on its meta key.
				 *
				 * This filter mimics the native get_post_metadata filter documented here:
				 * https://developer.wordpress.org/reference/functions/get_metadata/
				 * If it returns a not null value, then use the returned value instead of the original.
				 * It will hijack legacy '_wpcf_belongs_{slug}_id' meta keys and return the current m2m-compatible
				 * parent value, if any.
				 *
				 * @param null
				 * @param $id int|string The ID of the post that the postmeta belongs to
				 * @patam $fields[ $key ] string The key of the postmeta
				 * @param true Return always a single value, not an array
				 *
				 * @since m2m
				 */
				$postmeta_access_m2m_value = apply_filters( 'toolset_postmeta_access_m2m_get_post_metadata', null, $id, $fields[ $key ], true );
				$value = ( null === $postmeta_access_m2m_value )
					? $value
					: $postmeta_access_m2m_value;
			}

			if ( '' === $value ) {
				$value = "''";
			}

			if ( is_numeric( $value ) ) {
				$is_numeric = true;
			}

			if ( 'array' === gettype( $value ) ) {
				$is_array = true;
				// workaround for datepicker data to cover all cases
				if ( array_key_exists( 'timestamp', $value ) ) {
					if ( is_numeric( $value['timestamp'] ) ) {
						$value = $value['timestamp'];
					} elseif ( is_array( $value['timestamp'] ) ) {
						$value = implode( ',', array_values( $value['timestamp'] ) );
					}
				} elseif ( array_key_exists( 'datepicker', $value ) ) {
					if ( is_numeric( $value['datepicker'] ) ) {
						$value = $value['datepicker'];
					} elseif ( is_array( $value['datepicker'] ) ) {
						$value = implode( ',', array_values( $value['datepicker'] ) );
					}
				} else {
					$value = implode( ',', array_values( $value ) );
				}
			}

			if (
				! empty( $value )
				&& "''" !== $value
				&& ! $is_numeric
				&& ! $is_array
			) {
				$value = str_replace( "'", "", $value );
				$value = '\'' . $value . '\'';
			}

			// First replace the $(field_name) format
			$evaluate = str_replace( '$(' . $fields[ $key ] . ')', $value, $evaluate );
			// next replace the $field_name format
			$evaluate = str_replace( '$' . $fields[ $key ], $value, $evaluate );
		}

		return $evaluate;
	}

	/**
	 * Undocumented function. This shoudl be reviewed.
	 *
	 * @param string $evaluate
	 * @return string
	 */
	private function handle_user_function( $evaluate ) {
		$evaluate = stripcslashes( $evaluate );
		$occurrences = preg_match_all( '/(\\w+)\(([^\)]*)\)/', $evaluate, $matches );

		if ( $occurrences > 0 ) {
			for( $i = 0; $i < $occurrences; $i++ ) {
				$result = false;
				$function = $matches[1][ $i ];
				$field = isset( $matches[2] ) ? rtrim( $matches[2][ $i ], ',' ) : '';

				if ( 'USER' === $function ) {
					$result = WPV_Handle_Users_Functions::get_user_field( $field );
				}

				if ( $result ) {
					$evaluate = str_replace( $matches[0][ $i ], $result, $evaluate );
				}
			}
		}

		return $evaluate;
	}

	/**
	 * Process the conditional shortcode on its own, if included on a given content.
	 *
	 * @param string $content
	 * @return string
	 * @since 2.7.3
	 */
	public function process_conditional_shortcodes( $content ) {
		if ( false === strpos( $content, '[' . self::SHORTCODE_NAME ) ) {
			return $content;
		}

		global $shortcode_tags;

		// Back up current registered shortcodes and clear them all out
		$orig_shortcode_tags = $shortcode_tags;
		remove_all_shortcodes();

		add_shortcode( self::SHORTCODE_NAME, array( $this, 'resolve_shortcode' ) );

		$expression = '/\\[wpv-conditional((?!\\[wpv-conditional).)*\\[\\/wpv-conditional\\]/isU';
		$counts = preg_match_all( $expression, $content, $matches );

		while ( $counts ) {
			foreach ( $matches[0] as $match ) {

				// this will only processes the [wpv-conditional] shortcode
				$pattern = get_shortcode_regex();
				$match_corrected = $match;
				if ( 0 !== preg_match( "/$pattern/s", $match, $match_data ) ) {
					// Base64 Encode the inside part of the expression so the WP can't strip out any data it doesn't like.
					// Be sure to prevent base64_encoding more than just the needed: only do it if there are inner shortcodes
					if ( strpos( $match_data[5], '[' ) !== false ) {
						$match_corrected = str_replace( $match_data[5], 'wpv-b64-' . base64_encode( $match_data[5] ), $match_corrected );
					}

					$match_attributes = wpv_shortcode_parse_condition_atts( $match_data[3] );
					if ( isset( $match_attributes['if'] ) ) {
						$match_evaluate_corrected = str_replace( '<=', 'lte', $match_attributes['if'] );
						$match_evaluate_corrected = str_replace( '<>', 'ne', $match_evaluate_corrected );
						$match_evaluate_corrected = str_replace( '<', 'lt', $match_evaluate_corrected );
						$match_corrected = str_replace( $match_attributes['if'], $match_evaluate_corrected, $match_corrected );
					}
				}

				$shortcode = do_shortcode( $match_corrected );
				$content = str_replace( $match, $shortcode, $content );

			}

			$counts = preg_match_all( $expression, $content, $matches );
		}

		// Put the original shortcodes back
		$shortcode_tags = $orig_shortcode_tags;

		return $content;
	}

}
