<?php

/**
 * @file
 * Provides an rules based api for implementing order quantity
 * and stock validation
 *
 * This module provides the events to trigger the stock checks and
 * configurable actions to take if the check fails "out of stock:
 * It is the job of sub modules to implement the stock check by
 * configuring rules and their conditions.
 */

/**
 * Implements hook_menu().
 */
function commerce_stock_menu() {
  $items = array();

  $items['admin/commerce/config/stock'] = array(
    'title' => 'Stock management',
    'description' => 'Configure stock management.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('commerce_stock_admin_form'),
    'access arguments' => array('administer commerce_stock settings'),
    'file' => 'includes/commerce_stock.admin.inc',
    'type'  => MENU_NORMAL_ITEM,
  );
  $items['admin/commerce/config/stock/api'] = array(
    'title' => 'Stock management API',
    'description' => 'Configure stock management API.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('commerce_stock_admin_form'),
    'access arguments' => array('administer commerce_stock settings'),
    'file' => 'includes/commerce_stock.admin.inc',
    'type'  => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );

  return $items;
}

/**
 * Implements hook_permission().
 */
function commerce_stock_permission() {
  return array(
    'administer commerce_stock settings' => array(
      'title' => t('Administer commerce stock settings'),
    ),
    'make rule based changes to commerce_stock' => array(
      'title' => t('Make rule based changes to commerce stock'),
    ),

  );
}


/**
 * Implements hook_form_alter().
 *
 * Alters the add-to-cart form to show out-of-stock items and add a validator.
 */
function commerce_stock_form_alter(&$form, &$form_state, $form_id) {
  if (strpos($form_id, "commerce_cart_add_to_cart_form") === 0) {
    // Check if product is disabled.
    if (isset($form['submit']['#attributes']['disabled']) && ($form['submit']['#attributes']['disabled'] == 'disabled')) {
      return;
    }
    // Check to see if product has options (multiple products using
    // the default dropdown).
    if (isset($form['product_id']['#options'])) {
      // Set validation.
      $form['#validate'][] = 'commerce_stock_add_to_cart_validate';
      commerce_stock_cart_state_validate_options($form_id, $form, $form_state);
    }
    // A single product or uses attributes (like colour & size).
    elseif (isset($form['product_id']['#value'])) {
      // @todo new rules event for handling options - do we need it?
      // Add validation to the add to cart
      $form['#validate'][] = 'commerce_stock_add_to_cart_validate';
      // Check if the add to cart form should be enabled (in stock).
      commerce_stock_cart_state_validate($form_id, $form, $form_state);
    }
  }
  elseif ($form_id == 'views_form_commerce_cart_form_default') {
    // Add validate function to the cart form.
    $form['#validate'][] = 'commerce_stock_form_commerce_cart_validate';
  }
  elseif ($form_id == 'commerce_checkout_form_checkout') {
    // Add validate function to the checkout form.
    $form['buttons']['continue']['#validate'][] = 'commerce_stock_checkout_form_validate';
  }
  elseif ($form_id == 'commerce_checkout_form_review') {
    // Add validate function to the review form.
    // @todo: would be good to prompt the user with some contextual info
    // as he was about to pay.
    $form['buttons']['continue']['#validate'][] = 'commerce_stock_checkout_form_validate';
  }

}

/**
 * Implements hook_commerce_checkout_pane_info().
 *
 * This creates the stock checkout pane. It should be placed on the first stage
 * of checkout. It checks if all items are in stock and if not redirects the
 * user to their cart.
 */
function commerce_stock_commerce_checkout_pane_info() {
  $checkout_panes = array();

  $checkout_panes['stock_validation_checkout_pane'] = array(
    'title' => t('check if all items are in stock at checkout'),
    'base' => 'commerce_stock_commerce_checkout_pane',
    'page' => 'checkout',
    'fieldset' => FALSE,
  );

  return $checkout_panes;
}

/**
 * Form validation handler for commerce_cart_add_to_cart_form().
 *
 * For products with options (product dropdown) checks if the add to cart form
 * should be enabled (in stock).
 *
 * @see commerce_cart_add_to_cart_form()
 */
function commerce_stock_cart_state_validate_options($form_id, &$form, &$form_state) {
  $product_id = $form['product_id']['#default_value'];
  $product = commerce_product_load($product_id);
  $qty_ordered = commerce_stock_check_cart_product_level($product_id);

  // Initialize the form.
  $form['submit']['#value'] = t('Add to cart');
  $form['submit']['#disabled'] = FALSE;
  $form['#attributes']['class']['stock'] = 'in-stock';

  // Set global form for stock actions.
  global $stock_cart_check_data;
  $stock_cart_check_data = array(
    'form' => &$form,
  );

  // Integration with rules_form_alter().
  if (module_exists('rules_form_alter')) {
    // make sure rules_form_alter actions work from the stock event.
    global $rules_form_alter_data;
    $rules_form_alter_data = array(
      'id'  => $form_id,
      'form' => &$form,
      'state' => &$form_state,
    );

  }
  // Invoke the stock check event.
  rules_invoke_event('commerce_stock_check_add_to_cart_form_state', $product, $qty_ordered, $form);
}

/**
 * Form validation handler for commerce_cart_add_to_cart_form().
 *
 * For product display with one product or attributes. Validates the product and
 * quantity to add to the cart. Also checks if the add to cart form should be
 * enabled (in stock).
 *
 * @see commerce_cart_add_to_cart_form()
 */
function commerce_stock_add_to_cart_validate($form, &$form_state) {
  if ($form_state['submitted']) {
    // Get product and quantity.
    $qty = $form_state['values']['quantity'];
    $product_id = $form_state['values']['product_id'];
    $product = commerce_product_load($product_id);
    $qty_ordered = commerce_stock_check_cart_product_level($product_id);
    // Check using rules.
    commerce_stock_check_product_rule($product, $qty, $qty_ordered, $stock_state, $message);
    // Action.
    if ($stock_state == 1) {
      form_set_error("stock_error", $message);
    }
    elseif ($stock_state == 2) {
      $form_state['values']['quantity'] = $qty;
      drupal_set_message($message);
    }
  }
}

/**
 * Form validation handler for views_form_commerce_cart_form_default().
 *
 * Checks each line item to make sure that they have not requested more items
 * than are in stock.
 */
function commerce_stock_form_commerce_cart_validate($form, &$form_state) {
  // If a user choses to remove an item, then ts valid.
  if ($form_state['triggering_element']['#value'] == t('Remove')) {
    return;
  }
  $line_item_index = array_keys($form_state['line_items']);
  if (isset($form_state['input']['edit_quantity'])) {
    foreach ($form_state['values']['edit_quantity'] as $index => $qty) {
      $line_item = $form_state['line_items'][$line_item_index[$index]];
      $line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
      if (in_array($line_item_wrapper->getBundle(), commerce_product_line_item_types())) {
        $product_id = $line_item_wrapper->commerce_product->product_id->value();
        $product = commerce_product_load($product_id);
        // Check using rules.
        commerce_stock_check_product_checkout_rule($product, $qty, $stock_state, $message);
        // @todo: TEST and update error structure.
        if ($stock_state == 1) {
          form_set_error("stock_error", $message);
        }
        elseif ($stock_state == 2) {
          drupal_set_message($message);
        }
      }
    }
  }
}

/**
 * Form validation handler for commerce_checkout_form_checkout().
 *
 * Make sure all items in the cart are in stock before continuing. This should
 * not be reached as this is now handled by the stock checkout pane, but as that
 * can be disabled it may be safe to keep this extra check.
 */
function commerce_stock_checkout_form_validate($form, &$form_state) {
  $order_wrapper = entity_metadata_wrapper('commerce_order', $form_state['order']);
  commerce_stock_checkout_validate($order_wrapper);
}

/**
 * Form constructor for the stock checkout pane form.
 *
 * Validating the stock when displaying this form will allow redirecting the
 * user before they start checkout.
 */
function commerce_stock_commerce_checkout_pane_checkout_form($form, &$form_state, $checkout_pane, $order) {
  $order_wrapper = entity_metadata_wrapper('commerce_order', $form_state['order']);
  commerce_stock_checkout_validate($order_wrapper);
}

/**
 * Form validation handler for commerce_cart_add_to_cart_form().
 *
 * Checks if the add to cart form should be enabled (in stock).
 */
function commerce_stock_cart_state_validate($form_id, &$form, &$form_state) {
  $product_id = $form['product_id']['#value'];
  $product = commerce_product_load($product_id);
  $qty_ordered = commerce_stock_check_cart_product_level($product_id);

  // Initialize the form.
  $form['submit']['#value'] = t('Add to cart');
  $form['submit']['#disabled'] = FALSE;
  $form['#attributes']['class']['stock'] = 'in-stock';

  global $stock_cart_check_data;
  $stock_cart_check_data = array(
    'form' => &$form,
  );

  // Integration with rules_form_alter().
  if (module_exists('rules_form_alter')) {
    // Make sure rules_form_alter actions work from the stock event.
    global $rules_form_alter_data;
    $rules_form_alter_data = array(
      'id'  => $form_id,
      'form' => &$form,
      'state' => &$form_state,
    );
  }

  // Invoke the stock check event.
  rules_invoke_event('commerce_stock_check_add_to_cart_form_state', $product, $qty_ordered);
}

/**
 * Implements hook_token_info().
 */
function commerce_stock_token_info() {
  $info['tokens']['commerce-product']['user-quantity-ordered'] = array(
    'name' => t('Quantity already ordered'),
    'description' => t('The quantity already ordered (in the basket) for the user'),
  );
  return $info;
}


/**
 * Implements hook_tokens().
 */
function commerce_stock_tokens($type, $tokens, array $data = array(), array $options = array()) {
  $replacements = array();
  if ($type == 'commerce-product' && !empty($data['commerce-product'])) {
    $product = entity_metadata_wrapper('commerce_product', $data['commerce-product']);
    foreach ($tokens as $name => $original) {
      switch ($name) {
        case 'user-quantity-ordered':
          $replacements[$original] = commerce_stock_check_cart_product_level($product->product_id->value());
          break;
      }
    }
  }
  return $replacements;
}

/**
 * Checks and returns quanity of the product and returns the value.
 *
 * The value is cached as is called more then once (including tokens)
 */
function commerce_stock_check_cart_product_level($product_id) {

  static $cart_product_level = array();

  if (isset($cart_product_level[$product_id])) {
    return $cart_product_level[$product_id];
  }
  else {
    $cart_qty = 0;
    global $user;
    // Load the current cart if it exists.
    $order = commerce_cart_order_load($user->uid);
    if (!$order) {
      $cart_qty = 0;
    }
    else {
      $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
      if ($order_wrapper) {
        // Cycle throw each line item ID.
        foreach ($order_wrapper->commerce_line_items as $index => $line_item_wrapper) {
          if (in_array($line_item_wrapper->getBundle(), commerce_product_line_item_types())) {
            if ($line_item_wrapper->commerce_product->product_id->value() == $product_id){
              $cart_qty += $line_item_wrapper->quantity->value();
            }
          }
        }
      }
    }
    $cart_product_level[$product_id] = $cart_qty;
    return $cart_qty;
  }
}

/**
 * Check the stock using rules.
 *
 * Invokes the rule event and return the result of its action.
 */
function commerce_stock_check_product_rule($product, &$qty, $qty_ordered, &$stock_state, &$message) {
  // Set defaults to the global stock check array.
  global $stock_check_data;
  $stock_check_data = array(
    'state' => '0',
    'message' => '',
    'qty' => $qty
  );

  // Invoke the stock check event.
  rules_invoke_event('commerce_stock_add_to_cart_check_product', $product, $qty, $qty_ordered, $qty + $qty_ordered);

  // If state not ok, do nothing then return the value set by the action.
  if ($stock_check_data['state'] <> 0) {
    $stock_state = $stock_check_data['state'];
    $message = $stock_check_data['message'];
    $qty = $stock_check_data['qty'];
  }
}

/**
 * Check stock using rules at the point of checkout.
 *
 * Invoke the rule event and return the result of its action.
 */
function commerce_stock_check_product_checkout_rule($product, $qty_ordered, &$stock_state, &$message) {
  // Set defaults to the global stock check array.
  global $stock_check_data;
  $stock_check_data = array(
    'state' => '0',
    'message' => '',
    'qty' => $qty_ordered
  );

  // Invoke the stock check event.
  rules_invoke_event('commerce_stock_check_product_checkout', $product, $qty_ordered);

  // If state not ok, do nothing then return the value set by the action.
  if ($stock_check_data['state'] <> 0) {
    $stock_state = $stock_check_data['state'];
    $message = $stock_check_data['message'];
    $qty = $stock_check_data['qty'];
  }
}

/**
 * Form after_build handler: Validates that the product is in stock.
 */
function commerce_stock_form_after_build($form, &$form_state) {
  $prod_id = $form['product_id']['#value'];
  if (isset($form['product_id']['#stock_enabled']) && isset($form['product_id']['#stock_enabled'][$prod_id]) && $form['product_id']['#stock_enabled'][$prod_id]) {
    if (isset($form['product_id']['#stock']) && isset($form['product_id']['#stock'][$prod_id])) {
      $prod_stock = $form['product_id']['#stock'][$prod_id];
      if ($prod_stock <= 0) {
        // Remove the add to cart button.
        $form['submit']['#access'] = FALSE;
        // Remove quantity if enabled.
        if (isset($form['submit'])) {
          $form['quantity']['#access'] = FALSE;
        }
      }
    }
  }
  return $form;
}

/**
 * Common stock validation function.
 */
function commerce_stock_checkout_validate($order_wrapper) {
  $found_errors = FALSE;
  // Check each line item.
  foreach ($order_wrapper->commerce_line_items as $index => $line_item_wrapper) {
    if (in_array($line_item_wrapper->getBundle(), commerce_product_line_item_types())) {
      $product_id = $line_item_wrapper->commerce_product->product_id->value();
      $product = commerce_product_load($product_id);
      $qty_ordered = commerce_stock_check_cart_product_level($product_id);
      // Check using rules.
      commerce_stock_check_product_checkout_rule($product, $qty_ordered, $stock_state, $message);
      // @todo: TEST and update error structure
      if ($stock_state == 1) {
        form_set_error("stock_error", $message);
        $found_errors = TRUE;
      }
      elseif ($stock_state == 2) {
        drupal_set_message($message);
      }
    }
  }
  // If out of stock items send back to the cart form.
  if ($found_errors) {
    drupal_set_message(t('Please adjust quantities before continuing to checkout.'));
    $cart_url = url('cart', array('absolute' => TRUE));
    drupal_goto($cart_url);
  }
}

/**
 * Determine whether an order has items which are out of stock.
 *
 * @return bool
 *   TRUE if the order has items which are out of stock, FALSE otherwise.
 */
function commerce_stock_order_has_out_of_stock($order) {
  $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
  $outofstock = FALSE;
  // Check each line item.
  foreach ($order_wrapper->commerce_line_items as $index => $line_item_wrapper) {
    if (in_array($line_item_wrapper->getBundle(), commerce_product_line_item_types())) {
      $product_id = $line_item_wrapper->commerce_product->product_id->value();
      $product = commerce_product_load($product_id);
      $qty_ordered = commerce_stock_check_cart_product_level($product_id);
      // Check using rules.
      commerce_stock_check_product_checkout_rule($product, $qty_ordered, $stock_state, $message);
      // Both 1 and 2 are errors.
      if (($stock_state == 1)|| ($stock_state == 2)) {
        $outofstock = TRUE;
        break;
      }
    }
  }
  return $outofstock;
}

/**
 * A demo action for the "Advanced configuration of the add to cart form".
 *
 * Demonstrates how you can write your own custom actions to handle the add to
 * cart.
 */
function commerce_stock_test_cart_action($form, &$form_state) {
  $product_id = $form_state['values']['product_id'];
  $product = commerce_product_load($product_id);
  drupal_set_message(t('%title was not added to your cart as this is a test action only.', array('%title' => $product->title)), 'error');
  // Ensure that page redirects back to its original URL without losing query parameters, such as pagers.
  // @todo Remove when http://drupal.org/node/171267 is fixed.
  $form_state['redirect'] = array(current_path(), array('query' => drupal_get_query_parameters()));
}
