<?php

/**
 * @file
 * Defines a field type for referencing orders from other entities.
 */

/**
 * Implements hook_field_extra_fields().
 *
 * This implementation will define "extra fields" on node bundles with order
 * reference fields to correspond with availabled fields on orders. These
 * fields will then also be present in the node view.
 */
function commerce_order_reference_field_extra_fields() {
  $extra = array();

  // Loop through the order reference fields.
  foreach (commerce_info_fields('commerce_order_reference') as $field_name => $field) {
    foreach ($field['bundles'] as $entity_type => $bundles) {
      if ($entity_type == 'commerce_order') {
        // Don't allow merging of fields into self
        continue;
      }

      foreach ($bundles as $bundle_name) {
        // Attach "extra fields" to the bundle representing fields on orders
        // that may be visible on the bundle.
        foreach (field_info_instances('commerce_order') as $order_bundle_name => $order_fields) {
          foreach ($order_fields as $order_field_name => $order_field) {
            $extra[$entity_type][$bundle_name]['display']['order:' . $order_field_name] = array(
              'label' => t('Order: @label', array('@label' => $order_field['label'])),
              'description' => t('Field from a referenced order.'),
              'weight' => $order_field['widget']['weight'],
            );
          }
        }
      }
    }
  }

  return $extra;
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function commerce_order_reference_form_field_ui_field_edit_form_alter(&$form, &$form_state) {
  // Alter the form if the user has selected to not display a widget for a
  // order reference field.
  if ($form['#instance']['widget']['type'] == 'commerce_order_reference_hidden') {
    // Add a help message to the top of the page.
    $form['hidden_order_reference_help'] = array(
      '#markup' => '<div class="messages status">' . t('This field has been configured to not display a widget. There is no way to enter values for this field via the user interface, so you must have some alternate way of adding data to these fields. The settings for the field will still govern what type of orders can be referenced and whether or not their fields will be rendered into the referencing entity on display.') . '</div>',
      '#weight' => -20,
    );

    // Hide options from the form that pertain to UI based data entry.
    $form['instance']['description']['#access'] = FALSE;
    $form['instance']['required']['#access'] = FALSE;
    $form['instance']['default_value_widget']['#access'] = FALSE;
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * When an order is being deleted. Display a message on the confirmation form
 * saying how many times the order is referenced in all order reference
 * fields.
 */
function commerce_order_reference_form_commerce_order_order_delete_form_alter(&$form, &$form_state) {
  $items = array();

  // Check the data in every order reference field.
  foreach (commerce_info_fields('commerce_order_reference') as $field_name => $field) {
    // Query for any entity referencing the deleted order in this field.
    $query = new EntityFieldQuery();
    $query->fieldCondition($field_name, 'order_id', $form_state['order']->order_id, '=');
    $result = $query->execute();

    // If results were returned...
    if (!empty($result)) {
      // Loop over results for each type of entity returned.
      foreach ($result as $entity_type => $data) {
        if (($count = count($data)) > 0) {
          // For line item references, display a message about the inability of
          // the order to be deleted and disable the submit button.
          if ($entity_type == 'commerce_order') {
            // Load the referencing line item.
            $order = reset($data);
            $order = commerce_order_load($order->order_id);

            // Implement a soft dependency on the Order module to show a little
            // more information in the non-deletion message.
            $description = t('This order is referenced by Order @order_number and therefore cannot be deleted.', array('@order_number' => $order->order_number));

            $form['description']['#markup'] .= '<p>' . $description . '</p>';
            $form['actions']['submit']['#disabled'] = TRUE;
            return;
          }

          // Load the entity information.
          $entity_info = entity_get_info($entity_type);

          // Add a message regarding the references.
          $items[] = t('@entity_label: @count', array('@entity_label' => $entity_info['label'], '@count' => $count));
        }
      }
    }
  }

  if (!empty($items)) {
    $form['description']['#markup'] .= '<p>' . t('This order is referenced by the following entities: !entity_list', array('!entity_list' => theme('item_list', array('items' => $items)))) . '</p>';
  }
}

/**
 * Implements hook_entity_view().
 *
 * This implementation adds order fields to the content array of an entity on
 * display if the order contains a non-empty order reference field.
 */
function commerce_order_reference_entity_view($entity, $entity_type, $view_mode, $langcode) {
  $wrapper = entity_metadata_wrapper($entity_type, $entity);
  list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);

  $instances = field_info_instances($entity_type, $bundle);

  // Loop through order reference fields to see if any exist on this entity
  // bundle that is either hidden or displayed with the Add to Cart form display
  // formatter.
  foreach (commerce_info_fields('commerce_order_reference', $entity_type) as $field_name => $field) {
    if (isset($instances[$field_name])) {
      // Find the default order based on the cardinality of the field.
      if ($field['cardinality'] == 1) {
        $order = $wrapper->{$field_name}->value();
      }
      else {
        $order = $wrapper->{$field_name}->get(0)->value();
      }

      // If we found a order and the reference field enables field injection...
      if (!empty($order) && $instances[$field_name]['settings']['field_injection']) {

        // Add the display context for these field to the order.
        $order->display_context = array(
          'entity_type' => $entity_type,
          'entity' => $entity,
        );

        // Loop through the fields on the referenced order's type.
        foreach (field_info_instances('commerce_order', $order->type) as $order_field_name => $order_field) {
          // Add the order field to the entity's content array.
          $display = $entity_type . '_' . $view_mode;
          if ($entity_type == 'commerce_order') {
            // Self references don't use field injection
            $display = 'reference';
          }
          $entity->content['order:' . $order_field_name] = field_view_field('commerce_order', $order, $order_field_name, $display, $langcode);

          // For multiple value references, add context information so the cart
          // form can do dynamic replacement of fields.
          if ($field['cardinality'] != 1) {
            $class = array($entity_type, $id, 'order', $order_field_name);

            $entity->content['order:' . $order_field_name] += array(
              '#prefix' => '<span class="' . drupal_html_class(implode('-', $class)) . '">',
              '#suffix' => '</span>',
            );
          }
        }
      }
    }
  }
}

/**
 * Implements hook_views_api().
 */
function commerce_order_reference_views_api() {
  return array(
    'api' => 3,
    'path' => drupal_get_path('module', 'commerce_order_reference') . '/includes/views',
  );
}

/**
 * Implements hook_field_info().
 */
function commerce_order_reference_field_info() {
  return array(
    'commerce_order_reference' => array(
      'label' => t('Order reference'),
      'description' => t('This field stores the ID of a related order as an integer value.'),
      'settings' => array(),
      'instance_settings' => array('referenceable_types' => array(), 'field_injection' => TRUE),
      'default_widget' => 'options_select',
      'default_formatter' => 'commerce_order_reference_link',
      'property_type' => 'commerce_order',
      'property_callbacks' => array('commerce_order_reference_property_info_callback'),
    ),
  );
}

/**
 * Implements hook_field_instance_settings_form().
 */
function commerce_order_reference_field_instance_settings_form($field, $instance) {
  $settings = $instance['settings'];
  $form = array();

  $form['field_injection'] = array(
    '#type' => 'checkbox',
    '#title' => t('Render fields from the referenced order when viewing this entity.'),
    '#description' => t('If enabled, the appearance of order fields on this entity is governed by the display settings for the fields on the order type.'),
    '#default_value' => isset($settings['field_injection']) ? $settings['field_injection'] : TRUE,
    '#weight' => -9,
  );

  // Build an options array of the order types.
  $options = array();

  foreach (commerce_order_type_get_name() as $type => $name) {
    $options[$type] = check_plain($name);
  }

  $form['referenceable_types'] = array(
    '#type' => 'checkboxes',
    '#title' => t('Order types that can be referenced'),
    '#description' => t('If no types are selected, any type of order may be referenced.'),
    '#options' => $options,
    '#default_value' => is_array($settings['referenceable_types']) ? $settings['referenceable_types'] : array(),
    '#multiple' => TRUE,
    '#weight' => -3,
  );

  return $form;
}

/**
 * Implements hook_field_validate().
 *
 * Possible error codes:
 * - 'invalid_order_id': order_id is not valid for the field (not a valid
 *                         order id, or the order is not referenceable).
 */
function commerce_order_reference_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
  // Extract order_ids to check.
  $order_ids = array();

  // First check non-numeric order_id's to avoid losing time with them.
  foreach ($items as $delta => $item) {
    if (is_array($item) && !empty($item['order_id'])) {
      if (is_numeric($item['order_id'])) {
        $order_ids[] = $item['order_id'];
      }
      else {
        $errors[$field['field_name']][$langcode][$delta][] = array(
          'error' => 'invalid_order_id',
          'message' => t('%name: you have specified an invalid order for this reference field.', array('%name' => $instance['label'])),
        );
      }
    }
  }

  // Prevent performance hog if there are no ids to check.
  if ($order_ids) {
    $refs = commerce_order_reference_match_orders($field, $instance, $order_ids);

    foreach ($items as $delta => $item) {
      if (is_array($item)) {
        if (!empty($item['order_id']) && !isset($refs[$item['order_id']])) {
          $errors[$field['field_name']][$langcode][$delta][] = array(
            'error' => 'invalid_order_id',
            'message' => t('%name: you have specified an invalid order for this reference field.', array('%name' => $instance['label'])),
          );
        }
      }
    }
  }
}

/**
 * Implements hook_field_is_empty().
 */
function commerce_order_reference_field_is_empty($item, $field) {
  // order_id = 0 îs empty too, which is exactly what we want.
  return empty($item['order_id']);
}

/**
 * Implements hook_field_formatter_info().
 */
function commerce_order_reference_field_formatter_info() {
  return array(
    'commerce_order_reference_link' => array(
      'label' => t('Order (link)'),
      'description' => t('Display the link to the order page (if access allowed).'),
      'field types' => array('commerce_order_reference'),
    ),
  );
}

/**
 * Implements hook_field_formatter_view().
 */
function commerce_order_reference_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  $result = array();

  // Collect the list of order IDs.
  $order_ids = array();

  foreach ($items as $delta => $item) {
    $order_ids[$item['order_id']] = $item['order_id'];
  }

  $orders = commerce_order_load_multiple($order_ids, array('status' => 1));

  switch ($display['type']) {
    case 'commerce_order_reference_link':
      foreach ($items as $delta => $item) {
        if (!empty($orders[$item['order_id']])) {
          if (commerce_order_access('view', $orders[$item['order_id']])) {
            $result[$delta] = array(
              '#type' => 'link',
              '#title' => t('Order @id', array('@id' => $item['order_id'])),
              '#href' => 'admin/commerce/orders/' . $item['order_id'],
            );
          }
          else {
            $result[$delta] = array(
              '#markup' => t('Order @id', array('@id' => $item['order_id'])),
            );
          }
        }
      }
      break;
  }

  return $result;
}

/**
 * Implements hook_field_widget_info().
 *
 * Defines widgets available for use with field types as specified in each
 * widget's $info['field types'] array.
 */
function commerce_order_reference_field_widget_info() {
  $widgets = array();

  // Do not show the widget on forms; useful in cases where reference fields
  // have a lot of data that is maintained automatically.
  $widgets['commerce_order_reference_hidden'] = array(
    'label' => t('Do not show a widget'),
    'description' => t('Will not display the order reference field on forms. Use only if you maintain order references some other way.'),
    'field types' => array('commerce_order_reference'),
    'behaviors' => array(
      'multiple values' => FIELD_BEHAVIOR_CUSTOM,
    ),
  );

  return $widgets;
}

/**
 * Implements hook_field_widget_info_alter().
 */
function commerce_order_reference_field_widget_info_alter(&$info) {
  $info['options_select']['field types'][] = 'commerce_order_reference';
  $info['options_buttons']['field types'][] = 'commerce_order_reference';
}

/**
 * Implements hook_field_widget_settings_form().
 */
function commerce_order_reference_field_widget_settings_form($field, $instance) {
  $widget = $instance['widget'];
  $defaults = field_info_widget_settings($widget['type']);
  $settings = array_merge($defaults, $widget['settings']);

  $form = array();
  // There are no settings - we don't offer an autocomplete widget for orders.

  return $form;
}

/**
 * Implements hook_field_widget_form().
 *
 * Used to define the form element for custom widgets.
 */
function commerce_order_reference_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  if ($instance['widget']['type'] == 'commerce_order_reference_hidden') {
    return array();
  }
}

/**
 * Implements hook_field_widget_error().
 */
function commerce_order_reference_field_widget_error($element, $error) {
  form_error($element, $error['message']);
}

/**
 * Implements hook_options_list().
 */
function commerce_order_reference_options_list($field) {
  $options = array();

  // Loop through all order matches.
  foreach (commerce_order_reference_match_orders($field) as $order_id => $data) {
    // Add them to the options list in optgroups by order type.
    $name = check_plain(commerce_order_type_get_name($data['type']));
    $options[$name][$order_id] = t('Order @id', array('@id' => $data['order_id']));
  }

  // Simplify the options list if only one optgroup exists.
  if (count($options) == 1) {
    $options = reset($options);
  }

  return $options;
}

/**
 * Implements hook_entity_info_alter().
 *
 * Adds the order display-specific views mode to the order.
 */
function commerce_order_reference_entity_info_alter(&$entity_info) {
  $entity_info['commerce_order']['view modes']['reference'] = array(
    'label' => t('Referenced'),
    'custom settings' => TRUE,
  );

  // Query the field tables directly to avoid creating a loop in the Field API:
  // it is not legal to call any of the field_info_*() in
  // hook_entity_info(), as field_read_instances() calls entity_get_info().
  $query = db_select('field_config_instance', 'fci', array('fetch' => PDO::FETCH_ASSOC));
  $query->join('field_config', 'fc', 'fc.id = fci.field_id');
  $query->fields('fci');
  $query->condition('fc.type', 'commerce_order_reference');

  foreach ($query->execute() as $instance) {
    $entity_type = $instance['entity_type'];
    foreach ($entity_info[$entity_type]['view modes'] as $view_mode => $view_mode_info) {
      $entity_info['commerce_order']['view modes'][$entity_type . '_' . $view_mode] = array(
        'label' => t('@entity_type: @view_mode', array('@entity_type' => $entity_info[$entity_type]['label'], '@view_mode' => $view_mode_info['label'])),

        // UX: Enable the 'Node: teaser' mode by default, if present.
        'custom settings' => $entity_type == 'node' && $view_mode == 'teaser',
      );
    }
  }
}

/**
 * Creates a required, locked instance of a order reference field on the
 * specified bundle.
 *
 * @param $field_name
 *   The name of the field; if it already exists, a new instance of the existing
 *     field will be created. For fields governed by the Commerce modules, this
 *     should begin with commerce_.
 * @param $entity_type
 *   The type of entity the field instance will be attached to.
 * @param $bundle
 *   The bundle name of the entity the field instance will be attached to.
 * @param $label
 *   The label of the field instance.
 * @param $weight
 *   The default weight of the field instance widget and display.
 */
function commerce_order_reference_create_instance($field_name, $entity_type, $bundle, $label, $weight = 0, $widget = 'commerce_order_reference_hidden') {
  // If a field type we know should exist isn't found, clear the Field cache.
  if (!field_info_field_types('commerce_order_reference')) {
    field_cache_clear();
  }

  // Look for or add the specified field to the requested entity bundle.
  $field = field_info_field($field_name);
  $instance = field_info_instance($entity_type, $field_name, $bundle);

  if (empty($field)) {
    $field = array(
      'field_name' => $field_name,
      'type' => 'commerce_order_reference',
      'cardinality' => 1,
      'entity_types' => array($entity_type),
      'translatable' => FALSE,
      'locked' => TRUE,
    );
    $field = field_create_field($field);
  }

  if (empty($instance)) {
    $instance = array(
      'field_name' => $field_name,
      'entity_type' => $entity_type,
      'bundle' => $bundle,

      'label' => $label,
      'required' => TRUE,
      'settings' => array(),

      'widget' => array(
        'type' => $widget,
        'weight' => $weight,
      ),

      'display' => array(
        'display' => array(
          'label' => 'hidden',
          'weight' => $weight,
        ),
      ),
    );
    field_create_instance($instance);
  }
}

/**
 * Fetches an array of all order matching the given order ids.
 *
 * @param $field
 *   The field description.
 * @param $ids
 *   Optional order ids to lookup (used when $string and $match arguments are
 *   not given).
 * @param $limit
 *   If non-zero, limit the size of the result set.
 * @param $access_tag
 *   Boolean indicating whether or not an access control tag should be added to
 *   the query to find matching order data. Defaults to FALSE.
 *
 * @return
 *   An array of valid orders in the form:
 *   array(
 *     order_id => array(
 *       'order_id' => The order id
 *       'type' => The order type
 *     ),
 *     ...
 *   )
 */
function commerce_order_reference_match_orders($field, $instance = NULL, $ids = array(), $limit = NULL, $access_tag = FALSE) {
  $results = &drupal_static(__FUNCTION__, array());

  // Create unique id for static cache.
  $cid = implode(':', array(
    $field['field_name'],
    implode('-', $ids),
    $limit,
  ));

  if (!isset($results[$cid])) {
    $matches = _commerce_order_reference_match_orders_standard($instance, $ids, $limit, $access_tag);

    // Store the results.
    $results[$cid] = !empty($matches) ? $matches : array();
  }

  return $results[$cid];
}

/**
 * Helper function for commerce_order_reference_match_orders().
 *
 * Returns an array of orders matching the specific parameters.
*/
function _commerce_order_reference_match_orders_standard($instance, $ids = array(), $limit = NULL, $access_tag = FALSE) {
  // Build the query object with the necessary fields.
  $query = db_select('commerce_order', 'co');

  // Add the access control tag if specified.
  if ($access_tag) {
    $query->addTag('commerce_order_access');
  }

  $order_id_alias = $query->addField('co', 'order_id');
  $order_type_alias = $query->addField('co', 'type');

  // Add a condition to the query to filter by matching order types.
  if (!empty($instance['settings']['referenceable_types'])) {
    $types = array_diff(array_values($instance['settings']['referenceable_types']), array(0, NULL));

    // Only filter by type if some types have been specified.
    if (!empty($types)) {
      $query->condition('co.type', $types, 'IN');
    }
  }
  if (!empty($ids)) {
    // Specific order ids
    $query->condition($order_id_alias, $ids, 'IN', $ids);
  }

  // Order the results by order id
  $query
    ->orderBy($order_id_alias);

  // Add a limit if specified.
  if ($limit) {
    $query->range(0, $limit);
  }

  // Execute the query and build the results array.
  $result = $query->execute();

  $matches = array();

  foreach ($result->fetchAll() as $order) {
    $matches[$order->order_id] = array(
      'order_id' => $order->order_id, 
      'type' => $order->type, 
    );
  }

  return $matches;
}

/**
 * Callback to alter the property info of the reference fields.
 *
 * @see commerce_order_reference_field_info().
 */
function commerce_order_reference_property_info_callback(&$info, $entity_type, $field, $instance, $field_type) {
  $name = $field['field_name'];
  $property = &$info[$entity_type]['bundles'][$instance['bundle']]['properties'][$name];

  $property['type'] = ($field['cardinality'] != 1) ? 'list<commerce_order>' : 'commerce_order';
  $property['options list'] = 'entity_metadata_field_options_list';
  $propery['getter callback'] = 'entity_metadata_field_property_get';
  $property['setter callback'] = 'entity_metadata_field_property_set';
  $property['property info'] = commerce_order_reference_field_data_property_info();

  unset($property['query callback']);
}

/**
 * Defines info for the properties of the Order reference field data structure.
 */
function commerce_order_reference_field_data_property_info($name = NULL) {
  return array(
    'order_id' => array(
      'label' => t('Order ID'),
      'description' => t('The ID of the referenced order.'),
      'type' => 'integer',
      'getter callback' => 'entity_property_verbatim_get',
      'setter callback' => 'entity_property_verbatim_set',
    ),
  );
}
