<?php
/**
 * WooCommerce Compatibility functions
 *
 * Functions to take advantage of APIs added to new versions of WooCommerce while maintaining backward compatibility.
 *
 * @author Prospress
 * @category Core
 * @package WooCommerce Subscriptions/Functions
 * @version     2.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}


/**
 * Display a tooltip in the WordPress administration area.
 *
 * @since 2.1.0
 *
 * @param string $tip        The content to display in the tooltip.
 * @param bool   $allow_html Allow sanitized HTML if true or escape. Optional. False by default.
 * @param string $class      The help tip's class attribute. Optional. Default is 'woocommerce-help-tip'.
 *
 * @return string The helptip HTML.
 */
function wcs_help_tip( $tip, $allow_html = false, $class = 'woocommerce-help-tip' ) {
	$help_tip = wc_help_tip( $tip, $allow_html );

	if ( 'woocommerce-help-tip' !== $class ) {
		$help_tip = str_replace( 'woocommerce-help-tip', esc_attr( $class ), $help_tip );
	}

	return $help_tip;
}

/**
 * Access an object's property in a way that is compatible with CRUD and non-CRUD APIs for different versions of WooCommerce.
 *
 * We don't want to force the use of a custom legacy class for orders, similar to WC_Subscription_Legacy, because 3rd party
 * code may expect the object type to be WC_Order with strict type checks.
 *
 * A note on dates: in WC 3.0+, dates are returned a timestamps in the site's timezone :upside_down_face:. In WC < 3.0, they were
 * returned as MySQL strings in the site's timezone. We return them from here as MySQL strings in UTC timezone because that's how
 * dates are used in Subscriptions in almost all cases, for sanity's sake.
 *
 * @param WC_Order|WC_Product|WC_Subscription $object   The object whose property we want to access.
 * @param string                              $property The property name.
 * @param string                              $single   Whether to return just the first piece of meta data with the given property key, or all meta data.
 * @param mixed                               $default  (optional) The value to return if no value is found - defaults to single -> null, multiple -> array().
 *
 * @since  2.2.0
 * @return mixed
 */
function wcs_get_objects_property( $object, $property, $single = 'single', $default = null ) {
	$value = ! is_null( $default ) ? $default : ( ( 'single' === $single ) ? null : array() );

	if ( ! is_object( $object ) ) {
		return $value;
	}

	$prefixed_key          = wcs_maybe_prefix_key( $property );
	$property_function_map = array(
		'order_version'  => 'version',
		'order_currency' => 'currency',
		'order_date'     => 'date_created',
		'date'           => 'date_created',
		'cart_discount'  => 'total_discount',
	);

	if ( isset( $property_function_map[ $property ] ) ) {
		$property = $property_function_map[ $property ];
	}

	switch ( $property ) {
		case 'post':
			// In order to keep backwards compatibility it's required to use the parent data for variations.
			if ( method_exists( $object, 'is_type' ) && $object->is_type( 'variation' ) ) {
				$value = get_post( wcs_get_objects_property( $object, 'parent_id' ) );
			} else {
				$value = get_post( wcs_get_objects_property( $object, 'id' ) );
			}
			break;

		case 'post_status':
			$value = wcs_get_objects_property( $object, 'post' )->post_status;
			break;

		case 'variation_data':
			$value = wc_get_product_variation_attributes( wcs_get_objects_property( $object, 'id' ) );
			break;

		default:
			$function_name = 'get_' . $property;

			if ( is_callable( array( $object, $function_name ) ) ) {
				$value = $object->$function_name();
			} else {
				// If we don't have a method for this specific property, but we are using WC 3.0, it may be set as meta data on the object so check if we can use that.
				if ( method_exists( $object, 'get_meta' ) && $object->meta_exists( $prefixed_key ) ) {
					if ( 'single' === $single ) {
						$value = $object->get_meta( $prefixed_key, true );
					} else {
						// WC_Data::get_meta() returns an array of stdClass objects with id, key & value properties when meta is available.
						$value = wp_list_pluck( $object->get_meta( $prefixed_key, false ), 'value' );
					}
				} elseif ( 'single' === $single && isset( $object->$property ) ) { // WC < 3.0.
					$value = $object->$property;
				} elseif ( strtolower( $property ) !== 'id' && metadata_exists( 'post', wcs_get_objects_property( $object, 'id' ), $prefixed_key ) ) {
					// If we couldn't find a property or function, fallback to using post meta as that's what many __get() methods in WC < 3.0 did.
					if ( 'single' === $single ) {
						$value = get_post_meta( wcs_get_objects_property( $object, 'id' ), $prefixed_key, true );
					} else {
						// Get all the meta values.
						$value = get_post_meta( wcs_get_objects_property( $object, 'id' ), $prefixed_key, false );
					}
				}
			}
			break;
	}

	return $value;
}

/**
 * Set an object's property in a way that is compatible with CRUD and non-CRUD APIs for different versions of WooCommerce.
 *
 * @param WC_Order|WC_Product|WC_Subscription $object The object whose property we want to access.
 * @param string $key The meta key name without '_' prefix
 * @param mixed $value The data to set as the value of the meta
 * @param string $save Whether to write the data to the database or not. Use 'save' to write to the database, anything else to only update it in memory.
 * @param int $meta_id The meta ID of existing meta data if you wish to overwrite an existing piece of meta.
 * @param string $prefix_meta_key Whether the key should be prefixed with an '_' when stored in meta. Defaulted to 'prefix_meta_key', pass any other value to bypass automatic prefixing (optional)
 * @since  2.2.0
 * @return mixed
 */
function wcs_set_objects_property( &$object, $key, $value, $save = 'save', $meta_id = '', $prefix_meta_key = 'prefix_meta_key' ) {

	$prefixed_key = wcs_maybe_prefix_key( $key );

	// WC will automatically set/update these keys when a shipping/billing address attribute changes so we can ignore these keys
	if ( in_array( $prefixed_key, array( '_shipping_address_index', '_billing_address_index' ) ) ) {
		return;
	}

	// Special cases where properties with setters which don't map nicely to their function names
	$meta_setters_map = array(
		'_cart_discount'         => 'set_discount_total',
		'_cart_discount_tax'     => 'set_discount_tax',
		'_customer_user'         => 'set_customer_id',
		'_order_tax'             => 'set_cart_tax',
		'_order_shipping'        => 'set_shipping_total',
		'_sale_price_dates_from' => 'set_date_on_sale_from',
		'_sale_price_dates_to'   => 'set_date_on_sale_to',
	);

	// If we have a 3.0 object with a predefined setter function, use it
	if ( isset( $meta_setters_map[ $prefixed_key ] ) && is_callable( array( $object, $meta_setters_map[ $prefixed_key ] ) ) ) {
		$function = $meta_setters_map[ $prefixed_key ];
		$object->$function( $value );

	// If we have a 3.0 object, use the setter if available.
	} elseif ( is_callable( array( $object, 'set' . $prefixed_key ) ) ) {

		// Prices include tax is stored as a boolean in props but saved in the database as a string yes/no, so we need to normalise it here to make sure if we have a string (which can be passed to it by things like wcs_copy_order_meta()) that it's converted to a boolean before being set
		if ( '_prices_include_tax' === $prefixed_key && ! is_bool( $value ) ) {
			$value = 'yes' === $value;
		}

		$object->{ "set$prefixed_key" }( $value );

	// If there is a setter without the order prefix (eg set_order_total -> set_total)
	} elseif ( is_callable( array( $object, 'set' . str_replace( '_order', '', $prefixed_key ) ) ) ) {
		$function_name = 'set' . str_replace( '_order', '', $prefixed_key );
		$object->$function_name( $value );

	// If there is no setter, treat as meta within the 3.0.x object.
	} elseif ( is_callable( array( $object, 'update_meta_data' ) ) ) {
		$meta_key = ( 'prefix_meta_key' === $prefix_meta_key ) ? $prefixed_key : $key;
		$object->update_meta_data( $meta_key, $value, $meta_id );

	// 2.6.x handling for name which is not meta.
	} elseif ( 'name' === $key ) {
		$object->post->post_title = $value;

	// 2.6.x handling for everything else.
	} else {
		$object->$key = $value;
	}

	// Save the data
	if ( 'save' === $save ) {
		if ( is_callable( array( $object, 'save' ) ) ) { // WC 3.0+
			$object->save();
		} elseif ( 'date_created' == $key ) { // WC < 3.0+
			wp_update_post(
				array(
					'ID'            => wcs_get_objects_property( $object, 'id' ),
					'post_date'     => get_date_from_gmt( $value ),
					'post_date_gmt' => $value,
				)
			);
		} elseif ( 'name' === $key ) { // the replacement for post_title added in 3.0, need to update post_title not post meta
			wp_update_post(
				array(
					'ID'         => wcs_get_objects_property( $object, 'id' ),
					'post_title' => $value,
				)
			);
		} else {
			$meta_key = ( 'prefix_meta_key' === $prefix_meta_key ) ? $prefixed_key : $key;

			if ( ! empty( $meta_id ) ) {
				update_metadata_by_mid( 'post', $meta_id, $value, $meta_key );
			} else {
				update_post_meta( wcs_get_objects_property( $object, 'id' ), $meta_key, $value );
			}
		}
	}
}

/**
 * Delete an object's property in a way that is compatible with CRUD and non-CRUD APIs for different versions of WooCommerce.
 *
 * @param WC_Order|WC_Product|WC_Subscription $object The object whose property we want to access.
 * @param string $key The meta key name without '_' prefix
 * @param mixed $value The data to set as the value of the meta
 * @param string $save Whether to save the data or not, 'save' to save the data, otherwise it won't be saved.
 * @since  2.2.0
 * @return mixed
 */
function wcs_delete_objects_property( &$object, $key, $save = 'save', $meta_id = '' ) {

	$prefixed_key = wcs_maybe_prefix_key( $key );

	if ( ! empty( $meta_id ) && is_callable( array( $object, 'delete_meta_data_by_mid' ) ) ) {
		$object->delete_meta_data_by_mid( $meta_id );
	} elseif ( is_callable( array( $object, 'delete_meta_data' ) ) ) {
		$object->delete_meta_data( $prefixed_key );
	} elseif ( isset( $object->$key ) ) {
		unset( $object->$key );
	}

	// Save the data
	if ( 'save' === $save ) {
		if ( is_callable( array( $object, 'save' ) ) ) { // WC 3.0+
			$object->save();
		} elseif ( ! empty( $meta_id ) ) {
			delete_metadata_by_mid( 'post', $meta_id );
		} else {
			delete_post_meta( wcs_get_objects_property( $object, 'id' ), $prefixed_key );
		}
	}
}

/**
 * Check whether an order is a standard order (i.e. not a refund or subscription) in version compatible way.
 *
 * WC 3.0 has the $order->get_type() API which returns 'shop_order', while WC < 3.0 provided the $order->order_type
 * property which returned 'simple', so we need to check for both.
 *
 * @param WC_Order $order
 * @since  2.2.0
 * @return bool
 */
function wcs_is_order( $order ) {

	if ( is_callable( array( $order, 'get_type' ) ) ) {
		$is_order = ( 'shop_order' === $order->get_type() );
	} else {
		$is_order = ( isset( $order->order_type ) && 'simple' === $order->order_type );
	}

	return $is_order;
}

/**
 * Find and return the value for a deprecated property property.
 *
 * Product properties should not be accessed directly with WooCommerce 3.0+, because of that, a lot of properties
 * have been deprecated/removed in the subscription product type classes. This function centralises the handling
 * of deriving deprecated properties. This saves duplicating the __get() method in WC_Product_Subscription,
 * WC_Product_Variable_Subscription and WC_Product_Subscription_Variation.
 *
 * @param string $property
 * @param WC_Product $product
 * @since  2.2.0
 * @return mixed
 */
function wcs_product_deprecated_property_handler( $property, $product ) {

	$message_prefix = 'Product properties should not be accessed directly with WooCommerce 3.0+.';
	$function_name  = 'get_' . str_replace( 'subscription_', '', str_replace( 'subscription_period_', '', $property ) );
	$class_name     = get_class( $product );
	$value          = null;

	if ( in_array( $property, array( 'product_type', 'parent_product_type', 'limit_subscriptions', 'subscription_limit', 'subscription_payment_sync_date', 'subscription_one_time_shipping' ) ) || ( is_callable( array( 'WC_Subscriptions_Product', $function_name ) ) && false !== strpos( $property, 'subscription' ) ) ) {

		switch ( $property ) {
			case 'product_type':
				$value       = $product->get_type();
				$alternative = $class_name . '::get_type()';
				break;

			case 'parent_product_type':
				if ( $product->is_type( 'subscription_variation' ) ) {
					$value       = 'variation';
					$alternative = 'WC_Product_Variation::get_type()';
				} else {
					$value       = 'variable';
					$alternative = 'WC_Product_Variable::get_type()';
				}
				break;

			case 'limit_subscriptions':
			case 'subscription_limit':
				$value       = wcs_get_product_limitation( $product );
				$alternative = 'wcs_get_product_limitation( $product )';
				break;

			case 'subscription_one_time_shipping':
				$value       = WC_Subscriptions_Product::needs_one_time_shipping( $product );
				$alternative = 'WC_Subscriptions_Product::needs_one_time_shipping( $product )';
				break;

			case 'subscription_payment_sync_date':
				$value       = WC_Subscriptions_Synchroniser::get_products_payment_day( $product );
				$alternative = 'WC_Subscriptions_Synchroniser::get_products_payment_day( $product )';
				break;

			case 'max_variation_period':
			case 'max_variation_period_interval':
				$meta_key = '_' . $property;
				if ( '' === $product->get_meta( $meta_key ) ) {
					WC_Product_Variable::sync( $product->get_id() );
				}
				$value       = $product->get_meta( $meta_key );
				$alternative = $class_name . '::get_meta( ' . $meta_key . ' ) or wcs_get_min_max_variation_data( $product )';
				break;

			default:
				$value       = call_user_func( array( 'WC_Subscriptions_Product', $function_name ), $product );
				$alternative = sprintf( 'WC_Subscriptions_Product::%s( $product )', $function_name );
				break;
		}

		wcs_deprecated_argument( $class_name . '::$' . $property, '2.2.0', sprintf( '%s Use %s', $message_prefix, $alternative ) );
	}

	return $value;
}

/**
 * Access a coupon's property in a way that is compatible with CRUD and non-CRUD APIs for different versions of WooCommerce.
 *
 * Similar to @see wcs_get_objects_property
 *
 * @param WC_Coupon $coupon The coupon whose property we want to access.
 * @param string $property The property name.
 * @since  2.2
 * @return mixed
 */
function wcs_get_coupon_property( $coupon, $property ) {

	$value = '';

	if ( wcs_is_woocommerce_pre( '3.0' ) ) {
		$value = $coupon->$property;
	} else {
		// Some coupon properties don't map nicely to their corresponding getter function. This array contains those exceptions.
		$property_to_getter_map = array(
			'type'                       => 'get_discount_type',
			'exclude_product_ids'        => 'get_excluded_product_ids',
			'expiry_date'                => 'get_date_expires',
			'exclude_product_categories' => 'get_excluded_product_categories',
			'customer_email'             => 'get_email_restrictions',
			'enable_free_shipping'       => 'get_free_shipping',
			'coupon_amount'              => 'get_amount',
		);

		switch ( true ) {
			case 'exists' == $property:
				$value = $coupon->get_id() > 0;
				break;
			case isset( $property_to_getter_map[ $property ] ) && is_callable( array( $coupon, $property_to_getter_map[ $property ] ) ):
				$function = $property_to_getter_map[ $property ];
				$value    = $coupon->$function();
				break;
			case is_callable( array( $coupon, 'get_' . $property ) ):
				$value = $coupon->{ "get_$property" }();
				break;
		}
	}

	return $value;
}

/**
 * Set a coupon's property in a way that is compatible with CRUD and non-CRUD APIs for different versions of WooCommerce.
 *
 * Similar to @see wcs_set_objects_property
 *
 * @param WC_Coupon $coupon The coupon whose property we want to set.
 * @param string $property The property name.
 * @param mixed $value The data to set as the value
 * @since  2.2
 */
function wcs_set_coupon_property( &$coupon, $property, $value ) {

	if ( wcs_is_woocommerce_pre( '3.0' ) ) {
		$coupon->$property = $value;
	} else {
		// Some coupon properties don't map nicely to their corresponding setter function. This array contains those exceptions.
		$property_to_setter_map = array(
			'type'                       => 'set_discount_type',
			'exclude_product_ids'        => 'set_excluded_product_ids',
			'expiry_date'                => 'set_date_expires',
			'exclude_product_categories' => 'set_excluded_product_categories',
			'customer_email'             => 'set_email_restrictions',
			'enable_free_shipping'       => 'set_free_shipping',
			'coupon_amount'              => 'set_amount',
		);

		switch ( true ) {
			case 'individual_use' == $property:
				// set_individual_use expects a boolean, the individual_use property use to be either 'yes' or 'no' so we need to accept both types
				if ( ! is_bool( $value ) ) {
					$value = 'yes' === $value;
				}

				$coupon->set_individual_use( $value );
				break;
			case isset( $property_to_setter_map[ $property ] ) && is_callable( array( $coupon, $property_to_setter_map[ $property ] ) ):
				$function = $property_to_setter_map[ $property ];
				$coupon->$function( $value );

				break;
			case is_callable( array( $coupon, 'set_' . $property ) ):
				$coupon->{ "set_$property" }( $value );
				break;
		}
	}
}

/**
 * Generate an order/subscription key.
 *
 * This is a compatibility wrapper for @see wc_generate_order_key() which was introduced in WC 3.5.4.
 *
 * @return string $order_key.
 * @since 2.5.0
 */
function wcs_generate_order_key() {

	if ( function_exists( 'wc_generate_order_key' ) ) {
		$order_key = wc_generate_order_key();
	} else {
		$order_key = 'wc_' . apply_filters( 'woocommerce_generate_order_key', 'order_' . wp_generate_password( 13, false ) );
	}

	return $order_key;
}

/**
 * Update a single option for a WC_Settings_API object.
 *
 * This is a compatibility wrapper for @see WC_Settings_API::update_option() which was introduced in WC 3.4.0.
 *
 * @param WC_Settings_API $settings_api The object to update the option for.
 * @param string $key Option key.
 * @param mixed $value Value to set.
 * @since 2.5.1
 */
function wcs_update_settings_option( $settings_api, $key, $value ) {

	// WooCommerce 3.4+
	if ( is_callable( array( $settings_api, 'update_option' ) ) ) {
		$settings_api->update_option( $key, $value );
	} else {
		if ( empty( $settings_api->settings ) ) {
			$settings_api->init_settings();
		}

		$settings_api->settings[ $key ] = $value;

		return update_option( $settings_api->get_option_key(), apply_filters( 'woocommerce_settings_api_sanitized_fields_' . $settings_api->id, $settings_api->settings ), 'yes' );
	}
}

/**
 * Determines if the request is a non-legacy REST API request.
 *
 * This function is a compatibility wrapper for WC()->is_rest_api_request() which was introduced in WC 3.6.
 *
 * @since 2.5.7
 *
 * @return bool True if it's a REST API request, false otherwise.
 */
function wcs_is_rest_api_request() {

	if ( function_exists( 'WC' ) && is_callable( array( WC(), 'is_rest_api_request' ) ) ) {
		return WC()->is_rest_api_request();
	}

	if ( empty( $_SERVER['REQUEST_URI'] ) ) {
		return false;
	}

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

	return apply_filters( 'woocommerce_is_rest_api_request', $is_rest_api_request );
}

/**
 * Determines whether the current request is a WordPress cron request.
 *
 * This function is a compatibility wrapper for wp_doing_cron() which was introduced in WP 4.8.
 *
 * @since 2.5.7
 *
 * @return bool True if it's a WordPress cron request, false otherwise.
 */
function wcs_doing_cron() {
	return function_exists( 'wp_doing_cron' ) ? wp_doing_cron() : defined( 'DOING_CRON' ) && DOING_CRON;
}

/**
 * Determines whether the current request is a WordPress Ajax request.
 *
 * @since 2.5.7
 *
 * @return bool True if it's a WordPress Ajax request, false otherwise.
 */
function wcs_doing_ajax() {
	return function_exists( 'wp_doing_ajax' ) ? wp_doing_ajax() : defined( 'DOING_AJAX' ) && DOING_AJAX;
}

/**
 * A wrapper function for getting an order's used coupon codes.
 *
 * WC 3.7 deprecated @see WC_Abstract_Order::get_used_coupons() in favour of WC_Abstract_Order::get_coupon_codes().
 *
 * @since 2.6.0
 *
 * @param WC_Abstract_Order $order An order or subscription object to get the coupon codes for.
 * @return array The coupon codes applied to the $order.
 */
function wcs_get_used_coupon_codes( $order ) {
	return is_callable( array( $order, 'get_coupon_codes' ) ) ? $order->get_coupon_codes() : $order->get_used_coupons();
}

/**
 * Attach a function callback for a certain WooCommerce versions.
 *
 * Enables attaching a callback if WooCommerce is before, after, equal or not equal to a given version.
 * This function is a wrapper for @see WCS_Dependent_Hook_Manager::add_woocommerce_dependent_action().
 *
 * @since 2.6.0
 *
 * @param string $tag The action or filter tag to attach the callback too.
 * @param string|array $function The callable function to attach to the hook.
 * @param string $woocommerce_version The WooCommerce version to do a compare on. For example '3.0.0'.
 * @param string $operator The version compare operator to use. @see https://www.php.net/manual/en/function.version-compare.php
 * @param integer $priority The priority to attach this callback to.
 * @param integer $number_of_args The number of arguments to pass to the callback function
 */
function wcs_add_woocommerce_dependent_action( $tag, $function, $woocommerce_version, $operator, $priority = 10, $number_of_args = 1 ) {
	WCS_Dependent_Hook_Manager::add_woocommerce_dependent_action( $tag, $function, $woocommerce_version, $operator, $priority, $number_of_args );
}

/**
 * Checks if the installed version of WooCommerce is older than a specified version.
 *
 * @since 4.0.0
 *
 * @param  string The version string to check in a version_compare() compatible format.
 * @return bool   Whether the installed version of WC is prior to the given version string.
 */
function wcs_is_woocommerce_pre( $version ) {
	return ! defined( 'WC_VERSION' ) || version_compare( WC_VERSION, $version, '<' );
}
