<?php

/**
 * @file
 * Rules integration for orders.
 *
 * @addtogroup rules
 * @{
 */

/**
 * Implements hook_rules_condition_info().
 */
function commerce_order_rules_condition_info() {
  $conditions = array();

  $conditions['commerce_order_compare_address'] = array(
    'label' => t('Order address component comparison'),
    'parameter' => array(
      'commerce_order' => array(
        'type' => 'commerce_order',
        'label' => t('Order'),
        'description' => t('The order containing the profile reference with the address in question.'),
      ),
      'address_field' => array(
        'type' => 'text',
        'label' => t('Address'),
        'options list' => 'commerce_order_address_field_options_list',
        'description' => t('The address associated with this order whose component you want to compare.'),
        'restriction' => 'input',
      ),
      'address_component' => array(
        'type' => 'text',
        'label' => t('Address component'),
        'options list' => 'commerce_order_address_component_options_list',
        'description' => t('The actual address component you want to compare. Common names of address components are given in parentheses.'),
        'restriction' => 'input',
      ),
      'operator' => array(
        'type' => 'text',
        'label' => t('Operator'),
        'description' => t('The comparison operator.'),
        'optional' => TRUE,
        'default value' => 'equals',
        'options list' => 'commerce_order_address_comparison_operator_options_list',
        'restriction' => 'input',
      ),
      'value' => array(
        'type' => 'text',
        'label' => t('Value'),
        'description' => t('The value to compare against the address component. Bear in mind that addresses using select lists for various components may use a value different from the option you select. For example, countries are selected by name, but the value is the two letter abbreviation. For comparisons with multiple possible values, place separate values on new lines.'),
      ),
    ),
    'group' => t('Commerce Order'),
    'callbacks' => array(
      'execute' => 'commerce_order_rules_compare_address',
    ),
  );

  $conditions['commerce_order_contains_product'] = array(
    'label' => t('Order contains a particular product'),
    'parameter' => array(
      'commerce_order' => array(
        'type' => 'commerce_order',
        'label' => t('Order'),
        'description' => t('The order whose line items should be checked for the specified product. If the specified order does not exist, the comparison will act as if it is against a quantity of 0.'),
      ),
      'product_id' => array(
        'type' => 'text',
        'label' => t('Product SKU'),
        'description' => t('The SKU of the product to look for on the order.'),
      ),
      'operator' => array(
        'type' => 'text',
        'label' => t('Operator'),
        'description' => t('The operator used with the quantity value below to compare the quantity of the specified product on the order.'),
        'default value' => '>=',
        'options list' => 'commerce_numeric_comparison_operator_options_list',
        'restriction' => 'input',
      ),
      'value' => array(
        'type' => 'text',
        'label' => t('Quantity'),
        'default value' => '1',
        'description' => t('The value to compare against the quantity of the specified product on the order.'),
      ),
    ),
    'group' => t('Commerce Order'),
    'callbacks' => array(
      'execute' => 'commerce_order_rules_contains_product',
    ),
  );

  $conditions['commerce_order_contains_product_type'] = array(
    'label' => t('Order contains products of particular product types'),
    'parameter' => array(
      'commerce_order' => array(
        'label' => t('Order'),
        'type' => 'commerce_order',
        'description' => t('The order whose line items should be checked for the specified product type. If the specified order does not exist, the comparison will act as if it is against a quantity of 0.'),
      ),
      'product_type' => array(
        'label' => t('Product type(s)'),
        'type' => 'list<text>',
        'description' => t('The product type(s) to look for in the order.'),
        'options list' => 'commerce_product_type_options_list',
      ),
      'operator' => array(
        'label' => t('Operator'),
        'type' => 'text',
        'description' => t('The operator used with the quantity value below to compare against the quantity of products matching the specified product type(s) on the order.'),
        'default value' => '>=',
        'options list' => 'commerce_numeric_comparison_operator_options_list',
        'restriction' => 'input',
      ),
      'value' => array(
        'label' => t('Quantity'),
        'type' => 'text',
        'default value' => '1',
        'description' => t('The value to compare against the quantity of products of the specified product type(s) on the order.'),
      ),
    ),
    'group' => t('Commerce Order'),
    'callbacks' => array(
      'execute' => 'commerce_order_rules_contains_product_type',
    ),
  );

  $conditions['commerce_order_compare_total_product_quantity'] = array(
    'label' => t('Total product quantity comparison'),
    'parameter' => array(
      'commerce_order' => array(
        'type' => 'commerce_order',
        'label' => t('Order'),
        'description' => t('The order whose product line item quantities should be totalled. If the specified order does not exist, the comparison will act as if it is against a quantity of 0.'),
      ),
      'operator' => array(
        'type' => 'text',
        'label' => t('Operator'),
        'description' => t('The comparison operator to use against the total number of products on the order.'),
        'default value' => '>=',
        'options list' => 'commerce_numeric_comparison_operator_options_list',
        'restriction' => 'input',
      ),
      'value' => array(
        'type' => 'text',
        'label' => t('Quantity'),
        'default value' => 1,
        'description' => t('The value to compare against the total quantity of products on the order.'),
      ),
    ),
    'group' => t('Commerce Order'),
    'callbacks' => array(
      'execute' => 'commerce_order_rules_compare_total_quantity',
    ),
  );

  return $conditions;
}

/**
 * Options list callback: address fields for the address comparison condition.
 */
function commerce_order_address_field_options_list() {
  $options = array();

  // Retrieve a list of all address fields on customer profile bundles.
  $address_fields = commerce_info_fields('addressfield', 'commerce_customer_profile');

  // Loop over every customer profile reference field on orders.
  foreach (commerce_info_fields('commerce_customer_profile_reference', 'commerce_order') as $field_name => $field) {
    // Retrieve the type of customer profile referenced by this field.
    $type = $field['settings']['profile_type'];

    // Loop over every address field looking for any attached to this bundle.
    foreach ($address_fields as $address_field_name => $address_field) {
      if (in_array($type, $address_field['bundles']['commerce_customer_profile'])) {
        // Add it to the options list.
        $instance = field_info_instance('commerce_customer_profile', 'commerce_customer_address', $type);
        $translated_instance = commerce_i18n_object('field_instance', $instance);

        $options[commerce_customer_profile_type_get_name($type)][$field_name . '|' . $address_field_name] = check_plain($translated_instance['label']);
      }
    }
  }

  if (empty($options)) {
    drupal_set_message(t('No order addresses could be found for comparison.'), 'error');
  }

  return $options;
}

/**
 * Options list callback: components for the address comparison condition.
 */
function commerce_order_address_component_options_list() {
  return array(
    'country' => t('Country'),
    'name_line' => t('Full name'),
    'first_name' => t('First name'),
    'last_name' => t('Last name'),
    'organisation_name' => t('Company name'),
    'thoroughfare' => t('Thoroughfare (Street address)'),
    'premise' => t('Premise (Building)'),
    'sub_premise' => t('Sub-premise (Suite)'),
    'locality' => t('Locality (City)'),
    'dependent_locality' => t('Dependent locality (Town)'),
    'administrative_area' => t('Administrative area (State / Province)'),
    'sub_administrative_area' => t('Sub-administrative area (District)'),
    'postal_code' => t('Postal code'),
  );
}

/**
 * Options list callback: operators for the address comparison condition.
 */
function commerce_order_address_comparison_operator_options_list() {
  return array(
    'equals' => t('equals'),
    'begins with' => t('begins with'),
    'contains' => t('contains'),
    'is one of' => t('is one of'),
    'begins with one of' => t('begins with one of'),
  );
}

/**
 * Condition callback: compares an address component against the given value.
 */
function commerce_order_rules_compare_address($order, $address_field, $component, $operator, $value) {
  list($field_name, $address_field_name) = explode('|', $address_field);

  // If we actually received a valid order...
  if (!empty($order)) {
    $wrapper = entity_metadata_wrapper('commerce_order', $order);

    // And if we can actually find the requested address data...
    if (!empty($wrapper->{$field_name}) && !empty($wrapper->{$field_name}->{$address_field_name})) {
      $address = $wrapper->{$field_name}->{$address_field_name}->value();

      // Perform the comparison in upper case.
      $address_component = drupal_strtoupper($address[$component]);
      $value = drupal_strtoupper($value);

      // Make a comparison based on the operator.
      switch ($operator) {
        case 'equals':
          return $address_component == $value;
        case 'begins with':
          return strpos($address_component, $value) === 0;
        case 'contains':
          return strpos($address_component, $value) !== FALSE;
        case 'is one of':
          $list = preg_split('/[\n\r]+/', $value);
          return array_search($address_component, $list) !== FALSE;
        case 'begins with one of':
          $list = preg_split('/[\n\r]+/', $value);
          foreach ($list as $item) {
            if (strpos($address_component, $item) === 0) {
              return TRUE;
            }
          }
          break;
      }
    }
  }

  return FALSE;
}

/**
 * Condition callback: checks to see if a particular product exists on an order
 * in the specified quantity.
 */
function commerce_order_rules_contains_product($order, $sku, $operator, $value) {
  $count = 0;

  // If we got a valid order with line items...
  if (!empty($order) && !empty($order->commerce_line_items)) {
    // Collect all the line item IDs on the order.
    $line_item_ids = array();

    if ($items = field_get_items('commerce_order', $order, 'commerce_line_items')) {
      foreach ($items as $item) {
        $line_item_ids[] = $item['line_item_id'];
      }
    }

    // If we found valid line item IDs...
    if (!empty($line_item_ids)) {
      // Get the product ID matching the given SKU.
      $product_id = db_query("SELECT product_id FROM {commerce_product} WHERE sku = :sku", array(':sku' => $sku))->fetchField();

      // Build a query to find the line items matching the given SKU.
      $query = new EntityFieldQuery();
      $query
        ->entityCondition('entity_type', 'commerce_line_item')
        ->entityCondition('bundle', commerce_product_line_item_types(), 'IN')
        ->entityCondition('entity_id', $line_item_ids, 'IN')
        ->fieldCondition('commerce_product', 'product_id', $product_id);
      $result = $query->execute();

      if (!empty($result['commerce_line_item'])) {
        $count = commerce_line_items_quantity_by_id(array_keys($result['commerce_line_item']));
      }
    }
  }

  // Make a quantity comparison based on the operator.
  switch ($operator) {
    case '<':
      return $count < $value;
    case '<=':
      return $count <= $value;
    case '=':
      return $count == $value;
    case '>=':
      return $count >= $value;
    case '>':
      return $count > $value;
  }

  return FALSE;
}

/**
 * Condition callback: checks to see if one or more particular product types exist on an order
 * in the specified quantity.
 */
function commerce_order_rules_contains_product_type($order, $product_types, $operator, $value) {
  $quantity = 0;

  // If we got a valid order with line items...
  if (!empty($order) && !empty($order->commerce_line_items)) {
    // Collect all the line item IDs on the order.
    $line_item_ids = array();

    if ($items = field_get_items('commerce_order', $order, 'commerce_line_items')) {
      foreach ($items as $item) {
        $line_item_ids[] = $item['line_item_id'];
      }
    }

    // If we found valid line item IDs...
    if (!empty($line_item_ids)) {
      // Build a query to find the order's product line items.
      $query = new EntityFieldQuery();
      $query
        ->entityCondition('entity_type', 'commerce_line_item')
        ->entityCondition('bundle', commerce_product_line_item_types(), 'IN')
        ->entityCondition('entity_id', $line_item_ids, 'IN');
      $line_item_result = $query->execute();

      // If the query discovered product line items...
      if (!empty($line_item_result['commerce_line_item'])) {
        // Load the product line items.
        $line_items = commerce_line_item_load_multiple(array_keys($line_item_result['commerce_line_item']));

        // Build an array of referenced product IDs.
        $product_ids_to_line_items = array();

        foreach ($line_items as $line_item) {
          if ($items = field_get_items('commerce_line_item', $line_item, 'commerce_product')) {
            $item = reset($items);
            $product_ids_to_line_items[$item['product_id']][] = $line_item->line_item_id;
          }
        }

        // If the search resulted in products to load...
        if (!empty($product_ids_to_line_items)) {
          // Build a query to find products in the appropriate types.
          $query = new EntityFieldQuery();
          $query
            ->entityCondition('entity_type', 'commerce_product')
            ->entityCondition('entity_id', array_keys($product_ids_to_line_items), 'IN')
            ->entityCondition('bundle', $product_types, 'IN');
          $product_result = $query->execute();

          // If the query discovered valid products...
          if (!empty($product_result['commerce_product'])) {
            // Loop over all valid product stubs and add their related line
            // item quantities to the running total.
            foreach (array_keys($product_result['commerce_product']) as $product_id) {
              foreach ($product_ids_to_line_items[$product_id] as $line_item_id) {
                $quantity += $line_items[$line_item_id]->quantity;
              }
            }
          }
        }
      }
    }
  }

  // Make a quantity comparison based on the operator.
  switch ($operator) {
    case '<':
      return $quantity < $value;
    case '<=':
      return $quantity <= $value;
    case '=':
      return $quantity == $value;
    case '>=':
      return $quantity >= $value;
    case '>':
      return $quantity > $value;
  }

  return FALSE;
}

/**
 * Condition callback: compares the total quantity of products on an order
 * against the specified quantity.
 */
function commerce_order_rules_compare_total_quantity($order, $operator, $value) {
  $quantity = 0;

  // If we got a valid order with line items...
  if (!empty($order) && !empty($order->commerce_line_items)) {
    // Collect all the line item IDs on the order.
    $line_item_ids = array();

    if ($items = field_get_items('commerce_order', $order, 'commerce_line_items')) {
      foreach ($items as $item) {
        $line_item_ids[] = $item['line_item_id'];
      }
    }

    // If we found valid line item IDs...
    if (!empty($line_item_ids)) {
      $quantity = commerce_line_items_quantity_by_id($line_item_ids, commerce_product_line_item_types());
    }
  }

  // Make a quantity comparison based on the operator.
  switch ($operator) {
    case '<':
      return $quantity < $value;
    case '<=':
      return $quantity <= $value;
    case '=':
      return $quantity == $value;
    case '>=':
      return $quantity >= $value;
    case '>':
      return $quantity > $value;
  }

  return FALSE;
}

/**
 * Implements hook_rules_action_info().
 */
function commerce_order_rules_action_info() {
  $actions = array();

  $actions['commerce_order_update_state'] = array(
    'label' => t('Update the order state'),
    'parameter' => array(
      'commerce_order' => array(
        'type' => 'commerce_order',
        'label' => t('Order to update'),
      ),
      'order_state' => array(
        'type' => 'text',
        'label' => t('Order state'),
        'description' => t('Select the order state whose default status the order will be updated to.'),
        'options list' => 'commerce_order_state_options_list',
      ),
    ),
    'group' => t('Commerce Order'),
    'callbacks' => array(
      'execute' => 'commerce_order_rules_update_state',
    ),
  );

  $actions['commerce_order_update_status'] = array(
    'label' => t('Update the order status'),
    'parameter' => array(
      'commerce_order' => array(
        'type' => 'commerce_order',
        'label' => t('Order to update'),
      ),
      'order_status' => array(
        'type' => 'text',
        'label' => t('Order status'),
        'options list' => 'commerce_order_status_options_list',
      ),
    ),
    'group' => t('Commerce Order'),
    'callbacks' => array(
      'execute' => 'commerce_order_rules_update_status',
    ),
  );

  return $actions;
}

/**
 * Rules action: updates an order's status to the default status of the given
 *   order state.
 */
function commerce_order_rules_update_state($order, $name) {
  $order_state = commerce_order_state_load($name);
  commerce_order_status_update($order, $order_state['default_status'], FALSE, NULL, t('Order state updated via Rules.'));
}

/**
 * Rules action: updates an order's status using the Order API.
 */
function commerce_order_rules_update_status($order, $name) {
  commerce_order_status_update($order, $name, FALSE, NULL, t('Order status updated via Rules.'));
}

/**
 * @}
 */
