<?php

/**
 * @file
 * Defines tax rates and Rules integration for configuring tax rules for
 *   applicability and display.
 */


/**
 * Implements hook_hook_info().
 */
function commerce_tax_hook_info() {
  $hooks = array(
    'commerce_tax_type_info' => array(
      'group' => 'commerce',
    ),
    'commerce_tax_type_info_alter' => array(
      'group' => 'commerce',
    ),
    'commerce_tax_type_insert' => array(
      'group' => 'commerce',
    ),
    'commerce_tax_type_update' => array(
      'group' => 'commerce',
    ),
    'commerce_tax_type_delete' => array(
      'group' => 'commerce',
    ),
    'commerce_tax_rate_info' => array(
      'group' => 'commerce',
    ),
    'commerce_tax_rate_info_alter' => array(
      'group' => 'commerce',
    ),
    'commerce_tax_rate_insert' => array(
      'group' => 'commerce',
    ),
    'commerce_tax_rate_update' => array(
      'group' => 'commerce',
    ),
    'commerce_tax_rate_delete' => array(
      'group' => 'commerce',
    ),
    'commerce_tax_type_calculate_rates' => array(
      'group' => 'commerce',
    ),
  );

  return $hooks;
}

/**
 * Returns an array of tax type objects keyed by name.
 */
function commerce_tax_types() {
  // First check the static cache for a tax types array.
  $tax_types = &drupal_static(__FUNCTION__);

  // If it did not exist, fetch the types now.
  if (!isset($tax_types)) {
    $tax_types = array();

    // Find tax types defined by hook_commerce_tax_type_info().
    foreach (module_implements('commerce_tax_type_info') as $module) {
      foreach (module_invoke($module, 'commerce_tax_type_info') as $name => $tax_type) {
        // Initialize tax rate properties if necessary.
        $defaults = array(
          'name' => $name,
          'display_title' => $tax_type['title'],
          'description' => '',
          'display_inclusive' => FALSE,
          'round_mode' => COMMERCE_ROUND_NONE,
          'rule' => 'commerce_tax_type_' . $name,
          'module' => $module,
        );

        $tax_types[$name] = array_merge($defaults, $tax_type);
      }
    }

    // Last allow the info to be altered by other modules.
    drupal_alter('commerce_tax_type_info', $tax_types);
  }

  return $tax_types;
}

/**
 * Resets the cached list of tax types.
 */
function commerce_tax_types_reset() {
  $tax_types = &drupal_static('commerce_tax_types');
  $tax_types = NULL;
}

/**
 * Returns a single tax type object.
 *
 * @param $name
 *   The name of the tax type to return.
 *
 * @return
 *   The specified tax type object or FALSE if it did not exist.
 */
function commerce_tax_type_load($name) {
  $tax_types = commerce_tax_types();
  return empty($tax_types[$name]) ? FALSE : $tax_types[$name];
}

/**
 * Returns the titles of every tax type keyed by name.
 */
function commerce_tax_type_titles() {
  $titles = array();

  foreach (commerce_tax_types() as $name => $tax_type) {
    $titles[$name] = $tax_type['title'];
  }

  return $titles;
}

/**
 * Implements hook_commerce_price_component_type_info().
 */
function commerce_tax_commerce_price_component_type_info() {
  $components = array();

  // Add a price component type for each tax rate that specifies it.
  foreach (commerce_tax_rates() as $name => $tax_rate) {
    if ($tax_rate['price_component']) {
      $components[$tax_rate['price_component']] = array(
        'title' => $tax_rate['title'],
        'display_title' => $tax_rate['display_title'],
        'tax_rate' => $name,
      );
    }
  }

  return $components;
}

/**
 * Returns an array of tax rate objects keyed by name.
 */
function commerce_tax_rates() {
  // First check the static cache for a tax rates array.
  $tax_rates = &drupal_static(__FUNCTION__);

  // If it did not exist, fetch the types now.
  if (!isset($tax_rates)) {
    $tax_rates = array();

    // Find tax rates defined by hook_commerce_tax_rate_info().
    foreach (module_implements('commerce_tax_rate_info') as $module) {
      foreach (module_invoke($module, 'commerce_tax_rate_info') as $name => $tax_rate) {
        // Initialize tax rate properties if necessary.
        $defaults = array(
          'name' => $name,
          'display_title' => $tax_rate['title'],
          'description' => '',
          'rate' => 0,
          'type' => '',
          'rules_component' => 'commerce_tax_rate_' . $name,
          'default_rules_component' => TRUE,
          'price_component' => 'tax|' . $name,
          'calculation_callback' => 'commerce_tax_rate_calculate',
          'module' => $module,
        );

        $tax_rates[$name] = array_merge($defaults, $tax_rate);
      }
    }

    // Last allow the info to be altered by other modules.
    drupal_alter('commerce_tax_rate_info', $tax_rates);
  }

  return $tax_rates;
}

/**
 * Resets the cached list of tax rates.
 */
function commerce_tax_rates_reset() {
  $tax_rates = &drupal_static('commerce_tax_rates');
  $tax_rates = NULL;
}

/**
 * Returns a single tax rate object.
 *
 * @param $name
 *   The name of the tax rate to return.
 *
 * @return
 *   The specified tax rate object or FALSE if it did not exist.
 */
function commerce_tax_rate_load($name) {
  $tax_rates = commerce_tax_rates();
  return empty($tax_rates[$name]) ? FALSE : $tax_rates[$name];
}

/**
 * Returns the titles of every tax rate keyed by name.
 */
function commerce_tax_rate_titles() {
  $titles = array();

  foreach (commerce_tax_rates() as $name => $tax_rate) {
    $titles[$name] = $tax_rate['title'];
  }

  return $titles;
}

/**
 * Calculates taxes of a particular type by invoking any components that match
 * the tax type.
 *
 * @param $tax_type
 *   The tax type object whose rates should be calculated.
 * @param $line_item
 *   The line item to which the taxes should be applied.
 */
function commerce_tax_type_calculate_rates($tax_type, $line_item) {
  // Prepare an array of rules components to load.
  $component_names = array();

  // Loop over each tax rate in search of matching components.
  foreach (commerce_tax_rates() as $name => $tax_rate) {
    // If the current rate matches the type and specifies a default component...
    if ($tax_rate['type'] == $tax_type['name'] && !empty($tax_rate['rules_component'])) {
      $component_names[] = $tax_rate['rules_component'];
    }
  }

  // Load and invoke the tax rules components.
  if (!empty($component_names)) {
    foreach (rules_config_load_multiple($component_names) as $component_name => $component) {
      rules_invoke_component($component_name, $line_item);
    }
  }

  // Allow modules handling tax application on their own to apply rates of the
  // current type as well.
  module_invoke_all('commerce_tax_type_calculate_rates', $tax_type, $line_item);
}

/**
 * Applies a tax rate to the unit price of a line item.
 *
 * @param $tax_rate
 *   The tax rate to apply to the line item.
 * @param $line_item
 *   The line item whose unit price will be modified to include the tax.
 *
 * @return
 *   A price array representing the tax applied to the line item or FALSE if
 *   none was applied.
 */
function commerce_tax_rate_apply($tax_rate, $line_item) {
  // If a valid rate is specified...
  if (isset($tax_rate['rate']) && is_numeric($tax_rate['rate'])) {
    $wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);

    // Don't apply tax if the unit price has a NULL amount.
    if (is_null($wrapper->commerce_unit_price->value())) {
      return;
    }

    // Invoke the tax rate's calculation callback and apply the returned tax
    // price to the line item.
    if ($tax_price = $tax_rate['calculation_callback']($tax_rate, $wrapper)) {
      // Add the tax to the unit price's data array along with a display inclusive
      // property used to track whether or not the tax is included in the price.
      $included = FALSE;

      // If the rate specifies a valid tax type that is display inclusive...
      if (($tax_type = commerce_tax_type_load($tax_rate['type'])) &&
        $tax_type['display_inclusive']) {
        // Include the tax amount in the displayed unit price.
        $wrapper->commerce_unit_price->amount = $wrapper->commerce_unit_price->amount->value() + $tax_price['amount'];
        $included = TRUE;
      }

      // Update the data array with the tax component.
      $wrapper->commerce_unit_price->data = commerce_price_component_add(
        $wrapper->commerce_unit_price->value(),
        $tax_rate['price_component'],
        $tax_price,
        $included
      );

      return $tax_price;
    }
  }

  return FALSE;
}

/**
 * Calculates a price array for the tax on the unit price of a line item.
 *
 * @param $tax_rate
 *   The tax rate array for the tax to calculate.
 * @param $line_item_wrapper
 *   An entity_metadata_wrapper() for the line item whose unit price should be
 *     used in the tax calculation.
 *
 * @return
 *   The tax price array or FALSE if the tax is already applied.
 */
function commerce_tax_rate_calculate($tax_rate, $line_item_wrapper) {
  // By default, do not duplicate a tax that's already on the line item.
  if (!is_null($line_item_wrapper->commerce_unit_price->value()) &&
    !commerce_price_component_load($line_item_wrapper->commerce_unit_price->value(), $tax_rate['price_component'])) {
    // Calculate the tax amount.
    $amount = $line_item_wrapper->commerce_unit_price->amount->value() * $tax_rate['rate'];

    return array(
      'amount' => commerce_tax_rate_round_amount($tax_rate, $amount),
      'currency_code' => $line_item_wrapper->commerce_unit_price->currency_code->value(),
      'data' => array(
        'tax_rate' => $tax_rate,
      ),
    );
  }

  return FALSE;
}

/**
 * Rounds an amount for a given tax rate.
 *
 * @param $tax_rate
 *   The tax rate array for the tax to calculate.
 * @param $amount
 *   The amount of the tax to round.
 *
 * @return
 *   The amount rounded based on the type of tax it is for.
 */
function commerce_tax_rate_round_amount($tax_rate, $amount) {
  // Round the amount according to the tax type's specification.
  $tax_type = commerce_tax_type_load($tax_rate['type']);
  return commerce_round($tax_type['round_mode'], $amount);
}

/**
 * Implements hook_field_widget_form_alter().
 *
 * Alter price widgets on the product form to have tax inclusive price entry.
 * This hook was added in Drupal 7.8, so entering prices including VAT will
 * require at least that version.
 */
function commerce_tax_field_widget_form_alter(&$element, &$form_state, $context) {
  // Act on widgets for fields of type commerce_price on commerce_products.
  if ($context['field']['type'] == 'commerce_price' && $context['instance']['entity_type'] == 'commerce_product') {
    // Build an array of tax types that are display inclusive.
    $inclusive_types = array();

    foreach (commerce_tax_types() as $name => $tax_type) {
      if ($tax_type['display_inclusive']) {
        $inclusive_types[$name] = $tax_type['title'];
      }
    }

    // Build an options array of tax rates of these types.
    $options = array();

    foreach (commerce_tax_rates() as $name => $tax_rate) {
      if (in_array($tax_rate['type'], array_keys($inclusive_types))) {
        $options[$inclusive_types[$tax_rate['type']]][$name] = t('Including !title', array('!title' => $tax_rate['title']));
      }
    }

    if (!empty($options)) {
      // Find the default value for the tax included element.
      $default = '';

      if (!empty($element['data']['#default_value']['include_tax'])) {
        $default = $element['data']['#default_value']['include_tax'];
      }

      $element['currency_code']['#title'] = '&nbsp;';

      // Note that because this is a select element, the values in the
      // #options array have not been sanitized. They will be passed
      // through check_plain() in form_select_options() when this form
      // element is processed. If you alter the type of this element to
      // radios or checkboxes, you are responsible for sanitizing the
      // values of the #options array as well.
      $element['include_tax'] = array(
        '#type' => 'select',
        '#title' => t('Include tax in this price'),
        '#description' => t('Saving prices tax inclusive will bypass later calculations for the specified tax.'),
        '#options' => count($options) == 1 ? reset($options) : $options,
        '#default_value' => $default,
        '#required' => FALSE,
        '#empty_value' => '',
        '#suffix' => '<div class="commerce-price-tax-included-clearfix"></div>',
        '#attached' => array(
          'css' => array(drupal_get_path('module', 'commerce_tax') . '/theme/commerce_tax.theme.css'),
        ),
      );
    }

    // Append a validation handler to the price field's element validate
    // array to add the included tax price component after the price has
    // been converted from a decimal.
    $element['#element_validate'][] = 'commerce_tax_price_field_validate';
  }
}

/**
 * Validate callback for the tax inclusion select list that serves to reset the
 * data array based on the selected tax.
 */
function commerce_tax_price_field_validate($element, &$form_state) {
  // Build an array of form parents to the price array.
  $parents = $element['#parents'];

  // Get the price array from the form state.
  $price = $form_state['values'];

  foreach ($parents as $parent) {
    $price = $price[$parent];
  }

  // If a tax was specified...
  if (!empty($element['include_tax']['#value'])) {
    // Reset the components and store the tax name in the data array.
    $price['data']['components'] = array();
    $price['data']['include_tax'] = $element['include_tax']['#value'];
  }
  else {
    // Otherwise reset the components array.
    $price['data']['components'] = array();
    unset($price['data']['include_tax']);
  }

  // Add the data array to the form state.
  $parents[] = 'data';

  form_set_value(array('#parents' => $parents), $price['data'], $form_state);
}

/**
 * Implements hook_field_attach_load().
 */
function commerce_tax_field_attach_load($entity_type, $entities, $age, $options) {
  // If product entities are being loaded...
  if ($entity_type == 'commerce_product') {
    // Loop over all the products looking for prices needing tax calculation.
    foreach ($entities as $product) {
      // Examine every field instance attached to this product's bundle.
      foreach (field_info_instances('commerce_product', $product->type) as $field_name => $instance) {
        // Load the instance's field data.
        $field = field_info_field($instance['field_name']);

        // If the instance is of a price field...
        if ($field['type'] == 'commerce_price' && !empty($product->{$field_name})) {
          // Check to see if the product has specified an included tax.
          foreach ($product->{$field_name} as $langcode => $items) {
            foreach ($items as $delta => $item) {
              // If it specifies a tax and we can load it...
              if (!empty($item['data']['include_tax']) && $tax_rate = commerce_tax_rate_load($item['data']['include_tax'])) {
                // Clean the price's components array, as we must start with a
                // blank slate to rebuild the components for inclusive taxes.
                $item['data']['components'] = array();

                // Reverse apply the tax.
                $tax_amount = $item['amount'] - ($item['amount'] / (1 + $tax_rate['rate']));
                $tax_amount = commerce_tax_rate_round_amount($tax_rate, $tax_amount);

                // Add a base price to the data array.
                $component = array(
                  'amount' => $item['amount'] - $tax_amount,
                  'currency_code' => $item['currency_code'],
                  'data' => array(),
                );

                $item['data'] = commerce_price_component_add($item, 'base_price', $component, TRUE);

                // Add the tax to the data array.
                $component['amount'] = $tax_amount;
                $component['data']['tax_rate'] = $tax_rate;

                $item['data'] = commerce_price_component_add($item, $tax_rate['price_component'], $component, TRUE);

                // Set the new item on the product entity.
                $product->{$field_name}[$langcode][$delta] = $item;
              }
            }
          }
        }
      }
    }
  }
}

/**
 * Implements hook_commerce_line_item_rebase_unit_price().
 */
function commerce_tax_commerce_line_item_rebase_unit_price(&$price, $old_components, $line_item) {
  $inclusive_taxes = array();

  // Loop over the old components looking for taxes that were applied.
  foreach ($old_components as $key => $component) {
    // Find tax components based on the tax_rate property the Tax modules adds
    // to tax rate component types.
    if ($component_type = commerce_price_component_type_load($component['name'])) {
      // Ensure the tax rate still exists.
      if (!empty($component_type['tax_rate']) &&
        $tax_rate = commerce_tax_rate_load($component_type['tax_rate'])) {
        // If this tax is displayed inclusively with product prices, add it to an
        // array that we'll calculate in reverse order later.
        if ($component['included']) {
          $inclusive_taxes[] = $tax_rate;
        }
        else {
          // Otherwise assume we'll just have sales taxes and add this one now.
          // Note that this means component arrays that mix display inclusive
          // and non-display inclusive tax types will not be supported; however,
          // this shouldn't be possible in real world scenarios.
          $tax_price = $tax_rate['calculation_callback']($tax_rate, entity_metadata_wrapper('commerce_line_item', $line_item));

          // If we received a valid price array, add it as a component.
          if (!empty($tax_price)) {
            $price['data'] = commerce_price_component_add($price, $tax_rate['price_component'], $tax_price, FALSE);
          }
        }
      }
    }
  }

  // If this unit price had inclusive taxes...
  if (!empty($inclusive_taxes)) {
    // We assume the first price component is the base price component that we
    // will deduct the included tax amount from. If it isn't, exit without
    // applying taxes because we would not be able to update the base price.
    if ($price['data']['components'][0]['name'] != 'base_price') {
      return;
    }

    // Prepare an array of tax price components.
    $tax_components = array();

    // Calculate these taxes in reverse order to accommodate cumulative display
    // inclusive tax rates.
    foreach (array_reverse($inclusive_taxes) as $tax_rate) {
      // The amount of this tax is determined by dividing the current base price
      // by 1 + the tax rate expressed as a decimal (i.e. 1.1 for a 10% tax).
      // The result is the base price against which the tax would have been
      // applied, so the difference becomes our tax amount.
      $tax_amount = $price['data']['components'][0]['price']['amount'] - $price['data']['components'][0]['price']['amount'] / (1 + $tax_rate['rate']);
      $tax_amount = commerce_tax_rate_round_amount($tax_rate, $tax_amount);

      $pretax_base = $price['data']['components'][0]['price']['amount'] - $tax_amount;

      // Update the base price component.
      $price['data']['components'][0]['price']['amount'] = $pretax_base;

      // Prepare a tax component that will be added to the price after every tax
      // has been calculated.
      $tax_components[$tax_rate['price_component']] = array(
        'amount' => $tax_amount,
        'currency_code' => $price['currency_code'],
        'data' => array(
          'tax_rate' => $tax_rate,
        ),
      );
    }

    // Add their components to the price in their order of appearance, though.
    foreach (array_reverse($tax_components, TRUE) as $name => $tax_component) {
      $price['data'] = commerce_price_component_add($price, $name, $tax_component, TRUE);
    }
  }
}

/**
 * Returns the total amount of tax included in a price components array.
 *
 * @param $components
 *   A price's components array including potential tax components.
 * @param $included
 *   Boolean indicating whether or not to include in the total taxes that were
 *   included in the price amount already.
 * @param $currency_code
 *   The currency to return the tax amount in.
 *
 * @return
 *   The consolidated tax collected for an order expressed as an integer amount.
 */
function commerce_tax_total_amount($components, $included, $currency_code) {
  $component_types = commerce_tax_commerce_price_component_type_info();
  $amount = 0;

  // Loop over each component passed in...
  foreach ($components as $component) {
    // Looking for components that match one of the defined tax price components.
    if (in_array($component['name'], array_keys($component_types))) {
      // If the component matches the requested "included" value...
      if ((!$included && empty($component['included'])) ||
        $included && !empty($component['included'])) {
        // Add the converted price amount to the running total.
        $amount += commerce_currency_convert($component['price']['amount'], $component['price']['currency_code'], $currency_code);
      }
    }
  }

  return $amount;
}

/**
 * Returns an array of tax components from a price components array.
 *
 * @param $components
 *   A price's components array including potential tax components.
 *
 * @return
 *   An array of tax price component arrays.
 */
function commerce_tax_components($components) {
  $component_types = commerce_tax_commerce_price_component_type_info();
  $tax_components = array();

  // Loop over each component passed in...
  foreach ($components as $component) {
    // Looking for components that match one of the defined tax price components.
    if (in_array($component['name'], array_keys($component_types))) {
      // Add the component to the tax components array.
      $tax_components[] = $component;
    }
  }

  return $tax_components;
}
