<?php
// $Id: tax.module,v 1.16.2.4.2.7 2007/03/11 12:31:57 gordon Exp $

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

/**
 * Implementation of hook_help()
 */
function tax_help($section = 'admin/help#tax') {
  switch ($section) {
    case 'admin/modules#description':
      return t('<b>EC:</b> Enable a tax API for ecommerce.');
    case 'admin/store/tax':
      return t('Enter tax rules here.');
    case 'admin/settings/tax':
      return t('tax calculation methods are <a href="%tax_calc_url">here</a>.', array('%tax_calc_url' => url('admin/store/tax')));
  }
}

/**
 * Implementation of hook_menu()
 */
function tax_menu($may_cache) {
  $items = array();
  $access = user_access('administer store');
  if ($may_cache) {
    $items[] = array(
      'path' => 'admin/store/tax',
      'title' => t('taxes'),
      'access' => $access,
      'callback' => 'tax_admin_overview',
    );
    $items[] = array(
      'path' => 'admin/store/tax/list',
      'title' => t('taxes'),
      'access' => $access,
      'type' => MENU_DEFAULT_LOCAL_TASK,
      'weight' => -10,
    );
    $items[] = array(
      'path' => 'admin/store/tax/add',
      'title' => t('add tax'),
      'access' => $access,
      'callback' => 'tax_form_add',
      'type' => MENU_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/store/tax/edit',
      'title' => t('edit tax'),
      'callback' => 'tax_form_edit',
      'access' => $access,
      'type' => MENU_CALLBACK,
    );
    $items[] = array(
      'path' => 'admin/store/tax/delete',
      'title' => t('delete tax'),
      'callback' => 'tax_delete_form',
      'access' => $access,
      'type' => MENU_CALLBACK,
    );
    $items[] = array(
      'path' => 'admin/store/tax/add/autocomplete',
      'title' => t('tax autocomplete'),
      'callback' => 'tax_autocomplete_state',
      'access' => $access,
      'type' => MENU_CALLBACK,
    );
  }
  return $items;
}

/**
 * Implementation of hook_checkoutapi().
 */
function tax_checkoutapi(&$txn, $op, $arg3 = NULL, $arg4 = NULL) {
  $output = '';
  switch ($op) {
    case 'review':
      if ($txn == 'tax') return TRUE;
      $box['subject'] = t('Tax');
      $billing = $txn->address['billing'];
      $rules = tax_get_rules();
      $states = store_build_states(variable_get('ec_country', 'us'));
      $total_tax = 0;
      $taxable_amount = 0;

      foreach ($txn->items as $item) {
        if (product_is_shippable($item->vid)) {
          $taxable_amount += store_adjust_misc($txn, $item) * $item->qty;
        }
      }

      foreach ($rules as $rule) {
        switch ($rule->realm) {
          case 'city':
            if (drupal_strtoupper($billing->city) == drupal_strtoupper($rule->realm_value) && tax_rule_product_match($txn, $rule)) {
              if ($tax = tax_calculate($taxable_amount, $rule->operand, $rule->operator)) {
                $total_tax += $tax;
              }
            }
            break;
          case 'country':
            if (drupal_strtoupper($billing->country) == drupal_strtoupper($rule->realm_value) && tax_rule_product_match($txn, $rule)) {
              if ($tax = tax_calculate($taxable_amount, $rule->operand, $rule->operator)) {
                $total_tax += $tax;
              }
            }
            break;
          case 'state':
            if ($billing->state) {
              if (drupal_strtoupper($states[$billing->state]) == drupal_strtoupper($rule->realm_value) && tax_rule_product_match($txn, $rule)) {
                if ($tax = tax_calculate($taxable_amount, $rule->operand, $rule->operator)) {
                  $total_tax += $tax;
                }
              }
            }
            break;
        }
      }
      if ($total_tax) {
        if (($key = store_search_misc(array('type' => 'tax'), $txn)) !== false) {
          $txn->misc[$key]->price = round($total_tax, 2);
        }
        else {
          $misc = array(
            'type' => 'tax',
            'description' => 'Tax',
            'price' => round($total_tax, 2),
            'weight' => 10
          );
          $txn->misc[] = (object)$misc;
        }
      }

      return;
  }
}

/**
 * Show an overview of all the tax rules.
 */
function tax_admin_overview() {
  $output = '';
  $header = array(t('rule'), t('operations'));

  $result = db_query('SELECT * FROM {ec_tax}');
  while ($rule = db_fetch_object($result)) {
    $rows[] = array(t("Add %operator%operand where %realm = '%realm-value'", array('%operator' => $rule->operator, '%operand' => ($rule->operand == '+' ? '' : '%'), '%realm' => $rule->realm, '%realm-value' => $rule->realm_value)), l(t('edit'), "admin/store/tax/edit/$rule->taxid"). ' | '. l(t('delete'), "admin/store/tax/delete/$rule->taxid"));
  }

  if (count($rows) == 0) {
    $rows[] = array(array('data' => '<em>'. t('There are currently no tax rules.') .'</em>', 'colspan' => 5));
  }
  $output .= theme('table', $header, $rows);

  return $output;
}

/********************************************************************
 * Module Functions :: Save Routines
 ********************************************************************/

/**
 * Save operator.  Automatically handles updating or inserting based on $edit['taxid']
 * 
 * @param $edit
 *   an array of tax fields to be saved
 */
function tax_save_rule($edit) {
  $fields = tax_fields();
  if ($edit['taxid'] > 0 && db_result(db_query('SELECT COUNT(taxid) FROM {ec_tax} WHERE taxid = %d', $edit['taxid']))) {
    // Prepare the query:
    foreach ($edit as $key => $value) {
      if (in_array($key, $fields)) {
        if ($key == 'ptype') {
          $value = implode(',', $value);
        }
        $q[] = db_escape_string($key) ." = '%s'";
        $v[] = $value;
      }
    }

    $v[] = $edit['taxid'];
    db_query('UPDATE {ec_tax} SET '. implode(', ', $q) .' WHERE taxid = %d', $v);
  }
  else {
    $edit['taxid'] = db_next_id('{ec_tax}_taxid');
    // Prepare the query:
    foreach ($edit as $key => $value) {
      if (in_array((string) $key, $fields)) {
        if ($key == 'ptype') {
          $value = implode(',', $value);
        }
        $k[] = db_escape_string($key);
        $v[] = $value;
        $s[] = "'%s'";
      }
    }
    
    db_query('INSERT INTO {ec_tax} ('. implode(', ', $k) .') VALUES('. implode(', ', $s) .')', $v);
  }
}

/**
 * Delete operator.
 * 
 * @param $taxid
 *   taxid of rule to be deleted.
 */
function tax_delete_rule($taxid) {
  db_query('DELETE FROM {ec_tax} WHERE taxid = %d', $taxid);
}

/**
 * Menu callback for admin/store/tax/add
 * 
 * @param $realm
 *   optional realm arguement used to fake the multipart form.
 */
function tax_form_add($realm = NULL) {
  if ($realm) {
    $form = tax_form_screen2(array('realm' => $realm));
    return drupal_get_form('tax_form_screen2', $form);
  }  

  $form = tax_form_screen1();
  return drupal_get_form('tax_form_screen1', $form);
}

/**
 * Menu callback for admin/store/tax/edit
 * 
 * @param $taxid
 *   taxid of rule to be edited.
 */
function tax_form_edit($taxid) {
  $edit = tax_get_rule($taxid);
  $form = tax_form_screen1();
  unset($form['submit']);
  $form = $form + (array)tax_form_screen2($edit);
  return drupal_get_form('tax_form_screen2', $form);  
}

/**
 * Menu callback for admin/store/tax/delete
 * 
 * @param $taxid
 *   callback arguement specifying which taxid to delete.
 */
function tax_delete_form($taxid = NULL) {
  if ($taxid) {
    $form['taxid'] = array(
      '#type' => 'value',
      '#value' => $taxid,
    );
    return confirm_form(
      'tax_delete_form',
      $form,
      t('Are you sure you want to delete this tax item?'),
      'admin/store/tax',
      t('This action cannot be undone.'),
      t('Delete'), t('Cancel')
    );
  }  
}

/**
 * Submit handler for first screen of tax rule deletion form.
 */
function tax_delete_form_submit($form_id, $form_values) {
  tax_delete_rule($form_values['taxid']);
  return 'admin/store/tax';
}

/**
 * Screen 1 of our multipart addition form.
 */
function tax_form_screen1() {
  $locations = array(
    'state' => t('State / Province / Region'), 
    'country' => t('Country'), 
    'city' => t('City')
  );
  $form['realm'] = array(
    '#type' => 'select',
    '#title' => t('Type of Location to define the new tax rule for'),
    '#options' => $locations,
    '#description' => t('State / Province / Region'),
  );
  $form['submit'] = array(
    '#type' => 'submit', 
    '#value' => t('Next')
  );
  return $form;
}

/**
 * Screen 2 of our multipart addition form.
 */
function tax_form_screen2($edit = array()) {
  $help = t('This value will be checked against the customer\'s shipping address.');
  if ($edit['realm'] == 'state') {
    $form['realm_value'] = array(
      '#type' => 'textfield',
      '#title' => t('Value for State / Province / Region'),
      '#default_value' => $edit['realm_value'],
      '#size' => 32,
      '#maxlength' => 32,
      '#autocomplete_path' => 'admin/store/tax/add/autocomplete',
      '#required' => TRUE,
      '#description' => $help,
    );
  }
  elseif ($edit['realm'] == 'country') {
    $country = store_build_countries();
    array_unshift($country, t('Please choose...'));
    $form['realm_value'] = array(
      '#type' => 'select',
      '#title' => t('Country'),
      '#default_value' => ($edit['realm_value'] ? $edit['realm_value'] : variable_get('ec_country', '')),
      '#options' => $country,
      '#required' => TRUE,
      '#description' => t('Country'),
    );
  }
  elseif ($edit['realm'] == 'city') {
    $form['realm_value'] = array(
      '#type' => 'textfield',
      '#default_value' => $edit['realm_value'], 
      '#maxlength' =>  32, 
      '#required' => TRUE,
      '#description' => t('This value will be checked against the customer\'s shipping address.'), 
    );
  }
  else {
    #$form = tax_invoke_taxapi($edit, 'admin form');
    #$output .= $form[0];
    
    /*
    * The above is not solved yet, don't really know what this call is for?!
    * Should there ever be an else to this structure?
    */
  }
  $form['realm'] = array(
    '#type' => 'hidden', 
    '#value' => $edit['realm']
  );
  $form['rate'] = array(
    '#type' => 'textfield',
    '#title' => t('Adjustment'),
    '#default_value' => $edit['rate'], 
    '#maxlength' =>  13, 
    '#required' => TRUE,
    '#description' => t('This rate can be a simple price addition or a percentage multiplier. For example, to add a 5.00 tax, enter +5.00. To multiply the gross price times 75%, enter 75%. If no operand is given, addition is assumed.'), 
  );
  $form['operator'] = array(
    '#type' => 'value',
  );
  $form['operand'] = array(
    '#type' => 'value',
  );

  $ptypes = product_get_ptypes();
  asort($ptypes);
  $form['ptype'] = array(
    '#type' => 'checkboxes',
    '#title' => t('Product type'),
    '#default_value' => $edit['ptype'],
    '#options' => $ptypes,
    '#required' => TRUE,
    '#description' => t('Check the product types this tax rule applies to.'),
  );
  if ($edit['taxid']) {
    $form['taxid'] = array(
      '#type' => 'hidden', 
      '#value' => $edit['taxid']
    );
    $form['submit'] = array(
      '#type' => 'submit', 
      '#value' => t('Update')
    );
  }
  else {
    $form['submit'] = array(
      '#type' => 'submit', 
      '#value' => t('Submit')
    );
  }
  return $form;
}

/**
 * Submit handler for first screen of tax addition form.
 */
function tax_form_screen1_submit($form_id, $form_values) {
  return 'admin/store/tax/add/'. $form_values['realm'];
}

/**
 * Validate a tax rule.
 */
function tax_form_screen2_validate($form_id, $form_values, &$form) {
  if (strstr($form_values['rate'], '%')) {
    form_set_value($form['operand'] , '%');
    $operator = rtrim($form_values['rate'], '%');
  }
  else {
    form_set_value($form['operand'] , '+');
    $operator = ltrim($form_values['rate'], '+');
  }

  form_set_value($form['operator'], $operator);
  if (!is_numeric($operator)) {
    form_set_error('rate', t('Invalid rate operator'));
  }
}

/**
 * implementation of form_hook_submit() for tax_form_screen2.
 */
function tax_form_screen2_submit($form_id, $form_values) {
  tax_save_rule($form_values);
  return 'admin/store/tax';
}

/**
 * Retrieve a pipe delimited string of autocomplete suggestions for existing locations
 */
function tax_autocomplete_state($string) {
  $states = store_build_states();
  $matches = array();
  foreach ($states as $country) {
    if ($state = preg_grep("/^$string(.+?)/i", $country)) {
      $matches = array_merge($matches, $state);
    }
  }

  print drupal_to_js($matches);
  exit();
}

/**
 * Invoke a hook_nodeapi() operation in all modules.
 *
 * @param &$txn
 *   A txn object.
 * @param $op
 *   A string containing the name of the taxapi operation.
 * @param $a3, $a4
 *   Arguments to pass on to the hook, after the $txn and $op arguments.
 * @return
 *   The returned value of the invoked hooks.
 */
function tax_invoke_taxapi(&$txn, $op, $a3 = NULL, $a4 = NULL) {
  $return = array();
  foreach (module_list() as $name) {
    $function = $name .'_taxapi';
    if (function_exists($function)) {
      $result = $function($txn, $op, $a3, $a4);
      if (is_array($result)) {
        $return = array_merge($return, $result);
      }
      else if (isset($result)) {
        $return[] = $result;
      }
    }
  }
  return $return;
}

/**
 * Get tax rule from the DB.
 */
function tax_get_rule($tid) {
  $result = db_query('SELECT * FROM {ec_tax} WHERE taxid = %d', (int) $tid);
  $rule = db_fetch_array($result);
  $rule['rate'] = $rule['operand'] == '%' ? $rule['operator'] . $rule['operand'] : $rule['operand'] . $rule['operator'];
  $rule['ptype'] = explode(',', $rule['ptype']);
  return $rule;
}

/**
 * Get all rules from the DB.
 */
function tax_get_rules() {
  $rules = array();
  $result = db_query('SELECT * FROM {ec_tax}');
  while ($rule = db_fetch_object($result)) {
    $rules[] = $rule;
  }
  return $rules;
}

/**
 * Return the tax cost for a given rule.
 */
function tax_calculate($gross, $operand, $op2) {
  switch ($operand) {
    case '+':
      return $op2;
    case '%':
      return($gross * ($op2/100));
  }
}

/**
 * Return TRUE if $rule applies to the current products in the cart.
 */
function tax_rule_product_match($txn, $rule) {
  $ptypes = explode(',', $rule->ptype);
  foreach ($txn->items as $item) {
    if (in_array($item->ptype, $ptypes)) {
      return TRUE;
    }
  }
  return FALSE;
}

/**
 * Returnt the table names in the database.
 */
function tax_fields() {
  return array('taxid', 'realm', 'realm_value', 'ptype', 'operator', 'operand');
}
