<?php
//$Id: uc_discounts.module,v 1.29.2.10 2010/08/09 23:20:00 davexoxide Exp $


/**
 * @file
 * Discounts module for Ubercart 2.0+.
 *
 * Allows discount rules to be set up and applied to orders.
 */

define("QUALIFYING_TYPE_MINIMUM_PRICE", 1);
define("QUALIFYING_TYPE_MINIMUM_QUANTITY", 2);

define("DISCOUNT_TYPE_FREE_ITEMS", 1);
define("DISCOUNT_TYPE_PERCENTAGE_OFF", 2);
define("DISCOUNT_TYPE_FIXED_AMOUNT_OFF", 3);
define("DISCOUNT_TYPE_FIXED_AMOUNT_OFF_PER_QUALIFYING_ITEM", 4);
define("DISCOUNT_TYPE_PERCENTAGE_OFF_PER_QUALIFYING_ITEM", 5);

define("FILTER_TYPE_PRODUCTS", 1);
define("FILTER_TYPE_TERMS", 2);
define("FILTER_TYPE_SKUS", 3);
define("FILTER_TYPE_CLASS", 4);

define("ALL_PRODUCTS", "-1");
define("ALL_TERMS", "-1");
define("ALL_SKUS", "");
define("ALL_CLASSES", "");
define("ALL_ROLES", "-1");

define("REQUIRES_SINGLE_PRODUCT_TO_QUALIFY_DEFAULT", FALSE);
define("REQUIRES_CODE_DEFAULT", TRUE);
define("HAS_QUALIFYING_AMOUNT_MAX_DEFAULT", FALSE);
define("FILTER_TYPE_DEFAULT", FILTER_TYPE_PRODUCTS);
define("HAS_ROLE_FILTER_DEFAULT", FALSE);
define("MAX_TIMES_APPLIED_DEFAULT", 1);
define("CAN_BE_COMBINED_WITH_OTHER_DISCOUNTS_DEFAULT", FALSE);
define("MAX_USES_DEFAULT", 0);
define("MAX_USES_PER_USER_DEFAULT", 0);
define("MAX_USES_PER_CODE_DEFAULT", 0);
define("HAS_EXPIRATION_DEFAULT", FALSE);
define("IS_ACTIVE_DEFAULT", FALSE);
define("IS_ACTIVE", TRUE);

define("LINE_ITEM_KEY_NAME", "uc_discounts");
// (1 to follow subtotal)
define("LINE_ITEM_WEIGHT", 1);
define("CALCULATE_DISCOUNT_RESPONSE_LINE_ITEMS_KEY", "line_items");
define("CALCULATE_DISCOUNT_RESPONSE_ERRORS_KEY", "errors");
define("CALCULATE_DISCOUNT_RESPONSE_MESSAGES_KEY", "messages");


///////////////////////////////////////////////////////////////////
//Drupal Hooks
///////////////////////////////////////////////////////////////////

/**
 * Implementation of hook_init().
 */
function uc_discounts_init() {
  drupal_add_css(drupal_get_path("module", "uc_discounts") ."/uc_discounts.css");
}

function uc_discounts_help($path, $arg) {
  switch ($path) {
    case "admin/store/settings/uc_discounts":
      return t("Add and review discounts");
  }
  return $output;
}

/**
 * Implementation of hook_perm().
 */
function uc_discounts_perm() {
  return array("configure discounts");
}

/**
 * Implementation of hook_menu().
 */
function uc_discounts_menu() {
  $items = array();

  $items["admin/store/settings/uc_discounts"] = array(
    "title" => "Discount settings",
    "description" => "Configure discount settings.",
    "page callback" => "uc_discounts_admin_settings",
    "access arguments" => array("configure discounts"),
    "file" => "uc_discounts.admin.inc",
  );
  $items["admin/store/settings/uc_discounts/edit"] = array(
    "title" => "Edit discount rule",
    "page callback" => "drupal_get_form",
    "page arguments" => array("uc_discounts_form"),
    "access arguments" => array("configure discounts"),
    "type" => MENU_CALLBACK,
    "file" => "uc_discounts.admin.inc",
  );
  $items["admin/store/settings/uc_discounts/copy"] = array(
    "page callback" => "uc_discounts_copy",
    "access arguments" => array("configure discounts"),
    "type" => MENU_CALLBACK,
    "file" => "uc_discounts.admin.inc",
  );
  $items["admin/store/settings/uc_discounts/delete"] = array(
    "title" => "Delete discount rule",
    "page callback" => "drupal_get_form",
    "page arguments" => array("uc_discounts_delete"),
    "access arguments" => array("configure discounts"),
    "type" => MENU_CALLBACK,
    "file" => "uc_discounts.admin.inc",
  );
  $items["cart/checkout/uc_discounts/calculate"] = array(
    "page callback" => "uc_discounts_js_calculate",
    "access arguments" => array("access content"),
    "type" => MENU_CALLBACK,
  );
  $items["admin/reports/uc_discounts/all"] = array(
    "title" => "Ubercart discounts usage data",
    "description" => "View usage data for each Ubercart discount.",
    "page callback" => "uc_discounts_report",
    "access arguments" => array("access site reports"),
    "file" => "uc_discounts.admin.inc",
  );
  $items["admin/reports/uc_discounts/discount"] = array(
    "title" => "Ubercart discount usage data",
    "page callback" => "uc_discounts_report_for_discount",
    "access arguments" => array("access site reports"),
    "type" => MENU_CALLBACK,
    "file" => "uc_discounts.admin.inc",
  );

  return $items;
}

/**
 * Implementation of hook_order().
 *
 * Manages order->uc_discounts_codes (array of code) and order->discounts (array of uses)
 */
function uc_discounts_order($op, &$arg1, $arg2) {
  switch ($op) {
    case "load":
      //Get order's codes from database
      $codes_string = uc_discounts_order_codes_get($arg1->order_id);
      $arg1->uc_discounts_codes = get_codes($codes_string);
      break;

    case "save":
      //If discount line items need updating
      if ($arg1->uc_discounts_line_items_need_updating) {
        //Delete existing order codes
        uc_discounts_order_codes_delete($arg1->order_id);

        //Save order's codes as string
        $codes_string = create_codes_string($arg1->uc_discounts_codes);
        uc_discounts_order_codes_insert($arg1->order_id, $codes_string);

        //Get order line items
        if (is_array($arg1->line_items)) {
          $existing_line_items = $arg1->line_items;
        }
        else $existing_line_items = uc_order_load_line_items($arg1->order_id, TRUE);

        //Use new_order_line_items to populate $arg1->line_items by:
        //	Storing all non-discounts line items
        //	Storing new discount line items
        $new_order_line_items = array();

        //Delete existing discount line items
        foreach ($existing_line_items as $line_item) {
          if ($line_item["type"] == LINE_ITEM_KEY_NAME) {
            uc_order_delete_line_item($line_item["line_item_id"]);
          }
          //Otherwise store non-discount line item
          else $new_order_line_items[] = $line_item;
        }

        //Add discount line items
        foreach ($arg1->uc_discounts_line_items as $line_item) {
          uc_order_line_item_add($arg1->order_id, $line_item["type"], $line_item["title"],
          $line_item["amount"], $line_item["weight"], $line_item["data"]
          );
          $new_order_line_items[] = $line_item;
        }

        //Update order line items (see new_order_line_items notes above)
        $arg1->line_items = $new_order_line_items;

        //Force tax recalculation (currently unused but may be required if line item weights change)
        //module_invoke("uc_taxes", "order", "save", $arg1, "");

        $arg1->uc_discounts_line_items_need_updating = FALSE;
      }
      break;

    case "update":
      //If status changes to "cancelled", delete order uses
      if ($arg2 == "cancelled") {
        uc_discounts_uses_delete_for_order($arg1->order_id);
      }
      break;

    case "submit":
      //Ensure stored discounts are accurate (recalculate and match new amounts against stored amounts)

      //Store existing discount amounts
      $existing_discount_amounts = array();
      foreach (get_existing_discount_line_items($arg1) as $line_item) $existing_discount_amounts[] = uc_currency_format($line_item["amount"]);


      //Regenerate discount amounts

      $errors    = array();
      $warnings  = array();
      $messages  = array();
      $discounts = get_discounts_for_order($arg1, $errors, $warnings, $messages);

      //If there were errors, print and return FALSE
      if (!empty($errors)) {
        foreach ($errors as $error) drupal_set_message($error, "error");
        return array(array('pass' => FALSE,
            'message' => t('Discounts have changed.  Please review your cart and continue checkout.'),
          ));
      }

      //Add discount line items to order
      add_discount_line_items_to_order($arg1, $discounts);

      $new_discount_amounts = array();
      foreach ($arg1->uc_discounts_line_items as $line_item) $new_discount_amounts[] = uc_currency_format($line_item["amount"]);

      $discount_intersection = array_intersect($existing_discount_amounts, $new_discount_amounts);
      if (count($discount_intersection) != count($existing_discount_amounts)) {
        //Save new discount line items
        $arg1->uc_discounts_line_items_need_updating = TRUE;
        uc_discounts_order("save", $arg1, NULL);

        return array(array('pass' => FALSE,
            'message' => t('Discounts have changed.  Please review your cart and continue checkout.'),
          ));
      }

      //Delete existing uses for order
      uc_discounts_uses_delete_for_order($arg1->order_id);

      //Insert uses (for best results use discounts returned by call to get_discounts_for_order)
      foreach ($discounts as $discount) {
        $code          = (!empty($discount->code)) ? $discount->code : "";
        $times_applied = (is_numeric($discount->times_applied)) ? $discount->times_applied : 1;
        $amount        = (is_numeric($discount->amount)) ? $discount->amount : 0;

        uc_discounts_uses_insert($discount->discount_id, $arg1->uid,
          $arg1->order_id, $code, $times_applied, $amount
        );
      }

      //Add discount messages to order
      uc_order_log_changes($arg1->order_id, $messages);
      break;

    case "delete":
      //Delete existing order codes
      uc_discounts_order_codes_delete($arg1->order_id);
      //TO DO: determine if uses should be deleted or put global setting in for user to decide
      break;
  }
}

/**
 * Implementation of Ubercart's hook_line_item().
 *
 * Displays all discounts as a single line item
 *
 * @see hook_line_item()
 */
function uc_discounts_line_item() {
  $line_items[] = array(
    "id" => LINE_ITEM_KEY_NAME,
    "title" => t("Discount"),
    "weight" => LINE_ITEM_WEIGHT,
    "stored" => TRUE,
    // Added to total
    "calculated" => TRUE,
    "display_only" => FALSE,
  );
  return $line_items;
}

/**
 * Implementation of hook_cart_pane().
 *
 * @see hook_cart_pane()
 */
function uc_discounts_cart_pane($items) {
  global $user;

  //Create phony order object to call to get_discounts_for_order
  $order           = new stdClass();
  $order->uid      = $user->uid;
  $order->products = $items;

  $errors    = array();
  $warnings  = array();
  $messages  = array();
  $discounts = get_discounts_for_order($order, $errors, $warnings, $messages);

  //If there are no discounts, do not alter cart
  if (count($discounts) == 0) {
    return array();
  }

  //Calculate subtotal with discounts
  $subtotal = 0;
  if (is_array($items)) {
    foreach ($items as $item) $subtotal += $item->price * $item->qty;
  }
  $total_discount_amount = 0;
  if (is_array($discounts)) {
    foreach ($discounts as $discount) $total_discount_amount += $discount->amount;
  }
  $subtotal_including_discounts = $subtotal - $total_discount_amount;

  //Add total discount message
  $messages[] = "<strong>". t("Total discount") .":</strong>&nbsp;". uc_currency_format($total_discount_amount);

  //Add new subtotal message
  $messages[] = "<strong>". t("Subtotal including discounts") .":</strong>&nbsp;". uc_currency_format($subtotal_including_discounts);

  //Start row index at item count + 2 (1 for subtotal row in cart form, 1 more for our first row)
  $i = count($items) + 2;

  //Create table to hold discount messages
  $body = "<div class='uc-discounts-cart-pane-container'><table class='uc-discounts-cart-pane-table'>";
  foreach ($messages as $message) {
    $evenOddClass = (($i % 2) == 0) ? "even" : "odd";
    $body .= sprintf("<tr class='%s'><td class='%s'>", $evenOddClass, "uc-discounts-cart-pane-table-cell") . $message ."</td></tr>";
    $i += 1;
  }

  //Close table
  $body .= "</table></div>";

  //Write table using javascript between items and cart form buttons
  drupal_add_js(sprintf("$(document).ready(function()
		{
			$(\"#cart-form-buttons\").before(\"%s\");
		});", $body), "inline");
}

/**
 * Implementation of hook_checkout_pane().
 *
 * @see hook_checkout_pane()
 */
function uc_discounts_checkout_pane() {
  $panes[] = array(
    "id" => "uc_discounts",
    "callback" => "uc_checkout_pane_discounts",
    'process' => TRUE,
    "title" => t("Enter discount codes"),
    "weight" => 5,
  );
  return $panes;
}

/**
 * Discounts checkout pane callback
 *
 * More information at http://www.ubercart.org/docs/developer/245/checkout
 */
function uc_checkout_pane_discounts($op, &$arg1, $arg2) {
  global $user;

  switch ($op) {
    case "view":
      //Add form to checkout pane

      $description = t("Enter discount codes in the box below (one per line).");

      //If viewing an existing order, load order's codes
      if (!empty($arg1->order_id)) {
        $codes_string = uc_discounts_order_codes_get($arg1->order_id);
      }

      $contents["uc-discounts-codes"] = array(
        "#type" => "textarea",
        "#default_value" => $codes_string,
        "#rows" => 5,
        "#prefix" => "<div class='discount-codes-wrapper'>",
        "#suffix" => "</div>",
      );

      $contents["uc-discounts-placeholder"] = array(
        "#type" => "hidden",
        "#prefix" => "<div class='uc-discounts-messages-container'>",
        "#suffix" => "</div>",
      );

      $contents["uc-discounts-button"] = array(
        "#type" => "button",
        "#value" => t("Click to calculate discounts"),
      );

      drupal_add_js(array(
          "uc_discounts" => array(
            "line_item_key_name" => LINE_ITEM_KEY_NAME,
            "line_item_weight" => LINE_ITEM_WEIGHT,
            "total_discount_text" => t("Total discount"),
            "calculate_discount_response_line_items_key" => CALCULATE_DISCOUNT_RESPONSE_LINE_ITEMS_KEY,
            "calculate_discount_response_errors_key" => CALCULATE_DISCOUNT_RESPONSE_ERRORS_KEY,
            "calculate_discount_response_messages_key" => CALCULATE_DISCOUNT_RESPONSE_MESSAGES_KEY,
            "progress_msg" => t("Calculating discounts..."),
            "no_codes_entered" => t("Please enter at least one code"),
            "no_applicable_discounts" => t("No applicable discounts"),
            "err_msg" => t("There were problems determining if any discounts apply.  Please try again shortly.\nIf this does not resolve the issue, please call @phone to complete your order.",
              array("@phone" => variable_get("uc_store_phone", NULL))
            ),
            "response_parse_err_msg" => t("Unable to parse response text: "),
          ),
        ), "setting");

      drupal_add_js("$(document).ready(function() { uc_discountsOnLoad(); });", "inline");
      drupal_add_js("misc/progress.js");
      drupal_add_js(drupal_get_path("module", "uc_discounts") ."/uc_discounts.js");

      return array("description" => $description, "contents" => $contents);

    case "process":
      //TO DO: only update codes if codes_string ($arg2["uc-discounts-codes"]) has changed

      //Save form values from checkout pane in order ($arg1)
      $arg1->uc_discounts_codes = get_codes($arg2["uc-discounts-codes"]);

      $has_code_errors = FALSE;

      $errors    = array();
      $warnings  = array();
      $discounts = get_discounts_for_order($arg1, $errors, $warnings);

      foreach ($errors as $error) drupal_set_message($error, "error");
      foreach ($warnings as $warning) drupal_set_message(t("Warning:") . $warning, "error");

      //If there were errors, return FALSE
      if (!empty($errors)) {
        return FALSE;
      }

    //Add discount line items to order
    add_discount_line_items_to_order($arg1, $discounts);

    //Mark order as needing discount line items updated
    $arg1->uc_discounts_line_items_need_updating = TRUE;
    break;
  }
}

/**
 * Add discount line items to order
 *
 * Note: assumes discount objects are the result of a call to get_discounts_for_order()
 */
function add_discount_line_items_to_order($order, $discounts) {
  //Create line items for discounts and store in order's uc_discounts_line_items field
  $line_items = array();
  foreach ($discounts as $discount) {
    $line_item = array(
      "type" => LINE_ITEM_KEY_NAME,
      "title" => $discount->short_description,
      "amount" => -$discount->amount,
      "weight" => LINE_ITEM_WEIGHT,
    	"data" => array('discount_id' => $discount->discount_id),
    );

    $line_items[] = $line_item;
  }
  $order->uc_discounts_line_items = $line_items;
}

/**
 * Returns existing discounts line items for order.
 */
function get_existing_discount_line_items($order) {
  if (is_array($order->line_items)) {
    $existing_line_items = $order->line_items;
  }
  else $existing_line_items = uc_order_load_line_items($order->order_id, TRUE);

  $line_items = array();
  foreach ($existing_line_items as $line_item) {
    //If line item type is LINE_ITEM_KEY_NAME, add it to array
    if ($line_item["type"] == LINE_ITEM_KEY_NAME) {
      $line_items[] = $line_item;
    }
  }

  return $line_items;
}

/**
 * AJAX callback for discounts calculation.
 *
 * Calculate discount for an order in the checkout page.
 */
function uc_discounts_js_calculate() {
  global $user;

  //If session order exists, use it
  if (!empty($_SESSION["cart_order"])) {
    $order_id = $_SESSION['cart_order'];
    $order = uc_order_load($order_id);
    if (is_null($order)) {
      print "{}";
      exit();
      return;
    }
  }
  //Otherwise create phony order
  else {
    $order           = new stdClass();
    $order->uid      = $user->uid;
    $order->products = uc_cart_get_contents();
  }

  $order->uc_discounts_codes = get_codes($_POST["uc-discounts-codes"]);

  $line_items = array();
  $errors     = array();
  $warnings   = array();
  $messages   = array();

  $discounts = get_discounts_for_order($order, $errors, $warnings, $messages);

  $i = 0;
  foreach ($discounts as $discount) {
    $line_item           = array();
    $line_item["id"]     = LINE_ITEM_KEY_NAME . $i++;
    $line_item["type"]   = $discount->type;
    $line_item["title"]  = $discount->title;
    $line_item["amount"] = -$discount->amount;
    $line_item["weight"] = $discount->weight;
    $line_items[]        = $line_item;
  }

  if (!empty($warnings)) {
    $warnings2 = array();
    foreach ($warnings as $warning) $warnings2[] = t("Warning:") . $warning;
    $errors = array_merge($errors, $warnings2);
  }

  $calculate_discount_response = array(CALCULATE_DISCOUNT_RESPONSE_LINE_ITEMS_KEY => $line_items,
    CALCULATE_DISCOUNT_RESPONSE_ERRORS_KEY => $errors,
    CALCULATE_DISCOUNT_RESPONSE_MESSAGES_KEY => $messages,
  );
  drupal_json($calculate_discount_response);
  exit();
}


///////////////////////////////////////////////////////////////////
//Database operations
///////////////////////////////////////////////////////////////////

/**
 * Returns array of uc_discounts column names.
 */
function get_uc_discounts_column_names() {
  return array("name", "short_description", "description", "qualifying_type",
    "qualifying_amount", "has_qualifying_amount_max", "qualifying_amount_max", "discount_type",
    "discount_amount", "requires_code", "filter_type", "has_role_filter",
    "requires_single_product_to_qualify", "required_product", "max_times_applied", "can_be_combined_with_other_discounts", "max_uses",
    "max_uses_per_user", "max_uses_per_code", "has_expiration", "expiration",
    "is_active", "weight",
  );
}

/**
 * Returns array of uc_discounts insert column names.
 */
function get_uc_discounts_insert_column_names() {
  return array_merge(get_uc_discounts_column_names(), array("insert_timestamp"));
}

/**
 * Returns array of uc_discounts update column names.
 */
function get_uc_discounts_update_column_names() {
  return get_uc_discounts_column_names();
}

/**
 * Returns array of uc_discounts column printf wildcards.
 */
function get_uc_discounts_column_printf_wildcards() {
  return array("'%s'", "'%s'", "'%s'", "%d",
    "%f", "%d", "%f", "%d",
    "%f", "%d", "%d", "%d",
    "%d", "'%s'", "%d", "%d", "%d",
    "%d", "%d", "%d", "%d",
    "%d", "%d",
  );
}

/**
 * Returns array of uc_discounts insert column printf wildcards.
 */
function get_uc_discounts_column_insert_printf_wildcards() {
  return array_merge(get_uc_discounts_column_printf_wildcards(), array("%d"));
}

/**
 * Returns array of uc_discounts update column printf wildcards.
 */
function get_uc_discounts_column_update_printf_wildcards() {
  return get_uc_discounts_column_printf_wildcards();
}

/**
 * Inserts a uc_discounts row and returns its discount_id.
 */
function uc_discounts_insert($name, $short_description, $description, $qualifying_type,
  $qualifying_amount, $has_qualifying_amount_max, $qualifying_amount_max, $discount_type,
  $discount_amount, $requires_code, $filter_type, $has_role_filter,
  $requires_single_product_to_qualify, $required_product, $max_times_applied, $can_be_combined_with_other_discounts, $max_uses,
  $max_uses_per_user, $max_uses_per_code, $has_expiration, $expiration,
  $is_active, $weight, $insert_timestamp = NULL
) {
  if (is_null($insert_timestamp)) {
    $insert_timestamp = time();
  }

  $query = "INSERT INTO {uc_discounts}(". join(",", get_uc_discounts_insert_column_names()) .") VALUES(". join(",", get_uc_discounts_column_insert_printf_wildcards()) .")";
  uc_discounts_log("query=". $query);
  db_query($query,
    $name, $short_description, $description, $qualifying_type,
    $qualifying_amount, $has_qualifying_amount_max, $qualifying_amount_max, $discount_type,
    $discount_amount, $requires_code, $filter_type, $has_role_filter,
    $requires_single_product_to_qualify, $required_product, $max_times_applied, $can_be_combined_with_other_discounts, $max_uses,
    $max_uses_per_user, $max_uses_per_code, $has_expiration, $expiration,
    $is_active, $weight, $insert_timestamp
  );
  $discount_id = db_last_insert_id("uc_discounts", "discount_id");
  uc_discounts_log("new discount_id=". $discount_id);
  return $discount_id;
}

/**
 * Updates a uc_discounts row.
 */
function uc_discounts_update($discount_id,
  $name, $short_description, $description, $qualifying_type,
  $qualifying_amount, $has_qualifying_amount_max, $qualifying_amount_max, $discount_type,
  $discount_amount, $requires_code, $filter_type, $has_role_filter,
  $requires_single_product_to_qualify, $required_product, $max_times_applied, $can_be_combined_with_other_discounts, $max_uses,
  $max_uses_per_user, $max_uses_per_code, $has_expiration, $expiration,
  $is_active, $weight
) {

  $query = "UPDATE {uc_discounts} SET ";

  $column_names = get_uc_discounts_update_column_names();
  $printf_wildcards = get_uc_discounts_column_update_printf_wildcards();

  for ($i = 0; $i < count($column_names); $i++) {
    if ($i != 0) {
      $query .= ", ";
    }
    $query .= $column_names[$i] ."=". $printf_wildcards[$i];
  }

  $query .= " WHERE discount_id=%d";

  uc_discounts_log("query=". $query);
  db_query($query,
    $name, $short_description, $description, $qualifying_type,
    $qualifying_amount, $has_qualifying_amount_max, $qualifying_amount_max, $discount_type,
    $discount_amount, $requires_code, $filter_type, $has_role_filter,
    $requires_single_product_to_qualify, $required_product, $max_times_applied, $can_be_combined_with_other_discounts, $max_uses,
    $max_uses_per_user, $max_uses_per_code, $has_expiration, $expiration,
    $is_active, $weight, $discount_id
  );
}

/**
 * Deletes a uc_discounts row and all dependencies.
 */
function uc_discounts_delete_all($discount_id) {
  db_query("DELETE FROM {uc_discounts_uses} WHERE discount_id=%d", $discount_id);
  db_query("DELETE FROM {uc_discounts_products} WHERE discount_id=%d", $discount_id);
  db_query("DELETE FROM {uc_discounts_terms} WHERE discount_id=%d", $discount_id);
  db_query("DELETE FROM {uc_discounts_skus} WHERE discount_id=%d", $discount_id);
  db_query("DELETE FROM {uc_discounts_roles} WHERE discount_id=%d", $discount_id);
  db_query("DELETE FROM {uc_discounts_codes} WHERE discount_id=%d", $discount_id);
  db_query("DELETE FROM {uc_discounts} WHERE discount_id=%d", $discount_id);
}

/**
 * Returns codes for discount.
 */
function get_codes_for_discount($discount_id) {
  $codes = array();

  //Get codes from database
  $result = db_query("SELECT code FROM {uc_discounts_codes} WHERE discount_id=%d", $discount_id);
  while ($row = db_fetch_array($result)) $codes[] = $row["code"];
  return $codes;
}

/**
 * Inserts a uc_discounts_codes row and returns its discount_code_id.
 */
function uc_discounts_codes_insert($discount_id, $code) {
  $query = "INSERT INTO {uc_discounts_codes}(discount_id, code) VALUES(%d, '%s')";
  uc_discounts_log("query=". $query);
  db_query($query, $discount_id, $code);
  return db_last_insert_id("uc_discounts_codes", "discount_code_id");
}

/**
 * Deletes all uc_discounts_codes rows for a discount.
 */
function uc_discounts_codes_delete($discount_id) {
  $query = "DELETE FROM {uc_discounts_codes} WHERE discount_id=%d";
  uc_discounts_log("query=". $query);
  db_query($query, $discount_id);
}

/**
 * Returns product_ids for discount object.
 * Note: this function correctly returns all products for term-based discounts.
 *
 * @param $discount object
 * @param $exclude_all_products bool
 *
 * @return array of product ids (nids)
 */
function get_product_ids_for_discount_object($discount, $exclude_all_products = FALSE) {
  switch ($discount->filter_type) {
    case FILTER_TYPE_PRODUCTS:
      return get_product_ids_for_discount($discount->discount_id, $exclude_all_products);

    case FILTER_TYPE_TERMS:
      $product_ids = array();

      //Get products for terms
      $query = "SELECT DISTINCT p.nid FROM {uc_products} p
				INNER JOIN {term_node} tn ON p.nid=tn.nid
				INNER JOIN {uc_discounts_terms} dt ON tn.tid=dt.term_id
				WHERE dt.discount_id=%d";
      uc_discounts_log($query);
      $result = db_query($query, $discount->discount_id);
      while ($row = db_fetch_array($result)) $product_ids[] = $row["nid"];
      return $product_ids;

    case FILTER_TYPE_SKUS:
      $query = "SELECT DISTINCT p.nid FROM {uc_products} p
				INNER JOIN {uc_discounts_skus} ds ON p.model=ds.sku
				WHERE ds.discount_id=%d";
      uc_discounts_log($query);
      $result = db_query($query, $discount->discount_id);
      while ($row = db_fetch_array($result)) $product_ids[] = $row["nid"];
      return $product_ids;

    case FILTER_TYPE_CLASS:
      $query = "SELECT DISTINCT n.nid FROM {node} n
				INNER JOIN {uc_discounts_classes} dcl ON n.type=dcl.class
				WHERE dcl.discount_id=%d";
      uc_discounts_log($query);
      $result = db_query($query, $discount->discount_id);
      while ($row = db_fetch_array($result)) $product_ids[] = $row["nid"];
      return $product_ids;
  }
  return array();
}

/**
 * Returns product_ids for discount.
 * Note: this function does not check filter_type so a discount with filter_type other than
 *	  FILTER_TYPE_PRODUCTS will return no values.
 *
 * @param $discount_id
 * @param $exclude_all_products
 *
 * @return array of product ids (nids)
 */
function get_product_ids_for_discount($discount_id, $exclude_all_products = FALSE) {
  $product_ids = array();
  //Get products from database
  if ($exclude_all_products) {
    $result = db_query("SELECT product_id FROM {uc_discounts_products} WHERE discount_id=%d AND product_id<>%d",
      $discount_id, ALL_PRODUCTS
    );
  }
  else $result = db_query("SELECT product_id FROM {uc_discounts_products} WHERE discount_id=%d", $discount_id);

  while ($row = db_fetch_array($result)) $product_ids[] = $row["product_id"];
  return $product_ids;
}

/**
 * Inserts a uc_discounts_products row and returns its discount_product_id.
 */
function uc_discounts_products_insert($discount_id, $product_id) {
  $query = "INSERT INTO {uc_discounts_products}(discount_id, product_id) VALUES(%d, %d)";
  uc_discounts_log("query=". $query);
  db_query($query, $discount_id, $product_id);
  return db_last_insert_id("uc_discounts_products", "discount_product_id");
}

/**
 * Deletes all uc_discounts_products rows for a discount.
 */
function uc_discounts_products_delete($discount_id) {
  $query = "DELETE FROM {uc_discounts_products} WHERE discount_id=%d";
  uc_discounts_log("query=". $query);
  db_query($query, $discount_id);
}

/**
 * Returns term_ids for discount.
 */
function get_term_ids_for_discount($discount_id, $exclude_all_terms = FALSE) {
  $term_ids = array();

  //Get terms from database
  if ($exclude_all_terms) {
    $result = db_query("SELECT term_id FROM {uc_discounts_terms} WHERE discount_id=%d AND term_id<>%d",
      $discount_id, ALL_TERMS
    );
  }
  else $result = db_query("SELECT term_id FROM {uc_discounts_terms} WHERE discount_id=%d", $discount_id);

  while ($row = db_fetch_array($result)) $term_ids[] = $row["term_id"];
  return $term_ids;
}

/**
 * Returns SKUs for discount.
 */
function get_skus_for_discount($discount_id, $exclude_all_skus = FALSE) {
  $skus = array();

  //Get SKUs from database
  if ($exclude_all_skus) {
    $result = db_query("SELECT sku FROM {uc_discounts_skus} WHERE discount_id=%d AND sku<>%d",
      $discount_id, ALL_SKUS
    );
  }
  else $result = db_query("SELECT sku FROM {uc_discounts_skus} WHERE discount_id=%d", $discount_id);

  while ($row = db_fetch_array($result)) $skus[] = $row["sku"];
  return $skus;
}

/**
 * Returns Product Class names for discount.
 */
function get_classes_for_discount($discount_id, $exclude_all_classes = FALSE) {
  $classes = array();

  //Get Classes from database
  if ($exclude_all_classes) {
    $result = db_query("SELECT class FROM {uc_discounts_classes} WHERE discount_id=%d AND class<>%d",
      $discount_id, ALL_CLASSES
    );
  }
  else $result = db_query("SELECT class FROM {uc_discounts_classes} WHERE discount_id=%d", $discount_id);

  while ($row = db_fetch_array($result)) $classes[] = $row["class"];
  return $classes;
}

/**
 * Returns role_ids for discount.
 */
function get_role_ids_for_discount($discount_id, $exclude_all_roles = FALSE) {
  $role_ids = array();

  //Get roles from database
  if ($exclude_all_roles) {
    $result = db_query("SELECT role_id FROM {uc_discounts_roles} WHERE discount_id=%d AND role_id<>%d",
      $discount_id, ALL_ROLES
    );
  }
  else $result = db_query("SELECT role_id FROM {uc_discounts_roles} WHERE discount_id=%d", $discount_id);

  while ($row = db_fetch_array($result)) $role_ids[] = $row["role_id"];
  return $role_ids;
}

/**
 * Inserts a uc_discounts_terms row and returns its discount_term_id.
 */
function uc_discounts_terms_insert($discount_id, $term_id) {
  $query = "INSERT INTO {uc_discounts_terms}(discount_id, term_id) VALUES(%d, %d)";
  uc_discounts_log("query=". $query);
  db_query($query, $discount_id, $term_id);
  return db_last_insert_id("uc_discounts_terms", "discount_term_id");
}

/**
 * Deletes all uc_discounts_terms rows for a discount.
 */
function uc_discounts_terms_delete($discount_id) {
  $query = "DELETE FROM {uc_discounts_terms} WHERE discount_id=%d";
  uc_discounts_log("query=". $query);
  db_query($query, $discount_id);
}

/**
 * Inserts a uc_discounts_skus row and returns its discount_sku_id.
 */
function uc_discounts_skus_insert($discount_id, $sku) {
  $query = "INSERT INTO {uc_discounts_skus}(discount_id, sku) VALUES(%d, '%s')";
  uc_discounts_log("query=". $query);
  db_query($query, $discount_id, $sku);
  return db_last_insert_id("uc_discounts_skus", "discount_sku_id");
}

/**
 * Deletes all uc_discounts_skus rows for a discount.
 */
function uc_discounts_skus_delete($discount_id) {
  $query = "DELETE FROM {uc_discounts_skus} WHERE discount_id=%d";
  uc_discounts_log("query=". $query);
  db_query($query, $discount_id);
}

/**
 * Inserts a uc_discounts_classes row and returns its discount_class_id.
 */
function uc_discounts_classes_insert($discount_id, $class) {
  $query = "INSERT INTO {uc_discounts_classes}(discount_id, class) VALUES(%d, '%s')";
  uc_discounts_log("query=". $query);
  db_query($query, $discount_id, $class);
  return db_last_insert_id("uc_discounts_classes", "discount_class_id");
}

/**
 * Deletes all uc_discounts_classes rows for a discount.
 */
function uc_discounts_classes_delete($discount_id) {
  $query = "DELETE FROM {uc_discounts_classes} WHERE discount_id=%d";
  uc_discounts_log("query=". $query);
  db_query($query, $discount_id);
}

/**
 * Inserts a uc_discounts_roles row and returns its discount_role_id.
 */
function uc_discounts_roles_insert($discount_id, $role_id) {
  $query = "INSERT INTO {uc_discounts_roles}(discount_id, role_id) VALUES(%d, %d)";
  uc_discounts_log("query=". $query);
  db_query($query, $discount_id, $role_id);
  return db_last_insert_id("uc_discounts_roles", "discount_role_id");
}

/**
 * Deletes all uc_discounts_roles rows for a discount.
 */
function uc_discounts_roles_delete($discount_id) {
  $query = "DELETE FROM {uc_discounts_roles} WHERE discount_id=%d";
  uc_discounts_log("query=". $query);
  db_query($query, $discount_id);
}

/**
 * Returns discounts for order.
 * Note: $order->uc_discounts_codes must be set
 *
 * @param $order Order to get discounts for
 * @param $errors Reference to array to add error messages to
 * @param $messages Reference to array to add success messages to
 *
 * @return array of discount objects
 */
function get_discounts_for_order($order, &$errors = NULL, &$warnings = NULL, &$messages = NULL) {
  // Product NIDS in cart => subtotal of individual item
  $order_product_id_subtotal_map = array();
  // Product NIDS in cart => quantity of individual item
  $order_product_id_quantity_map = array();
  // Product NIDS in cart
  $order_product_ids = array();
  // Product NIDS in cart=> bool
  $order_product_ids_set = array();
  // Product objects in cart
  $order_product_id_product_array_map = array();
  $order_subtotal = 0;

  //Create IN string of product node IDs in order
  if (is_array($order->products) && !empty($order->products)) {
    foreach ($order->products as $product) {
      $nid = $product->nid;
      $order_product_ids_set[$nid] = TRUE;
      if (is_array($product->data) && !empty($product->data['kit_id'])) {
        $kit_id = $product->data['kit_id'];
        $order_product_ids_set[$kit_id] = TRUE;
        $kits[$kit_id]['product_qty'] += $product->qty;
      }

      uc_discounts_add_to_existing_map_number_value($order_product_id_subtotal_map,
        $nid, $product->price * $product->qty
      );
      uc_discounts_add_to_existing_map_number_value($order_product_id_quantity_map, $nid, $product->qty);

      $a = $order_product_id_product_array_map[$nid];
      if (!is_array($a)) {
        $a = array();
      }
      $a[] = $product;
      $order_product_id_product_array_map[$nid] = $a;

      $order_subtotal += $product->price * $product->qty;
    }
    if (is_array($kits) &&  !empty($kits)) {
      foreach ($kits as $kit_id => $value) {
        $kit_node = node_load($kit_id);
        foreach ($kit_node->products as $product_in_kit) {
          $pik_nid = $product_in_kit->nid;
          foreach ($order->products as $key => $product) {
            if ($product->nid == $pik_nid && $product->data['kit_id'] == $kit_id) {
              $kits[$kit_id]['kit_qty'] = $product->qty / $product_in_kit->qty;
              break;
            }
          }
        }
        uc_discounts_add_to_existing_map_number_value($order_product_id_quantity_map, $kit_id, $kits[$kit_id]['kit_qty']);
      }
    }
  }
  // Populate product NID array with NIDs from the order
  $order_product_ids = array_keys($order_product_ids_set);
  $temp_product_ids   = $order_product_ids;
  $temp_product_ids[] = ALL_PRODUCTS;
  $product_ids_clause = sprintf("d.filter_type<>%d OR dp.product_id IN(%s)",
    FILTER_TYPE_PRODUCTS, join(",", $temp_product_ids)
  );

  //Create IN string of term TIDs in order
  $temp_term_ids = array();
  $temp_term_ids[] = ALL_TERMS;
  if (is_array($order->products) && !empty($order->products)) {
    //Get terms for order's products
    $result = db_query("SELECT DISTINCT tid FROM {term_node} WHERE nid IN(%s)", join(",", $order_product_ids));
    while ($row = db_fetch_array($result)) {
      $temp_term_ids[] = $row["tid"];
      $order_term_ids[] = $row["tid"];
    }
  }
  $term_ids_clause = sprintf("d.filter_type<>%d OR dt.term_id IN(%s)",
    FILTER_TYPE_TERMS, join(",", $temp_term_ids)
  );

  //Create IN string of SKUs in order
  $temp_skus = array();
  $temp_skus[] = "'". db_escape_string(ALL_SKUS) ."'";
  if (is_array($order->products) && !empty($order->products)) {
    //Get SKUs for order's products
    $result = db_query("SELECT DISTINCT model FROM {uc_products} WHERE nid IN(%s)", join(",", $order_product_ids));
    while ($row = db_fetch_array($result)) $temp_skus[] = "'". db_escape_string($row["model"]) ."'";
  }
  $skus_clause = sprintf("d.filter_type<>%d OR ds.sku IN(%s)",
    FILTER_TYPE_SKUS, join(",", $temp_skus)
  );

  //Create IN string of classes in order
  $temp_classes = array();
  $temp_classes[] = "'". db_escape_string(ALL_CLASSES) ."'";
  if (is_array($order->products) && !empty($order->products)) {
    //Get classes for order's products
    $result = db_query("SELECT DISTINCT type FROM {node} WHERE nid IN(%s)", join(",", $order_product_ids));
    while ($row = db_fetch_array($result)) $temp_classes[] = "'". db_escape_string($row["type"]) ."'";
  }
  $classes_clause = sprintf("d.filter_type<>%d OR dcl.class IN(%s)",
    FILTER_TYPE_CLASS, join(",", $temp_classes)
  );

  //Create codes clause
  $escaped_codes_string = NULL;
  if (!empty($order->uc_discounts_codes)) {
    //Create IN string of product node IDs in order
    $escaped_codes = array();
    foreach ($order->uc_discounts_codes as $code) $escaped_codes[] = "'". db_escape_string($code) ."'";
    $escaped_codes_string = join(",", $escaped_codes);

    $codes_clause = sprintf(" OR d.discount_id IN( SELECT discount_id FROM {uc_discounts_codes} WHERE code IN(%s) )",
      $escaped_codes_string
    );
  }
  else $codes_clause = "";

  //Create roles clause
  $auth_rid = ($order->uid != 0) ? DRUPAL_AUTHENTICATED_RID : DRUPAL_ANONYMOUS_RID;
  $roles_clause = sprintf(" OR d.discount_id IN(SELECT dr.discount_id FROM {uc_discounts_roles} dr LEFT JOIN {users_roles} ur ON (dr.role_id=ur.rid AND ur.uid=%d) WHERE ur.uid IS NOT NULL OR dr.role_id=%d OR dr.role_id=%d)",
    $order->uid, ALL_ROLES, $auth_rid
  );

  //Add warnings for expired discounts with codes (if necessary)
  if (!empty($order->uc_discounts_codes)) {
    $query = sprintf("SELECT DISTINCT d.*, dc.code code FROM {uc_discounts} d
			LEFT JOIN {uc_discounts_products} dp ON d.discount_id=dp.discount_id
			LEFT JOIN {uc_discounts_terms} dt ON d.discount_id=dt.discount_id
			LEFT JOIN {uc_discounts_skus} ds ON d.discount_id=ds.discount_id
			LEFT JOIN {uc_discounts_roles} dr ON d.discount_id=dr.discount_id
			LEFT JOIN {uc_discounts_codes} dc ON d.discount_id=dc.discount_id
			LEFT JOIN {uc_discounts_classes} dcl ON d.discount_id=dcl.discount_id
			WHERE dc.code IN(%s)
			AND (d.has_role_filter=0%s)
			AND (%s)
			AND (%s)
			AND (%s)
			AND (d.has_expiration<>0 AND d.expiration<=%d)
			AND (d.is_active=%d)
			ORDER BY weight", $escaped_codes_string, $roles_clause, $product_ids_clause,
      $term_ids_clause, $skus_clause, $classes_clause, time(), IS_ACTIVE
    );
    uc_discounts_log("query=". $query);
    $result = db_query($query);
    while ($discount = db_fetch_object($result)) {
      $warnings[] = t('The discount for code "@code" has expired.',
        array("@code" => $discount->code)
      );
    }
  }

  $query = sprintf("SELECT DISTINCT d.* FROM {uc_discounts} d
		LEFT JOIN {uc_discounts_products} dp ON d.discount_id=dp.discount_id
		LEFT JOIN {uc_discounts_terms} dt ON d.discount_id=dt.discount_id
		LEFT JOIN {uc_discounts_skus} ds ON d.discount_id=ds.discount_id
		LEFT JOIN {uc_discounts_classes} dcl ON d.discount_id=dcl.discount_id
		WHERE (d.requires_code=0%s)
		AND (d.has_role_filter=0%s)
		AND (%s)
		AND (%s)
		AND (d.has_expiration=0 OR d.expiration>%d)
		AND (d.is_active=%d)
		ORDER BY weight", $codes_clause, $roles_clause, $product_ids_clause,
    $term_ids_clause, $skus_clause, $classes_clause, time(), IS_ACTIVE
  );
  
  $result = db_query($query);
  $total_discount_amount = 0;
  $discounts = array();

  // Appears to check if order qualifies for each discount then applies discount.  Functionality should be separated, no?
  while ($discount = db_fetch_object($result)) {
    //Get code for discount (if one exists)
    $discount->code = NULL;
    if (!empty($escaped_codes_string)) {
      $query = sprintf("SELECT code FROM {uc_discounts_codes} WHERE code IN(%s) AND discount_id=%d",
        $escaped_codes_string, $discount->discount_id
      );
      $row = db_fetch_array(db_query($query));
      if (!empty($row)) {
        $discount->code = $row["code"];
      }
    }

    //The query handled valid codes and expiration, this block must:
    //	check max uses (if applicable)
    //	check if discount is being combined and can be combined
    //	check if order qualifies (type, requires_single_product_to_qualify, required_product, can_be_combined_with_other_discounts)
    //	determine number of times to apply discount

    //If this discount has a max uses amount, check max uses
    if ($discount->max_uses > 0) {
      $row = db_fetch_array(db_query("SELECT COUNT(*) as uses_count FROM {uc_discounts_uses} WHERE discount_id=%d",
          $discount->discount_id
        ));
      if ($row["uses_count"] >= $discount->max_uses) {
        //If this is a coded discount, add error message
        if (!is_null($warnings) && !is_null($discount->code)) {
          $warnings[] = t('The discount for code "@code" has reached its max number of uses.',
            array("@code" => $discount->code)
          );
        }
        continue;
      }

      $discount->uses_count = $row["uses_count"];
    }

    //If this discount has a max uses per user amount, check max uses per user
    if ($discount->max_uses_per_user > 0) {
      $row = db_fetch_array(db_query("SELECT COUNT(*) as user_uses_count FROM {uc_discounts_uses} WHERE discount_id=%d AND user_id=%d",
          $discount->discount_id, $order->uid
        ));
      if ($row["user_uses_count"] >= $discount->max_uses_per_user) {
        //If this is a coded discount, add warning message
        if (!is_null($warnings) && !is_null($discount->code)) {
          $warnings[] = t('The discount for code "@code" has reached its max number of uses.',
            array("@code" => $discount->code)
          );
        }
        continue;
      }

      $discount->user_uses_count = $row["user_uses_count"];
    }

    //If code exists and this discount has a max uses per code amount, check max uses per code
    if (!is_null($discount->code) && ($discount->max_uses_per_code > 0)) {
      $row = db_fetch_array(db_query("SELECT COUNT(*) as code_uses_count FROM {uc_discounts_uses} WHERE discount_id=%d AND code='%s'",
          $discount->discount_id, $discount->code
        ));
      if ($row["code_uses_count"] >= $discount->max_uses_per_code) {
        //Add warning message
        if (!is_null($warnings)) {
          $warnings[] = t('The discount code "@code" has reached its max number of uses.',
            array("@code" => $discount->code)
          );
        }
        continue;
      }

      $discount->code_uses_count = $row["code_uses_count"];
    }

    //If there are applied discounts, check if discount is being combined and can be combined
    if (count($discounts) > 0) {
      if (!$discount->can_be_combined_with_other_discounts) {
        //If this is a coded discount, add error message
        if (!is_null($warnings) && !is_null($discount->code)) {
          $warnings[] = t('The discount for code "@code" cannot be combined with other discounts.',
            array("@code" => $discount->code)
          );
        }
        continue;
      }

      // Check if the first discount can't be combined
      if (!$discounts[0]->can_be_combined_with_other_discounts) {
        //If first discount was a coded discount, add error message (only print warning if both
        //discounts have codes)
        if (!is_null($warnings) && !empty($discounts[0]->code) && !is_null($discount->code)) {
          $warnings[] = t('The discount for code "@code" cannot be combined with other discounts.',
            array("@code" => $discounts[0]->code)
          );
        }
        continue;
      }
    }

    //Check if order qualifies for this discount (check type, requires_single_product_to_qualify, required_product)
    //Get product IDs for discount
    $discount_product_ids = get_product_ids_for_discount_object($discount);
    if (in_array(ALL_PRODUCTS, $discount_product_ids)) {
      $discount_product_ids = $order_product_ids;
    }

    //Determine total qualifying amount of order (store in order_qualifying_amount)
    //and qualifying subtotal of order (store in order_qualifying_subtotal)

    $order_qualifying_amount = 0;
    $order_qualifying_subtotal = 0;

    // Check if required_product is set.
    if (isset($discount->required_product) && $discount->required_product != 'none') {
      // Get nid of required_product
      $required_product_result = db_query("SELECT DISTINCT nid from {uc_products} WHERE model='%s'", $discount->required_product);
      $required_product_id = db_result($required_product_result);
      // Check if required_product nid is in cart
      if (in_array($required_product_id, $order_product_ids)) {
        switch ($discount->qualifying_type) {
          // Buy $50 of X product
          case QUALIFYING_TYPE_MINIMUM_PRICE:
            //Determine the maximum product subtotal
            $order_qualifying_amount = max($order_qualifying_amount, $order_product_id_subtotal_map[$required_product_id]);
            //Subtract already discounted amount
            $order_qualifying_amount -= $total_discount_amount;
            //TO DO: keep track of each item's subtotal and decrement upon applying discount
            break;
            // Buy 5 of X product...
          case QUALIFYING_TYPE_MINIMUM_QUANTITY:
            //Determine the maximum product quantity
            $order_qualifying_amount = max($order_qualifying_amount, $order_product_id_quantity_map[$required_product_id]);
            //TO DO: keep track of each item's quantity and decrement upon applying discount
            break;
        }
      }
    }

    //Otherwise sum product subtotals or quantities
    else {
      switch ($discount->qualifying_type) {
        case QUALIFYING_TYPE_MINIMUM_PRICE:
          //Determine the total subtotal of discount's products
          foreach ($discount_product_ids as $product_id) {
            if (isset($order_product_id_subtotal_map[$product_id])) {
              if ($discount->requires_single_product_to_qualify) {
                if ($order_product_id_subtotal_map[$product_id] >= $order->qualifying_amount) {
                  //In this case, $order_qualifying amount should be the sum of prices of products that both qualify and meet the minimum qualification amount based on their individual price.
                  $order_qualifying_amount += $order_product_id_subtotal_map[$product_id];
                }
              }
              else {
                $order_qualifying_amount += $order_product_id_subtotal_map[$product_id];
              }
            }
          }
          //Subtract already discounted amount
          $order_qualifying_amount -= $total_discount_amount;
          break;

        case QUALIFYING_TYPE_MINIMUM_QUANTITY:
          //Determine the total quantity of discount's products
          foreach ($discount_product_ids as $product_id) {
            if (isset($order_product_id_quantity_map[$product_id])) {
              if ($discount->requires_single_product_to_qualify) {
                if ($order_product_id_quantity_map[$product_id] >= $discount->qualifying_amount) {
                  //In this case, $order_qualifying amount should be the sum of products that both qualify and meet the minimum qualification amount based on their quantity.  
                  $order_qualifying_amount += $order_product_id_quantity_map[$product_id];
                }
              }
              else {
                $order_qualifying_amount += $order_product_id_quantity_map[$product_id];
              }
            }
          }
          //TO DO: keep track of each item's subtotal and decrement upon applying discount
          break;
      }
    }

    //If order does not qualify for this discount
    if ($order_qualifying_amount < $discount->qualifying_amount) {
      //If this is a coded discount, add warning message
      if (!is_null($warnings) && !is_null($discount->code)) {
        switch ($discount->qualifying_type) {
          case QUALIFYING_TYPE_MINIMUM_PRICE:
            $qualifying_amount = uc_currency_format($discount->qualifying_amount);
            $warnings[] = t('The discount for code "@code" requires a minimum price of @qualifying_amount to qualify.',
              array("@code" => $discount->code, "@qualifying_amount" => $qualifying_amount)
            );
            break;

          case QUALIFYING_TYPE_MINIMUM_QUANTITY:
            $warnings[] = t('The discount for code "@code" requires a minimum quantity of @qualifying_amount to qualify.',
              array("@code" => $discount->code, "@qualifying_amount" => $discount->qualifying_amount)
            );
            break;
        }
      }
      continue;
    }

    //If this discount has a maximum qualifying amount and the order exceeds it
    if ($discount->has_qualifying_amount_max && ($order_qualifying_amount > $discount->qualifying_amount_max)) {
      //If this is a coded discount, add error message
      if (!is_null($warnings) && !is_null($discount->code)) {
        $qualifying_amount_max = uc_currency_format($discount->qualifying_amount_max);
        switch ($discount->qualifying_type) {
          case QUALIFYING_TYPE_MINIMUM_PRICE:
            $warnings[] = t('The discount for code "@code" cannot exceed the price of @qualifying_amount_max to qualify.',
              array("@code" => $discount->code, "@qualifying_amount_max" => $qualifying_amount_max)
            );
            break;

          case QUALIFYING_TYPE_MINIMUM_QUANTITY:
            $warnings[] = t('The discount for code "@code" cannot exceed the quantity of @qualifying_amount_max to qualify.',
              array("@code" => $discount->code, "@qualifying_amount_max" => $discount->qualifying_amount_max)
            );
            break;
        }
      }
      continue;
    }

    //Determine number of times to apply discount
    if ($discount->qualifying_amount != 0) {
      $discount->times_applied = (int)($order_qualifying_amount / $discount->qualifying_amount);
    }
    else $discount->times_applied = 1;
    if ($discount->max_times_applied != 0) {
      $discount->times_applied = min($discount->times_applied, $discount->max_times_applied);
    }

    //Get product IDs in order that are in discount
    $order_and_discount_product_ids = array_intersect($discount_product_ids, $order_product_ids);

    //Create array of product objects in cart to which this discount gets applied.
    $order_and_discount_products = array();
    foreach ($order_and_discount_product_ids as $product_id) {
      if (array_key_exists($product_id, $order_product_id_product_array_map)) {
        $order_and_discount_products = array_merge($order_and_discount_products,
          $order_product_id_product_array_map[$product_id]
        );
      }
    }

    switch ($discount->discount_type) {
      case DISCOUNT_TYPE_PERCENTAGE_OFF_PER_QUALIFYING_ITEM:
        $order_and_discount_product_ids = array_intersect($discount_product_ids, $order_product_ids);
        foreach ($order->products as $this_product) {
          if (in_array($this_product->nid, $order_and_discount_product_ids)) {
            $this_product_price = $this_product->price;
            $product_sum = $product_sum + $this_product_price;
          }
        }
        $discount->amount = $product_sum * $discount->discount_amount;
        break;
      case DISCOUNT_TYPE_FREE_ITEMS:
        //The variable discount_amount is the monitary amount of discount
        $discount_amount = 0;

        //The variable free_items_remaining is the [max] number of free items for the order
        $free_items_remaining = $discount->discount_amount * $discount->times_applied;

        //Loop until all free items have been applied or there are no more products to
        //discount (discount cheapest first)
        while ($free_items_remaining > 0) {
          //Determine cheapest remaining qualifying item
          $cheapest_product = NULL;
          foreach ($order_and_discount_products as $product) {
            //If this product has been fully discounted, continue
            if ($product->uc_discounts_is_fully_discounted) {
              continue;
            }

            //If no current cheapest product exists, use this product
            if (is_null($cheapest_product)) {
              $cheapest_product = $product;
            }
            else {
              //If this product is cheaper than the current cheapest product,
              //use this product instead
              if ($product->price < $cheapest_product->price) {
                $cheapest_product = $product;
              }
            }
          }

          //If no cheapest product could be found, there are no more products to
          //discount, break
          if (is_null($cheapest_product))
          break;

          //Discount up to the lesser of cheapest product quantity and free_items_remaining
          $discount_count = min($cheapest_product->qty, $free_items_remaining);

          //Add current discount amount to running total
          $discount_amount += $discount_count * $cheapest_product->price;

          //Mark item fully discounted
          $cheapest_product->uc_discounts_is_fully_discounted = TRUE;

          $free_items_remaining -= $discount_count;
        }

        $discount->amount = $discount_amount;
        break;
      case DISCOUNT_TYPE_PERCENTAGE_OFF:
        //Always apply once
        $discount->times_applied = 1;

        //If this discount uses all products and previous discount is:
        //	same weight as this discount
        //	percentage off
        //	products of discounts must match
        //discount using same subtotal as last discount
        if (count($discounts) > 0) {
          $last_discount = $discounts[count($discounts) - 1];
          if (($last_discount->weight == $discount->weight)
            && ($last_discount->discount_type == DISCOUNT_TYPE_PERCENTAGE_OFF)
          ) {
            //Last discount's and this discount's products must match exactly
            $are_equal = TRUE;
            $last_discount_product_ids = get_product_ids_for_discount_object($last_discount);
            $this_discount_product_ids = get_product_ids_for_discount_object($discount);
            //If both contain "all products" they are equal
            if (in_array(ALL_PRODUCTS, $last_discount_product_ids)
              && in_array(ALL_PRODUCTS, $this_discount_product_ids)
            ) $are_equal = TRUE;
            //Otherwise check arrays for equality
            else {
              foreach ($this_discount_product_ids as $product_id) {
                if (!in_array($product_id, $last_discount_product_ids)) {
                  $are_equal = FALSE;
                  break;
                }
              }

              if ($are_equal) {
                foreach ($last_discount_product_ids as $product_id) {
                  if (!in_array($product_id, $this_discount_product_ids)) {
                    $are_equal = FALSE;
                    break;
                  }
                }
              }
            }

            if ($are_equal) {
              //($last_discount->amount / $last_discount->discount_amount) == last discount's subtotal
              $local_order_subtotal = ($last_discount->amount / $last_discount->discount_amount);
              $discount->amount = $local_order_subtotal * $discount->discount_amount;
              break;
            }
          }
        }

        //Start patch from lutegrass:
        //This fixes the problem where a percent discount does not apply to all products
        //(but doesn't fix the problem where the products being discounted have already been discounted
        //in full, or the case where the cart consists only of the products included in this discount)
        // Get qualifying products -- ignore "all products" selection
        $discount_product_ids = get_product_ids_for_discount_object($discount, TRUE);

        // Do we have any products
        if (count($discount_product_ids) > 0) {
          $discounted_products_amount = 0;
          foreach ($order_and_discount_products as $product) {
            $discounted_products_amount += $product->price * $product->qty;
          }
          $discount->amount = $discounted_products_amount * $discount->discount_amount;
          // Discount the subtotal so far
        }
        else {
          $discount->amount = max($order_subtotal - $total_discount_amount, 0) * $discount->discount_amount;
        }
        //End patch from lutegrass
        break;

      case DISCOUNT_TYPE_FIXED_AMOUNT_OFF:
        $discount->amount = $discount->discount_amount * $discount->times_applied;
        break;

      case DISCOUNT_TYPE_FIXED_AMOUNT_OFF_PER_QUALIFYING_ITEM:
        //Discount is the total quantity of qualifying items in order (order_qualifying_amount)
        //times the discount amount
        $discount->amount = $discount->discount_amount * $order_qualifying_amount;
        break;
    }

    if (!is_null($messages)) {
      $options = array("@short_description" => $discount->short_description,
        "@code" => $discount->code,
        "@times_applied" => $discount->times_applied,
        "@discount_amount" => uc_currency_format($discount->amount),
        "@time_string" => $time_string,
      );
      if (!is_null($discount->code)) {
        if ($discount->times_applied == 1) $messages[] = t("The discount, '@short_description', with code '@code' was applied for a discount of @discount_amount.",
          $options
        );
        else $messages[] = t("The discount, '@short_description', with code '@code' was applied @times_applied times for a discount of @discount_amount.",
          $options
        );
      }
      else {
        if ($discount->times_applied == 1) $messages[] = t("The discount, '@short_description', was applied for a discount of @discount_amount.",
          $options
        );
        else $messages[] = t("The discount, '@short_description', was applied @times_applied times for a discount of @discount_amount.",
          $options
        );
      }
    }

    //Round the discount to two places
    $discount->amount = round($discount->amount, 2);

    //Add this discount's amount to running total
    $total_discount_amount += $discount->amount;

    //Add this discount to list of discounts applied to order
    $discounts[] = $discount;
  }
  // end of db fetch while loop
  return $discounts;
}

/**
 * Returns all codeless discounts for product.
 *
 * @param $product_id Node ID for product
 */
function get_codeless_discounts_for_product($product, $sort_column = "weight", $is_ascending_sort = TRUE) {
  return get_codeless_discounts_for_product_and_quantity($product, NULL,
    $sort_column, $is_ascending_sort
  );
}

/**
 * Returns all codeless discounts for product when specified quantity is purchased.
 *
 * @param $product_id Node ID for product
 */
function get_codeless_discounts_for_product_and_quantity($product, $quantity = NULL, $sort_column = "weight",
  $is_ascending_sort = TRUE
) {
  if (is_null($product) || !$product) {
    return array();
  }

  //If quantity was specified
  if (!is_null($quantity)) {
    global $user;

    //Create phony order and get discounts for order
    $product->price  = $product->sell_price;
    $product->qty    = $quantity;
    $order           = new stdClass();
    $order->uid      = $user->uid;
    $order->products = array($product);
    return get_discounts_for_order($order);
  }

  //Otherwise make special query

  //Get terms for product
  $term_ids   = array();
  $term_ids[] = ALL_TERMS;
  $result     = db_query("SELECT DISTINCT tid FROM {term_node} WHERE nid=%d", $product->nid);
  while ($row = db_fetch_array($result)) $term_ids[] = $row["tid"];

  //Get SKUs for product
  $skus   = array();
  $skus[] = "'". db_escape_string(ALL_SKUS) ."'";
  $result = db_query("SELECT DISTINCT model FROM {uc_products} WHERE nid=%d", $product->nid);
  while ($row = db_fetch_array($result)) $skus[] = "'". db_escape_string($row["model"]) ."'";

  //Get classes for product
  $classes   = array();
  $classes[] = "'". db_escape_string(ALL_CLASSES) ."'";
  $result    = db_query("SELECT DISTINCT type FROM {node} WHERE nid=%d", $product->nid);
  while ($row = db_fetch_array($result)) $classes[] = "'". db_escape_string($row["type"]) ."'";

  //Create roles clause
  global $user;
  $auth_rid = ($user->uid != 0) ? DRUPAL_AUTHENTICATED_RID : DRUPAL_ANONYMOUS_RID;
  $roles_clause = sprintf(" OR d.discount_id IN(SELECT dr.discount_id FROM {uc_discounts_roles} dr" . ", {users_roles} ur WHERE (dr.role_id=ur.rid AND ur.uid=%d) OR dr.role_id=%d OR dr.role_id=%d)",
    $user->uid, ALL_ROLES, $auth_rid
  );

  $product_ids = array(ALL_PRODUCTS, $product->nid);
  $product_ids_clause = sprintf("d.filter_type<>%d OR dp.product_id IN(%s)",
    FILTER_TYPE_PRODUCTS, join(",", $product_ids)
  );

  $term_ids_clause = sprintf("d.filter_type<>%d OR dt.term_id IN(%s)",
    FILTER_TYPE_TERMS, join(",", $term_ids)
  );

  $skus_clause = sprintf("d.filter_type<>%d OR ds.sku IN(%s)",
    FILTER_TYPE_SKUS, join(",", $skus)
  );

  $classes_clause = sprintf("d.filter_type<>%d OR dcl.class IN(%s)",
    FILTER_TYPE_CLASS, join(",", $classes)
  );

  $sort_order_string = (is_ascending_sort) ? "ASC" : "DESC";

  $query = sprintf("SELECT d.* FROM {uc_discounts} d
		LEFT JOIN {uc_discounts_products} dp ON d.discount_id=dp.discount_id
		LEFT JOIN {uc_discounts_terms} dt ON d.discount_id=dt.discount_id
		LEFT JOIN {uc_discounts_skus} ds ON d.discount_id=ds.discount_id
		LEFT JOIN {uc_discounts_classes} dcl ON d.discount_id=dcl.discount_id
		WHERE d.requires_code=0
		AND (d.has_role_filter=0%s)
		AND (%s)
		AND (%s)
		AND (%s)
		AND (%s)
		AND (d.has_expiration=0 OR d.expiration>%d)
		ORDER BY d.%s %s", $roles_clause, $product_ids_clause, $term_ids_clause, $skus_clause, $classes_clause, time(),
  $sort_column, $sort_order_string
  );
  uc_discounts_log("query=". $query);
  $result = db_query($query);

  $discounts = array();
  while ($discount = db_fetch_object($result)) $discounts[] = $discount;
  return $discounts;
}

/**
 * Inserts a uc_discounts_uses row and returns its discount_use_id.
 */
function uc_discounts_uses_insert($discount_id, $user_id, $order_id, $code, $times_applied, $amount,
  $insert_timestamp = NULL
) {
  if (is_null($insert_timestamp)) {
    $insert_timestamp = time();
  }

  $query = "INSERT INTO {uc_discounts_uses}(discount_id, user_id, order_id, code, times_applied, amount, insert_timestamp)" . " VALUES(%d, %d, %d, '%s', %d, %f, %d)";
  uc_discounts_log("query=". $query);
  db_query($query, $discount_id, $user_id, $order_id, $code, $times_applied, $amount, $insert_timestamp);
  return db_last_insert_id("uc_discounts_uses", "discount_use_id");
}

/**
 * Deletes all uc_discounts_uses rows for a discount.
 */
function uc_discounts_uses_delete_for_discount($discount_id) {
  $query = "DELETE FROM {uc_discounts_uses} WHERE discount_id=%d";
  uc_discounts_log("query=". $query);
  db_query($query, $discount_id);
}

/**
 * Deletes all uc_discounts_uses rows for an order.
 */
function uc_discounts_uses_delete_for_order($order_id) {
  $query = "DELETE FROM {uc_discounts_uses} WHERE order_id=%d";
  uc_discounts_log("query=". $query);
  db_query($query, $order_id);
}

/**
 * Returns order codes for order.
 */
function uc_discounts_order_codes_get($order_id) {
  //Get order's codes from database
  $row = db_fetch_array(db_query(
      "SELECT codes FROM {uc_discounts_order_codes} WHERE order_id=%d", $order_id
    ));
  return (!empty($row)) ? $row["codes"] : NULL;
}

/**
 * Inserts a uc_discounts_order_codes row.
 */
function uc_discounts_order_codes_insert($order_id, $codes_string) {
  $query = "INSERT INTO {uc_discounts_order_codes}(order_id, codes) VALUES(%d, '%s')";
  uc_discounts_log("query=". $query);
  db_query($query, $order_id, $codes_string);
}

/**
 * Deletes all uc_discounts_order_codes rows for an order.
 */
function uc_discounts_order_codes_delete($order_id) {
  $query = "DELETE FROM {uc_discounts_order_codes} WHERE order_id=%d";
  uc_discounts_log("query=". $query);
  db_query($query, $order_id);
}


///////////////////////////////////////////////////////////////////
//Misc. helper functions
///////////////////////////////////////////////////////////////////

/**
 * Returns a string list of codes into an array of codes
 */
function get_codes($codes_string) {
  $codes = array();

  if (empty($codes_string)) {

    return $codes;

  }

  $raw_codes = explode("\n", $codes_string);

  foreach ($raw_codes as $raw_code) {
    $code = trim($raw_code);
    if (!empty($code)) {
      $codes[] = $code;
    }
  }

  return $codes;
}

/**
 * Create a codes string from passed codes array.
 * Note: returns "" if passed array is null
 */
function create_codes_string($codes) {
  if (empty($codes)) {
    return "";
  }
  return join("\n", $codes);
}

/**
 * Returns an array of qualifying types with descriptions.
 */
function qualifying_type_options() {
  static $options = NULL;

  if (is_null($options)) {
    $options = array(QUALIFYING_TYPE_MINIMUM_PRICE => t("Minimum price"),
      QUALIFYING_TYPE_MINIMUM_QUANTITY => t("Minimum quantity"),
    );
  }

  return $options;
}

function qualifying_type_name($qualifying_type) {
  $options = qualifying_type_options();
  return $options[$qualifying_type];
}

/**
 * Returns an array of discount types with descriptions.
 */
function discount_type_options() {
  static $options = NULL;

  if (is_null($options)) {
    $options = array(DISCOUNT_TYPE_PERCENTAGE_OFF => t("Percent off"),
      DISCOUNT_TYPE_FIXED_AMOUNT_OFF => t("Fixed amount off"),
      DISCOUNT_TYPE_FIXED_AMOUNT_OFF_PER_QUALIFYING_ITEM => t("Fixed amount off per qualifying item"),
      DISCOUNT_TYPE_FREE_ITEMS => t("Free items"),
      DISCOUNT_TYPE_PERCENTAGE_OFF_PER_QUALIFYING_ITEM => t("Percentage off per qualifying item"),
    );
  }

  return $options;
}

function discount_type_name($discount_type) {
  $options = discount_type_options();
  return $options[$discount_type];
}

function uc_discounts_add_to_existing_map_number_value(&$a, $key, $value) {
  $a[$key] = (array_key_exists($key, $a)) ? $a[$key] + $value : $value;
}

function uc_discounts_log($s) {
  //	error_log($s);
}

