<?php

/**
 * @file
 * API functions to manipulate messages.
 */

/**
 * Define the name of the message realm field.
 */
define('MESSAGE_FIELD_MESSAGE_TEXT', 'message_text');

/**
 * The maximal amount of messages to be purged upon hook_cron()
 */
define('MESSAGE_PURGE_LIMIT', 100);

/**
 * Implementation of hook_views_api().
 */
function message_views_api() {
  return array(
    'api' => 3,
    'path' => drupal_get_path('module', 'message') . '/includes/views',
  );
}

 /**
 * Implements hook_ctools_plugin_directory().
 */
function message_ctools_plugin_directory($module, $plugin) {
  if ($module == 'ctools') {
    return 'ctools/' . $plugin;
  }
}

/**
 * Implements hook_theme().
 */
function message_theme() {
  $info['message'] = array(
    'render element' => 'elements',
    'template' => 'message',
  );

  return $info;
}

/**
 * Implements hook_message_view_alter().
 */
function message_message_view_alter(&$build) {
  $build['#theme'] = 'message';
}

/**
 * Process variables for message.tpl.php.
 */
function template_preprocess_message(&$variables) {
  $message = $variables['elements']['#entity'];

  $variables['view_mode'] = $variables['elements']['#view_mode'];
  $variables['message'] = $message;

  // Helpful $content variable for templates.
  $variables['content'] = array();
  foreach (element_children($variables['elements']) as $key) {
    $variables['content'][$key] = $variables['elements'][$key];
  }

  // Make the field variables available with the appropriate language.
  field_attach_preprocess('message', $message, $variables['content'], $variables);
  list(, , $bundle) = entity_extract_ids('message', $message);

  // Gather CSS classes.
  $variables['classes_array'][] = drupal_html_class('entity-message');
  $variables['classes_array'][] = drupal_html_class('message-' . $bundle);

  // Add suggestions.
  $variables['theme_hook_suggestions'][] = 'message';
  $variables['theme_hook_suggestions'][] = 'message__' . $bundle;
  $variables['theme_hook_suggestions'][] = 'message__' . $bundle . '__' . $variables['view_mode'];

  if ($id = entity_id('message', $message)) {
    $variables['theme_hook_suggestions'][] = 'message__' . $id;
  }
}

/**
 * Implements hook_permission().
 */
function message_permission() {
  $permissions = array();
  $permissions['administer message types'] = array(
    'title' => t('Administer message types'),
    'description' => t('Administer message types that can be used to log an event.'),
  );
  $permissions['create messages'] = array(
    'title' => t('Create messages'),
    'description' => t('Log new messages.'),
  );
  return $permissions;
}

/**
 * Implements hook_entity_info().
 */
function message_entity_info() {
  $items['message_type_category'] = array(
    'label' => t('Message type category'),
    'controller class' => 'EntityAPIControllerExportable',
    'entity class' => 'MessageTypeCategory',
    'base table' => 'message_type_category',
    'fieldable' => TRUE,
    'entity keys' => array(
      'id' => 'id',
      'label' => 'description',
      'name' => 'category',
      'language' => 'language',
    ),
    'exportable' => TRUE,
    'export' => array(
      'default hook' => 'default_message_type_category',
    ),
    'bundle of' => 'message_type',
    'module' => 'message',
    'access callback' => 'message_type_category_access',
    // We don't use entity API's admin UI.
  );

  $items['message_type'] = array(
    'label' => t('Message type'),
    'controller class' => 'EntityAPIControllerExportable',
    'entity class' => 'MessageType',
    'base table' => 'message_type',
    'fieldable' => TRUE,
    'entity keys' => array(
      'id' => 'id',
      'label' => 'description',
      'name' => 'name',
      'bundle' => 'category',
      'language' => 'language',
    ),
    'bundles' => array(
      'message_type' => array(
        'label' => t('Message type'),
      ),
    ),
    'bundle keys' => array(
      'bundle' => 'category',
    ),
    'exportable' => TRUE,
    'export' => array(
      'default hook' => 'default_message_type',
    ),
    'bundle of' => 'message',
    'module' => 'message',
    'access callback' => 'message_type_access',
    'entity cache' => module_exists('entitycache'),
    // Enable the entity API's admin UI.
    'admin ui' => array(
      'path' => 'admin/structure/messages',
      'file' => 'includes/message.admin.inc',
      'controller class' => 'MessageTypeUIController',
    ),
  );

  if (module_exists('locale')) {
    $items['message_type']['translation']['locale'] = TRUE;
  }

  // Add bundle info but bypass entity_load() as we cannot use it here.
  if (db_table_exists('message_type_category')) {
    $message_categories = db_select('message_type_category', 'mtc')
      ->fields('mtc')
      ->execute()
      ->fetchAllAssoc('category');

    foreach ($message_categories as $category_name => $category) {
      $items['message_type']['bundles'][$category_name] = array(
        'label' => $category->category,
      );
    }
  }

  $items['message'] = array(
    'label' => t('Message'),
    'controller class' => 'EntityAPIController',
    'entity class' => 'Message',
    'base table' => 'message',
    'fieldable' => TRUE,
    'access callback' => 'message_access',
    'entity keys' => array(
      'id' => 'mid',
      // The message has no label.
      'label' => FALSE,
      'bundle' => 'type',
      'language' => 'language',
    ),
    'bundles' => array(),
    'bundle keys' => array(
      'bundle' => 'name',
    ),
    'view modes' => array(
      'full' => array(
        'label' => t('Full'),
        'custom settings' => FALSE,
      ),
    ),
    'module' => 'message',
    'metadata controller class' => 'MessageMetadataController',
    'views controller class' => 'MessageViewsController',
    'entity cache' => module_exists('entitycache'),
  );

  // Add bundle info but bypass entity_load() as we cannot use it here.
  if (db_table_exists('message_type')) {
    $message_types = db_select('message_type', 'mt')
      ->fields('mt')
      ->execute()
      ->fetchAllAssoc('name');
  }

  foreach ($message_types as $type_name => $type) {
    $items['message']['bundles'][$type_name] = array(
      'label' => $type->description,
      'admin' => array(
        'path' => 'admin/structure/messages/manage/%message_type',
        'real path' => 'admin/structure/messages/manage/' . $type->name,
        'bundle argument' => 4,
        'access arguments' => array('administer message types'),
      ),
    );
  }

  return $items;
}

/**
 * Implements hook_menu().
 */
function message_menu() {
  $items = array();

  $items['admin/config/system/message'] = array(
    'title' => 'Message settings',
    'description' => 'Manage message purging upon cron.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('message_user_admin_settings'),
    'access arguments' => array('administer message types'),
    'file' => 'includes/message.admin.inc',
  );

  $items['admin/config/system/message/text-copy'] = array(
    'title' => 'Copy messages text fields',
    'description' => 'Copy messages text fields from one language to others',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('message_admin_text_copy'),
    'access arguments' => array('administer message types'),
    'file' => 'includes/message.admin.inc',
  );

  return $items;
}

/**
 * Implements hook_cron().
 *
 * Fetch all message types and purge old messages.
 */
function message_cron() {
  // The maximal amount of messages to purge per cron run.
  $purge_limit = variable_get('message_delete_cron_limit', MESSAGE_PURGE_LIMIT);
  // Messages to be deleted.
  $purge_messages = array();
  // Names of non global-purge-settings overriding message types.
  $no_override_type_names = array();
  // Message types that override global purge settings.
  $override_types = array();

  // Iterate all message types to distinguish between overriding and non-
  // overriding types.
  foreach (message_type_load() as $message_type) {
    if (empty($message_type->data['purge']['override'])) {
      $no_override_type_names[] = $message_type->name;
    }
    else {
      // For overriding types, store the type and not its name to later extract
      // the specific purge settings.
      $override_types[] = $message_type;
    }
  }

  // Gather purgeable messages of overriding types.
  foreach ($override_types as $message_type) {
    // Ignore message type with unchecked "Purge messages". Also make sure that
    // purging settings are present.
    if (empty($message_type->data['purge']) || empty($message_type->data['purge']['enabled'])) {
      continue;
    }

    $purge_messages += message_get_purgeable_by_type($purge_limit, $message_type->name, $message_type->data['purge']);
  }

  // Gather purgeable messages of non-overriding types according to global
  // settings.
  if (!empty($no_override_type_names)) {
    // Do nothing if purge isn't enabled.
    if (variable_get('message_purge_enable', FALSE)) {
      $purge_settings = array(
        'quota' => variable_get('message_purge_quota', NULL),
        'days' => variable_get('message_purge_days', NULL),
      );

      $purge_messages += message_get_purgeable_by_type($purge_limit, $no_override_type_names, $purge_settings);
    }
  }

  // Delete all gathered messages.
  if (!empty($purge_messages)) {
    message_delete_multiple(array_keys($purge_messages));
  }
}

/**
 * Implements hook_entity_delete().
 *
 * Handles messages deletion when referenced entities are being deleted.
 *
 * @param $entity
 *  The entity object.
 * @param $entity_type
 *  The type of entity being deleted (i.e. node, user, comment).
 */
function message_entity_delete($entity, $entity_type) {
  if ($entity_type == 'message') {
    return;
  }

  if (!$field_names = _message_get_referenced_fields($entity_type, $entity)) {
    return;
  }

  list($entity_id) = entity_extract_ids($entity_type, $entity);

  // A deleted message with no reference doesn't need to create a queue worker.
  $delete_referenced_entities = FALSE;
  foreach ($field_names as $field_name) {
    $field = field_info_field($field_name);
    $columns = array_keys($field['columns']);
    // Fetch messages with fields referencing the deleted entity.
    $query = new EntityFieldQuery();
    $result = $query->entityCondition('entity_type', 'message')
      ->fieldCondition($field['field_name'], $columns[0], $entity_id)
      ->execute();

    if (!empty($result['message'])) {
      $delete_referenced_entities = TRUE;
      break;
    }
  }

  if (!$delete_referenced_entities) {
    return;
  }

  if (variable_get('message_use_queue')) {
    $data = array(
      'range' => 200,
      'last_mid' => 0,
      'field_names' => $field_names,
      'entity_id' => $entity_id,
    );
    $queue = DrupalQueue::get('message_entity_delete');

    return $queue->createItem($data);
  }

  _message_delete_messages($field_names, $entity_id);
}

/**
 * Helper function for getting the fields which holds the reference to the
 * message.
 *
 * @param $entity_type
 *  The type of entity being deleted (i.e. node, user, comment).
 * @param $entity
 *  The entity object.
 */
function _message_get_referenced_fields($entity_type, $entity) {
  $entity_types = variable_get('message_delete_on_entity_delete',  array('node', 'user', 'taxonomy_term', 'comment'));
  if (!$entity_types || !in_array($entity_type, $entity_types)) {
    return;
  }

  list(,,$bundle) = entity_extract_ids($entity_type, $entity);

  $field_names = array();

  // Search for fields in which messages referenced the deleted entity.
  foreach (field_info_fields() as $field) {
    if (empty($field['bundles']['message'])) {
      // This field isn't used in any message.
      continue;
    }

    // Only delete messages due to referenced entity or referenced
    // taxonomy term deletion.
    if ($field['type'] == 'entityreference') {
      // Check if the field references entities of the given type.
      if ($entity_type != $field['settings']['target_type']) {
        continue;
      }
      // Check if the field references specific bundles. If so, check if the
      // field references the deleted entity's bundle.
      if ($field['settings']['handler_settings']['target_bundles'] && !in_array($bundle, $field['settings']['handler_settings']['target_bundles'])) {
        continue;
      }
    }
    elseif ($field['type'] == 'taxonomy_term_reference') {
      if ($entity_type != 'taxonomy_term') {
        continue;
      }
    }
    else {
      continue;
    }

    $field_names[] = $field['field_name'];
  }

  return $field_names;
}

/**
 * Delete messages that the reference to is still exists.
 *
 * @param $field_names
 *  Array of fields names which holds the reference to the message.
 * @param $entity_id
 *  The message referenced entity that were deleted.
 * @param $last_mid
 *  The last Message ID we process.
 * @param $range
 *  Optional;The number of message to process each time.
 * @return
 *  The last Message ID we process.
 */
function _message_delete_messages($field_names, $entity_id, $last_mid = 0, $range = NULL) {
  // List of messages to delete.
  $deletable_mids = array();
  // List of messages that might be deleted;
  // Messages with references to fields with multiple cardinality will be
  // stored in $check_mids in order to check if the entity being deleted
  // is the last one referenced by a given field.
  // Keyed by message ID, pointing to array of the relevant field names.
  $check_mids = array();

  foreach ($field_names as $field_name) {
    $field = field_info_field($field_name);
    $columns = array_keys($field['columns']);
    // Fetch messages with fields referencing the deleted entity.
    $query = new EntityFieldQuery();
    $query->entityCondition('entity_type', 'message')
      ->propertyCondition('mid', $last_mid, '>')
      ->fieldCondition($field['field_name'], $columns[0], $entity_id);

    if ($range) {
      $query->range(0, $range);
    }

    $result = $query->execute();

    // Continue to next field if no such messages exist.
    if (empty($result['message'])) {
      continue;
    }

    // If the field has single cardinality it's safe to delete the
    // messages.
    if ($field['cardinality'] == 1) {
      $deletable_mids += array_keys($result['message']);
    }
    // Otherwise, store the messages IDs in $check_mids for further
    // inspection.
    else {
      foreach ($result['message'] as $mid => $message) {
        // Each message ID is stored with the field it was found by, in
        // order to handle multiple fields.
        if (empty($check_mids[$mid])) {
          $check_mids[$mid] = array();
        }
        $check_mids[$mid][] = $field['field_name'];
      }
    }
  }

  // Check messages with multiple cardinality references; Only delete such
  // messages if the entity being deleted is the last one referenced by the
  // message.
  if ($check_mids) {
    $messages = message_load_multiple(array_keys($check_mids));
    foreach ($messages as $message) {
      foreach ($check_mids[$message->mid] as $field_name) {
        $field = field_info_field($field_name);
        $wrapper = entity_metadata_wrapper('message', $message);
        $count = $field['cardinality'] == 1 ? 1 : $wrapper->{$field_name}->count();

        // The amount of entities actually referenced with this field.
        // Would have been nicer to use $count to determine the amount of
        // referenced entities, but it isn't necessary updated. So we have to
        // check each entity.
        $refrences_count = 0;
        for ($i = 0; $i < $count; $i++) {
          if ($wrapper->$field_name->get($i)->value()) {
            $refrences_count++;
          }
        }

        // Check if the message references exactly one entity with this field.
        if ($refrences_count == 1) {
          $deletable_mids[] = $message->mid;
        }
      }
    }
  }

  if ($deletable_mids) {
    message_delete_multiple($deletable_mids);
    return reset($deletable_mids);
  }
}

/**
 * Find purgeable messages according to type and purge settings.
 *
 * @param $purge_limit
 *   The maximal amount of messages to fetch. Decremented each time messages
 *   are fetched.
 * @param $message_type_name
 *   Either a single message type name or an array of names.
 * @param $purge_settings
 *   Array containing purging settings: 'quota' and 'days'.
 *
 * @return array
 *   Messages of the given type(s) that should be purged according to the given
 *   settings.
 */
function message_get_purgeable_by_type(&$purge_limit, $message_type_name, $purge_settings) {
  if ($purge_limit <= 0) {
    return;
  }

  // Messages to be deleted.
  $purge_messages = array();

  // Base query for both purging methods.
  $base_query = new EntityFieldQuery();
  $base_query->entityCondition('entity_type', 'message', '=')
    ->propertyCondition('type', $message_type_name, is_array($message_type_name) ? 'IN' : '=')
    // Sort descending, the get the newest first.
    ->propertyOrderBy('timestamp', 'DESC')
    ->propertyOrderBy('mid', 'DESC');

  // Purge according to quota definition.
  if (!empty($purge_settings['quota'])) {
    $quota_query = clone $base_query;
    $result = $quota_query
      ->range($purge_settings['quota'], $purge_limit)
      ->execute();

    if (!empty($result['message'])) {
      $purge_limit -= count($result['message']);
      $purge_messages += $result['message'];
    }
  }

  // Purge according to maximal age definition.
  if (!empty($purge_settings['days'])) {
    $age_query = clone $base_query;
    // Find messages older than current time - maximal age in days times
    // the seconds per day.
    $earlier_than = time() - $purge_settings['days'] * 86400;
    $result = $age_query->propertyCondition('timestamp', $earlier_than, '<')
      ->range(0, $purge_limit)
      ->execute();

    if (!empty($result['message'])) {
      $purge_limit -= count($result['message']);
      $purge_messages += $result['message'];
    }
  }

  return $purge_messages;
}

/**
 * Implements hook_field_extra_fields().
 */
function message_field_extra_fields() {
  $return = array();
  foreach (message_type_load() as $message_type) {

    // Iterate over any "message-text" field.
    foreach (field_info_instances('message_type', $message_type->category) as $field_name => $value) {
      $field = field_info_field($field_name);
      if (empty($field['settings']['message_text'])) {
        continue;
      }

      $field_items = field_get_items('message_type', $message_type, $field_name);
      $count = is_array($field_items) ? count($field_items) : 1;
      for ($delta = 0; $delta < $count; $delta++) {
        $params = array('@label' => $value['label'], '@delta' => $delta);
        $return['message'][$message_type->name]['display']['message__' . $field['field_name'] . '__' . $delta] = array(
          'label' => $count == 1 ? $value['label'] : t('@label @delta partial', $params),
          'description' => $count == 1 ? t('Complete rendered message text.') : t('Rendered message text of the @delta partial', $params),
          'weight' => $delta,
        );
      }
    }
  }

  return $return;
}

/**
 * Implements hook_field_attach_form().
 *
 * Add on every message-text field partail, indication to which view-mode
 * it is assigned to.
 */
function message_field_attach_form($entity_type, $entity, &$form, &$form_state, $langcode) {
  if ($entity_type != 'message_type' || empty($entity->name)) {
    return;
  }

  $bundle_settings = field_bundle_settings('message', $entity->name);
  $entity_info = entity_get_info('message');
  $current_view_modes = array();
  // Build array keyed by the delta, and the view-modes as value.
  foreach (message_get_text_fields() as $field_name) {
    foreach ($bundle_settings['extra_fields']['display'] as $delta => $view_modes) {
      $identeifer = "message__{$field_name}__";
      if (strpos($delta, $identeifer) !== 0) {
        // Field doesn't exist.
        continue;
      }

      $delta = str_replace($identeifer, '', $delta);
      foreach ($view_modes as $view_mode => $value) {
        if ($value['visible']) {
          $url = "admin/structure/messages/manage/{$entity->name}/display/{$view_mode}";
          $label = $view_mode == 'default' ? t('Default') : $entity_info['view modes'][$view_mode]['label'];
          $current_view_modes[$field_name][$delta][] = l($label, $url);
        }
      }
    }
  }

  foreach (message_get_text_fields() as $field_name) {
    if (empty($form[$field_name])) {
      continue;
    }

    $langcodes = $form[$field_name];
    foreach (element_children($langcodes) as $langcode) {
      $deltas = $form[$field_name][$langcode];
      foreach (element_children($deltas) as $delta) {
        if (empty($current_view_modes[$field_name][$delta])) {
          continue;
        }

        $form[$field_name][$langcode][$delta]['view_mode'] = array(
          '#markup' => t('View modes: !view-modes', array('!view-modes' => implode(', ', $current_view_modes[$field_name][$delta]))),
        );
      }
    }
  }
}

/**
 * Creates a new message type category.
 *
 * If a message type category already exists, an exception will be thrown.
 *
 * @return MessageTypeCategory
 *   Returns a new message type category object.
 */
function message_type_category_create($category, $values = array()) {
  global $language;
  // Make sure the message type doesn't already exist, to prevent duplicate key
  // error.
  if (message_type_category_load($category)) {
    throw new MessageException('Message type category' . check_plain($category) . ' already exists.');
  }
  $values['category'] = $category;
  $values += array(
    'language' => $language->language,
  );
  $return = entity_create('message_type_category', $values);

  return $return;
}

/**
 * Message type category loader.
 *
 * @param $name
 *   (optional) The name for this message type category. If no type is given all
 *   existing message types category are returned.
 *
 * @return MessageType
 *   Returns a fully-loaded message type category definition if a name is
 *   passed. Else an array containing all message types category is returned.
 */
function message_type_category_load($name = NULL) {
  // Replace dashes with underscores so this can be used as menu argument
  // loader too.
  $types = entity_load_multiple_by_name('message_type_category', isset($name) ? array(strtr($name, array('-' => '_'))) : FALSE);
  if (isset($name)) {
    return isset($types[$name]) ? $types[$name] : FALSE;
  }
  return $types;
}

/**
 * Inserts or updates a message type category object into the database.
 *
 * @param $message
 *   The message type category object to be inserted.
 *
 * @return
 *   Failure to write a record will return FALSE. Otherwise SAVED_NEW or
 *   SAVED_UPDATED is returned depending on the operation performed.
 */
function message_type_category_save($message) {
  return entity_save('message_type_category', $message);
}

/**
 * Deletes an existing message type category.
 *
 * @param $message
 *   The message type category object to be deleted.
 */
function message_type_category_delete($message) {
  return entity_delete('message_type_category', $message);
}

/**
 * Creates a new message type.
 *
 * If a message type already exists, an exception will be thrown.
 *
 * @return MessageType
 *   Returns a new message type object.
 */
function message_type_create($name, $values = array()) {
  global $language;
  // Make sure the message type doesn't already exist, to prevent duplicate key
  // error.
  if (message_type_load($name)) {
    throw new MessageException('Message type ' . check_plain($name) . ' already exists.');
  }
  $values['name'] = $name;
  $values += array(
    'language' => $language->language,
  );
  $return = entity_create('message_type', $values);

  return $return;
}

/**
 * Message type loader.
 *
 * @param $name
 *   (optional) The name for this message type. If no type is given all existing
 *   types are returned.
 *
 * @return MessageType
 *   Returns a fully-loaded message type definition if a type name is passed.
 *   Else an array containing all types is returned.
 */
function message_type_load($name = NULL) {
  // Replace dashes with underscores so this can be used as menu argument
  // loader too.
  $types = entity_load_multiple_by_name('message_type', isset($name) ? array(strtr($name, array('-' => '_'))) : FALSE);
  if (isset($name)) {
    return isset($types[$name]) ? $types[$name] : FALSE;
  }
  return $types;
}

/**
 * Inserts or updates a message object into the database.
 *
 * @param $message
 *   The message object to be inserted.
 *
 * @return
 *   Failure to write a record will return FALSE. Otherwise SAVED_NEW or
 *   SAVED_UPDATED is returned depending on the operation performed.
 */
function message_type_save($message) {
  return entity_save('message_type', $message);
}

/**
 * Deletes an existing message.
 *
 * @param $message
 *   The message object to be deleted.
 */
function message_type_delete($message) {
  return entity_delete('message_type', $message);
}

/**
 * Helper to easily create messages.
 *
 * @param $type
 *   The message type name.
 * @param $values
 *   Array with the following keys:
 *   - "arguments" - Array with arguments that should be replaced on run time in
 *     the message type.
 *   - "timestamp" - The unix timestamp of the creation time of the message. If
 *     empty the current time will be used.
 *   - "uid" - The user->uid to associate the message with, this will overwrite
 *     $account param, and it's useful when you don't have the full user object
 *     loaded into memory.
 * @param $account
 *   Optional; The user object to associate the message with. If empty, the
 *   current user will be used.
 */
function message_create($type, $values = array(), $account = NULL) {
  global $language;
  if (empty($account)) {
    global $user;
    $account = clone $user;
  }
  $values['type'] = $type;
  $values['user'] = $account;
  $values += array(
    'language' => $language->language,
  );
  return entity_create('message', $values);
}

/**
 * Message load.
 *
 * @param $mid
 *   The message ID.
 * @return Message
 *   A message object.
 */
function message_load($mid) {
  $result = entity_load('message', array($mid));
  return $result ? reset($result) : FALSE;
}

/**
 * Loads multiple messages.
 *
 * @see entity_load().
 */
function message_load_multiple($mids, $conditions = array()) {
  return entity_load('message', $mids, $conditions);
}

/**
 * Message save.
 *
 * @param $message_instance
 *   A message instance object.
 * @return
 *   The saved message instance object.
 */
function message_save($message) {
  return entity_save('message', $message);
}

/**
 * Message delete.
 *
 * @param $iid
 *   Message instance IDs array.
 */
function message_delete_multiple($mids = array()) {
  entity_delete_multiple('message', $mids);
}

/**
 * Access callback for the message entity.
 */
function message_access($op, $entity, $account = NULL, $entity_type = 'message') {
  return user_access('create messages', $account);
}

/**
 * Access callback for the message type entities.
 */
function message_type_access($op, $entity, $account, $entity_type) {
  return user_access('administer message types', $account);
}

/**
 * Access callback for the message type categories.
 */
function message_type_category_access($op, $entity, $account, $entity_type) {
  return user_access('administer message types', $account);
}

/**
 * Entity property info getter callback for getting arguments.
 */
function message_property_get_argument($arguments, array $options, $name, $type, $context) {
  return isset($arguments[$context['message_replace_char'] . $name]) ? $arguments[$context['message_replace_char'] . $name] : NULL;
}

/**
 * Entity property info setter callback for arguments.
 */
function message_property_set_argument(&$arguments, $name, $value, $langcode, $type, $context) {
  $arguments[$context['message_replace_char'] . $name] = $value;
}

/**
 * Entity property info getter callback for getting the final message text.
 */
function message_property_get_text($message, array $options) {
  $langcode = isset($options['language']) ? $options['language']->language : LANGUAGE_NONE;
  return $message->getText($langcode);
}

/**
 * Get the values of a message property.
 *
 * The value of the message, after intersecting with the same values
 * from the message-type. For example, it is possible to assign
 * $message_type->arguments and $message->arguments. In case there are the
 * same keys in the array, the $message will override the ones from the
 * message-type.
 *
 * @param $message
 *   The message object.
 * @param $name
 *   The property name.
 * @param $key
 *   Optional; If the property is an array, the key to be used to get the
 *   value.
 * @param $default_value
 *   Optional; The default value to assign, if no value is given.
 *
 * @return
 *   The merged values, or if no value if found and empty array.
 */
function message_get_property_values(Message $message, $name, $key = NULL, $default_value = array()) {
  $message_type = $message->getType();

  if (isset($key)) {
    $type_value = isset($message_type->{$name}[$key]) ? $message_type->{$name}[$key] : array();
    $value = isset($message->{$name}[$key]) ? $message->{$name}[$key] : array();
  }
  else {
    $type_value = isset($message_type->{$name}) ? $message_type->{$name} : array();
    $value = isset($message->{$name}) ? $message->{$name} : array();
  }

  if (isset($value) && !is_array($value)) {
    // Value was found on the message.
    return $value;
  }
  elseif (isset($type_value) && !is_array($type_value)) {
    // Value was found on the message type.
    return $type_value;
  }
  elseif (!empty($value) || !empty($type_value)){
    // Value was found in one of the entities, and it's an array, so merge.
    return array_merge($type_value, $value);
  }

  // No value found, so return the default value.
  return $default_value;
}

/**
 * Find all message text fields.
 *
 * @return
 *   Array of all field names that are marked as message-text.
 */
function message_get_text_fields() {
  $message_fields = array();
  foreach (field_info_fields() as $field_name => $field) {
    if (!empty($field['settings']['message_text'])) {
      $message_fields[] = $field_name;
    }
  }

  return $message_fields;
}

/**
 * Batch callback. Copy the message text fields from one language to others.
 *
 * @param $origin_lang
 *   The origin language.
 * @param $dest_langs
 *   List of destination languages.
 * @param $override
 *   Whether to override existing values.
 */
function message_admin_text_copy_batch($origin_lang, $dest_langs, $override, &$context) {
  if (empty($context['sandbox'])) {
    $context['sandbox']['progress'] = 0;
    $context['sandbox']['current_id'] = 0;
    $context['sandbox']['max'] = db_select('message_type')
      ->countQuery()
      ->execute()
      ->fetchField();
  }

  $result = db_select('message_type', 'mt')
    ->fields('mt', array('id'))
    ->condition('id', $context['sandbox']['current_id'], '>')
    ->orderBy('id')
    ->range(0, 50)
    ->execute();

  foreach ($result as $row) {
    $context['sandbox']['progress']++;
    $context['sandbox']['current_id'] = $row->id;

    $entity = entity_load_single('message_type', $row->id);
    // Ignore the current message type if it has no value in its origin
    // language.
    if (empty($entity->message_text[$origin_lang])) {
      continue;
    }
    foreach ($entity->message_text[$origin_lang] as $delta => $message_text) {
      // Copy the message text values from the origin language to the
      // destination languages.
      foreach ($dest_langs as $dest_lang) {
        foreach (message_get_text_fields() as $field_name) {
          // Check if there's already a value in the destination language. If so,
          // skip it unless override is checked.
          if (!$override && !empty($entity->{$field_name}[$dest_lang][$delta])) {
            continue;
          }
          // Copy the origin language text.
          $entity->{$field_name}[$dest_lang][$delta] = $message_text;
        }
      }
    }
    // Save the message type with the updated texts.
    message_type_save($entity);
  }

  if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  }
}

/**
 * Implements hook_features_message_type_alter().
 *
 * Add field_bundle_settings variable when a message-type is exported and
 * Strongarm module exists.
 */
function message_features_pipe_message_type_alter(&$pipe, $data, $export) {
  if (empty($data) || !module_exists('strongarm')) {
    return;
  }

  foreach ($data as $message_type) {
    $pipe['variable'][] = "field_bundle_settings_message__{$message_type}";
  }
}

/**
 * Implements hook_cron_queue_info().
 */
function message_cron_queue_info() {
  $items['message_entity_delete'] = array(
    'title' => t('Message'),
    'worker callback' => 'message_entity_delete_queue_worker',
    'time' => 60,
  );
  return $items;
}

/**
 * Queue API worker; Process a queue item.
 *
 * The item holds the fields information(field name, column, id and cardinality),
 * the last Message ID we processed and the range.
 */
function message_entity_delete_queue_worker($data) {
  extract($data);

  if (!$mid = _message_delete_messages($field_names, $entity_id, $last_mid, $range)) {
    return;
  }

  $data = array(
    'range' => 200,
    'last_mid' => $mid,
    'field_names' => $field_names,
    'entity_id' => $entity_id,
  );

  $queue = DrupalQueue::get('message_entity_delete');
  return $queue->createItem($data);
}
