<?php
// $Id: product.module,v 1.116.2.17.2.25 2007/06/03 02:54:36 gordon Exp $

/********************************************************************
 * Drupal Hooks
 ********************************************************************/

//TODO: Make sure only authenticated users can purchase recurring products.

/**
 * Theme function to render product node.
 */
function theme_product_price($node) {
  return '<div class="price"><strong>'. t('Price') .'</strong>: ' . module_invoke('payment', 'format', product_adjust_price($node, 'product')+product_get_specials($node, 'product', true)) . '</div>';
}

/**
 * The default view when calling index.php?q=product. I'm fraught with
 * indecision about this.  On one hand i could hardocde the layout here,
 * or i could keep it open and flexible so folks could control the layout
 * in the theme by checking $node->type == 'product' in the theme _node function.
 */
function theme_product_view_collection() {

  $columns  = 3;
  $rows     = 5;

  $result = pager_query(db_rewrite_sql('SELECT n.nid FROM {node} n INNER JOIN {ec_product} p ON n.vid = p.vid WHERE n.status = 1 AND p.pparent = 0 ORDER BY n.sticky DESC, n.created DESC'), $rows * $columns, 0);

  $output = '<table class="product-table">';
  for ($i = 0; $node = db_fetch_object($result); $i++) {

    if ($i % $columns == 0) {
      $output .= '<tr>';
    }

    $node = node_load($node->nid);
    $teaser = true;
    $page   = false;

    $node->body = str_replace('<!--break-->', '', $node->body);
    if (node_hook($node, 'view')) {
      node_invoke($node, 'view', $teaser, $page);
    }
    else {
      $node = node_prepare($node, $teaser);
    }
    node_invoke_nodeapi($node, 'view', $teaser, $page);

    $output .= '<td id="nid_'. $node->nid .'"><p>'. l($node->title, "node/$node->nid") ."</p><div>$node->teaser</div></td>\n";

    if ($i % $columns == $columns - 1) {
      $output .= "</tr>\n";
    }
  }

  if ($i % $columns != 0) {
    $output .= "</tr>\n";
  }
  $output .= '</table>';

  if ($pager = theme('pager', NULL, $rows * $columns, 0)) {
    $output .= $pager;
  }

  return $output;
}

function theme_invoice_form_product(&$form) {
  $output.= form_render($form);
  return $output;
}

function product_edit_form(&$invoice) {
  $form['#theme'] = 'product_edit_form';
  $form['products'] = array(
    '#type' => 'fieldset',
    '#title' => t('Products'),
    '#tree' => true,
    '#theme' => 'product_edit_form_product',
  );
  foreach ((array)$invoice->items as $key => $item) {
    $node = node_load($item->nid);
    $form['products'][$node->nid]['#node'] = $node;
    $form['products'][$node->nid]['title'] = array(
      '#value' => l($item->title, "node/" .($node->pparent ? $node->pparent : $node->nid)),
    );
    $price = store_adjust_misc($invoice, $item);
    $form['products'][$node->nid]['#total'] = product_has_quantity($node) ? $price * $item->qty : $price;
    if ($node->is_recurring) {
      $form['products'][$node->nid]['recurring'] = array(
        '#value' => product_recurring_nice_string($node)
      );
    }
    if ($node->ptype == 'tangible' && $node->availability != null && $node->availability != 1) {
      $form['products'][$node->nid]['availability'] = array(
        '#value' => availability_get_message($node->availability)
      );
    }
    $form['products'][$node->nid]['qty'] = array(
      '#type' => product_has_quantity($node) ? 'textfield' : 'value',
      '#default_value' => $item->qty, 
      '#size' => 2, 
      '#maxlength' => 2
    );
    $form['products'][$node->nid]['delete'] = array(
      '#type' => 'checkbox',
    );
    if ($extra = product_invoke_productapi($item, 'cart form', $invoice)) {
      $form['products'][$node->nid]['extra'] = $extra;
      $form['products'][$node->nid]['extra']['#parents'] = array('products', $node->nid);
    }
  }
  $form['add'] = array(
    '#type' => 'fieldset',
    '#title' => t('Add Products'),
    '#collapsible' => true,
    '#collapsed' => count($invoice->items) ? true : false,
  );
  $form['add']['new_products'] = array(
    '#type' => 'textfield',
    '#title' => t('Product Ids'),
    '#maxlength' => 60,
    '#autocomplete_path' => 'product/autocomplete/all',
    '#description' => t('Enter the id\'s of the products that you which to add to the invoice. Here is a <a href="%product_quicklist" onclick="window.open(this.href, \'%product_quicklist\', \'width=480,height=480,scrollbars=yes,status=yes\'); return false">list of all products</a>.', array('%product_quicklist' => url('admin/store/products/quicklist'))),
  );
  $form['submit']['invoiceop'] = array(
    '#type' => 'submit',
    '#value' => t('Update Products'),
    '#attributes' => array('id' => 'invoiceop'),
  );
  $form['submit']['continue'] = array(
    '#type' => 'submit',
    '#value' => t('continue'),
  );
  return $form;
}

function theme_product_edit_form(&$form) {

  $output.= form_render($form['products']);
  $output.= form_render($form);

  return $output;
}

function theme_product_edit_form_product(&$form) {
  $children = element_children($form);
  if (count($children)) {
    $extra = FALSE;

    foreach ($children as $nid) {
      if ($form[$nid]['extra']) {
        $extra = TRUE;
        break;
      }
    }

    foreach ($children as $nid) {
      $total+= $form[$nid]['#total'];
      $desc = form_render($form[$nid]['title']) .'<br />';
      if ($form[$nid]['recurring']) {
        $desc.= '<div class="recurring-details">'. form_render($form[$nid]['recurring']) .'</div>';
      }
      if ($form[$nid]['availability']) {
        $desc.= form_render($form[$nid]['availability']);
      }
      $desc.= '<p>'. payment_format($form[$nid]['#total']) .'</p>';
      $row = array(
        array('data' => $desc),
        array('data' => form_render($form[$nid]['qty'])),
      );
      if ($extra) {
        if ($form[$nid]['extra']) {
          $row[] = array('data' => form_render($form[$nid]['extra']));
          $extra = TRUE;
        }
        else {
          $row[] = '';
        }
      }
      $row[] = array('data' => form_render($form[$nid]['delete']), 'align' => 'center');
      $rows[] = $row;
    }
    $header = array(
      array('data' => t('Items')),
      array('data' => t('Qty')),
    );
    if ($extra) {
      $header[] = array('data' => '');
    }
    $header[] = array('data' => t('Delete'));
    $output.= theme('table', $header, $rows);
  }
  else {
    $output.= '<p>'. t('No Products Currently on this Invoice') .'</p>';
  }
  $output.= form_render($form);
  return $output;
}

/**
 * Define access restrictions
 *
 * Each product type module can implement its own hook_access()
 * function for access control.
 */
function product_access($op, $node) {
  // Get available product types
  $ptypes = product_get_ptypes();
  // If there are no product types then there is nothing to administer
  // this prevent a useless link under 'create content'
  if (empty($ptypes)) {
    return FALSE;
  }
  
  // Users with administer permissions have full access.
  if (user_access('administer products')) {
    return TRUE;
  }
  
  // The first argument is 'product' if there is no current node,
  // e.g., when node module is determining which node types can be created.
  if ($node == 'product') {
    // Iterate through each product type to see if any returns TRUE.
    foreach (array_keys($ptypes) as $ptype) {
      $access = module_invoke($ptype, 'access', $op, $node);
      if ($access == TRUE) {
        return TRUE;
      }
    }
    // If no product type module has allowed create, update, or delete access, return FALSE.
    if ($op == 'create' || $op == 'update' || $op == 'delete') {
      return FALSE;
    }
  }
  // If there is a node object, invoke its product type's permissions.
  else {
    return module_invoke($node->ptype, 'access', $op, $node);
  }
}

/**
 * Implementation of hook_form().
 */
function product_form(&$node) {

  $form['title'] = array(
    '#type' => 'textfield',
    '#title' => t('Name'),
    '#required' => TRUE,
    '#default_value' => $node->title,
    '#weight' => -5
  );
  $form['body_filter']['body'] = array(
    '#type' => 'textarea',
    '#title' => t('Description'),
    '#default_value' => $node->body,
    '#description' => t('Provide the product details to display on the page.'),
    '#rows' => 20,
    '#required' => TRUE
  );
  $form['body_filter']['format'] = filter_form($node->format);

  // Get base form elements.
  $form['product'] = product_base_form_elements($node);

  // Get the product-type-specific bits.
  $form['product'] = array_merge($form['product'], (array) module_invoke($node->ptype, 'productapi', $node, 'form'));

  return $form;
}

/**
 * Implementation of hook_help().
 */
function product_help($section = 'admin/help#product') {
  $output = "";

  switch ($section) {
    case 'admin/modules#description':
      $output = t('<b>EC:</b> Creates products for ecommerce.');
      break;

    case 'node/add#product':
      $output = t('A product is a good or service that you wish to sell on your site.');
      $output .= '<br /><br />' . product_types_listing();
      break;
  }

  return $output;
}

/**
 * Define internal Drupal links
 *
 * This hook enables modules to add links to many parts of Drupal. Links
 * may be added in nodes, in the global navigation bar, and in the main
 * site navigation menu, for example.
 */
function product_link($type, $node = null, $teaser = false) {
  $links = array();

  if ($type == 'node' && $node->ptype) {
    /* Determine whether or not the 'add to cart' link needs to be displayed. */
    $item = module_invoke('cart', 'get_items');
    if (variable_get('product_cart_addition_by_link', 1) && $node->hide_cart_link == 0) {

      /* Right here we need to check if a given product type is in stock */
      if (product_get_attributes($node, 'in_stock') !== false) {
        // Is it already in our cart?
        if ($item[$node->nid]->qty) {
          $links[] = t('This item is in <a href="%cart_view">your shopping cart</a>.', array('%cart_view' => url('cart/view')));
        }
        else {
          $links[] = l(t('add to cart'), "cart/add/$node->nid", array('class' => 'cart_link', 'title' => t('Add this item to your shopping cart.')), variable_get('product_cart_is_destination', 0) ? NULL : drupal_get_destination());
        }
      }
      else {
        $links[] = t('sold out');
      }
    }
  }

  return $links;
}

/**
 * Implementation of hook_load().
 */
function product_load($node) {
  static $products = array(), $vid_xref = array();

  if (!$node->vid) {
    $node->vid = db_result(db_query('SELECT n.vid FROM {node} n WHERE n.nid = %d', $node->nid));
  }

  if ($products[$node->vid] === NULL) {
    $product = db_fetch_object(db_query('SELECT * FROM {ec_product} WHERE vid = %d', $node->vid));
    /* Merge the product info for the specific type. */
    if ($product_type = module_invoke($product->ptype, 'productapi', $product, 'load')) {
      foreach ($product_type as $key => $value) {
        $product->$key = $value;
      }
    }

    $products[$node->vid] = $product;
  }

  return $products[$node->vid];
}

/**
 * Implementation of hook_menu()
 */
function product_menu($may_cache) {

  $items = array();

  if ($may_cache) {
    $access = user_access('administer products');

    $items[] = array(
     'path' => 'node/add/product',
     'title' => t('product'),
     'callback' => 'product_add',
     'access' => product_access('create', 'product')
    );
    $node = new StdClass();
    foreach (product_get_ptypes() as $ptype => $name) {
      $node->ptype = $ptype;
      $items[] = array(
       'path' => 'node/add/product/' . $ptype,
       'title' => $name,
       'access' => product_access('create', $node)
      );
    }
    $items[] = array(
      'path' => 'product',
      'title' => t('products'),
      'callback' => 'product_page',
      'access' => user_access('access content'),
    );
    $items[] = array(
      'path' => 'product/autocomplete',
      'callback' => 'product_ac_product',
      'type' => MENU_CALLBACK,
    );
    $items[] = array(
      'path' => 'admin/store/products',
      'title' => t('products'),
      'access' => $access, 
      'callback' => 'product_overview',
    );
    $items[] = array(
      'path' => 'admin/store/products/quicklist',
      'title' => t('product quicklist'),
      'callback' => 'product_quicklist',
      'access' => $access, 
      'type' => MENU_CALLBACK,
    );
  }
  else {

    foreach(product_get_ptypes() as $type => $description) {
      $items[] = array(
        'path' => 'admin/settings/content-types/product/' . $type,
        'title' => $description,
        'callback' => 'product_types_configure',
        'access' => user_access('administer products'),
        'type' => MENU_CALLBACK
      );
    }
  }

  return $items;
}

/**
 * Implementation of hook_node_info().
 */
function product_node_info() {
  return array('product' => array('name' => t('product'), 'base' => 'product'));
}

/**
 * Implementation of hook_node_name().
 */
function product_node_name($node) {
  return t('product');
}

/**
 * Implementation of hook_form_alter()
 */
function product_form_alter($form_id, &$form) {
  if ($form['type'] && $form_id == $form['type']['#value'] . '_node_form') {
    if (user_access('administer products') && $form['type']['#value'] != 'product') {
      if ($form['#node']->ptype) {
        $form['product'] = _product_transform_product_form($form['#node']);
      }
      else {
        $form['product_transform'] = _product_transform_form($form['#node']);
      }
      // skip validation if you are turning a node into a product.
      $form['#after_build'][] = '_product_transform_skip_validation';
    }
  }
  // Add a fieldset with links for configuring individual product types.
  if ($form_id == $form['type']['#value'] . '_node_settings') {
    $form['product_types'] = array(
      '#type' => 'fieldset',
      '#title' => t('Configure product types'),
      '#collapsible' => TRUE,
      '#weight' => -10,
      '#children' => product_types_configure()
    );
  }
}

/**
 * Implementation of hook_invoiceapi()
 */
function product_invoiceapi(&$invoice, $op) {
  if (is_string($invoice) && $op == 'form') return t('products');

  switch ($op) {
    case 'form':
      $form['products'] = product_edit_form($invoice);
      
      $form['seen']['#tree'] = true;
      $form['seen']['product'] = array(
        '#type' => 'value',
        '#value' => true,
      );
      return $form;
      break;

    case 'validate':
      if ($invoice->new_products) {
        foreach (explode(',', $invoice->new_products) as $nid) {
          if ($nid && (!($node = node_load($nid)) || !$node->ptype)) {
            form_set_error('', !$node ? t('Cannot Locate Product %nid', array('%nid' => $nid)) : t('Node "%title" is not a valid product', array('%title' => $node->title)));
          }
        }
      }
      foreach ((array)$invoice->products as $nid => $item) {
        if (!$item['qty'] || !is_numeric($item['qty']) || $item['qty'] < 1) {
          form_set_error("products][$nid][qty", t('The quantity must be a positive integer'));
        }
      }
    case 'submit':
      $change_products = false;
      if ($invoice->new_products) {
        foreach (explode(',', $invoice->new_products) as $nid) {
          if (!$invoice->items[$nid]) {
            product_invoice_add($invoice, $nid);
            $change_products = true;
          }
        }
        unset($invoice->new_products);
      }
      if ($invoice->products) {
        foreach ($invoice->products as $nid => $item) {
          if ($item['delete']) {
            unset($invoice->items[$nid]);
            $change_products = true;
          }
          else {
            if (!$invoice->items[$nid]) {
              product_invoice_add($invoice, $nid);
              $change_products = true;
            }
            foreach ($item as $key => $value) {
              if (!in_array($key, array('delete'))) {
                $invoice->items[$nid]->$key = $value;
              }
            }
          }
        }
        unset($invoice->products);
      }
      store_recalc_transaction($invoice);  

      /* reset the shippable flag */
      if ($change_products) {
        $invoice->shippable = false;
        foreach ($invoice->items as $item) {
          if (product_is_shippable($item->vid)) {
            $invoice->shippable = true;
            break;
          }
        }
      }
      if ($_POST['op'] == t('Update Products')) {
        return "admin/store/invoice/{$invoice->invoice_id}/edit/product";
      }
      break; 

    case 'post_update':
      if ($invoice->items) {
        foreach ($invoice->items as $nid => $item) {
          if ($item->nid != $nid) {
            if (!isset($invoice->items[$nid])) {
              $invoice->items[$item->nid] = $item;
            }
            unset($invoice->items[$nid]);
            $goto_products = TRUE;
          }
        }
        if ($goto_products) {
          invoice_save($invoice->invoice_id, $invoice);
          drupal_set_message(t('There has been a problem with the products entered. This has now been corrected. Please check your products and continue.'), 'error');
          drupal_goto("admin/store/invoice/{$invoice->invoice_id}/edit/product");
        }
      }
      break;

    case 'review':
      if (!$invoice->seen['product']) {
        drupal_goto('admin/store/invoice/'. arg(3) .'/edit/product');
      }
      break;
  }
}

/**
 * Generates product type options fieldset.
 */
function _product_transform_form(&$node) {
  // Product type collapsed fieldset
  $form = array(
    '#type' => 'fieldset',
    '#title' => t('Product'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE
  );
  // Choose a product type
  $ptypes[FALSE] = array('name' => t('not a product'), 'help' => t('This item not for sale.'));
  foreach (product_get_ptypes() as $key => $value) {
    $ptypes[$key] = array(
      'name' => $value,
      'help' => module_invoke($key, 'help', 'node/add/product#' . $key)
    );
  }
  // Display each radio option, with descriptive text
  foreach ($ptypes as $key => $data) {
    $form['ptype'][$key] = array(
      '#type' => 'radio',
      '#title' => $data['name'],
      '#return_value' => $key,
      '#default_value' => $node->ptype,
      '#description' => $data['help'],
      '#parents' => array('ptype'),
     );
  }
  $form['product_add'] = array(
    '#type' => 'button',
    '#value' => t('Add to store')
  );
  return $form;
}

/**
 * Generates product form fields to be added into the node form.
 */
function _product_transform_product_form(&$node) {
  // Get base form elements.
  $form = product_base_form_elements($node);
  // Change ptype from 'value' to 'hidden'
  $form['ptype']['#type'] = 'hidden';
  // Get the product-type-specific bits.
  $form = array_merge($form, (array) module_invoke($node->ptype, 'productapi', $node, 'form'));
  $form['product_remove'] = array(
    '#type' => 'checkbox',
    '#title' => t('Remove from store'),
    '#description' => t('Check here to delete product information.  Takes effect when changes are submitted.')
  );
  return $form;
}

/**
 * Skip price validation if we don't have submited price data.
 */
function _product_transform_skip_validation($form, $edit) {
  if ($_POST['op'] == t('Add to store')) {
    _product_skip_validation($form);
  }
  return $form;
}

function _product_skip_validation(&$element) {
  foreach (element_children($element) as $key) {
    if (isset($element[$key]) && $element[$key]) {
      _product_skip_validation($element[$key]);
    }
  }

  $element['#validated'] = TRUE;
}

/**
 * Menu callback; presents each product type configuration page.
 */
function product_types_configure() {
  if (arg(4)) {
    $form = array('#ptype' => arg(4));
    return system_settings_form('product_type_settings', $form);
  }
  else {
    $header = array(t('Type'), t('Operations'));

    $rows = array();
    foreach (product_get_ptypes() as $type => $name) {
      $form = array('#ptype' => $type);
      // Only show this link if at least one module has added settings.
      // We test for > 1 because we are sending one element, the ptype.
      // We don't use module_invoke because we need to pass by reference.
      foreach (module_implements('form_alter') as $module) {
        $function = $module . '_form_alter';
        $function('product_type_settings', $form);
      }
      if (count($form) > 1) {
        $rows[] = array($name, l(t('configure'), 'admin/settings/content-types/product/'. $type));
      }
    }

    return theme('table', $header, $rows);
  }
}

/**
 * Implementation of hook_nodeapi().
 *
 * Provide product functionality for nodes that are not products
 * per se but have been assigned product properties.
 */
function product_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {

  switch ($op) {
    case 'load':
      if ($node->type != 'product') {
        $product = product_load($node);
        if($product) {
          foreach ($product as $key => $value) {
            $node->$key = $value;
          }
        }
      }
      break;
    case 'prepare':
      if ($node->type != 'product' && !$node->ptype) {
        $node->ptype = $_POST['edit']['ptype'];
      }
      break;

    case 'validate':
      if ($node->type != 'product' && $node->ptype) {
        product_validate($node, $a3);
      }
      break;

    case 'insert':
    case 'update':
      if ($node->product_remove) {
        // user has checked 'remove this item from store'.
        product_delete($node);
      }
      else if ($node->ptype) {
        product_save($node);
      }
      break;
 
    case 'view':
      /* If we have a pseudo product, add some product specific theming to it. */
      if ($node->type == 'product' && $a4) {
        $breadcrumb[] = l(t('Home'), '');
        $breadcrumb[] = l(t('product'), 'product');
        drupal_set_breadcrumb($breadcrumb);
      }
      if ($node->ptype) {
        if ($a3) {
          $node->teaser.= theme('product_price', $node);
        }
        else {
          $node->body.= theme('product_price', $node);
        }
        if ($node->ptype && $a4 && !variable_get('product_cart_addition_by_link', 1)) {
          $form = product_cart_form($node);
          $node->body.= drupal_get_form('product_cart_form', $form);
        }
      }
      break;

    case 'delete':
      if ($node->type != 'product' && $node->ptype) {
        product_delete($node);
      }
      break;
  }
}

/**
 * Implementation of hook_perm().
 */
function product_perm() {
  return array('administer products');
}

/********************************************************************
 * Module Functions
 ********************************************************************/

function product_base_form_elements(&$node) {
  $form['ptype'] = array(
    '#type' => 'value',
    '#value' => $node->ptype
  );
  
  $form['price'] = array(
    '#type' => 'price',
    '#title' => t('Price'),
    '#size' => 25,
    '#maxlength' => 50,
    '#required' => TRUE,
    '#default_value' => module_invoke('payment', 'format', $node->price),
    '#description' => t('How much does this product retail for? Note: This price may be different from the selling price due to price adjustments elsewhere.'),
    '#weight' => -20,
  );

  $form['sku'] = array(
    '#type' => 'textfield',
    '#title' => t('SKU'),
    '#size' => 25,
    '#maxlength' => 50,
    '#default_value' => $node->sku,
    '#description' => t('If you have an unique identifier for this product from another system or database, enter that here. This is optional, as system IDs are automatically created for each product.'),
    '#weight' => -10,
  );

  $form['hide_cart_link'] = array(
    '#type' => 'radios',
    '#title' => t("'Add to cart' link"),
    '#default_value' => $node->hide_cart_link ? $node->hide_cart_link : 0,
    '#options' => array(t('Visible'), t('Hidden')),
    '#description' =>  t('Choose whether or not you want the \'Add to cart\' link visible for this product.'),
    '#weight' => -5,
  );

  return $form;
}

/**
 * Present a product submission form or a set of links to such forms.
 *
 * This function is adapted from node_add().
 */
function product_add() {
  global $user;

  $edit = isset($_POST['edit']) ? $_POST['edit'] : '';

  // If a product type has been specified, validate its existence.

  $ptypes = product_get_ptypes();
  if (arg(3) && in_array(arg(3), array_keys($ptypes))) {
    $type = arg(3);

    // Initialize settings:
    $node = array('uid' => $user->uid, 'name' => $user->name, 'type' => 'product', 'ptype' => $type);

    // Allow the following fields to be initialized via $_GET (e.g. for use
    // with a "blog it" bookmarklet):
    foreach (array('title', 'teaser', 'body') as $field) {
      if ($_GET['edit'][$field]) {
        $node[$field] = $_GET['edit'][$field];
      }
    }
    $output = node_form($node);
    drupal_set_title(t('Submit %name', array('%name' => $ptypes[$type])));
  }
  else {
    $output = product_types_listing();
  }

  return $output;
}

function product_types_listing($link = 'node/add/product') {
  // If no (valid) product type has been provided, display a product type overview.
  $node = new StdClass();
  foreach (product_get_ptypes() as $ptype => $name) {
    $node->ptype = $ptype;
    if (product_access('create', $node)) {
      $out = '<dt>'. l($name, "$link/$ptype", array('title' => t('Add a %s.', array('%s' => $name)))) .'</dt>';
      $out .= '<dd>'. implode("\n", module_invoke_all('help', 'node/add/product#'. $ptype)) .'</dd>';
      $item[$name] = $out;
    }
  }

  if (isset($item)) {
    ksort($item);
    $output = t('Choose from the following available product types:') .'<dl>'. implode('', $item) .'</dl>';
  }
  else {
    $output = t('You are not allowed to create products.');
  }
  return $output;
}

/**
 * The controller for the plugin API hooks. It iterates through the enabled
 * plugins and calls their *_productapi hook if one exists, passing it the
 * action (via $op) to execute.
 *
 * @param &$node
 *   Either a node object, node array, or a string containing the node type.
 * @param $op
 *   A string containing the name of the productapi operation.
 * @param $a3, $a4
 *   Arguments to pass on to the hook, after the $node and $op arguments.
 * @return
 *   The returned value of the invoked hooks.
 */
function product_invoke_productapi(&$node, $op, $a3 = NULL, $a4 = NULL, $a5 = NULL) {
  $func = $node->ptype .'_productapi';
  if (function_exists($func)) {
    return $func($node, $op, $a3, $a4, $a5);
  }
}

/*
 *  Helper function to list available product types
 *  In normal use this should not be empty, so the watchdog warning is added.
 *  This warning will occur when someone enables the product module but no modules
 *  that implement the product_api.
 * 
 * @return
 *   The returned array of enabled product types 
 *  (e.g. [file] => 'file download', [tangible] => 'shippable product').
 */
function product_get_ptypes(){
  static $ptypes = NULL;
  if (!is_array($ptypes)) {
    $ptypes = array();
    foreach (module_implements('productapi') as $name) {
      if ($name != 'payment') {
        $func = $name .'_productapi';
        if ($extra = $func($name, 'wizard_select')) {
          $ptypes = array_merge($ptypes, $extra);
        }
      }
    }
    if(empty($ptypes)) {
      watchdog('product', t('No product types are available.'),WATCHDOG_WARNING);
    }
  }
  return $ptypes;
}

/**
 * product_get_specials()
 */
function product_get_specials($node, $type, $total_only = false, $txn = NULL) {
  $specials = array();
  if (!(product_get_attributes($node, 'no_discounts'))) {
    foreach (module_implements('product_specials') as $name) {
      if ($extra = module_invoke($name, 'product_specials', $node, $specials, $type, $txn)) {
        $specials = array_merge($specials, $extra);
      }
    }
  }
  if ($total_only) {
    $total = 0;
    foreach ($specials as $special) {
      if (is_array($special)) {
        $total+= $special['price']*(isset($special['qty']) ? $special['qty'] : (isset($node->qty) ? $node->qty : 1));
      }
      else {
        $total+= $special*(isset($node->qty) ? $node->qty : 1);
      }
    }
    return $total;
  }
  else {
    return $specials;
  }
}

/**
 * Get a list of all the product attributes, or check that 1 exists
 *
 * Calling this function will return a list of all atributes that is
 * connected to the product type.
 *
 * @param $node
 *  The product node which has all the product details.
 *
 * @param $attr
 *  If the process wants to just check to see if a attribute exists in the
 *  product, then pass the attribute to check.
 *
 * @return
 *  If an $attr is past then TRUE or FALSE a returned to the calling
 *  program, otherwise all the attributes are past back.
 */
function product_get_attributes($node, $attr = NULL) {
  $attributes = product_invoke_productapi($node, 'attributes', $attr);

  if (!is_array($attributes) && empty($attributes)) {
    $attributes = array();
  }
  else if (!is_array($attributes)) {
    $attributes = (array)$attributes;
  }

  if ($attr) {
    return in_array($attr, $attributes);
  }
  else {
    return $attributes;
  }
}

/**
 * The controller for viewing products.  Callback happens via menu().
 *
 * @return string Completely themed HTML page.
 */
function product_page() {
  return theme('product_view_collection');
}

function product_elements() {
  $items['price'] = array('#input' => true, '#validate' => array('valid_price' => array()));

  return $items;
}

function valid_price($element) {
  $price = normalize_price($element['#value']);
  if ($price === FALSE) {
    $blank = (!$element['#required'] ? t(' or blank') : '');
    form_error($element, t('Please enter a numeric%blank value for the product price.', array('%blank' => $blank)));
  }
  else {
    form_set_value($element, $price);
  }
}

function theme_price($element) {
  return theme('textfield', $element);
}

/**
 * Returns a price normalized for the price database fields in ecommerce. The
 * price is normalized to a float format and that value is returned. If $price
 * contains characters other than a leading payment_symbol, decimal point or
 * thousands separator this function returns FALSE.
 *
 * @param $price String representation of the price.
 * @return Normalized price string (as a float) or FALSE if $price contains
 * invalid characters. If $price is an empty string it is returned without
 * flagging it as an error.
 */
function normalize_price($price) {
  $price = trim($price);

  if (!empty($price)) {
    $symb = variable_get('payment_symbol', '$');
    $symblen = strlen($symb);
    $symbpos = (variable_get('payment_symbol_position', 1) == 1 ? 0 : -$symblen);
    $pricestart = ($symbpos == 0 ? $symblen : 0);
    $decimal = variable_get('payment_decimal', '.');
    
    if (substr($price, $symbpos, $symblen) == $symb) {
      $price = substr($price, $pricestart, strlen($price) - $symblen);
    }

    $price = str_replace(variable_get('payment_thousands', ','), '', $price);
    $price = str_replace(variable_get('payment_decimal', '.'), '.', $price);

    if (is_numeric($price)) {
      return $price;
    }
  } else {
    return '';
  }

  return FALSE;
}

function product_form_validate(&$edit, $form) {
  $f = $edit->ptype. '_productapi';
  if (function_exists($f)) {
    $f($edit, 'validate', $form);
  }

  return ((form_get_errors()) ? false : true);
}

/**
 * Assumes the node data has already been saved to its respective tables and
 * only updates the main product table and the plugin of the product type
 * (e.g., product_tangible).
 */
function product_save($node) {
  if ($node->ptype) {
    $fields = product_fields();
    /* Be very sure that we have a product entry to update! */
    if (db_result(db_query('SELECT COUNT(vid) FROM {ec_product} WHERE vid = %d', $node->vid))) {
      foreach ($fields as $field) {
        if (isset($node->$field)) {
          $q[] = "{$field} = '%s'";
          $v[] = $node->$field;
        }
      }
      $v[] = $node->vid;
      db_query('UPDATE {ec_product} SET '. implode(', ', $q) ." WHERE vid = %d", $v);
      module_invoke($node->ptype, 'productapi', $node, 'update');
    }
    else {
      foreach ($fields as $field) {
        if (isset($node->$field)) {
          $k[] = $field;
          $v[] = $node->$field;
          $s[] = "'%s'";
        }
      }

      db_query('INSERT INTO {ec_product} ('. implode(', ', $k) .') VALUES('. implode(', ', $s) .')', $v);
      module_invoke($node->ptype, 'productapi', $node, 'insert');
    }
  }
}

function product_validate(&$node, $form) {
  product_form_validate($node, $form);
}

/**
 * Implementation of hook_insert().
 */
function product_insert($node) {
  product_save($node);
}

/**
 * Implementation of hook_update().
 */
function product_update($node) {
  product_save($node);
}

function product_delete($node, $revision_only = false) {
  if ($revision_only) {
    db_query('DELETE FROM {ec_product} WHERE nid = %d AND vid = %d', $node->nid, $node->vid);
  }
  else {
    db_query('DELETE FROM {ec_product} WHERE nid = %d', $node->nid);
  }
  module_invoke('cart', 'productapi', $node, 'delete');
  module_invoke($node->ptype, 'productapi', $node, 'delete', $revision_only);

  drupal_set_message(t('product deleted'));
}

/**
 * Display all products in a table format.
 */
function product_overview() {
  $header = array(
    array('data' => t('nid'), 'field' => 'p.nid', 'sort' => 'asc'),
    array('data' => t('sku'), 'field' => 'p.sku'),
    array('data' => t('title'), 'field' => 'n.title'),
    array('data' => t('price'), 'field' => 'p.price'),
    array('data' => t('type'), 'field' => 'p.ptype'),
  );
  $sql = 'SELECT n.nid, n.title, p.* FROM {node} as n, {ec_product} as p WHERE n.vid = p.vid AND n.status = 1'. tablesort_sql($header);
  $result = pager_query(db_rewrite_sql($sql), 50);

  while ($data = db_fetch_object($result)) {
    $rows[] = array(l($data->nid, "node/$data->nid/edit"), $data->sku, l($data->title, "node/$data->nid"), module_invoke('payment', 'format', product_adjust_price($data, 'product')+product_get_specials($data, 'product', true)), $data->ptype);
  }

  $pager = theme('pager', NULL, 50, 0);
  if (!empty($pager)) {
    $rows[] = array(array('data' => $pager, 'colspan' => 6));
  }

  if ($rows) {
    $output = theme('table', $header, $rows);
  }
  else {
    $output =  t('<p>There are no products to view.');
  }

  return $output;
}

function product_quicklist() {

  global $base_url;
  $base_url .= '/';

  $html = "<html>\n<head>\n<base href=\"$base_url\" />\n<style type=\"text/css\" media=\"all\">\n@import url(misc/drupal.css);\n</style>\n</head>\n<body>\n";

  $header = array(
    array('data' => t('nid'), 'field' => 'p.nid', 'sort' => 'asc'),
    array('data' => t('sku'), 'field' => 'p.sku'),
    array('data' => t('title'), 'field' => 'n.title'),
    array('data' => t('price'), 'field' => 'p.price'),
    array('data' => t('type'), 'field' => 'p.ptype')
  );
  $sql = 'SELECT * FROM {node} n, {ec_product} p WHERE n.vid = p.vid AND n.status = 1'. tablesort_sql($header);
  $result = pager_query(db_rewrite_sql($sql), 50);

  while ($data = db_fetch_object($result)) {
    $rows[] = array($data->nid, $data->sku, $data->title, module_invoke('payment', 'format', product_adjust_price($data, 'product')+product_get_specials($data, 'product', true)), $data->ptype);
  }

  $pager = theme("pager", NULL, 50, 0);
  if (!empty($pager)) {
    $rows[] = array(array('data' => $pager, 'colspan' => 5));
  }

  print $html;
  if ($rows) {
    print theme("table", $header, $rows);
  }
  else {
    print t('<p>There are no transactions to view. You can manually <a href="%add_transaction">add transactions</a>, however.</p>', array('%add_transaction' => 'admin/store/transaction/add'));
  }
  print '</body></html>';
}

/**
 * Product Autocomplete
 */
function product_ac_product($type, $string) {
  switch ($type) {
    case 'parent':
      $where = '(p.pparent = 0)';
      break;

    case 'child':
      $where = '(p.pparent != 0)';
      break;

    default:
      $where = '1';
  }
  if ((strpos($string, ',')) !== false) {
    $pre_string = substr($string, 0, strrpos($string, ',')+1);
    $string = substr($string, strrpos($string, ',')+1);
  }
  $result = db_query(db_rewrite_sql("SELECT n.nid, n.title, p.sku, p.price FROM {node} n INNER JOIN {ec_product} p ON n.vid = p.vid WHERE n.status = 1 AND (n.nid LIKE '%s%%' OR n.title LIKE '%s%%' OR p.sku LIKE '%s%%') AND ". $where ." ORDER BY n.title ASC, p.sku ASC"), $string, $string, $string);

  while ($product = db_fetch_object($result)) {
    $matches[$pre_string. $product->nid] = theme('product_ac_product', $product);
  }
  print drupal_to_js($matches);
  exit();
}

function theme_product_ac_product($product) {
  $output.= "<b>{$product->nid}</b> - ". check_plain($product->title). ($product->sku ? '[<i>'. check_plain($product->sku). '</i>]' : '');
  return $output;
}

/**
 * Determine if a product or a product type is shippable.
 *
 * If a vid is given, the test is for a particular product,
 * whereas if a ptype is given it's for the ptype.
 *
 * @param $vid
 *   Version id of product.
 * @param $ptype
 *   Product type.
 * @return
 *    Boolean.
 */
function product_is_shippable($vid = NULL, $ptype = NULL) {

  $product = $vid ? db_fetch_object(db_query('SELECT ptype FROM {ec_product} WHERE vid = %d', $vid)) : (object) array('ptype' => $ptype);

  $attributes = (array) module_invoke($product->ptype, 'productapi', $product, 'attributes');

  if (in_array('is_shippable', $attributes)) {
    return TRUE;
  }

  return FALSE;
}

/**
 * Determine if the product has a quantity.
 */
function product_has_quantity($node) {
  static $items = array();
  if (!$node->vid) {
    $node = product_load($node);
  }
  if (!isset($items[$node->vid])) {
    $attributes = module_invoke($node->ptype, 'productapi', $node, 'attributes', 'no_quantity');
    if (!$attributes) {
      $attributes = array();
    }
    $items[$node->vid] = in_array('no_quantity', $attributes);
  }
  return !$items[$node->vid];
}

function product_adjust_price(&$node, $type = 'cart', $txn = NULL) {
  $old_price  = $node->price;
  $new_price = product_invoke_productapi($node, 'adjust_price', $old_price, $type, $txn);
  return (($new_price > 0) ? $new_price : $old_price);
}

/**
 * The names of the database columns in the table.
 */
function product_fields($table = 'ec_product') {
  return array('nid', 'vid', 'pparent', 'sku', 'price', 'ptype', 'hide_cart_link');
}

function product_cart_form($node) {
  $items = cart_get_items();
  $form['nid'] = array(
    '#type' => 'value',
    '#value' => $node->nid,
  );
  $form['cart'] = array(
    '#prefix' => '<div id="product-add-item">',
    '#suffix' => '</div>',
  );

  if (product_has_quantity($node)) {
    $form['cart']['qty'] = array(
      '#type' => 'textfield',
      '#title' => t('Quantity'),
      '#name' => 'qty',
      '#default_value' => $items[$node->nid] ? $items[$node->nid]->qty : 1,
      '#size' => 5,
    );
  }

  $form['cart']['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Add to Cart'),
  );

  return $form;
}

function product_cart_form_validate($form_id, $form_values) {
  if (isset($form_values['qty'])) {
    if (!is_numeric($form_values['qty'])) {
      form_set_error('qty', t('Quantity must be a numeric value'));
    }
  }
}

function product_cart_form_submit($form_id, $form_values) {
  if ($message = cart_add_item($form_values['nid'], isset($form_values['qty']) ? $form_values['qty'] : NULL)) {
    drupal_set_message($message);
  }
  if (variable_get('product_cart_is_destination', 0)) {
    return 'cart/view';
  }
}

function product_invoice_add(&$invoice, $nid, $data = null) {
  if ($node = node_load($nid)) {
    $invoice->items[$node->nid] = $node;
    $invoice->items[$node->nid]->qty = 1;
    $invoice->items[$node->nid]->data = $data;
    $invoice->items[$node->nid]->price = product_adjust_price($invoice->items[$nid], $invoice);

    if (product_is_shippable($invoice->items[$node->nid]->vid)) {
      $invoice->shippable = true;
    }
  }
}

/**
 * Implements hook_token_list() from the token module.
 */
function product_token_list($type = 'all') {
  $vars = array();
  
  if ($type == 'all' || $type == 'product') {
    $vars = node_token_list('all');
    $vars['product']['product-sku'] = t('Product SKU.');
    $vars['product']['product-price'] = t('Product price.');
  }
  
  return $vars;
}

/**
 * Implements hook_token_values() from the token module.
 */
function product_token_values($type, $object = NULL) {
  if ($type == 'product') {
    $node = (object)$object;
    $values = node_token_values('node', $node);
    $values['product-sku'] = $node->sku;
    $values['product-price'] = payment_format($node->price);
  }
  
  return $values;
}

/**
 * Check a form to see if it is a node that has been turned into a product.
 *
 * @param $form_id
 *  The Id of the form that is being past.
 *
 * @param $form
 *  The form array which needs to be checked.
 *
 * @param #ptype
 *  If the form is dependent on a type of product, then this can be past to
 *  add an additional check.
 */
function product_form_is_product($form_id, $form, $ptype = NULL) {
  if (isset($form['type']) && $form['type']['#value'] .'_node_form' == $form_id && $form['#node']->ptype) {
    return ($ptype ? ($node['#node']->ptype == $ptype ? TRUE : FALSE) : TRUE);
  }
  return FALSE;
}
