<?php

/**
 * @file
 * Entity API for handling entities like nodes or users.
 */

use Drupal\entity\EntityMalformedException;

/**
 * Implements hook_help().
 */
function entity_help($path, $arg) {
  switch ($path) {
    case 'admin/help#entity':
      $output = '';
      $output .= '<h3>' . t('About') . '</h3>';
      $output .= '<p>' . t('The Entity module provides an API for managing entities like nodes and users, i.e. an API for loading and identifying entities. For more information, see the online handbook entry for <a href="!url">Entity module</a>', array('!url' => 'http://drupal.org/documentation/modules/entity')) . '</p>';
      return $output;
  }
}

/**
 * Implements hook_modules_preenable().
 */
function entity_modules_preenable() {
  entity_info_cache_clear();
}

/**
 * Implements hook_modules_disabled().
 */
function entity_modules_disabled() {
  entity_info_cache_clear();
}

/**
 * Gets the entity info array of an entity type.
 *
 * @param $entity_type
 *   The entity type, e.g. node, for which the info shall be returned, or NULL
 *   to return an array with info about all types.
 *
 * @see hook_entity_info()
 * @see hook_entity_info_alter()
 */
function entity_get_info($entity_type = NULL) {
  global $language_interface;

  // Use the advanced drupal_static() pattern, since this is called very often.
  static $drupal_static_fast;
  if (!isset($drupal_static_fast)) {
    $drupal_static_fast['entity_info'] = &drupal_static(__FUNCTION__);
  }
  $entity_info = &$drupal_static_fast['entity_info'];

  // hook_entity_info() includes translated strings, so each language is cached
  // separately.
  $langcode = $language_interface->langcode;

  if (empty($entity_info)) {
    if ($cache = cache()->get("entity_info:$langcode")) {
      $entity_info = $cache->data;
    }
    else {
      $entity_info = module_invoke_all('entity_info');
      // Merge in default values.
      foreach ($entity_info as $name => $data) {
        $entity_info[$name] += array(
          'fieldable' => FALSE,
          'controller class' => 'Drupal\entity\EntityController',
          'static cache' => TRUE,
          'field cache' => TRUE,
          'load hook' => $name . '_load',
          'bundles' => array(),
          'view modes' => array(),
          'entity keys' => array(),
          'translation' => array(),
        );
        $entity_info[$name]['entity keys'] += array(
          'revision' => '',
          'bundle' => '',
        );
        foreach ($entity_info[$name]['view modes'] as $view_mode => $view_mode_info) {
          $entity_info[$name]['view modes'][$view_mode] += array(
            'custom settings' => FALSE,
          );
        }
        // If no bundle key is provided, assume a single bundle, named after
        // the entity type.
        if (empty($entity_info[$name]['entity keys']['bundle']) && empty($entity_info[$name]['bundles'])) {
          $entity_info[$name]['bundles'] = array($name => array('label' => $entity_info[$name]['label']));
        }
        // Prepare entity schema fields SQL info for
        // Drupal\entity\EntityControllerInterface::buildQuery().
        if (isset($entity_info[$name]['base table'])) {
          $entity_info[$name]['schema_fields_sql']['base table'] = drupal_schema_fields_sql($entity_info[$name]['base table']);
          if (isset($entity_info[$name]['revision table'])) {
            $entity_info[$name]['schema_fields_sql']['revision table'] = drupal_schema_fields_sql($entity_info[$name]['revision table']);
          }
        }
      }
      // Let other modules alter the entity info.
      drupal_alter('entity_info', $entity_info);
      cache()->set("entity_info:$langcode", $entity_info);
    }
  }

  if (empty($entity_type)) {
    return $entity_info;
  }
  elseif (isset($entity_info[$entity_type])) {
    return $entity_info[$entity_type];
  }
}

/**
 * Resets the cached information about entity types.
 */
function entity_info_cache_clear() {
  drupal_static_reset('entity_get_info');
  // Clear all languages.
  cache()->deletePrefix('entity_info:');
}

/**
 * Extracts ID, revision ID, and bundle name from an entity.
 *
 * @param $entity_type
 *   The entity type; e.g. 'node' or 'user'.
 * @param $entity
 *   The entity from which to extract values.
 *
 * @return
 *   A numerically indexed array (not a hash table) containing these
 *   elements:
 *   - 0: Primary ID of the entity.
 *   - 1: Revision ID of the entity, or NULL if $entity_type is not versioned.
 *   - 2: Bundle name of the entity.
 */
function entity_extract_ids($entity_type, $entity) {
  $info = entity_get_info($entity_type);

  // Objects being created might not have id/vid yet.
  $id = isset($entity->{$info['entity keys']['id']}) ? $entity->{$info['entity keys']['id']} : NULL;
  $vid = ($info['entity keys']['revision'] && isset($entity->{$info['entity keys']['revision']})) ? $entity->{$info['entity keys']['revision']} : NULL;

  if (!empty($info['entity keys']['bundle'])) {
    // Explicitly fail for malformed entities missing the bundle property.
    if (!isset($entity->{$info['entity keys']['bundle']}) || $entity->{$info['entity keys']['bundle']} === '') {
      throw new EntityMalformedException(t('Missing bundle property on entity of type @entity_type.', array('@entity_type' => $entity_type)));
    }
    $bundle = $entity->{$info['entity keys']['bundle']};
  }
  else {
    // The entity type provides no bundle key: assume a single bundle, named
    // after the entity type.
    $bundle = $entity_type;
  }

  return array($id, $vid, $bundle);
}

/**
 * Assembles an object structure with initial IDs.
 *
 * This function can be seen as reciprocal to entity_extract_ids().
 *
 * @param $entity_type
 *   The entity type; e.g. 'node' or 'user'.
 * @param $ids
 *   A numerically indexed array, as returned by entity_extract_ids(),
 *   containing these elements:
 *   - 0: Primary ID of the entity.
 *   - 1: Revision ID of the entity, or NULL if $entity_type is not versioned.
 *   - 2: Bundle name of the entity, or NULL if $entity_type has no bundles.
 *
 * @return
 *   An entity object, initialized with the IDs provided.
 */
function entity_create_stub_entity($entity_type, $ids) {
  $entity = new stdClass();
  $info = entity_get_info($entity_type);
  $entity->{$info['entity keys']['id']} = $ids[0];
  if (!empty($info['entity keys']['revision']) && isset($ids[1])) {
    $entity->{$info['entity keys']['revision']} = $ids[1];
  }
  if (!empty($info['entity keys']['bundle']) && isset($ids[2])) {
    $entity->{$info['entity keys']['bundle']} = $ids[2];
  }
  return $entity;
}

/**
 * Loads an entity from the database.
 *
 * @param string $entity_type
 *   The entity type to load, e.g. node or user.
 * @param int $id
 *   The id of the entity to load.
 * @param bool $reset
 *   Whether to reset the internal cache for the requested entity type.
 *
 * @return object
 *   The entity object, or FALSE if there is no entity with the given id.
 *
 * @see hook_entity_info()
 * @see entity_load_multiple()
 * @see Drupal\entity\EntityControllerInterface
 * @see Drupal\entity\EntityController
 * @see Drupal\entity\EntityFieldQuery
 */
function entity_load($entity_type, $id, $reset = FALSE) {
  $entities = entity_load_multiple($entity_type, array($id), array(), $reset);
  return isset($entities[$id]) ? $entities[$id] : FALSE;
}

/**
 * Loads multiple entities from the database.
 *
 * This function should be used whenever you need to load more than one entity
 * from the database. The entities are loaded into memory and will not require
 * database access if loaded again during the same page request.
 *
 * The actual loading is done through a class that has to implement the
 * Drupal\entity\EntityControllerInterface interface. By default,
 * Drupal\entity\EntityController is used. Entity types can specify
 * that a different class should be used by setting the 'controller class' key
 * in hook_entity_info(). These classes can either implement the
 * Drupal\entity\EntityControllerInterface interface, or, most
 * commonly, extend the Drupal\entity\EntityController class. See
 * node_entity_info() and the NodeController in node.module as an example.
 *
 * @param string $entity_type
 *   The entity type to load, e.g. node or user.
 * @param array|bool $ids
 *   An array of entity IDs, or FALSE to load all entities.
 * @param array $conditions
 *   (deprecated) An associative array of conditions on the base table, where
 *   the keys are the database fields and the values are the values those
 *   fields must have. Instead, it is preferable to use EntityFieldQuery to
 *   retrieve a list of entity IDs loadable by this function.
 * @param bool $reset
 *   Whether to reset the internal cache for the requested entity type.
 *
 * @return array
 *   An array of entity objects indexed by their ids.
 *
 * @todo Remove $conditions in Drupal 8.
 *
 * @see hook_entity_info()
 * @see Drupal\entity\EntityControllerInterface
 * @see Drupal\entity\EntityController
 * @see Drupal\entity\EntityFieldQuery
 */
function entity_load_multiple($entity_type, $ids = FALSE, $conditions = array(), $reset = FALSE) {
  if ($reset) {
    entity_get_controller($entity_type)->resetCache();
  }
  return entity_get_controller($entity_type)->load($ids, $conditions);
}

/**
 * Loads the unchanged, i.e. not modified, entity from the database.
 *
 * Unlike entity_load() this function ensures the entity is directly loaded from
 * the database, thus bypassing any static cache. In particular, this function
 * is useful to determine changes by comparing the entity being saved to the
 * stored entity.
 *
 * @param $entity_type
 *   The entity type to load, e.g. node or user.
 * @param $id
 *   The ID of the entity to load.
 *
 * @return
 *   The unchanged entity, or FALSE if the entity cannot be loaded.
 */
function entity_load_unchanged($entity_type, $id) {
  entity_get_controller($entity_type)->resetCache(array($id));
  $result = entity_get_controller($entity_type)->load(array($id));
  return reset($result);
}

/**
 * Deletes multiple entities permanently.
 *
 * @param $entity_type
 *   The type of the entity.
 * @param $ids
 *   An array of entity IDs of the entities to delete.
 */
function entity_delete_multiple($entity_type, $ids) {
  entity_get_controller($entity_type)->delete($ids);
}

/**
 * Constructs a new entity object, without permanently saving it.
 *
 * @param $entity_type
 *   The type of the entity.
 * @param $values
 *   An array of values to set, keyed by property name. If the entity type has
 *   bundles the bundle key has to be specified.
 *
 * @return Drupal\entity\EntityInterface
 *   A new entity object.
 */
function entity_create($entity_type, array $values) {
  return entity_get_controller($entity_type)->create($values);
}

/**
 * Gets the entity controller class for an entity type.
 *
 * @return Drupal\entity\EntityStorageControllerInterface
 */
function entity_get_controller($entity_type) {
  $controllers = &drupal_static(__FUNCTION__, array());
  if (!isset($controllers[$entity_type])) {
    $type_info = entity_get_info($entity_type);
    $class = $type_info['controller class'];
    $controllers[$entity_type] = new $class($entity_type);
  }
  return $controllers[$entity_type];
}

/**
 * Invokes hook_entity_prepare_view().
 *
 * If adding a new entity similar to nodes, comments or users, you should
 * invoke this function during the ENTITY_build_content() or
 * ENTITY_view_multiple() phases of rendering to allow other modules to alter
 * the objects during this phase. This is needed for situations where
 * information needs to be loaded outside of ENTITY_load() - particularly
 * when loading entities into one another - i.e. a user object into a node, due
 * to the potential for unwanted side-effects such as caching and infinite
 * recursion. By convention, entity_prepare_view() is called after
 * field_attach_prepare_view() to allow entity level hooks to act on content
 * loaded by field API.
 *
 * @param $entity_type
 *   The type of entity, i.e. 'node', 'user'.
 * @param $entities
 *   The entity objects which are being prepared for view, keyed by object ID.
 *
 * @see hook_entity_prepare_view()
 */
function entity_prepare_view($entity_type, $entities) {
  // To ensure hooks are only run once per entity, check for an
  // entity_view_prepared flag and only process items without it.
  // @todo: resolve this more generally for both entity and field level hooks.
  $prepare = array();
  foreach ($entities as $id => $entity) {
    if (empty($entity->entity_view_prepared)) {
      // Add this entity to the items to be prepared.
      $prepare[$id] = $entity;

      // Mark this item as prepared.
      $entity->entity_view_prepared = TRUE;
    }
  }

  if (!empty($prepare)) {
    module_invoke_all('entity_prepare_view', $prepare, $entity_type);
  }
}

/**
 * Returns the uri elements of an entity.
 *
 * @param $entity_type
 *   The entity type; e.g. 'node' or 'user'.
 * @param $entity
 *   The entity for which to generate a path.
 *
 * @return
 *   An array containing the 'path' and 'options' keys used to build the uri of
 *   the entity, and matching the signature of url(). NULL if the entity has no
 *   uri of its own.
 *
 * @todo
 *   Remove once all entity types are implementing the EntityInterface.
 */
function entity_uri($entity_type, $entity) {
  $info = entity_get_info($entity_type);
  list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);

  // A bundle-specific callback takes precedence over the generic one for the
  // entity type.
  if (isset($info['bundles'][$bundle]['uri callback'])) {
    $uri_callback = $info['bundles'][$bundle]['uri callback'];
  }
  elseif (isset($info['uri callback'])) {
    $uri_callback = $info['uri callback'];
  }
  else {
    return NULL;
  }

  // Invoke the callback to get the URI. If there is no callback, return NULL.
  if (isset($uri_callback)) {
    $uri = $uri_callback($entity);
    // Pass the entity data to url() so that alter functions do not need to
    // lookup this entity again.
    $uri['options']['entity_type'] = $entity_type;
    $uri['options']['entity'] = $entity;
    return $uri;
  }
}

/**
 * Returns the label of an entity.
 *
 * See the 'label callback' component of the hook_entity_info() return value
 * for more information.
 *
 * @param $entity_type
 *   The entity type; e.g., 'node' or 'user'.
 * @param $entity
 *   The entity for which to generate the label.
 *
 * @return
 *   The entity label, or FALSE if not found.
 *
 * @todo
 *   Remove once all entity types are implementing the EntityInterface.
 */
function entity_label($entity_type, $entity) {
  $label = FALSE;
  $info = entity_get_info($entity_type);
  if (isset($info['label callback'])) {
    $label = $info['label callback']($entity_type, $entity);
  }
  elseif (!empty($info['entity keys']['label']) && isset($entity->{$info['entity keys']['label']})) {
    $label = $entity->{$info['entity keys']['label']};
  }

  return $label;
}

/**
 * Attaches field API validation to entity forms.
 */
function entity_form_field_validate($entity_type, $form, &$form_state) {
  // All field attach API functions act on an entity object, but during form
  // validation, we don't have one. $form_state contains the entity as it was
  // prior to processing the current form submission, and we must not update it
  // until we have fully validated the submitted input. Therefore, for
  // validation, act on a pseudo entity created out of the form values.
  $pseudo_entity = (object) $form_state['values'];
  field_attach_form_validate($entity_type, $pseudo_entity, $form, $form_state);
}

/**
 * Copies submitted values to entity properties for simple entity forms.
 *
 * During the submission handling of an entity form's "Save", "Preview", and
 * possibly other buttons, the form state's entity needs to be updated with the
 * submitted form values. Each entity form implements its own builder function
 * for doing this, appropriate for the particular entity and form, whereas
 * modules may specify additional builder functions in $form['#entity_builders']
 * for copying the form values of added form elements to entity properties.
 * Many of the main entity builder functions can call this helper function to
 * re-use its logic of copying $form_state['values'][PROPERTY] values to
 * $entity->PROPERTY for all entries in $form_state['values'] that are not field
 * data, and calling field_attach_submit() to copy field data. Apart from that
 * this helper invokes any additional builder functions that have been specified
 * in $form['#entity_builders'].
 *
 * For some entity forms (e.g., forms with complex non-field data and forms that
 * simultaneously edit multiple entities), this behavior may be inappropriate,
 * so the builder function for such forms needs to implement the required
 * functionality instead of calling this function.
 */
function entity_form_submit_build_entity($entity_type, $entity, $form, &$form_state) {
  $info = entity_get_info($entity_type);
  list(, , $bundle) = entity_extract_ids($entity_type, $entity);

  // Copy top-level form values that are not for fields to entity properties,
  // without changing existing entity properties that are not being edited by
  // this form. Copying field values must be done using field_attach_submit().
  $values_excluding_fields = $info['fieldable'] ? array_diff_key($form_state['values'], field_info_instances($entity_type, $bundle)) : $form_state['values'];
  foreach ($values_excluding_fields as $key => $value) {
    $entity->$key = $value;
  }

  // Invoke all specified builders for copying form values to entity properties.
  if (isset($form['#entity_builders'])) {
    foreach ($form['#entity_builders'] as $function) {
      $function($entity_type, $entity, $form, $form_state);
    }
  }

  // Copy field values to the entity.
  if ($info['fieldable']) {
    field_attach_submit($entity_type, $entity, $form, $form_state);
  }
}
