<?php
/**
 * WooCommerce Plugin Framework
 *
 * This source file is subject to the GNU General Public License v3.0
 * that is bundled with this package in the file license.txt.
 * It is also available through the world-wide-web at this URL:
 * http://www.gnu.org/licenses/gpl-3.0.html
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@skyverge.com so we can send you a copy immediately.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade the plugin to newer
 * versions in the future. If you wish to customize the plugin for your
 * needs please refer to http://www.skyverge.com
 *
 * @package   SkyVerge/WooCommerce/Plugin/Classes
 * @author    SkyVerge
 * @copyright Copyright (c) 2013-2024, SkyVerge, Inc.
 * @license   http://www.gnu.org/licenses/gpl-3.0.html GNU General Public License v3.0
 */

namespace SkyVerge\WooCommerce\PluginFramework\v5_15_10;

use SkyVerge\WooCommerce\PluginFramework\v5_15_10\Helpers\NumberHelper;
use WC_Data;
use WP_Post;

defined( 'ABSPATH' ) or exit;

if ( ! class_exists( '\\SkyVerge\\WooCommerce\\PluginFramework\\v5_15_10\\SV_WC_Helper' ) ) :


/**
 * SkyVerge Helper Class
 *
 * The purpose of this class is to centralize common utility functions that
 * are commonly used in SkyVerge plugins
 *
 * @since 2.2.0
 */
#[\AllowDynamicProperties]
class SV_WC_Helper {


	/** encoding used for mb_*() string functions */
	const MB_ENCODING = 'UTF-8';


	/** String manipulation functions (all multi-byte safe) ***************/

	/**
	 * Returns true if the haystack string starts with needle
	 *
	 * Note: case-sensitive
	 *
	 * @since 2.2.0
	 * @param string $haystack
	 * @param string $needle
	 * @return bool
	 */
	public static function str_starts_with( $haystack, $needle ) {

		if ( self::multibyte_loaded() ) {

			if ( '' === $needle ) {
				return true;
			}

			return 0 === mb_strpos( $haystack, $needle, 0, self::MB_ENCODING );

		} else {

			$needle = self::str_to_ascii( $needle );

			if ( '' === $needle ) {
				return true;
			}

			return 0 === strpos( self::str_to_ascii( $haystack ), self::str_to_ascii( $needle ) );
		}
	}


	/**
	 * Return true if the haystack string ends with needle
	 *
	 * Note: case-sensitive
	 *
	 * @since 2.2.0
	 * @param string $haystack
	 * @param string $needle
	 * @return bool
	 */
	public static function str_ends_with( $haystack, $needle ) {

		if ( '' === $needle ) {
			return true;
		}

		if ( self::multibyte_loaded() ) {

			return mb_substr( $haystack, -mb_strlen( $needle, self::MB_ENCODING ), null, self::MB_ENCODING ) === $needle;

		} else {

			$haystack = self::str_to_ascii( $haystack );
			$needle   = self::str_to_ascii( $needle );

			return substr( $haystack, -strlen( $needle ) ) === $needle;
		}
	}


	/**
	 * Returns true if the needle exists in haystack
	 *
	 * Note: case-sensitive
	 *
	 * @since 2.2.0
	 * @param string $haystack
	 * @param string $needle
	 * @return bool
	 */
	public static function str_exists( $haystack, $needle ) {

		if ( self::multibyte_loaded() ) {

			if ( '' === $needle ) {
				return false;
			}

			return false !== mb_strpos( $haystack, $needle, 0, self::MB_ENCODING );

		} else {

			$needle = self::str_to_ascii( $needle );

			if ( '' === $needle ) {
				return false;
			}

			return false !== strpos( self::str_to_ascii( $haystack ), self::str_to_ascii( $needle ) );
		}
	}


	/**
	 * Truncates a given $string after a given $length if string is longer than
	 * $length. The last characters will be replaced with the $omission string
	 * for a total length not exceeding $length
	 *
	 * @since 2.2.0
	 * @param string $string text to truncate
	 * @param int $length total desired length of string, including omission
	 * @param string $omission omission text, defaults to '...'
	 * @return string
	 */
	public static function str_truncate( $string, $length, $omission = '...' ) {

		if ( self::multibyte_loaded() ) {

			// bail if string doesn't need to be truncated
			if ( mb_strlen( $string, self::MB_ENCODING ) <= $length ) {
				return $string;
			}

			$length -= mb_strlen( $omission, self::MB_ENCODING );

			return mb_substr( $string, 0, $length, self::MB_ENCODING ) . $omission;

		} else {

			$string = self::str_to_ascii( $string );

			// bail if string doesn't need to be truncated
			if ( strlen( $string ) <= $length ) {
				return $string;
			}

			$length -= strlen( $omission );

			return substr( $string, 0, $length ) . $omission;
		}
	}


	/**
	 * Returns a string with all non-ASCII characters removed.
	 *
	 * This is useful for any string functions that expect only ASCII chars and can't safely handle UTF-8.
	 * Note this only allows ASCII chars in the range 33-126 (newlines/carriage returns are stripped).
	 *
	 * @since 2.2.0
	 *
	 * @param string|mixed $string string to make ASCII
	 * @return string|false
	 */
	public static function str_to_ascii( $string ) {

		// strip ASCII chars 32 and under
		$string = filter_var( $string, FILTER_SANITIZE_FULL_SPECIAL_CHARS, FILTER_FLAG_STRIP_LOW );

		// strip ASCII chars 127 and higher
		return filter_var( $string, FILTER_SANITIZE_FULL_SPECIAL_CHARS, FILTER_FLAG_STRIP_HIGH );
	}


	/**
	 * Return a string with insane UTF-8 characters removed, like invisible
	 * characters, unused code points, and other weirdness. It should
	 * accept the common types of characters defined in Unicode.
	 *
	 * The following are allowed characters:
	 *
	 * p{L} - any kind of letter from any language
	 * p{Mn} - a character intended to be combined with another character without taking up extra space (e.g. accents, umlauts, etc.)
	 * p{Mc} - a character intended to be combined with another character that takes up extra space (vowel signs in many Eastern languages)
	 * p{Nd} - a digit zero through nine in any script except ideographic scripts
	 * p{Zs} - a whitespace character that is invisible, but does take up space
	 * p{P} - any kind of punctuation character
	 * p{Sm} - any mathematical symbol
	 * p{Sc} - any currency sign
	 *
	 * pattern definitions from http://www.regular-expressions.info/unicode.html
	 *
	 * @since 4.0.0
	 *
	 * @param string $string
	 * @return string
	 */
	public static function str_to_sane_utf8( $string ) {

		$sane_string = preg_replace( '/[^\p{L}\p{Mn}\p{Mc}\p{Nd}\p{Zs}\p{P}\p{Sm}\p{Sc}]/u', '', $string );

		// preg_replace with the /u modifier can return null or false on failure
		return ( is_null( $sane_string ) || false === $sane_string ) ? $string : $sane_string;
	}


	/**
	 * Formats a number as a percentage.
	 *
	 * @since 5.10.9
	 *
	 * @NOTE The second and third parameter below are directly passed to {@see wc_format_decimal()} in case the decimal output or rounding needs to be tweaked.
	 *
	 * @param float|int|string $fraction the fraction to format as percentage
	 * @param int|string|false number of decimal points to use, empty string to use {@see woocommerce_price_num_decimals(), or false to avoid rounding (optional, default).
	 * @param bool $trim_zeros from end of string (optional, default false)
	 * @return string fraction formatted as percentage
	 */
	public static function format_percentage( $fraction, $decimal_points = false, $trim_zeros = false ) {

		return sprintf( '%s%%', (string) wc_format_decimal( $fraction * 100, $decimal_points, $trim_zeros ) );
	}


	/**
	 * Helper method to check if the multibyte extension is loaded, which
	 * indicates it's safe to use the mb_*() string methods
	 *
	 * @since 2.2.0
	 * @return bool
	 */
	protected static function multibyte_loaded() {

		return extension_loaded( 'mbstring' );
	}


	/** Array functions ***************************************************/


	/**
	 * Insert the given element after the given key in the array
	 *
	 * Sample usage:
	 *
	 * given
	 *
	 * array( 'item_1' => 'foo', 'item_2' => 'bar' )
	 *
	 * array_insert_after( $array, 'item_1', array( 'item_1.5' => 'w00t' ) )
	 *
	 * becomes
	 *
	 * array( 'item_1' => 'foo', 'item_1.5' => 'w00t', 'item_2' => 'bar' )
	 *
	 * @since 2.2.0
	 * @param array $array array to insert the given element into
	 * @param string $insert_key key to insert given element after
	 * @param array $element element to insert into array
	 * @return array
	 */
	public static function array_insert_after( Array $array, $insert_key, Array $element ) {

		$new_array = array();

		foreach ( $array as $key => $value ) {

			$new_array[ $key ] = $value;

			if ( $insert_key == $key ) {

				foreach ( $element as $k => $v ) {
					$new_array[ $k ] = $v;
				}
			}
		}

		return $new_array;
	}


	/**
	 * Convert array into XML by recursively generating child elements
	 *
	 * First instantiate a new XML writer object:
	 *
	 * $xml = new XMLWriter();
	 *
	 * Open in memory (alternatively you can use a local URI for file output)
	 *
	 * $xml->openMemory();
	 *
	 * Then start the document
	 *
	 * $xml->startDocument( '1.0', 'UTF-8' );
	 *
	 * Don't forget to end the document and output the memory
	 *
	 * $xml->endDocument();
	 *
	 * $your_xml_string = $xml->outputMemory();
	 *
	 * @since 2.2.0
	 *
	 * @param \XMLWriter $xml_writer XML writer instance
	 * @param string|array $element_key name for element, e.g. <per_page>
	 * @param string|array $element_value value for element, e.g. 100
	 */
	public static function array_to_xml( $xml_writer, $element_key, $element_value = array() ) {

		if ( is_array( $element_value ) ) {

			// handle attributes
			if ( '@attributes' === $element_key ) {

				foreach ( $element_value as $attribute_key => $attribute_value ) {

					$xml_writer->startAttribute( $attribute_key );
					$xml_writer->text( $attribute_value );
					$xml_writer->endAttribute();
				}

				return;
			}

			// handle multi-elements (e.g. multiple <Order> elements)
			if ( is_numeric( key( $element_value ) ) ) {

				// recursively generate child elements
				foreach ( $element_value as $child_element_key => $child_element_value ) {

					$xml_writer->startElement( $element_key );

					foreach ( $child_element_value as $sibling_element_key => $sibling_element_value ) {
						self::array_to_xml( $xml_writer, $sibling_element_key, $sibling_element_value );
					}

					$xml_writer->endElement();
				}

			} else {

				// start root element
				$xml_writer->startElement( $element_key );

				// recursively generate child elements
				foreach ( $element_value as $child_element_key => $child_element_value ) {
					self::array_to_xml( $xml_writer, $child_element_key, $child_element_value );
				}

				// end root element
				$xml_writer->endElement();
			}

		} else {

			// handle single elements
			if ( '@value' === $element_key ) {

				$xml_writer->text( $element_value );

			} else {

				// wrap element in CDATA tags if it contains illegal characters
				if ( false !== strpos( $element_value, '<' ) || false !== strpos( $element_value, '>' ) ) {

					$xml_writer->startElement( $element_key );
					$xml_writer->writeCdata( $element_value );
					$xml_writer->endElement();

				} else {

					$xml_writer->writeElement( $element_key, $element_value );
				}
			}
		}
	}


	/**
	 * Lists an array as text.
	 *
	 * Takes an array and returns a list like "one, two, three, and four"
	 * with a (mandatory) oxford comma.
	 *
	 * @since 5.2.0
	 *
	 * @param array $items items to list
	 * @param string|null $conjunction coordinating conjunction, like "or" or "and"
	 * @param string $separator list separator, like a comma
	 * @return string
	 */
	public static function list_array_items( array $items, $conjunction = null, $separator = '' ) {

		if ( ! is_string( $conjunction ) ) {
			$conjunction = _x( 'and', 'coordinating conjunction for a list of items: a, b, and c', 'woocommerce-plugin-framework' );
		}

		// append the conjunction to the last item
		if ( count( $items ) > 1 ) {

			$last_item = array_pop( $items );

			array_push( $items, trim( "{$conjunction} {$last_item}" ) );

			// only use a comma if needed and no separator was passed
			if ( count( $items ) < 3 ) {
				$separator = ' ';
			} elseif ( ! is_string( $separator ) || '' === $separator ) {
				$separator = ', ';
			}
		}

		return implode( $separator, $items );
	}


	/**
	 * Joins the array elements into a string using natural language.
	 *
	 * For example, the array `['US', 'Canada', 'Mexico']` would become `'US, Canada, and Mexico'`.
	 *
	 * When using this method to create user-facing text, it is recommended to supply a localized conjunction.
	 *
	 * @since 5.11.8
	 *
	 * @param array<scalar> $array
	 * @param string|null $conjunction one of 'and' or 'or'
	 * @param string|null $pattern a custom sprintf pattern, with placeholders %1$s and %2$s
	 * @return string
	 */
	public static function array_join_natural( array $array, ?string $conjunction = 'and', ?string $pattern = '' ) : string
	{
		$last = array_pop( $array );

		if ( $array ) {
			if ( ! $pattern ) {
				switch ( $conjunction ) {
					case 'or':
						/* translators: A list of items, for example: "US or Canada", or "US, Canada, or Mexico". English uses Oxford comma before the conjunction ("or") if there are at least 2 items preceding it - hence the use of plural forms. If your locale does not use Oxford comma, you can just provide the same translation to all plural forms. Placeholders: %1$s - a comma-separated list of item, %2$s - the final item in the list */
						$pattern = _n( '%1$s or %2$s', '%1$s, or %2$s', count( $array ), 'woocommerce-plugin-framework' );
						break;

					case 'and':
					default:
						/* translators: A list of items, for example: "US and Canada", or "US, Canada, and Mexico". English uses Oxford comma before the conjunction ("and") if there are at least 2 items preceding it - hence the use of plural forms. If your locale does not use Oxford comma, you can just provide the same translation to all plural forms. Placeholders: %1$s - a comma-separated list of items, %2$s - the final item in the list */
						$pattern = _n( '%1$s and %2$s', '%1$s, and %2$s', count( $array ), 'woocommerce-plugin-framework' );
						break;
				}
			}

			return sprintf( $pattern, implode( ', ', $array ), $last );
		}

		return (string) $last;
	}


	/** Number helper functions *******************************************/


	/**
	 * Format a number with 2 decimal points, using a period for the decimal
	 * separator and no thousands separator.
	 *
	 * Commonly used for payment gateways which require amounts in this format.
	 *
	 * @deprecated 5.15.0 - use {@see NumberHelper::format()} instead
	 *
	 * @since 3.0.0
	 * @param float $number
	 * @return string
	 */
	public static function number_format( $number ) {
		return NumberHelper::format($number);
	}


	/** WooCommerce helper functions **************************************/


	/**
	 * Gets order line items (products) as an array of objects.
	 *
	 * Object properties:
	 *
	 * + id          - item ID
	 * + name        - item name, usually product title, processed through htmlentities()
	 * + description - formatted item meta (e.g. Size: Medium, Color: blue), processed through htmlentities()
	 * + quantity    - item quantity
	 * + item_total  - item total (line total divided by quantity, excluding tax & rounded)
	 * + line_total  - line item total (excluding tax & rounded)
	 * + meta        - formatted item meta array
	 * + product     - item product or null if getting product from item failed
	 * + item        - raw item array
	 *
	 * @since 3.0.0
	 *
	 * @param \WC_Order $order
	 * @return \stdClass[] array of line item objects
	 */
	public static function get_order_line_items( $order ): array {

		$line_items = [];

		/** @var \WC_Order_Item_Product $item */
		foreach ( $order->get_items() as $id => $item ) {

			$line_item = new \stdClass();
			$product   = $item->get_product();
			$name      = $item->get_name();
			$quantity  = $item->get_quantity();
			$sku       = $product instanceof \WC_Product ? $product->get_sku() : '';
			$item_desc = [];

			// add SKU to description if available
			if ( ! empty( $sku ) ) {
				$item_desc[] = sprintf( 'SKU: %s', $sku );
			}

			$meta_data = $item->get_formatted_meta_data( '_', true );
			$item_meta = [];

			foreach ( $meta_data as $meta ) {
				$item_meta[] = array(
					'label' => $meta->display_key,
					'value' => $meta->value,
				);
			}

			if ( ! empty( $item_meta ) ) {
				foreach ( $item_meta as $meta ) {
					$item_desc[] = sprintf( '%s: %s', $meta['label'], $meta['value'] );
				}
			}

			$item_desc = implode( ', ', $item_desc );

			$line_item->id          = $id;
			$line_item->name        = htmlentities( $name, ENT_QUOTES, 'UTF-8', false );
			$line_item->description = htmlentities( $item_desc, ENT_QUOTES, 'UTF-8', false );
			$line_item->quantity    = $quantity;
			$line_item->item_total  = $item['recurring_line_total'] ?? $order->get_item_total( $item );
			$line_item->line_total  = $order->get_line_total( $item );
			$line_item->meta        = $item_meta;
			$line_item->product     = is_object( $product ) ? $product : null;
			$line_item->item        = $item;

			$line_items[] = $line_item;
		}

		return $line_items;
	}


	/**
	 * Determines if an order contains only virtual products.
	 *
	 * @since 4.5.0
	 *
	 * @param \WC_Order $order the order object
	 * @return bool
	 */
	public static function is_order_virtual( \WC_Order $order ) {

		$is_virtual = true;

		/** @var \WC_Order_Item_Product $item */
		foreach ( $order->get_items() as $item ) {

			$product = $item->get_product();

			// once we've found one non-virtual product we know we're done, break out of the loop
			if ( $product && ! $product->is_virtual() ) {

				$is_virtual = false;
				break;
			}
		}

		return $is_virtual;
	}


	/**
	 * Determines if a shop has any published virtual products.
	 *
	 * @since 5.10.0
	 *
	 * @return bool
	 */
	public static function shop_has_virtual_products() {

		$virtual_products = wc_get_products( [
			'virtual' => true,
			'status'  => 'publish',
			'limit'   => 1,
		] );

		return sizeof( $virtual_products ) > 0;
	}


	/**
	 * Safely gets a value from $_POST.
	 *
	 * If the expected data is a string also trims it.
	 *
	 * @since 5.5.0
	 *
	 * @param string $key posted data key
	 * @param int|float|array|bool|null|string $default default data type to return (default empty string)
	 * @return int|float|array|bool|null|string posted data value if key found, or default
	 */
	public static function get_posted_value( $key, $default = '' ) {

		$value = $default;

		if ( isset( $_POST[ $key ] ) ) {
			$value = is_string( $_POST[ $key ] ) ? trim( $_POST[ $key ] ) : $_POST[ $key ];
		}

		return $value;
	}


	/**
	 * Safely gets a value from $_REQUEST.
	 *
	 * If the expected data is a string also trims it.
	 *
	 * @since 5.5.0
	 *
	 * @param string $key posted data key
	 * @param int|float|array|bool|null|string $default default data type to return (default empty string)
	 * @return int|float|array|bool|null|string posted data value if key found, or default
	 */
	public static function get_requested_value( $key, $default = '' ) {

		$value = $default;

		if ( isset( $_REQUEST[ $key ] ) ) {
			$value = is_string( $_REQUEST[ $key ] ) ? trim( $_REQUEST[ $key ] ) : $_REQUEST[ $key ];
		}

		return $value;
	}


	/**
	 * Get the count of notices added, either for all notices (default) or for one
	 * particular notice type specified by $notice_type.
	 *
	 * WC notice functions are not available in the admin
	 *
	 * @since 3.0.2
	 * @param string $notice_type The name of the notice type - either error, success or notice. [optional]
	 * @return int
	 */
	public static function wc_notice_count( $notice_type = '' ) {

		if ( function_exists( 'wc_notice_count' ) ) {
			return wc_notice_count( $notice_type );
		}

		return 0;
	}


	/**
	 * Add and store a notice.
	 *
	 * WC notice functions are not available in the admin
	 *
	 * @since 3.0.2
	 * @param string $message The text to display in the notice.
	 * @param string $notice_type The singular name of the notice type - either error, success or notice. [optional]
	 */
	public static function wc_add_notice( $message, $notice_type = 'success' ) {

		// the session sanity check is necessary as WC doesn't provide one of its own
		if ( function_exists( 'wc_add_notice' ) && ! empty( WC()->session ) ) {
			wc_add_notice( $message, $notice_type );
		}
	}


	/**
	 * Print a single notice immediately
	 *
	 * WC notice functions are not available in the admin
	 *
	 * @since 3.0.2
	 * @param string $message The text to display in the notice.
	 * @param string $notice_type The singular name of the notice type - either error, success or notice. [optional]
	 */
	public static function wc_print_notice( $message, $notice_type = 'success' ) {

		if ( function_exists( 'wc_print_notice' ) ) {
			wc_print_notice( $message, $notice_type );
		}
	}


	/**
	 * Gets the full URL to the log file for a given $handle
	 *
	 * @since 4.0.0
	 * @param string $handle log handle
	 * @return string URL to the WC log file identified by $handle
	 */
	public static function get_wc_log_file_url( $handle ) {
		return admin_url( sprintf( 'admin.php?page=wc-status&tab=logs&log_file=%s-%s-log', $handle, sanitize_file_name( wp_hash( $handle ) ) ) );
	}


	/**
	 * Gets the current WordPress site name.
	 *
	 * This is helpful for retrieving the actual site name instead of the
	 * network name on multisite installations.
	 *
	 * @since 4.6.0
	 * @return string
	 */
	public static function get_site_name() {

		return ( is_multisite() ) ? get_blog_details()->blogname : get_bloginfo( 'name' );
	}

	/**
	 * Determines whether we're on the WooCommerce checkout "pay page".
	 *
	 * @since 5.12.7
	 * @return bool
	 */
	public static function isCheckoutPayPage(): bool
	{
		global $wp_query;

		if (function_exists('is_checkout_pay_page') && ! empty($wp_query) && is_checkout_pay_page() === true) {
			return true;
		}

		/**
		 * This is a fallback, in case we need to check for the pay page very early in the lifecycle, when
		 * {@see is_checkout_pay_page()} would normally return false.
		 * An example of this is in {@see SV_WC_Payment_Gateway::get_order_button_text()}
		 */
		return isset($_GET['pay_for_order']);
	}


	/** JavaScript helper functions ***************************************/


	/**
	 * Enhanced search JavaScript (Select2)
	 *
	 * Enqueues JavaScript required for AJAX search with Select2.
	 *
	 * @codeCoverageIgnore no need to unit test this since it's mostly JavaScript
	 *
	 * @since 3.1.0
	 */
	public static function render_select2_ajax() {

		if ( ! did_action( 'sv_wc_select2_ajax_rendered' ) ) {

			$javascript = "( function(){
				if ( ! $().select2 ) return;
			";

			// Ensure localized strings are used.
			$javascript .= "

				function getEnhancedSelectFormatString() {

					if ( 'undefined' !== typeof wc_select_params ) {
						wc_enhanced_select_params = wc_select_params;
					}

					if ( 'undefined' === typeof wc_enhanced_select_params ) {
						return {};
					}

					var formatString = {
						formatMatches: function( matches ) {
							if ( 1 === matches ) {
								return wc_enhanced_select_params.i18n_matches_1;
							}

							return wc_enhanced_select_params.i18n_matches_n.replace( '%qty%', matches );
						},
						formatNoMatches: function() {
							return wc_enhanced_select_params.i18n_no_matches;
						},
						formatAjaxError: function( jqXHR, textStatus, errorThrown ) {
							return wc_enhanced_select_params.i18n_ajax_error;
						},
						formatInputTooShort: function( input, min ) {
							var number = min - input.length;

							if ( 1 === number ) {
								return wc_enhanced_select_params.i18n_input_too_short_1
							}

							return wc_enhanced_select_params.i18n_input_too_short_n.replace( '%qty%', number );
						},
						formatInputTooLong: function( input, max ) {
							var number = input.length - max;

							if ( 1 === number ) {
								return wc_enhanced_select_params.i18n_input_too_long_1
							}

							return wc_enhanced_select_params.i18n_input_too_long_n.replace( '%qty%', number );
						},
						formatSelectionTooBig: function( limit ) {
							if ( 1 === limit ) {
								return wc_enhanced_select_params.i18n_selection_too_long_1;
							}

							return wc_enhanced_select_params.i18n_selection_too_long_n.replace( '%qty%', number );
						},
						formatLoadMore: function( pageNumber ) {
							return wc_enhanced_select_params.i18n_load_more;
						},
						formatSearching: function() {
							return wc_enhanced_select_params.i18n_searching;
						}
					};

					return formatString;
				}
			";

			$javascript .= "

				$( 'select.sv-wc-enhanced-search' ).filter( ':not(.enhanced)' ).each( function() {

					var select2_args = {
						allowClear:         $( this ).data( 'allow_clear' ) ? true : false,
						placeholder:        $( this ).data( 'placeholder' ),
						minimumInputLength: $( this ).data( 'minimum_input_length' ) ? $( this ).data( 'minimum_input_length' ) : '3',
						escapeMarkup:       function( m ) {
							return m;
						},
						ajax:               {
							url:            '" . esc_js( admin_url( 'admin-ajax.php' ) ) . "',
							dataType:       'json',
							cache:          true,
							delay:          250,
							data:           function( params ) {
								return {
									term:         params.term,
									request_data: $( this ).data( 'request_data' ) ? $( this ).data( 'request_data' ) : {},
									action:       $( this ).data( 'action' ) || 'woocommerce_json_search_products_and_variations',
									security:     $( this ).data( 'nonce' )
								};
							},
							processResults: function( data, params ) {
								var terms = [];
								if ( data ) {
									$.each( data, function( id, text ) {
										terms.push( { id: id, text: text } );
									});
								}
								return { results: terms };
							}
						}
					};

					select2_args = $.extend( select2_args, getEnhancedSelectFormatString() );

					$( this ).select2( select2_args ).addClass( 'enhanced' );
				} );
			";

			$javascript .= '} )();';

			wc_enqueue_js( $javascript );

			/**
			 * WC Select2 Ajax Rendered Action.
			 *
			 * Fired when an Ajax select2 is rendered.
			 *
			 * @since 3.1.0
			 */
			do_action( 'sv_wc_select2_ajax_rendered' );
		}
	}


	/** Framework translation functions ***********************************/


	/**
	 * Gettext `__()` wrapper for framework-translated strings
	 *
	 * Warning! This function should only be used if an existing
	 * translation from the framework is to be used. It should
	 * never be called for plugin-specific or untranslated strings!
	 * Untranslated = not registered via string literal.
	 *
	 * @since 4.1.0
	 * @param string $text
	 * @return string translated text
	 */
	public static function f__( $text ) {

		return __( $text, 'woocommerce-plugin-framework' );
	}


	/**
	 * Gettext `_e()` wrapper for framework-translated strings
	 *
	 * Warning! This function should only be used if an existing
	 * translation from the framework is to be used. It should
	 * never be called for plugin-specific or untranslated strings!
	 * Untranslated = not registered via string literal.
	 *
	 * @since 4.1.0
	 * @param string $text
	 */
	public static function f_e( $text ) {

		_e( $text, 'woocommerce-plugin-framework' );
	}


	/**
	 * Gettext `_x()` wrapper for framework-translated strings
	 *
	 * Warning! This function should only be used if an existing
	 * translation from the framework is to be used. It should
	 * never be called for plugin-specific or untranslated strings!
	 * Untranslated = not registered via string literal.
	 *
	 * @since 4.1.0
	 *
	 * @param string $text
	 * @param string $context
	 * @return string translated text
	 */
	public static function f_x( $text, $context ) {

		return _x( $text, $context, 'woocommerce-plugin-framework' );
	}


	/** Misc functions ****************************************************/


	/**
	 * Gets the WordPress current screen.
	 *
	 * @see get_current_screen() replacement which is always available, unlike the WordPress core function
	 *
	 * @since 5.4.2
	 *
	 * @return \WP_Screen|null
	 */
	public static function get_current_screen() {
		global $current_screen;

		return $current_screen ?: null;
	}


	/**
	 * Checks if the current screen matches a specified ID.
	 *
	 * This helps avoiding using the get_current_screen() function which is not always available,
	 * or setting the substitute global $current_screen every time a check needs to be performed.
	 *
	 * @since 5.4.2
	 *
	 * @param string $id id (or property) to compare
	 * @param string $prop optional property to compare, defaults to screen id
	 * @return bool
	 */
	public static function is_current_screen( $id, $prop = 'id' ) {
		global $current_screen;

		return isset( $current_screen->$prop ) && $id === $current_screen->$prop;
	}


	/**
	 * Determines if viewing an enhanced admin screen.
	 *
	 * @since 5.6.0
	 *
	 * @return bool
	 */
	public static function is_enhanced_admin_screen() : bool
	{
		return is_admin() && SV_WC_Plugin_Compatibility::is_enhanced_admin_available() && (\Automattic\WooCommerce\Admin\PageController::is_admin_page() || \Automattic\WooCommerce\Admin\PageController::is_embed_page());
	}


	/**
	 * Determines whether the new WooCommerce enhanced navigation is supported and enabled.
	 *
	 * @since 5.10.6
	 *
	 * @deprecated with no alternatives
	 *
	 * @return bool
	 */
	public static function is_wc_navigation_enabled() : bool
	{
		if (SV_WC_Plugin_Compatibility::is_wc_version_gte('9.3')) {
			self::enhancedNavigationDeprecationNotice();
			return false;
		}

		return self::isEnhancedNavigationFeatureEnabled();
	}

	/**
	 * Determines whether Woo's Enhanced Eavigation feature is enabled.
	 *
	 * @since 5.15.1
	 *
	 * @return bool
	 */
	protected static function isEnhancedNavigationFeatureEnabled() : bool
	{
		return is_callable([\Automattic\WooCommerce\Admin\Features\Navigation\Screen::class, 'register_post_type']) &&
			is_callable([\Automattic\WooCommerce\Admin\Features\Navigation\Menu::class, 'add_plugin_item']) &&
			is_callable([\Automattic\WooCommerce\Admin\Features\Navigation\Menu::class, 'add_plugin_category']) &&
			is_callable([\Automattic\WooCommerce\Admin\Features\Features::class, 'is_enabled']) &&
			\Automattic\WooCommerce\Admin\Features\Features::is_enabled('navigation');
	}

	/**
	 * Logs a notice for the Enhanced Navigation feature being deprecated.
	 *
	 * @since 5.15.1
	 *
	 * @return void
	 */
	protected static function enhancedNavigationDeprecationNotice() : void
	{
		error_log('The Enhanced navigation feature has been deprecated since WooCommerce 9.3 with no alternative. Navigation classes will be removed in WooCommerce 9.4');
	}


	/**
	 * Determines if the current request is for a WC REST API endpoint.
	 *
	 * @see \WooCommerce::is_rest_api_request()
	 *
	 * @since 5.9.0
	 *
	 * @return bool
	 */
	public static function is_rest_api_request() {

		if ( is_callable( 'WC' ) && is_callable( [ WC(), 'is_rest_api_request' ] ) ) {
			return (bool) WC()->is_rest_api_request();
		}

		if ( empty( $_SERVER['REQUEST_URI'] ) || ! function_exists( 'rest_get_url_prefix' ) ) {
			return false;
		}

		$rest_prefix         = trailingslashit( rest_get_url_prefix() );
		$is_rest_api_request = false !== strpos( $_SERVER['REQUEST_URI'], $rest_prefix );

		/* applies WooCommerce core filter */
		return (bool) apply_filters( 'woocommerce_is_rest_api_request', $is_rest_api_request );
	}


	/**
	 * Displays a notice if the provided hook has not yet run.
	 *
	 * @since 5.2.0
	 *
	 * @param string $hook action hook to check
	 * @param string $method method/function name
	 * @param string $version version the notice was added
	 */
	public static function maybe_doing_it_early( $hook, $method, $version ) {

		if ( ! did_action( $hook ) ) {
			wc_doing_it_wrong( $method, "This should only be called after '{$hook}'", $version );
		}
	}


	/**
	 * Triggers a PHP error.
	 *
	 * This wrapper method ensures AJAX isn't broken in the process.
	 *
	 * @since 4.6.0
	 * @param string $message the error message
	 * @param int $type Optional. The error type. Defaults to E_USER_NOTICE
	 */
	public static function trigger_error( $message, $type = E_USER_NOTICE ) {

		if ( is_callable( 'wp_doing_ajax' ) && wp_doing_ajax() ) {

			switch ( $type ) {

				case E_USER_NOTICE:
					$prefix = 'Notice: ';
				break;

				case E_USER_WARNING:
					$prefix = 'Warning: ';
				break;

				default:
					$prefix = '';
			}

			error_log( $prefix . $message );

		} else {

			trigger_error( $message, $type );
		}
	}


	/**
	 * Converts an array of strings to a comma separated list of strings, escaped for SQL use.
	 *
	 * This can be safely used in SQL IN clauses.
	 *
	 * @since 5.10.9
	 *
	 * @param string[] $values
	 * @return string
	 */
	public static function get_escaped_string_list( array $values ) {
		global $wpdb;

		return (string) $wpdb->prepare( implode( ', ', array_fill( 0, count( $values ), '%s' ) ), $values );
	}


	/**
	 * Converts an array of numerical integers into a comma separated list of IDs.
	 *
	 * This can be safely used for SQL IN clauses.
	 *
	 * @since 5.10.9
	 *
	 * @param int[] $ids
	 * @return string
	 */
	public static function get_escaped_id_list( array $ids ) {

		return implode( ',', array_unique( array_map( 'intval', $ids ) ) );
	}


	/**
	 * Gets value of a meta key from WooCommerce object based on its data type.
	 *
	 * @param WP_Post|WC_Data $object
	 * @param string $field
	 * @param bool $single
	 *
	 * @return array|mixed|string|null
	 */
	public static function getWooCommerceObjectMetaValue($object, string $field, bool $single = true)
	{
		if ($object instanceof WP_Post) {
			return static::getPostOrObjectMetaCompat($object, null, $field, $single);
		}

		if ($object instanceof WC_Data) {
			return static::getPostOrObjectMetaCompat(null, $object, $field, $single);
		}

		return null;
	}

	/**
	 * Adds local compatibility for Woo's internal OrderUtil::get_post_or_object_meta() method.
	 *
	 * @param WP_Post|null $post
	 * @param WC_Data|null $data
	 * @param string       $key
	 * @param bool         $single
	 *
	 * @return array|false|mixed|string
	 */
	public static function getPostOrObjectMetaCompat(?\WP_Post $post, ?\WC_Data $data, string $key, bool $single)
	{
		if (isset($data)) {
			if (method_exists($data, "get$key")) {
				return $data->{"get$key"}();
			}
			return $data->get_meta($key, $single);
		} else {
			return isset($post->ID) ? get_post_meta($post->ID, $key, $single) : false;
		}
	}

}


endif;
