<?php
/**
 * WooCommerce Subscriptions Order Functions
 *
 * @author   Prospress
 * @category Core
 * @package  WooCommerce Subscriptions/Functions
 * @version  2.0
 */

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

/**
 * Get the subscription related to an order, if any.
 *
 * @param WC_Order|int $order An instance of a WC_Order object or the ID of an order
 * @param array $args A set of name value pairs to filter the returned value.
 *    'subscriptions_per_page' The number of subscriptions to return. Default set to -1 to return all.
 *    'offset' An optional number of subscription to displace or pass over. Default 0.
 *    'orderby' The field which the subscriptions should be ordered by. Can be 'start_date', 'trial_end_date', 'end_date', 'status' or 'order_id'. Defaults to 'start_date'.
 *    'order' The order of the values returned. Can be 'ASC' or 'DESC'. Defaults to 'DESC'
 *    'customer_id' The user ID of a customer on the site.
 *    'product_id' The post ID of a WC_Product_Subscription, WC_Product_Variable_Subscription or WC_Product_Subscription_Variation object
 *    'order_id' The post ID of a shop_order post/WC_Order object which was used to create the subscription
 *    'subscription_status' Any valid subscription status. Can be 'any', 'active', 'cancelled', 'on-hold', 'expired', 'pending' or 'trash'. Defaults to 'any'.
 *    'order_type' Get subscriptions for the any order type in this array. Can include 'any', 'parent', 'renewal' or 'switch', defaults to parent.
 * @return WC_Subscription[] Subscription details in post_id => WC_Subscription form.
 * @since  2.0
 */
function wcs_get_subscriptions_for_order( $order, $args = array() ) {

	$subscriptions = array();

	if ( ! is_a( $order, 'WC_Order' ) ) {
		$order = wc_get_order( $order );
	}

	if ( ! is_a( $order, 'WC_Order' ) ) {
		return $subscriptions;
	}

	$args = wp_parse_args(
		$args,
		array(
			'subscriptions_per_page' => -1,
			'order_type'             => array( 'parent', 'switch' ),
		)
	);

	// Accept either an array or string (to make it more convenient for singular types, like 'parent' or 'any')
	if ( ! is_array( $args['order_type'] ) ) {
		$args['order_type'] = array( $args['order_type'] );
	}

	$get_all = in_array( 'any', $args['order_type'] );

	if ( $get_all || in_array( 'parent', $args['order_type'] ) ) {

		$get_subscriptions_args = array_merge( $args, array(
			'order_id' => wcs_get_objects_property( $order, 'id' ),
		) );

		$subscriptions = wcs_get_subscriptions( $get_subscriptions_args );
	}

	$all_relation_types = WCS_Related_Order_Store::instance()->get_relation_types();
	$relation_types     = $get_all ? $all_relation_types : array_intersect( $all_relation_types, $args['order_type'] );

	foreach ( $relation_types as $relation_type ) {

		$subscription_ids = WCS_Related_Order_Store::instance()->get_related_subscription_ids( $order, $relation_type );

		foreach ( $subscription_ids as $subscription_id ) {
			if ( wcs_is_subscription( $subscription_id ) ) {
				$subscriptions[ $subscription_id ] = wcs_get_subscription( $subscription_id );
			}
		}
	}

	return $subscriptions;
}

/**
 * Copy the billing, shipping or all addresses from one order to another (including custom order types, like the
 * WC_Subscription order type).
 *
 * @param WC_Order $to_order The WC_Order object to copy the address to.
 * @param WC_Order $from_order The WC_Order object to copy the address from.
 * @param string $address_type The address type to copy, can be 'shipping', 'billing' or 'all'
 * @return WC_Order The WC_Order object with the new address set.
 * @since  2.0
 */
function wcs_copy_order_address( $from_order, $to_order, $address_type = 'all' ) {

	if ( in_array( $address_type, array( 'shipping', 'all' ) ) ) {
		$to_order->set_address( array(
			'first_name' => wcs_get_objects_property( $from_order, 'shipping_first_name' ),
			'last_name'  => wcs_get_objects_property( $from_order, 'shipping_last_name' ),
			'company'    => wcs_get_objects_property( $from_order, 'shipping_company' ),
			'address_1'  => wcs_get_objects_property( $from_order, 'shipping_address_1' ),
			'address_2'  => wcs_get_objects_property( $from_order, 'shipping_address_2' ),
			'city'       => wcs_get_objects_property( $from_order, 'shipping_city' ),
			'state'      => wcs_get_objects_property( $from_order, 'shipping_state' ),
			'postcode'   => wcs_get_objects_property( $from_order, 'shipping_postcode' ),
			'country'    => wcs_get_objects_property( $from_order, 'shipping_country' ),
		), 'shipping' );
	}

	if ( in_array( $address_type, array( 'billing', 'all' ) ) ) {
		$to_order->set_address( array(
			'first_name' => wcs_get_objects_property( $from_order, 'billing_first_name' ),
			'last_name'  => wcs_get_objects_property( $from_order, 'billing_last_name' ),
			'company'    => wcs_get_objects_property( $from_order, 'billing_company' ),
			'address_1'  => wcs_get_objects_property( $from_order, 'billing_address_1' ),
			'address_2'  => wcs_get_objects_property( $from_order, 'billing_address_2' ),
			'city'       => wcs_get_objects_property( $from_order, 'billing_city' ),
			'state'      => wcs_get_objects_property( $from_order, 'billing_state' ),
			'postcode'   => wcs_get_objects_property( $from_order, 'billing_postcode' ),
			'country'    => wcs_get_objects_property( $from_order, 'billing_country' ),
			'email'      => wcs_get_objects_property( $from_order, 'billing_email' ),
			'phone'      => wcs_get_objects_property( $from_order, 'billing_phone' ),
		), 'billing' );
	}

	return apply_filters( 'woocommerce_subscriptions_copy_order_address', $to_order, $from_order, $address_type );
}

/**
 * Utility function to copy order meta between two orders. Originally intended to copy meta between
 * first order and subscription object, then between subscription and renewal orders.
 *
 * The hooks used here in those cases are
 * - wcs_subscription_meta_query
 * - wcs_subscription_meta
 * - wcs_renewal_order_meta_query
 * - wcs_renewal_order_meta
 *
 * @param  WC_Order $from_order Order to copy meta from
 * @param  WC_Order $to_order   Order to copy meta to
 * @param  string $type type of copy
 */
function wcs_copy_order_meta( $from_order, $to_order, $type = 'subscription' ) {
	global $wpdb;

	if ( ! is_a( $from_order, 'WC_Abstract_Order' ) || ! is_a( $to_order, 'WC_Abstract_Order' ) ) {
		throw new InvalidArgumentException( _x( 'Invalid data. Orders expected aren\'t orders.', 'In wcs_copy_order_meta error message. Refers to origin and target order objects.', 'woocommerce-subscriptions' ) );
	}

	if ( ! is_string( $type ) ) {
		throw new InvalidArgumentException( _x( 'Invalid data. Type of copy is not a string.', 'Refers to the type of the copy being performed: "copy_order", "subscription", "renewal_order", "resubscribe_order"', 'woocommerce-subscriptions' ) );
	}

	if ( ! in_array( $type, array( 'subscription', 'parent', 'renewal_order', 'resubscribe_order' ) ) ) {
		$type = 'copy_order';
	}

	$meta_query = $wpdb->prepare(
		"SELECT `meta_key`, `meta_value`
		 FROM {$wpdb->postmeta}
		 WHERE `post_id` = %d
		 AND `meta_key` NOT LIKE '_schedule_%%'
		 AND `meta_key` NOT IN (
			 '_paid_date',
			 '_date_paid',
			 '_completed_date',
			 '_date_completed',
			 '_edit_last',
			 '_subscription_switch_data',
			 '_order_key',
			 '_edit_lock',
			 '_wc_points_earned',
			 '_transaction_id',
			 '_billing_interval',
			 '_billing_period',
			 '_subscription_resubscribe',
			 '_subscription_renewal',
			 '_subscription_switch',
			 '_payment_method',
			 '_payment_method_title',
			 '_suspension_count',
			 '_requires_manual_renewal',
			 '_cancelled_email_sent',
			 '_trial_period',
			 '_created_via',
			 '_order_stock_reduced'
		 )",
		wcs_get_objects_property( $from_order, 'id' )
	);

	if ( in_array( $type, array( 'renewal_order', 'parent' ) ) ) {
		$meta_query .= " AND `meta_key` NOT LIKE '_download_permissions_granted' ";
	}

	// Allow extensions to add/remove order meta
	$meta_query = apply_filters( 'wcs_' . $type . '_meta_query', $meta_query, $to_order, $from_order );
	$meta       = $wpdb->get_results( $meta_query, 'ARRAY_A' );
	$meta       = apply_filters( 'wcs_' . $type . '_meta', $meta, $to_order, $from_order );

	// Pre WC 3.0 we need to save each meta individually, post 3.0 we can save the object once
	$save = wcs_is_woocommerce_pre( '3.0' ) ? 'save' : 'set_prop_only';

	foreach ( $meta as $meta_item ) {
		wcs_set_objects_property( $to_order, $meta_item['meta_key'], maybe_unserialize( $meta_item['meta_value'] ), $save, '', 'omit_key_prefix' );
	}

	if ( is_callable( array( $to_order, 'save' ) ) ) {
		$to_order->save();
	}
}

/**
 * Function to create an order from a subscription. It can be used for a renewal or for a resubscribe
 * order creation. It is the common in both of those instances.
 *
 * @param  WC_Subscription|int $subscription Subscription we're basing the order off of
 * @param  string $type        Type of new order. Default values are 'renewal_order'|'resubscribe_order'
 * @return WC_Order|WP_Error New order or error object.
 */
function wcs_create_order_from_subscription( $subscription, $type ) {

	$type = wcs_validate_new_order_type( $type );

	if ( is_wp_error( $type ) ) {
		return $type;
	}

	try {
		$transaction = new WCS_SQL_Transaction();
		$transaction->start();

		if ( ! is_object( $subscription ) ) {
			$subscription = wcs_get_subscription( $subscription );
		}

		$new_order = wc_create_order( array(
			'customer_id'   => $subscription->get_user_id(),
			'customer_note' => $subscription->get_customer_note(),
			'created_via'   => 'subscription',
		) );

		wcs_copy_order_meta( $subscription, $new_order, $type );

		// Copy over line items and allow extensions to add/remove items or item meta
		$items = apply_filters( 'wcs_new_order_items', $subscription->get_items( array( 'line_item', 'fee', 'shipping', 'tax', 'coupon' ) ), $new_order, $subscription );
		$items = apply_filters( "wcs_{$type}_items", $items, $new_order, $subscription );

		foreach ( $items as $item ) {
			$item_name = apply_filters( 'wcs_new_order_item_name', $item->get_name(), $item, $subscription );
			$item_name = apply_filters( "wcs_{$type}_item_name", $item_name, $item, $subscription );

			// Create order line item on the renewal order
			$order_item_id = wc_add_order_item( $new_order->get_id(), array(
				'order_item_name' => $item_name,
				'order_item_type' => $item->get_type(),
			) );

			$order_item = $new_order->get_item( $order_item_id );

			wcs_copy_order_item( $item, $order_item );
			$order_item->save();

			// If the line item we're adding is a product line item and that product still exists, set any applicable backorder meta.
			if ( $item->is_type( 'line_item' ) && $item->get_product() ) {
				$order_item->set_backorder_meta();
				$order_item->save();
			}
		}

		// If we got here, the subscription was created without problems
		$transaction->commit();

		return apply_filters( 'wcs_new_order_created', $new_order, $subscription, $type );

	} catch ( Exception $e ) {
		// There was an error adding the subscription
		$transaction->rollback();
		return new WP_Error( 'new-order-error', $e->getMessage() );
	}
}

/**
 * Function to create a post title based on the type and the current date and time for new orders. By
 * default it's either renewal or resubscribe orders.
 *
 * @param  string $type type of new order. By default 'renewal_order'|'resubscribe_order'
 * @return string       new title for a post
 */
function wcs_get_new_order_title( $type ) {
	wcs_deprecated_function( __FUNCTION__, '2.2.0' );

	$type = wcs_validate_new_order_type( $type );

	// translators: placeholders are strftime() strings.
	$order_date = strftime( _x( '%b %d, %Y @ %I:%M %p', 'Used in subscription post title. "Subscription renewal order - <this>"', 'woocommerce-subscriptions' ) ); // phpcs:ignore WordPress.WP.I18n.UnorderedPlaceholdersText

	switch ( $type ) {
		case 'renewal_order':
			// translators: placeholder is a date.
			$title = sprintf( __( 'Subscription Renewal Order &ndash; %s', 'woocommerce-subscriptions' ), $order_date );
			break;
		case 'resubscribe_order':
			// translators: placeholder is a date.
			$title = sprintf( __( 'Resubscribe Order &ndash; %s', 'woocommerce-subscriptions' ), $order_date );
			break;
		default:
			$title = '';
			break;
	}

	return apply_filters( 'wcs_new_order_title', $title, $type, $order_date );
}

/**
 * Utility function to check type. Filterable. Rejects if not in allowed new order types, rejects
 * if not actually string.
 *
 * @param  string $type type of new order
 * @return string|WP_Error the same type thing if no problems are found, or WP_Error.
 */
function wcs_validate_new_order_type( $type ) {
	if ( ! is_string( $type ) ) {
		return new WP_Error( 'order_from_subscription_type_type', sprintf( __( '$type passed to the function was not a string.', 'woocommerce-subscriptions' ), $type ) );
	}

	if ( ! in_array( $type, apply_filters( 'wcs_new_order_types', array( 'renewal_order', 'resubscribe_order', 'parent' ) ) ) ) {
		// translators: placeholder is an order type.
		return new WP_Error( 'order_from_subscription_type', sprintf( __( '"%s" is not a valid new order type.', 'woocommerce-subscriptions' ), $type ) );
	}

	return $type;
}

/**
 * Wrapper function to get the address from an order / subscription in array format
 * @param  WC_Order $order The order / subscription we want to get the order from
 * @param  string $address_type shipping|billing. Default is shipping
 * @return array
 */
function wcs_get_order_address( $order, $address_type = 'shipping' ) {
	if ( ! is_a( $order, 'WC_Abstract_Order' ) ) {
		return array();
	}

	if ( 'billing' == $address_type ) {
		$address = array(
			'first_name' => wcs_get_objects_property( $order, 'billing_first_name' ),
			'last_name'  => wcs_get_objects_property( $order, 'billing_last_name' ),
			'company'    => wcs_get_objects_property( $order, 'billing_company' ),
			'address_1'  => wcs_get_objects_property( $order, 'billing_address_1' ),
			'address_2'  => wcs_get_objects_property( $order, 'billing_address_2' ),
			'city'       => wcs_get_objects_property( $order, 'billing_city' ),
			'state'      => wcs_get_objects_property( $order, 'billing_state' ),
			'postcode'   => wcs_get_objects_property( $order, 'billing_postcode' ),
			'country'    => wcs_get_objects_property( $order, 'billing_country' ),
			'email'      => wcs_get_objects_property( $order, 'billing_email' ),
			'phone'      => wcs_get_objects_property( $order, 'billing_phone' ),
		);
	} else {
		$address = array(
			'first_name' => wcs_get_objects_property( $order, 'shipping_first_name' ),
			'last_name'  => wcs_get_objects_property( $order, 'shipping_last_name' ),
			'company'    => wcs_get_objects_property( $order, 'shipping_company' ),
			'address_1'  => wcs_get_objects_property( $order, 'shipping_address_1' ),
			'address_2'  => wcs_get_objects_property( $order, 'shipping_address_2' ),
			'city'       => wcs_get_objects_property( $order, 'shipping_city' ),
			'state'      => wcs_get_objects_property( $order, 'shipping_state' ),
			'postcode'   => wcs_get_objects_property( $order, 'shipping_postcode' ),
			'country'    => wcs_get_objects_property( $order, 'shipping_country' ),
		);
	}

	return apply_filters( 'wcs_get_order_address', $address, $address_type, $order );
}

/**
 * Checks an order to see if it contains a subscription.
 *
 * @param mixed $order A WC_Order object or the ID of the order which the subscription was purchased in.
 * @param array|string $order_type Can include 'parent', 'renewal', 'resubscribe' and/or 'switch'. Defaults to 'parent', 'resubscribe' and 'switch' orders.
 * @return bool True if the order contains a subscription that belongs to any of the given order types, otherwise false.
 * @since 2.0
 */
function wcs_order_contains_subscription( $order, $order_type = array( 'parent', 'resubscribe', 'switch' ) ) {

	// Accept either an array or string (to make it more convenient for singular types, like 'parent' or 'any')
	if ( ! is_array( $order_type ) ) {
		$order_type = array( $order_type );
	}

	if ( ! is_a( $order, 'WC_Abstract_Order' ) ) {
		$order = wc_get_order( $order );
	}

	$contains_subscription = false;
	$get_all               = in_array( 'any', $order_type );

	if ( ( in_array( 'parent', $order_type ) || $get_all ) && count( wcs_get_subscriptions_for_order( wcs_get_objects_property( $order, 'id' ), array( 'order_type' => 'parent' ) ) ) > 0 ) {
		$contains_subscription = true;

	} elseif ( ( in_array( 'renewal', $order_type ) || $get_all ) && wcs_order_contains_renewal( $order ) ) {
		$contains_subscription = true;

	} elseif ( ( in_array( 'resubscribe', $order_type ) || $get_all ) && wcs_order_contains_resubscribe( $order ) ) {
		$contains_subscription = true;

	} elseif ( ( in_array( 'switch', $order_type ) || $get_all ) && wcs_order_contains_switch( $order ) ) {
		$contains_subscription = true;

	}

	return $contains_subscription;
}

/**
 * Get all the orders that relate to a subscription in some form (rather than only the orders associated with
 * a specific subscription).
 *
 * @param string $return_fields The columns to return, either 'all' or 'ids'
 * @param array|string $order_type Can include 'any', 'parent', 'renewal', 'resubscribe' and/or 'switch'. Defaults to 'parent'.
 * @return array The orders that relate to a subscription, if any. Will contain either as just IDs or WC_Order objects depending on $return_fields value.
 * @since 2.1
 */
function wcs_get_subscription_orders( $return_fields = 'ids', $order_type = 'parent' ) {
	global $wpdb;

	// Accept either an array or string (to make it more convenient for singular types, like 'parent' or 'any')
	if ( ! is_array( $order_type ) ) {
		$order_type = array( $order_type );
	}

	$any_order_type = in_array( 'any', $order_type );
	$return_fields  = ( 'ids' == $return_fields ) ? $return_fields : 'all';

	$orders    = array();
	$order_ids = array();

	if ( $any_order_type || in_array( 'parent', $order_type ) ) {
		$order_ids = array_merge( $order_ids, $wpdb->get_col(
			"SELECT DISTINCT post_parent FROM {$wpdb->posts}
			 WHERE post_type = 'shop_subscription'
			 AND post_parent <> 0"
		) );
	}

	if ( $any_order_type || in_array( 'renewal', $order_type ) || in_array( 'resubscribe', $order_type ) || in_array( 'switch', $order_type ) ) {

		$meta_query = array(
			'relation' => 'OR',
		);

		if ( $any_order_type || in_array( 'renewal', $order_type ) ) {
			$meta_query[] = array(
				'key'     => '_subscription_renewal',
				'compare' => 'EXISTS',
			);
		}

		if ( $any_order_type || in_array( 'switch', $order_type ) ) {
			$meta_query[] = array(
				'key'     => '_subscription_switch',
				'compare' => 'EXISTS',
			);
		}

		// $any_order_type handled by 'parent' query above as all resubscribe orders are all parent orders
		if ( in_array( 'resubscribe', $order_type ) && ! in_array( 'parent', $order_type ) ) {
			$meta_query[] = array(
				'key'     => '_subscription_resubscribe',
				'compare' => 'EXISTS',
			);
		}

		if ( count( $meta_query ) > 1 ) {
			$order_ids = array_merge( $order_ids, get_posts( array(
				'posts_per_page' => -1,
				'post_type'      => 'shop_order',
				'post_status'    => 'any',
				'fields'         => 'ids',
				'orderby'        => 'ID',
				'order'          => 'DESC',
				'meta_query'     => $meta_query,
			) ) );
		}
	}

	if ( 'all' == $return_fields ) {
		foreach ( $order_ids as $order_id ) {
			$orders[ $order_id ] = wc_get_order( $order_id );
		}
	} else {
		foreach ( $order_ids as $order_id ) {
			$orders[ $order_id ] = $order_id;
		}
	}

	return apply_filters( 'wcs_get_subscription_orders', $orders, $return_fields, $order_type );
}

/**
 * A wrapper for getting a specific item from an order or subscription.
 *
 * WooCommerce has a wc_add_order_item() function, wc_update_order_item() function and wc_delete_order_item() function,
 * but no `wc_get_order_item()` function, so we need to add our own (for now).
 *
 * @param int                      $item_id The ID of an order item
 * @param WC_Order|WC_Subscription $order   The order or order object the item belongs to.
 *
 * @return WC_Order_Item|array The order item object or an empty array if the item doesn't exist.
 *
 * @since 2.0
 */
function wcs_get_order_item( $item_id, $order ) {

	$item = array();

	if ( ! is_a( $order, 'WC_Abstract_Order' ) ) {
		throw new InvalidArgumentException( __( 'Invalid data. No valid subscription / order was passed in.', 'woocommerce-subscriptions' ), 422 );
	}

	if ( ! absint( $item_id ) ) {
		throw new InvalidArgumentException( __( 'Invalid data. No valid item id was passed in.', 'woocommerce-subscriptions' ), 422 );
	}

	foreach ( $order->get_items() as $line_item_id => $line_item ) {
		if ( $item_id == $line_item_id ) {
			$item = $line_item;
			break;
		}
	}

	return $item;
}

/**
 * A wrapper for wc_update_order_item() which consistently deletes the cached item after update, unlike WC.
 *
 * @param int $item_id The ID of an order item
 * @param string $new_type The new type to set as the 'order_item_type' value on the order item.
 * @param int $order_or_subscription_id The order or subscription ID the line item belongs to - optional. Deletes the order item cache if provided.
 * @since 2.2.12
 */
function wcs_update_order_item_type( $item_id, $new_type, $order_or_subscription_id = 0 ) {
	wc_update_order_item( $item_id, array( 'order_item_type' => $new_type ) );
	wp_cache_delete( 'item-' . $item_id, 'order-items' );

	// When possible, also clear the order items' cache for the object to which this item relates (double cache :sob:)
	if ( ! empty( $order_or_subscription_id ) ) {
		wp_cache_delete( 'order-items-' . $order_or_subscription_id, 'orders' );
	}
}

/**
 * Get an instance of WC_Order_Item_Meta for an order item
 *
 * @param array
 * @return WC_Order_Item_Meta
 * @since 2.0
 */
function wcs_get_order_item_meta( $item, $product = null ) {
	if ( false === wcs_is_woocommerce_pre( '3.0' ) ) {
		wcs_deprecated_function( __FUNCTION__, '3.1 of WooCommerce and 2.2.9 of Subscriptions', 'WC_Order_Item_Product->get_formatted_meta_data() or wc_display_item_meta()' );
	}
	return new WC_Order_Item_Meta( $item, $product );
}

/**
 * Create a string representing an order item's name and optionally include attributes.
 *
 * @param array $order_item An order item.
 * @since 2.0
 */
function wcs_get_order_item_name( $order_item, $include = array() ) {

	$include = wp_parse_args( $include, array(
		'attributes' => false,
	) );

	$order_item_name = $order_item['name'];

	if ( $include['attributes'] && ! empty( $order_item['item_meta'] ) ) {

		$attribute_strings = array();

		foreach ( $order_item['item_meta'] as $meta_key => $meta_value ) {

			$meta_value = wcs_is_woocommerce_pre( 3.0 ) ? $meta_value[0] : $meta_value;

			// Skip hidden core fields
			if ( in_array( $meta_key, apply_filters( 'woocommerce_hidden_order_itemmeta', array(
				'_qty',
				'_tax_class',
				'_product_id',
				'_variation_id',
				'_line_subtotal',
				'_line_subtotal_tax',
				'_line_total',
				'_line_tax',
				'_switched_subscription_item_id',
			) ) ) ) {
				continue;
			}

			// Skip serialised or array meta values
			if ( is_serialized( $meta_value ) || is_array( $meta_value ) ) {
				continue;
			}

			// Get attribute data
			if ( taxonomy_exists( wc_sanitize_taxonomy_name( $meta_key ) ) ) {
				$term       = get_term_by( 'slug', $meta_value, wc_sanitize_taxonomy_name( $meta_key ) );
				$meta_key   = wc_attribute_label( wc_sanitize_taxonomy_name( $meta_key ) );
				$meta_value = isset( $term->name ) ? $term->name : $meta_value;
			} else {
				$meta_key   = wc_attribute_label( $meta_key );
			}

			$attribute_strings[] = sprintf( '%s: %s', wp_kses_post( rawurldecode( $meta_key ) ), wp_kses_post( rawurldecode( $meta_value ) ) );
		}

		$order_item_name = sprintf( '%s (%s)', $order_item_name, implode( ', ', $attribute_strings ) );
	}

	return apply_filters( 'wcs_get_order_item_name', $order_item_name, $order_item, $include );
}

/**
 * Get the full name for a order/subscription line item, including the items non hidden meta
 * (i.e. attributes), as a flat string.
 *
 * @param array
 * @return string
 */
function wcs_get_line_item_name( $line_item ) {

	$item_meta_strings = array();

	foreach ( $line_item['item_meta'] as $meta_key => $meta_value ) {

		$meta_value = wcs_is_woocommerce_pre( 3.0 ) ? $meta_value[0] : $meta_value;

		// Skip hidden core fields
		if ( in_array( $meta_key, apply_filters( 'woocommerce_hidden_order_itemmeta', array(
			'_qty',
			'_tax_class',
			'_product_id',
			'_variation_id',
			'_line_subtotal',
			'_line_subtotal_tax',
			'_line_total',
			'_line_tax',
			'_line_tax_data',
		) ) ) ) {
			continue;
		}

		// Skip serialised or array meta values
		if ( is_serialized( $meta_value ) || is_array( $meta_value ) ) {
			continue;
		}

		// Get attribute data
		if ( taxonomy_exists( wc_sanitize_taxonomy_name( $meta_key ) ) ) {
			$term       = get_term_by( 'slug', $meta_value, wc_sanitize_taxonomy_name( $meta_key ) );
			$meta_key   = wc_attribute_label( wc_sanitize_taxonomy_name( $meta_key ) );
			$meta_value = isset( $term->name ) ? $term->name : $meta_value;
		} else {
			$meta_key   = wc_attribute_label( $meta_key );
		}

		$item_meta_strings[] = sprintf( '%s: %s', rawurldecode( $meta_key ), rawurldecode( $meta_value ) );
	}

	if ( ! empty( $item_meta_strings ) ) {
		$line_item_name = sprintf( '%s (%s)', $line_item['name'], implode( ', ', $item_meta_strings ) );
	} else {
		$line_item_name = $line_item['name'];
	}

	return apply_filters( 'wcs_line_item_name', $line_item_name, $line_item );
}

/**
 * Display item meta data in a version compatible way.
 *
 * @since  2.2.0
 * @param  WC_Item $item
 * @param  WC_Order $order
 * @return void
 */
function wcs_display_item_meta( $item, $order ) {
	if ( function_exists( 'wc_display_item_meta' ) ) { // WC 3.0+
		wc_display_item_meta( $item );
	} else {
		$order->display_item_meta( $item );
	}
}

/**
 * Display item download links in a version compatible way.
 *
 * @since  2.2.0
 * @param  WC_Item $item
 * @param  WC_Order $order
 * @return void
 */
function wcs_display_item_downloads( $item, $order ) {
	wcs_deprecated_function( __FUNCTION__, '2.5.0', 'wc_display_item_downloads( $item )' );

	if ( function_exists( 'wc_display_item_downloads' ) ) { // WC 3.0+
		wc_display_item_downloads( $item );
	} else {
		$order->display_item_downloads( $item );
	}
}

/**
 * Copy the order item data and meta data from one item to another.
 *
 * @since  2.2.0
 * @param  WC_Order_Item $from_item The order item to copy data from
 * @param  WC_Order_Item $to_item The order item to copy data to
 */
function wcs_copy_order_item( $from_item, &$to_item ) {

	if ( wcs_is_woocommerce_pre( '3.0' ) ) {
		wcs_doing_it_wrong( __FUNCTION__, 'This function uses data structures introduced in WC 3.0. To copy line item meta use $from_item[\'item_meta\'] and wc_add_order_item_meta().', '2.2' );
		return;
	}

	foreach ( $from_item->get_meta_data() as $meta_data ) {
		if ( '_reduced_stock' === $meta_data->key ) {
			continue;
		}

		$to_item->update_meta_data( $meta_data->key, $meta_data->value );
	}

	switch ( $from_item->get_type() ) {
		case 'line_item':
			/** @var WC_Order_Item_Product $from_item */
			$to_item->set_props( array(
				'product_id'   => $from_item->get_product_id(),
				'variation_id' => $from_item->get_variation_id(),
				'quantity'     => $from_item->get_quantity(),
				'tax_class'    => $from_item->get_tax_class(),
				'subtotal'     => $from_item->get_subtotal(),
				'total'        => $from_item->get_total(),
				'taxes'        => $from_item->get_taxes(),
			) );
			break;
		case 'shipping':
			/** @var WC_Order_Item_Shipping $from_item */
			$to_item->set_props( array(
				'method_id' => $from_item->get_method_id(),
				'total'     => $from_item->get_total(),
				'taxes'     => $from_item->get_taxes(),
			) );

			// Post WC 3.4 the instance ID is stored separately.
			if ( ! wcs_is_woocommerce_pre( '3.4' ) ) {
				$to_item->set_instance_id( $from_item->get_instance_id() );
			}

			break;
		case 'tax':
			/**
			 * @var WC_Order_Item_Tax $from_item
			 * @var WC_Order_Item_Tax $to_item
			 */
			$to_item->set_props( array(
				'rate_id'            => $from_item->get_rate_id(),
				'label'              => $from_item->get_label(),
				'compound'           => $from_item->get_compound(),
				'tax_total'          => $from_item->get_tax_total(),
				'shipping_tax_total' => $from_item->get_shipping_tax_total(),
			) );

			// WC 3.7.0+ Compatibility.
			if ( is_callable( array( $from_item, 'get_rate_percent' ) ) ) {
				$to_item->set_rate_percent( $from_item->get_rate_percent() );
			}

			break;
		case 'fee':
			/** @var WC_Order_Item_Fee $from_item */
			$to_item->set_props( array(
				'tax_class'  => $from_item->get_tax_class(),
				'tax_status' => $from_item->get_tax_status(),
				'total'      => $from_item->get_total(),
				'taxes'      => $from_item->get_taxes(),
			) );
			break;
		case 'coupon':
			/** @var WC_Order_Item_Coupon $from_item */
			$to_item->set_props( array(
				'discount'     => $from_item->get_discount(),
				'discount_tax' => $from_item->get_discount_tax(),
			) );
			break;
	}
}

/**
 * Checks an order to see if it contains a manual subscription.
 *
 * @since 2.4.3
 * @param WC_Order|int $order The WC_Order object or ID to get related subscriptions from.
 * @param string|array $order_type The order relationship type(s). Can be single string or an array of order types. Optional. Default is 'any'.
 * @return bool
 */
function wcs_order_contains_manual_subscription( $order, $order_type = 'any' ) {
	$subscriptions = wcs_get_subscriptions_for_order( $order, array( 'order_type' => $order_type ) );
	$contains_manual_subscription = false;

	foreach ( $subscriptions as $subscription ) {
		if ( $subscription->is_manual() ) {
			$contains_manual_subscription = true;
			break;
		}
	}

	return $contains_manual_subscription;
}

/**
 * Copy payment method from a subscription to an order.
 *
 * @since 2.4.3
 * @param WC_Subscription $subscription
 * @param WC_Order $order
 */
function wcs_copy_payment_method_to_order( $subscription, $order ) {
	// Set the order's payment method to match the subscription.
	if ( $order->get_payment_method() !== $subscription->get_payment_method() ) {
		$order->set_payment_method( $subscription->get_payment_method() );
	}

	// We only need to copy the subscription's post meta to the order. All other payment related meta should already exist.
	// Both post_meta and postmeta keys are supported by wcs_set_payment_meta().
	$payment_meta = array_intersect_key( $subscription->get_payment_method_meta(), array_flip( array( 'post_meta', 'postmeta' ) ) );
	$payment_meta = (array) apply_filters( 'wcs_copy_payment_meta_to_order', $payment_meta, $order, $subscription );

	if ( ! empty( $payment_meta ) ) {
		wcs_set_payment_meta( $order, $payment_meta );
	}

}

/**
 * Returns how many minutes ago the order was created.
 *
 * @param WC_Order $order
 *
 * @return int
 * @since 2.5.3
 */
function wcs_minutes_since_order_created( $order ) {
	$now             = new WC_DateTime( 'now', $order->get_date_created()->getTimezone() );
	$diff_in_minutes = $now->diff( $order->get_date_created() );

	return absint( $diff_in_minutes->i );
}

/**
 * Returns how many seconds ago the order was created.
 *
 * @param WC_Order $order
 *
 * @return int
 * @since 2.5.3
 */
function wcs_seconds_since_order_created( $order ) {
	return time() - $order->get_date_created()->getTimestamp();
}

/**
 * Finds a corresponding subscription line item on an order.
 *
 * @since 2.6.0
 *
 * @param WC_Abstract_Order $order         The order object to look for the item in.
 * @param WC_Order_Item $subscription_item The line item on the the subscription to find on the order.
 * @param string $match_type               Optional. The type of comparison to make. Can be 'match_product_ids' to compare product|variation IDs or 'match_attributes' to also compare by item attributes on top of matching product IDs. Default 'match_product_ids'.
 *
 * @return WC_Order_Item|bool The order item which matches the subscription item or false if one cannot be found.
 */
function wcs_find_matching_line_item( $order, $subscription_item, $match_type = 'match_product_ids' ) {
	$matching_item = false;

	if ( 'match_attributes' === $match_type ) {
		$subscription_item_attributes = wp_list_pluck( $subscription_item->get_formatted_meta_data( '_', true ), 'value', 'key' );
	}

	$subscription_item_canonical_product_id = wcs_get_canonical_product_id( $subscription_item );

	foreach ( $order->get_items() as $order_item ) {
		if ( wcs_get_canonical_product_id( $order_item ) !== $subscription_item_canonical_product_id ) {
			continue;
		}

		// Check if we have matching meta key and value pairs loosely - they can appear in any order,
		if ( 'match_attributes' === $match_type && wp_list_pluck( $order_item->get_formatted_meta_data( '_', true ), 'value', 'key' ) != $subscription_item_attributes ) {
			continue;
		}

		$matching_item = $order_item;
		break;
	}

	return $matching_item;
}

/**
 * Checks if an order contains a product.
 *
 * @since 2.6.0
 *
 * @param WC_Order $order     An order object
 * @param WC_Product $product A product object
 *
 * @return bool $order_has_product Whether the order contains a line item matching that product
 */
function wcs_order_contains_product( $order, $product ) {
	$order_has_product = false;
	$product_id        = wcs_get_canonical_product_id( $product );

	foreach ( $order->get_items() as $line_item ) {
		if ( wcs_get_canonical_product_id( $line_item ) === $product_id ) {
			$order_has_product = true;
			break;
		}
	}

	return $order_has_product;
}

/**
 * Check if a given order is a subscription renewal order.
 *
 * @param WC_Order|int $order The WC_Order object or ID of a WC_Order order.
 * @since 2.3.0
 * @return bool True if the order contains an early renewal, otherwise false.
 */
function wcs_order_contains_early_renewal( $order ) {

	if ( ! is_object( $order ) ) {
		$order = wc_get_order( $order );
	}

	$subscription_id  = absint( wcs_get_objects_property( $order, 'subscription_renewal_early' ) );
	$is_early_renewal = wcs_is_order( $order ) && $subscription_id > 0;

	/**
	 * Allow third-parties to filter whether this order contains the early renewal flag.
	 *
	 * @since 2.3.0
	 * @param bool     $is_renewal True if early renewal meta was found on the order, otherwise false.
	 * @param WC_Order $order The WC_Order object.
	 */
	return apply_filters( 'woocommerce_subscriptions_is_early_renewal_order', $is_early_renewal, $order );
}
