<?php

/**
 * @file
 * Offers the ability to save searches and be notified of new results.
 */

/**
 * Implements hook_menu().
 */
function search_api_saved_searches_menu() {
  $items['admin/config/search/search_api/index/%search_api_index/saved_searches'] = array(
    'title' => 'Saved searches',
    'description' => 'Let users save searches on this index.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('search_api_saved_searches_index_edit', 5),
    'access arguments' => array('administer search_api_saved_searches'),
    'weight' => -1,
    'type' => MENU_LOCAL_TASK,
    'context' => MENU_CONTEXT_INLINE | MENU_CONTEXT_PAGE,
    'file' => 'search_api_saved_searches.admin.inc',
  );
  $items['user/%user/saved-searches'] = array(
    'title' => 'Saved searches',
    'description' => 'View and edit your saved searches.',
    'page callback' => 'search_api_saved_searches_user_listing',
    'page arguments' => array(1),
    'access callback' => 'search_api_saved_search_edit_access',
    'access arguments' => array(1),
    'weight' => 5,
    'type' => MENU_LOCAL_TASK,
    'file' => 'search_api_saved_searches.pages.inc',
  );
  $items['user/%user/saved-searches/add'] = array(
    'title' => 'Create saved search',
    'description' => 'Create a new saved search.',
    'page callback' => 'search_api_saved_searches_create_manual',
    'access callback' => 'search_api_saved_search_create_access',
    'access arguments' => array(NULL, TRUE),
    'type' => MENU_LOCAL_ACTION,
    'file' => 'search_api_saved_searches.pages.inc',
  );
  $items['search-api/saved-searches/add'] = array(
    'title' => 'Create saved search',
    'description' => 'Create a new saved search.',
    'page callback' => 'search_api_saved_searches_create_manual',
    'access callback' => 'search_api_saved_search_create_access',
    'access arguments' => array(NULL, TRUE),
    'file' => 'search_api_saved_searches.pages.inc',
  );
  $items['search-api/saved-searches/add/%search_api_saved_searches_settings'] = array(
    'title' => 'Create saved search',
    'description' => 'Create a new saved search.',
    'page callback' => 'search_api_saved_searches_create_manual',
    'page arguments' => array(3),
    'access callback' => 'search_api_saved_search_create_access',
    'access arguments' => array(3, TRUE),
    'file' => 'search_api_saved_searches.pages.inc',
  );
  $items['search-api/saved-search/%search_api_saved_search/activate/%'] = array(
    'title' => 'Activate saved search',
    'description' => 'Activate a new saved search.',
    'page callback' => 'search_api_saved_searches_activate_page',
    'page arguments' => array(2, 4),
    'access callback' => 'search_api_saved_search_edit_access',
    'access arguments' => array(NULL, 2, 4),
    'file' => 'search_api_saved_searches.pages.inc',
  );
  $items['search-api/saved-search/%search_api_saved_search/edit'] = array(
    'title' => 'Edit saved search',
    'description' => 'Edit a saved search.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('search_api_saved_searches_search_edit_form', 2),
    'access callback' => 'search_api_saved_search_edit_access',
    'access arguments' => array(NULL, 2, 4),
    'file' => 'search_api_saved_searches.pages.inc',
  );
  $items['search-api/saved-search/%search_api_saved_search/delete'] = array(
    'title' => 'Delete saved search',
    'description' => 'Delete a saved search.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('search_api_saved_searches_search_delete_form', 2),
    'access callback' => 'search_api_saved_search_edit_access',
    'access arguments' => array(NULL, 2, 4),
    'file' => 'search_api_saved_searches.pages.inc',
  );

  return $items;
}

/**
 * Implements hook_permission();
 */
function search_api_saved_searches_permission() {
  $perms['administer search_api_saved_searches'] = array(
    'title' => t('Administer saved searches'),
    'description' => t('Enable and configure saved searches for search indexes.'),
  );
  $perms['use search_api_saved_searches'] = array(
    'title' => t('Use saved searches'),
    'description' => t('Save searches and receive e-mail notifications.'),
  );
  return $perms;
}

/**
 * Implements hook_entity_info().
 */
function search_api_saved_searches_entity_info() {
  $info['search_api_saved_searches_settings'] = array(
    'label' => t('Saved search settings'),
    'controller class' => 'EntityAPIControllerExportable',
    'entity class' => 'SearchApiSavedSearchesSettings',
    'base table' => 'search_api_saved_searches_settings',
    'uri callback' => 'search_api_saved_searches_settings_url',
    'access callback' => 'search_api_saved_searches_settings_access',
    'module' => 'search_api_saved_searches',
    'exportable' => TRUE,
    'entity keys' => array(
      'id' => 'id',
      'name' => 'delta',
      'label' => 'delta',
    ),
  );
  $info['search_api_saved_search'] = array(
    'label' => t('Saved search'),
    'controller class' => 'EntityAPIController',
    'entity class' => 'SearchApiSavedSearch',
    'base table' => 'search_api_saved_search',
    'access callback' => 'search_api_saved_search_access',
    'module' => 'search_api_saved_searches',
    'exportable' => FALSE,
    'entity keys' => array(
      'id' => 'id',
      'label' => 'name',
    ),
  );

  return $info;
}

/**
 * Implements hook_entity_property_info_alter().
 *
 * Corrects the types which the Entity API automatically infers from the schema.
 * Otherwise, the "token" types would be "text", and "boolean" and "date" would
 * be "integer". Also, changes saved search results to be a list, not just a CSV
 * string.
 *
 * Fixing this here automatically also fixes the Views integration provided by
 * the Entity API, regarding these types.
 */
function search_api_saved_searches_entity_property_info_alter(array &$info) {
  $settings = &$info['search_api_saved_searches_settings']['properties'];
  $settings['index_id']['type'] = 'token';
  $settings['enabled']['type'] = 'boolean';
  $settings['module']['type'] = 'token';

  $searches = &$info['search_api_saved_search']['properties'];
  $searches['settings_id']['type'] = 'token';
  $searches['enabled']['type'] = 'boolean';
  $searches['created']['type'] = 'date';
  $searches['last_execute']['type'] = 'date';
  // We can't assign "duration" until Entity API Views integration supports
  // this.
  //$searches['notify_interval']['type'] = 'duration';

  $searches['results']['type'] = 'list<token>';
  $searches['results']['getter callback'] = 'search_api_saved_searches_get_results_property';
}

/**
 * Getter callback for the saved search results property.
 *
 * @param SearchApiSavedSearch $search
 *   The search whose results should be returned.
 * @param array $options
 *   Options for the property. Are ignored.
 * @param string $property
 *   The property to retrieve. Will always be "results".
 * @param string $entity_type
 *   The entity type. Will always be "search_api_saved_search".
 *
 * @return array
 *   An array with the IDs of all stored results.
 */
function search_api_saved_searches_get_results_property(SearchApiSavedSearch $search, array $options, $property, $entity_type) {
  return $search->results ? explode(',', $search->results) : array();
}

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

/**
 * URL callback for settings entities.
 */
function search_api_saved_searches_settings_url(SearchApiSavedSearchesSettings $settings) {
  return array('path' => 'admin/config/search/search_api/index/' . $settings->index_id . '/saved_searches');
}

/**
 * Access callback for settings entities.
 *
 * @param string $op
 *   The operation being performed. One of "view", "update", "create" or
 *   "delete".
 * @param SearchApiSavedSearchesSettings|null $settings
 *   (optional) The entity to check access for. If NULL is given, it will be
 *   determined whether access is allowed for all settings.
 * @param object|null $account
 *   The user to check for. NULL to check for the global user.
 *
 * @return bool
 *   Whether access is allowed or not.
 *
 * @see entity_access
 */
function search_api_saved_searches_settings_access($op, SearchApiSavedSearchesSettings $settings = NULL, $account = NULL) {
  return user_access('administer search_api_saved_searches', $account);
}

/**
 * Access callback for saved search entities.
 *
 * @param string $op
 *   The operation being performed. One of "view", "update", "create" or
 *   "delete".
 * @param SearchApiSavedSearch|null $search
 *   (optional) The entity to check access for. If NULL is given, it will be
 *   determined whether access is allowed for all searches.
 * @param object|null $account
 *   The user to check for. NULL to check for the global user.
 *
 * @return bool
 *   Whether access is allowed or not.
 *
 * @see entity_access
 */
function search_api_saved_search_access($op, SearchApiSavedSearch $search = NULL, $account = NULL) {
  if (user_access('administer search_api_saved_searches', $account)) {
    return TRUE;
  }
  if (!$account) {
    global $user;
    $account = $user;
  }
  switch ($op) {
    case 'create':
      return user_access('use search_api_saved_searches', $account);

    default:
      // If the search was created by an anonymous user, there's no way we can
      // correctly determine access here.
      if (!$search || !$search->uid) {
        return FALSE;
      }
      return $search->uid == $account->uid;
  }
}

/**
 * Implements hook_user_insert().
 *
 * If a new user already has saved searches with the same mail address,
 * associate them with the new user. However, only do this if the user is
 * already active.
 */
function search_api_saved_searches_user_insert(&$edit, $account, $category) {
  if (!empty($account->status)) {
    foreach (search_api_saved_search_load_multiple(FALSE, array('mail' => $account->mail, 'uid' => 0)) as $search) {
      $search->uid = $account->uid;
      if (empty($search->settings()->options['registered_user_delete_key'])) {
        unset($search->options['key']);
      }
      $search->save();
    }
  }
}

/**
 * Implements hook_user_update().
 *
 * If a user gets activated, associate saved searches with the same mail address
 * with them.
 *
 * Also, change mail address of saved searches when the user mail address
 * changes.
 */
function search_api_saved_searches_user_update(&$edit, $account, $category) {
  // Changes of mail and status.
  if (!empty($account->status) && empty($account->original->status)) {
    foreach (search_api_saved_search_load_multiple(FALSE, array('mail' => $account->mail, 'uid' => 0)) as $search) {
      $search->uid = $account->uid;
      if (empty($search->settings()->options['registered_user_delete_key'])) {
        unset($search->options['key']);
      }
      $search->save();
    }
  }
  if ($account->mail != $account->original->mail) {
    foreach (search_api_saved_search_load_multiple(FALSE, array('mail' => $account->mail, 'uid' => $account->uid)) as $search) {
      $search->mail = $account->mail;
      $search->save();
    }
  }
}

/**
 * Implements hook_user_delete().
 *
 * If a user is deleted, delete their saved searches, too.
 */
function search_api_saved_searches_user_delete($account) {
  entity_delete_multiple('search_api_saved_search', array_keys(search_api_saved_search_load_multiple(FALSE, array('uid' => $account->uid))));
}

// @todo Rules integration

/**
 * Implements hook_search_api_index_update().
 *
 * If the index got disabled, do the same with its search settings.
 */
function search_api_saved_searches_search_api_index_update(SearchApiIndex $index) {
  if (!$index->enabled && $index->original->enabled) {
    foreach (search_api_saved_searches_settings_load_multiple(FALSE, array('index_id' => $index->machine_name)) as $settings) {
      if ($settings->enabled) {
        $settings->enabled = FALSE;
        $settings->save();
      }
    }
  }
}

/**
 * Implements hook_search_api_index_delete().
 *
 * Deletes the settings associated with a search index.
 */
function search_api_saved_searches_search_api_index_delete(SearchApiIndex $index) {
  // Only react on real delete, not revert.
  if ($index->status & ENTITY_IN_CODE) {
    return;
  }
  foreach (search_api_saved_searches_settings_load_multiple(FALSE, array('index_id' => $index->machine_name)) as $settings) {
    $settings->delete();
  }
}

/**
 * Implements hook_search_api_saved_searches_settings_insert().
 *
 * Clear block caches when new enabled saved search settings are saved.
 */
function search_api_saved_searches_search_api_saved_searches_settings_insert(SearchApiSavedSearchesSettings $settings) {
  if ($settings->enabled) {
    block_flush_caches();
    cache_clear_all('*', 'cache_block', TRUE);
  }
}

/**
 * Implements hook_search_api_saved_searches_settings_update().
 *
 * Clear block caches when saved search settings are enabled or disabled.
 */
function search_api_saved_searches_search_api_saved_searches_settings_update(SearchApiSavedSearchesSettings $settings) {
  if ($settings->enabled != $settings->original->enabled) {
    block_flush_caches();
    cache_clear_all('*', 'cache_block', TRUE);
  }

  // React if the new results determination method was switched to/from the
  // ID-based method.
  $options = $settings->options + array('date_field' => NULL);
  $orig_options = $settings->original->options + array('date_field' => NULL);
  if ($options['date_field'] != $orig_options['date_field']) {
    if (!$options['date_field']) {
      // When we switch to the ID-based method from another one, we need to save
      // the current results.
      foreach (search_api_saved_search_load_multiple(FALSE, array('settings_id' => $settings->delta)) as $search) {
        // This will automatically populate the results.
        $search->save();
      }
    }
    elseif (!$orig_options['date_field']) {
      // If we previously used the ID-based method and are now using a
      // field-based one, set the saved results for all searches to NULL.
      db_update('search_api_saved_search')
        ->fields(array(
          'results' => NULL,
        ))
        ->condition('settings_id', $settings->delta)
        ->execute();
    }
  }
}

/**
 * Implements hook_search_api_saved_searches_settings_delete().
 *
 * Clear block caches when enabled saved search settings are deleted.
 */
function search_api_saved_searches_search_api_saved_searches_settings_delete(SearchApiSavedSearchesSettings $settings) {
  // Only react on real delete, not revert.
  if ($settings->status & ENTITY_IN_CODE) {
    return;
  }

  foreach (search_api_saved_search_load_multiple(FALSE, array('settings_id' => $settings->delta)) as $search) {
    $search->delete();
  }
  if ($settings->enabled) {
    block_flush_caches();
    cache_clear_all('*', 'cache_block', TRUE);
  }
}

/**
 * Loads a single settings object.
 *
 * @param int|string $id
 *   The settings' identifier or delta.
 * @param bool $reset
 *   If TRUE, will reset the internal entity cache.
 *
 * @return SearchApiSavedSearchesSettings
 *   The requested entity, or FALSE if no settings for that ID exist.
 */
function search_api_saved_searches_settings_load($id, $reset = FALSE) {
  $ret = search_api_saved_searches_settings_load_multiple(array($id), array(), $reset);
  return $ret ? reset($ret) : FALSE;
}

/**
 * Loads multiple settings objects.
 *
 * @param array|false $ids
 *   The settings' identifiers or deltas; or FALSE to load all settings objects.
 * @param array $conditions
 *   Associative array of field => value conditions that returned objects must
 *   satisfy.
 * @param bool $reset
 *   If TRUE, will reset the internal entity cache.
 *
 * @return array
 *   An array of SearchApiSavedSearchesSettings objects matching the conditions,
 *   keyed by delta.
 */
function search_api_saved_searches_settings_load_multiple($ids = FALSE, array $conditions = array(), $reset = FALSE) {
  $settings = entity_load('search_api_saved_searches_settings', $ids, $conditions, $reset);
  return entity_key_array_by_property($settings, 'delta');
}

/**
 * Loads a single saved search object.
 *
 * @param $id
 *   The saved search's ID.
 * @param $reset
 *   If TRUE, will reset the internal entity cache.
 *
 * @return SearchApiSavedSearch
 *   The requested entity, or FALSE if no settings for that ID exist.
 */
function search_api_saved_search_load($id, $reset = FALSE) {
  $ret = entity_load('search_api_saved_search', array($id), array(), $reset);
  return $ret ? reset($ret) : FALSE;
}

/**
 * Loads multiple saved search objects.
 *
 * @param $ids
 *   The saved search's IDs; or FALSE to load all saved searches.
 * @param array $conditions
 *   Associative array of field => value conditions that returned objects must
 *   satisfy.
 * @param $reset
 *   If TRUE, will reset the internal entity cache.
 *
 * @return array
 *   An array of SearchApiSavedSearch objects matching the conditions.
 */
function search_api_saved_search_load_multiple($ids = FALSE, array $conditions = array(), $reset = FALSE) {
  return entity_load('search_api_saved_search', $ids, $conditions, $reset);
}

/**
 * Determine whether the current user can create a saved search for specific settings.
 *
 * @param SearchApiSavedSearchesSettings $settings
 *   The settings to check for. May be NULL, if $manual is TRUE, to check if any
 *   saved searches can be created manually.
 * @param boolean $manual
 *   (optional) If TRUE, check access for creating a saved search manually.
 *
 * @return boolean
 *   TRUE iff the current user is allowed to create a new saved search.
 */
function search_api_saved_search_create_access(SearchApiSavedSearchesSettings $settings = NULL, $manual = FALSE) {
  if ($manual) {
    if (isset($settings)) {
      if (!$settings->enabled || empty($settings->options['manual']['allow'])) {
        return FALSE;
      }
    }
    else {
      foreach (search_api_saved_searches_settings_load_multiple(FALSE, array('enabled' => TRUE)) as $settings) {
        if (!empty($settings->options['manual']['allow'])) {
          $found = TRUE;
          break;
        }
      }
      if (empty($found)) {
        return FALSE;
      }
    }
  }
  elseif (!$settings->enabled) {
    return FALSE;
  }

  if (user_access('administer search_api_saved_searches')) {
    return TRUE;
  }
  if (!user_access('use search_api_saved_searches')) {
    return FALSE;
  }
  if (!isset($settings)) {
    return TRUE;
  }

  // @todo Check settings-specific access rules, when there are any.

  return TRUE;
}

/**
 * Determine access to the edit interface for saved searches of a given user.
 *
 * This is both used to determine whether the current user can edit a specific
 * saved search, or whether she can display the overview of the user's saved
 * searches.
 * For anonymous users' searches an access key is generated that allows
 * accessing and editing the searches.
 *
 * @param $account
 *   (optional) The user whose saved search(es) would be edited. NULL for guest.
 * @param SearchApiSavedSearch $search
 *   (optional) The saved search involved, if there is just a single one.
 * @param string $key
 *   (optional) The secret key to access the search.
 *
 * @return boolean
 *   TRUE iff the current user is allowed to edit the saved search(es).
 */
function search_api_saved_search_edit_access($account = NULL, SearchApiSavedSearch $search = NULL, $key = NULL) {
  global $user;

  if (empty($account)) {
    if (empty($search)) {
      return FALSE;
    }
    $account = (object) array('uid' => $search->uid);
  }

  if (user_access('administer search_api_saved_searches')) {
    return TRUE;
  }
  // Barring admins, the only way to edit anonymous users' saved searches is by
  // providing the access key. There is no overview of all saved searches.
  if (!empty($key) && !empty($search->options['key']) && $search->options['key'] == $key) {
    return TRUE;
  }
  if ($account->uid == 0) {
    return FALSE;
  }
  if ($account->uid != $user->uid || !user_access('use search_api_saved_searches')) {
    return FALSE;
  }
  if (isset($search)) {
    return $search->uid == $account->uid;
  }
  foreach (search_api_saved_searches_settings_load_multiple() as $settings) {
    // Allow access if users can manually create searches.
    if (!empty($settings->options['manual']['allow'])) {
      return TRUE;
    }
    // Allow access if the list should always be displayed.
    if (!empty($settings->options['show_empty_list'])) {
      return TRUE;
    }
  }
  // Let the user view the listing if there are any saved searches.
  $select = db_select('search_api_saved_search', 's')
    ->condition('uid', $account->uid);
  $select->addExpression('COUNT(1)');
  return (bool) $select->execute()->fetchField();
}

/**
 * Implements hook_block_info().
 */
function search_api_saved_searches_block_info() {
  $blocks = array();
  foreach (search_api_saved_searches_settings_load_multiple(FALSE, array('enabled' => TRUE)) as $settings) {
    try {
      $blocks[$settings->delta] = array(
        'info' => t('!index: Save search', array('!index' => $settings->index()->name)),
        // @todo Is this cache setting correct?
        'cache' => DRUPAL_CACHE_PER_ROLE | DRUPAL_CACHE_PER_PAGE,
      );
    }
    catch (SearchApiException $e) {}
  }
  return $blocks;
}

/**
 * Implements hook_block_configure().
 */
function search_api_saved_searches_block_configure($delta = '') {
  $settings = search_api_saved_searches_settings_load($delta);
  $form['settings_link'] = array(
    '#markup' => l(t('To saved search settings'), 'admin/config/search/search_api/index/' . $settings->index_id . '/saved_searches'),
  );
  return $form;
}

/**
 * Implements hook_block_view().
 */
function search_api_saved_searches_block_view($delta = '') {
  $searches = search_api_current_search();
  if (!$searches) {
    return;
  }
  if (!user_access('use search_api_saved_searches')) {
    return;
  }
  $settings = search_api_saved_searches_settings_load($delta);
  if (!$settings || !search_api_saved_search_create_access($settings)) {
    return;
  }
  $index_id = $settings->index_id;
  $options = $settings->options;
  $ids_list = drupal_map_assoc($options['ids_list']);
  $search_ids = variable_get('search_api_saved_searches_search_ids', array());
  foreach ($searches as $id => $data) {
    if ($data[0]->getIndex()->machine_name == $index_id){
      if (!isset($search_ids[$index_id][$id])) {
        $search_ids[$index_id][$id] = $id;
        $search_ids_updated = TRUE;
      }
      if (isset($ids_list[$id]) != $options['default_true']) {
        if (isset($query)) {
          watchdog('search_api_saved_searches', 'Two matching searches on index %index for saved search block.',
              array('%index' => $settings->index()->name), WATCHDOG_WARNING,
              l(t('view page'), $_GET['q'], array('query' => drupal_get_query_parameters())));
        }
        else {
          list($query, $results) = $data;
        }
      }
    }
  }
  if (isset($search_ids_updated)) {
    variable_set('search_api_saved_searches_search_ids', $search_ids);
  }
  if (empty($query)) {
    return;
  }

  return array(
    'subject' => t('Save search'),
    'content' => array('form' => drupal_get_form('search_api_saved_searches_save_form', $settings, $query)),
  );
}

/**
 * Form builder for creating a new saved search.
 *
 * @param SearchApiSavedSearchesSettings $settings
 *   The saved search settings with which to create a new saved search.
 * @param SearchApiQueryInterface $query
 *   (optional) If creating a saved search for an already executed query, the
 *   query.
 *
 * @see search_api_saved_searches_save_form_validate()
 * @see search_api_saved_searches_save_form_submit()
 * @ingroup forms
 */
function search_api_saved_searches_save_form(array $form, array &$form_state, SearchApiSavedSearchesSettings $settings, SearchApiQueryInterface $query = NULL) {
  global $user;

  if (!isset($form_state['query']) && isset($query)) {
    $options = $query->getOptions();
    // When checking for new results, we need all results.
    // @todo Make this configurable?
    unset($options['offset'], $options['limit']);
    $options['search id'] = $settings->delta . ':' . 'saved-search';
    $form_state['query'] = array(
      'index_id' => $query->getIndex()->machine_name,
      'keys' => $query->getKeys(),
      'original_keys' => $query->getOriginalKeys(),
      'fields' => $query->getFields(),
      'filters' => $query->getFilter()->getFilters(),
      'options' => $options,
    );
  }
  $form_state['settings'] = $settings;

  $description = $settings->getTranslatedOption('description');
  if (!empty($description)) {
    $form['description'] = array(
      '#type' => 'item',
      '#description' => _filter_autop(check_plain($description)),
    );
  }

  if (empty($form_state['query'])) {
    $form['query'] = _search_api_saved_searches_create_search_form($settings);
    $form['name'] = array(
      '#type' => 'textfield',
      '#title' => t('Name'),
      '#description' => t('Enter the name that will be displayed for this saved search.'),
      '#maxlength' => 50,
    );
  }
  else {
    $form['#prefix'] = '<div id="search-api-saved-searches-save-form-wrapper">';
    $form['#suffix'] = '</div>';
    if (empty($settings->options['choose_name'])) {
      $form['name'] = array(
        '#type' => 'value',
        '#value' => _search_api_saved_searches_create_name($form_state['query']),
      );
    }
    else {
      $form['name'] = array(
        '#type' => 'textfield',
        '#title' => t('Name'),
        '#maxlength' => 50,
        '#size' => 16,
        '#required' => TRUE,
        '#default_value' => _search_api_saved_searches_create_name($form_state['query']),
      );
    }
  }
  if (empty($user->mail) || $settings->options['registered_choose_mail']) {
    $form['mail'] = array(
      '#type' => 'textfield',
      '#title' => t('E-mail address'),
      '#maxlength' => 100,
      '#size' => 16,
      '#default_value' => isset($user->mail) ? $user->mail : '',
      '#required' => TRUE,
    );
  }
  else {
    $form['mail'] = array(
      '#type' => 'value',
      '#value' => $user->mail,
    );
  }

  if ($settings->options['user_select_interval'] && count($settings->options['interval_options']) > 1) {
    $form['notify_interval'] = array(
      '#type' => 'select',
      '#title' => t('Notification interval'),
      '#options' => $settings->getTranslatedOption('interval_options'),
      '#required' => TRUE,
    );
  }
  else {
    $form['notify_interval'] = array(
      '#type' => 'value',
      '#value' => $settings->options['user_select_interval'] ? reset($settings->options['interval_options']) : $settings->options['set_interval'],
    );
  }

  if (!empty($form_state['query'])) {
    $form_state['page'] = array(
      'path' => $_GET['q'],
      'query' => drupal_get_query_parameters(),
    );
  }

  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Save search'),
    '#ajax' => array(
      'callback' => 'search_api_saved_searches_save_form_ajax',
      'wrapper' => 'search-api-saved-searches-save-form-wrapper',
      'effect' => 'fade',
      'method' => 'replace',
    ),
    '#executes_submit_callback' => TRUE,
  );
  // For manual search creation we don't need AJAX functionality.
  if (empty($form_state['query'])) {
    unset($form['submit']['#ajax']);
  }

  return $form;
}

/**
 * Helper function for creating a form for manually creating a saved search.
 */
function _search_api_saved_searches_create_search_form(SearchApiSavedSearchesSettings $settings) {
  $index = $settings->index();
  $wrapper = $index->entityWrapper();
  $options = isset($settings->options['manual']) ? $settings->options['manual'] : array();

  $form['#tree'] = TRUE;
  $form['fields'] = array(
    '#type' => 'fieldset',
    '#title' => t('Search'),
  );
  if (!empty($options['fulltext'])) {
    $form['fields']['search_api_saved_searches_fulltext'] = array(
      '#type' => 'textfield',
      '#title' => t('Keywords'),
    );
  }
  if (!empty($options['fields'])) {
    foreach ($options['fields'] as $field) {
      if (!empty($index->options['fields'][$field])) {
        // Extract the necessary field information out of the wrapper.
        $tmp = $wrapper;
        foreach (explode(':', $field) as $part) {
          if (!isset($tmp->$part)) {
            continue 2;
          }
          $tmp = $tmp->$part;
        }
        $info = $tmp->info();
        $form['fields'][$field]['#title'] = isset($info['label']) ? $info['label'] : $field;
        if ($optList = $tmp->optionsList('view')) {
          $optList = array(NULL => t('- Any -')) + $optList;
          $form['fields'][$field]['#type'] = 'select';
          $form['fields'][$field]['#options'] = $optList;
        }
        else {
          $form['fields'][$field]['#type'] = 'textfield';
        }
      }
    }
  }

  return $form;
}

/**
 * AJAX submit handler for search_api_saved_searches_save_form().
 */
function search_api_saved_searches_save_form_ajax(array $form, array &$form_state) {
  return form_get_errors() ? $form : array('#theme' => 'status_messages');
}

/**
 * Form validation handler for search_api_saved_searches_save_form().
 *
 * @see search_api_saved_searches_save_form()
 * @see search_api_saved_searches_save_form_submit()
 */
function search_api_saved_searches_save_form_validate(array $form, array &$form_state) {
  if ($msg = user_validate_mail($form_state['values']['mail'])) {
    form_error($form['mail'], $msg);
  }
}

/**
 * Form validation handler for search_api_saved_searches_save_form().
 *
 * @return boolean
 *   TRUE iff the search was successfully saved.
 *
 * @see search_api_saved_searches_save_form()
 * @see search_api_saved_searches_save_form_validate()
 */
function search_api_saved_searches_save_form_submit(array $form, array &$form_state) {
  global $user;

  $values = $form_state['values'];
  $settings = $form_state['settings'];

  if (empty($form_state['query'])) {
    $fields = $values['query']['fields'];
    $query = array(
      'keys' => isset($fields['search_api_saved_searches_fulltext']) ? $fields['search_api_saved_searches_fulltext'] : NULL,
      'fields' => NULL,
      'filters' => array(),
      'options' => array(
        'search id' => $settings->delta . ':' . 'saved-search',
      ),
    );
    unset($fields['search_api_saved_searches_fulltext']);
    foreach ($fields as $field => $value) {
      if ($value || is_numeric($value)) {
        $query['filters'][] = array($field, $value, '=');
      }
      else {
        unset($fields[$field]);
      }
    }

    if (empty($values['name'])) {
      $query['original_keys'] = $query['keys'];
      $values['name'] = _search_api_saved_searches_create_name($query);
      unset($query['original_keys']);
    }

    if (empty($form_state['page']) && !empty($settings->options['manual']['page']['path'])) {
      $page_options = $settings->options['manual']['page'];
      $form_state['page'] = array(
        'path' => $page_options['path'],
        'query' => array(),
      );
      if (isset($query['keys'])) {
        if (empty($page_options['fulltext'])) {
          $form_state['page']['path'] .= '/' . $query['keys'];
        }
        else {
          $form_state['page']['query'][$page_options['fulltext']] = $query['keys'];
        }
      }
      foreach ($fields as $field => $value) {
        if (empty($page_options['direct_filter_params'])) {
          $form_state['page']['query']['filter'][$field][] = '"' . $value . '"';
        }
        else {
          $form_state['page']['query'][$field] = $value;
        }
      }
    }
  }
  else {
    $query = array_intersect_key($form_state['query'], drupal_map_assoc(array('keys', 'fields', 'filters', 'options')));
  }

  // Enable the saved search right away, if a logged-in user uses their own mail
  // address, or when they have admin privileges, or when activation mails are
  // generally deactivated, or if there are already active saved searches for
  // that user with that mail address. Otherwise, an activation mail will be
  // sent.
  $enabled = (!empty($user->mail) && $user->mail == $values['mail'])
      || user_access('administer search_api_saved_searches')
      || empty($settings->options['mail']['activate']['send'])
      || ($user->uid && search_api_saved_search_load_multiple(FALSE, array('enabled' => TRUE, 'uid' => $user->uid, 'mail' => $values['mail'])));
  // If an anonymous user uses an existing user's mail address to create a
  // saved search, file the saved search under that user right away.
  $uid = $user->uid;
  if (!$uid && ($users = user_load_multiple(FALSE, array('mail' => $values['mail'], 'status' => 1)))) {
    $uid = key($users);
  }
  $search = entity_create('search_api_saved_search', array(
    'uid' => $uid,
    'settings_id' => $settings->delta,
    'enabled' => $enabled,
    'name' => $values['name'],
    'mail' => $values['mail'],
    'created' => REQUEST_TIME,
    'last_execute' => REQUEST_TIME,
    'notify_interval' => $values['notify_interval'],
    'query' => $query,
    'options' => array(),
  ));
  // Choose where to redirect.
  if (!empty($form_state['page'])) {
    $search->options['page'] = $form_state['page'];
    $form_state['redirect'] = array($form_state['page']['path'], $form_state['page']);
  }
  elseif ($user->uid) {
    $form_state['redirect'] = 'user/' . $user->uid . '/saved-searches';
  }
  // Save saved search.
  $ret = $search->save();
  // Display success or error message.
  if (!$ret) {
    drupal_set_message(t('An error occurred while trying to save the search. Please contact the site administrator.'), 'error');
    $form_state['rebuild'] = TRUE;
    return FALSE;
  }
  else {
    if ($enabled) {
      if ($search->notify_interval < 0) {
        drupal_set_message(t('Your saved search was successfully created.'));
      }
      else {
        drupal_set_message(t('Your saved search was successfully created. You will receive e-mail notifications for new results in the future.'));
      }
    }
    else {
      drupal_set_message(t('Your saved search was successfully created. You will soon receive an e-mail with a confirmation link to activate it.'));
    }
    return TRUE;
  }
}

/**
 * Helper function for creating a name for a saved search with the given query.
 */
function _search_api_saved_searches_create_name(array $query) {
  if (!empty($query['original_keys']) && is_scalar($query['original_keys'])) {
    $ret[] = $query['original_keys'];
  }
  $name = isset($ret) ? implode(' / ', $ret) : t('Saved search');
  drupal_alter('search_api_saved_search_create_name', $name, $query);
  return $name;
}

/**
 * Implements hook_mail().
 *
 * Two mails are provided, which expect the following values in the $params
 * array:
 * - activate:
 *   - search: The SearchApiSavedSearch object that should be activated.
 *   - user: The user object to which the saved search belongs.
 * - notify:
 *   - user: The user to which the executed searches belong.
 *   - settings: The settings with which the searches are associated.
 *   - searches: An array containing arrays with the following keys:
 *     - search: A SearchApiSavedSearch object that was checked.
 *     - num_results: The number of new results for that saved search.
 *     - results: An array of entities representing the new results for that
 *       saved search.
 */
function search_api_saved_searches_mail($key, array &$message, array $params) {
  $language = $message['language'];
  switch ($key) {
    case 'activate':
      $search = $params['search'];
      $settings = $search->settings();
      $data = array(
        'user' => $params['user'],
        'search_api_saved_search_info' => array(
          'search' => $search,
          'results' => array(),
        ),
      );
      $title = $settings->getTranslatedOption('mail.activate.title', $language->language);
      $message['subject'] .= token_replace($title, $data, array('language' => $language, 'sanitize' => FALSE));
      $body = $settings->getTranslatedOption('mail.activate.body', $language->language);
      $message['body'][] = token_replace($body, $data, array('language' => $language, 'sanitize' => FALSE));
      break;

    case 'notify':
      $settings = $params['settings'];
      $data = array(
        'user' => $params['user'],
        'search_api_saved_searches' => $params['searches'],
      );
      $title = $settings->getTranslatedOption('mail.notify.title', $language->language);
      $message['subject'] .= token_replace($title, $data, array('language' => $language, 'sanitize' => FALSE));
      $body = $settings->getTranslatedOption('mail.notify.body', $language->language);
      $message['body'][] = token_replace($body, $data, array('language' => $language, 'sanitize' => FALSE));
      break;
  }
}

/**
 * Implements hook_cron().
 *
 * Queue the saved searches that should be checked for new items.
 */
function search_api_saved_searches_cron() {
  // Get all searches whose last execution lies more than the notify_interval
  // in the past. Add a small amount to the current time, so small differences
  // in execution time don't result in a delay until the next cron run.
  $ids = db_select('search_api_saved_search', 's')
    ->fields('s', array('id'))
    ->condition('enabled', 1)
    ->where('notify_interval >= 0 AND last_execute + notify_interval < :time', array(':time' => REQUEST_TIME + 15))
    ->execute()
    ->fetchCol();
  if (!$ids) {
    return;
  }
  // Get the queue and load the queries.
  $queue = DrupalQueue::get('search_api_saved_searches_check_updates');
  $searches = search_api_saved_search_load_multiple($ids);
  // Group the search according to mail and settings. Grouping by mail prevents
  // a user from getting several mails at once, for different searches. Grouping
  // by settings is necessary since the mails can differ between settings.
  foreach ($searches as $search) {
    $user_searches[$search->mail . ' ' . $search->settings_id][] = $search;
  }
  foreach ($user_searches as $searches) {
    $queue->createItem($searches);
  }
  // Set the last execution timestamp now, so the interval doesn't move and we
  // don't get problems if the next cron run occurrs before the queue is
  // completely executed.
  db_update('search_api_saved_search')
    ->fields(array(
      'last_execute' => REQUEST_TIME,
    ))
    ->condition('id', $ids, 'IN')
    ->execute();
}

/**
 * Implements hook_cron_queue_info().
 *
 * Defines a queue for saved searches that should be checked for new items.
 */
function search_api_saved_searches_cron_queue_info() {
  return array(
    'search_api_saved_searches_check_updates' => array(
      'worker callback' => 'search_api_saved_searches_check_updates',
      'time' => 10,
    ),
  );
}

/**
 * Checks for new results for saved searches, and sends a mail if necessary.
 *
 * @param array $searches
 *   An array of SearchApiSavedSearch objects to check for new results. All of
 *   these should have the same mail address and base settings.
 */
function search_api_saved_searches_check_updates(array $searches) {
  try {
    $search = reset($searches);
    $settings = $search->settings();
    $index = $settings->index();
    $mail_params = array();
    foreach ($searches as $search) {
      // Make sure we run the query as the user who owns the saved search.
      // Otherwise node access will not work properly.
      $search->query['options']['search_api_access_account'] = $search->uid;
      // Get actual results for the query.
      $query = $search->query();

      // If a date field is set, use that to filter results.
      if (!empty($settings->options['date_field'])) {
        $query->condition($settings->options['date_field'], $search->last_execute, '>');
      }
      $response = $query->execute();
      if (!empty($response['results'])) {
        $new = $results = drupal_map_assoc(array_keys($response['results']));
        if (empty($settings->options['date_field'])) {
          // ID-based method: Compare these results to the old ones.
          $old = drupal_map_assoc(explode(',', $search->results));
          $new = array_diff_key($results, $old);
        }
        if ($new) {
          // We have new results: send them to the user.
          // Only load those items that will be sent.
          $sent_new = $new;
          if (!empty($settings->options['mail']['notify']['max_results'])) {
            $sent_new = array_slice($new, 0, $settings->options['mail']['notify']['max_results']);
          }
          $sent_new = $index->loadItems($sent_new);
          $new_results = $sent_new + $new;
          // Let other modules alter these results.
          drupal_alter('search_api_saved_searches_new_results', $new_results, $search);
          if ($new_results) {
            // We have to slice again in case some items were moved around or
            // removed by alter hooks.
            $sent_new = $new_results;
            if (!empty($settings->options['mail']['notify']['max_results'])) {
              $sent_new = array_slice($new_results, 0, $settings->options['mail']['notify']['max_results']);
              // Now some of the top results might still be unloaded.
              if ($unloaded = array_filter($sent_new, 'is_scalar')) {
                $sent_new = $index->loadItems($unloaded) + $sent_new;
              }
            }
            $num_results = count($new_results);
            $mail_params['searches'][] = array(
              'search' => $search,
              'num_results' => $num_results,
              'results' => $sent_new,
            );
          }
        }
        if (empty($settings->options['date_field']) && ($new || array_diff($old, $results))) {
          // The results changed in some way: store the latest version.
          $search->results = implode(',', $results);
          $search->save();
        }
      }
    }
    // If we set any searches in the mail parameters, send the mail.
    if ($mail_params) {
      $mail_params['user'] = user_load($search->uid);
      $mail_params['settings'] = $settings;
      $message = drupal_mail('search_api_saved_searches', 'notify', $search->mail,
          user_preferred_language($mail_params['user']), $mail_params);
      if ($message['result']) {
        watchdog('search_api_saved_searches', 'A mail with new saved search results was sent to @mail.',
            array('@mail' => $search->mail), WATCHDOG_INFO);
      }
    }
  }
  catch (SearchApiException $e) {
    watchdog('search_api_saved_searches', $e->getMessage(), NULL, WATCHDOG_ERROR);
  }
}
