<?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.
 *
 * Uses wc_help_tip() when WooCommerce 2.5+ is active, otherwise it manually prints the HTML for a tooltip.
 *
 * @param string $tip The content to display in the tooltip.
 * @since  2.1.0
 * @return string
 */
function wcs_help_tip( $tip, $allow_html = false ) {

	if ( function_exists( 'wc_help_tip' ) ) {

		$help_tip = wc_help_tip( $tip, $allow_html );

	} else {

		if ( $allow_html ) {
			$tip = wc_sanitize_tooltip( $tip );
		} else {
			$tip = esc_attr( $tip );
		}

		$help_tip = sprintf( '<img class="help_tip" data-tip="%s" src="%s/assets/images/help.png" height="16" width="16" />', $tip, esc_url( WC()->plugin_url() ) );
	}

	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 ) {

	$prefixed_key = wcs_maybe_prefix_key( $property );

	$value = ! is_null( $default ) ? $default : ( ( 'single' == $single ) ? null : array() );

	switch ( $property ) {

		case 'name' : // the replacement for post_title added in 3.0
			if ( WC_Subscriptions::is_woocommerce_pre( '3.0' ) ) {
				$value = $object->post->post_title;
			} else { // WC 3.0+
				$value = $object->get_name();
			}
			break;

		case 'post' :
			if ( WC_Subscriptions::is_woocommerce_pre( '3.0' ) ) {
				$value = $object->post;
			} else { // WC 3.0+
				// 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( $object->get_parent_id() );
				} else {
					$value = get_post( $object->get_id() );
				}
			}
			break;

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

		case 'parent_id' :
			if ( method_exists( $object, 'get_parent_id' ) ) { // WC 3.0+ or an instance of WC_Product_Subscription_Variation_Legacy with WC < 3.0
				$value = $object->get_parent_id();
			} else { // WC 2.1-2.6
				$value = $object->get_parent();
			}
			break;

		case 'variation_data' :
			if ( function_exists( 'wc_get_product_variation_attributes' ) ) { // WC 3.0+
				$value = wc_get_product_variation_attributes( $object->get_id() );
			} else {
				$value = $object->$property;
			}
			break;

		case 'downloads' :
			if ( method_exists( $object, 'get_downloads' ) ) { // WC 3.0+
				$value = $object->get_downloads();
			} else {
				$value = $object->get_files();
			}
			break;

		case 'order_version' :
		case 'version' :
			if ( method_exists( $object, 'get_version' ) ) { // WC 3.0+
				$value = $object->get_version();
			} else { // WC 2.1-2.6
				$value = $object->order_version;
			}
			break;

		case 'order_currency' :
		case 'currency' :
			if ( method_exists( $object, 'get_currency' ) ) { // WC 3.0+
				$value = $object->get_currency();
			} else { // WC 2.1-2.6
				$value = $object->get_order_currency();
			}
			break;

		// Always return a PHP DateTime object in site timezone (or null), the same thing the WC_Order::get_date_created() method returns in WC 3.0+ to make it easier to migrate away from WC < 3.0
		case 'date_created' :
		case 'order_date' :
		case 'date' :
			if ( method_exists( $object, 'get_date_created' ) ) { // WC 3.0+
				$value = $object->get_date_created();
			} else {
				// Base the value off tht GMT value when possible and then set the DateTime's timezone based on the current site's timezone to avoid incorrect values when the timezone has changed
				if ( '0000-00-00 00:00:00' != $object->post->post_date_gmt ) {
					$value = new WC_DateTime( $object->post->post_date_gmt, new DateTimeZone( 'UTC' ) );
					$value->setTimezone( new DateTimeZone( wc_timezone_string() ) );
				} else {
					$value = new WC_DateTime( $object->post->post_date, new DateTimeZone( wc_timezone_string() ) );
				}
			}
			break;

		// Always return a PHP DateTime object in site timezone (or null), the same thing the getter returns in WC 3.0+ to make it easier to migrate away from WC < 3.0
		case 'date_paid' :
			if ( method_exists( $object, 'get_date_paid' ) ) { // WC 3.0+
				$value = $object->get_date_paid();
			} else {
				if ( ! empty( $object->paid_date ) ) {
					// Because the paid_date post meta value was set in the site timezone at the time it was set, this won't always be correct, but is the best we can do with WC < 3.0
					$value = new WC_DateTime( $object->paid_date, new DateTimeZone( wc_timezone_string() ) );
				} else {
					$value = null;
				}
			}
			break;

		case 'cart_discount' :
			if ( method_exists( $object, 'get_total_discount' ) ) { // WC 3.0+
				$value = $object->get_total_discount();
			} else { // WC 2.1-2.6
				$value = $object->cart_discount;
			}
			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' ) ) {
					if ( $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 ) && method_exists( $object, 'delete_meta_data_by_mid' ) ) {
		$object->delete_meta_data_by_mid( $meta_id );
	} elseif ( method_exists( $object, 'delete_meta_data' ) ) {
		$object->delete_meta_data( $prefixed_key );
	} elseif ( isset( $object->$key ) ) {
		unset( $object->$key );
	}

	// Save the data
	if ( 'save' === $save ) {
		if ( method_exists( $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 ( method_exists( $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 ( WC_Subscriptions::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 ( WC_Subscriptions::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;
}
