<?php
// $Id: apachesolr.module,v 1.1.2.12.2.155.2.114 2010/09/04 12:07:15 robertDouglass Exp $

/**
 * @file
 *   Integration with the Apache Solr search application.
 */

define('APACHESOLR_READ_WRITE', 0);
define('APACHESOLR_READ_ONLY', 1);

/**
 * Implementation of hook_init().
 *
 * PHP 5.1 compatability code.
 */
function apachesolr_init() {
  if (!function_exists('json_decode')) {
    // Zend files include other files.
    set_include_path(dirname(__FILE__) . PATH_SEPARATOR . get_include_path());
    require_once 'Zend/Json/Decoder.php';
    require_once 'Zend/Json/Encoder.php';
  
    /**
     * Substitute for missing PHP built-in functions.
     */
    function json_decode($string, $assoc = FALSE) {
      if ($assoc) {
        $objectDecodeType = Zend_Json::TYPE_ARRAY;
      }
      else {
        $objectDecodeType = Zend_Json::TYPE_OBJECT;
      }
      return Zend_Json_Decoder::decode($string, $objectDecodeType);
    }

    function json_encode($data) {
      return Zend_Json_Encoder::encode($data, $objectDecodeType);
    }
  }
}

/**
 * Implementation of hook_menu().
 */
function apachesolr_menu() {
  $items = array();
  $items['admin/settings/apachesolr'] = array(
    'title'              => 'Apache Solr',
    'description'        => 'Administer Apache Solr.',
    'page callback'      => 'drupal_get_form',
    'page arguments'     => array('apachesolr_settings'),
    'access callback'    => 'user_access',
    'access arguments'   => array('administer search'),
    'file'               => 'apachesolr.admin.inc',
  );
  $items['admin/settings/apachesolr/settings'] = array(
    'title'              => 'Settings',
    'weight'             => -10,
    'access arguments'   => array('administer search'),
    'file'               => 'apachesolr.admin.inc',
    'type'               => MENU_DEFAULT_LOCAL_TASK,
  );
  $items['admin/settings/apachesolr/enabled-filters'] = array(
    'title'              => 'Enabled filters',
    'page callback'      => 'drupal_get_form',
    'page arguments'     => array('apachesolr_enabled_facets_form'),
    'weight'             => -7,
    'access arguments'   => array('administer search'),
    'file'               => 'apachesolr.admin.inc',
    'type'               => MENU_LOCAL_TASK,
  );
  $items['admin/settings/apachesolr/index'] = array(
    'title'              => 'Search index',
    'page callback'      => 'apachesolr_index_page',
    'access arguments'   => array('administer search'),
    'weight'             => -8,
    'file'               => 'apachesolr.admin.inc',
    'type'               => MENU_LOCAL_TASK,
  );
  $items['admin/settings/apachesolr/index/confirm/clear'] = array(
    'title'              => 'Confirm the re-indexing of all content',
    'page callback'      => 'drupal_get_form',
    'page arguments'     => array('apachesolr_clear_index_confirm'),
    'access arguments'   => array('administer search'),
    'file'               => 'apachesolr.admin.inc',
    'type'               => MENU_CALLBACK,
  );
  $items['admin/settings/apachesolr/index/confirm/delete'] = array(
    'title'              => 'Confirm index deletion',
    'page callback'      => 'drupal_get_form',
    'page arguments'     => array('apachesolr_delete_index_confirm'),
    'access arguments'   => array('administer search'),
    'file'               => 'apachesolr.admin.inc',
    'type'               => MENU_CALLBACK,
  );
  $items['admin/reports/apachesolr'] = array(
    'title'              => 'Apache Solr search index',
    'page callback'      => 'apachesolr_index_report',
    'access arguments'   => array('access site reports'),
    'file'               => 'apachesolr.admin.inc',
  );
  $items['admin/reports/apachesolr/index'] = array(
    'title'              => 'Search index',
    'file'               => 'apachesolr.admin.inc',
    'type'               => MENU_DEFAULT_LOCAL_TASK,
  );
  $items['admin/reports/apachesolr/conf'] = array(
    'title'              => 'Configuration files',
    'page callback'      => 'apachesolr_config_files_overview',
    'access arguments'   => array('access site reports'),
    'file'               => 'apachesolr.admin.inc',
    'weight'             => 5,
    'type'               => MENU_LOCAL_TASK,
  );
  $items['admin/reports/apachesolr/conf/%'] = array(
    'title'              => 'Configuration file',
    'page callback'      => 'apachesolr_config_file',
    'page arguments'     => array(4),
    'access arguments'   => array('access site reports'),
    'file'               => 'apachesolr.admin.inc',
    'type'               => MENU_CALLBACK,
  );
  $items['admin/settings/apachesolr/mlt/add_block'] = array(
    'page callback'      => 'drupal_get_form',
    'page arguments'     => array('apachesolr_mlt_add_block_form'),
    'access arguments'   => array('administer search'),
    'file'               => 'apachesolr.admin.inc',
    'type'               => MENU_CALLBACK,
  );
  $items['admin/settings/apachesolr/mlt/delete_block/%'] = array(
    'page callback'      => 'drupal_get_form',
    'page arguments'     => array('apachesolr_mlt_delete_block_form', 5),
    'access arguments'   => array('administer search'),
    'file'               => 'apachesolr.admin.inc',
    'type'               => MENU_CALLBACK,
  );
  return $items;
}

/**
 * Determines Apache Solr's behavior when searching causes an exception (e.g. Solr isn't available.)
 * Depending on the admin settings, possibly redirect to Drupal's core search.
 *
 * @param $search_name
 *   The name of the search implementation.
 *
 * @param $querystring
 *   The search query that was issued at the time of failure.
 */
function apachesolr_failure($search_name, $querystring) {
  $fail_rule = variable_get('apachesolr_failure', 'show_error');

  switch ($fail_rule) {
    case 'show_error':
      drupal_set_message(t('The Apache Solr search engine is not available. Please contact your site administrator.'), 'error');
      break;
    case 'show_drupal_results':
      drupal_set_message(t("%search_name is not available. Your search is being redirected.", array('%search_name' => $search_name)));
      drupal_goto('search/node/' . drupal_urlencode($querystring));
      break;
    case 'show_no_results':
      return;
  }
}

/**
 * Like $site_key in _update_refresh() - returns a site-specific hash.
 */
function apachesolr_site_hash() {
  if (!($hash = variable_get('apachesolr_site_hash', FALSE))) {
    global $base_url;
    // Set a random 6 digit base-36 number as the hash.
    $hash = substr(base_convert(sha1(uniqid($base_url, TRUE)), 16, 36), 0, 6);
    variable_set('apachesolr_site_hash', $hash);
  }
  return $hash;
}

/**
 * Generate a unique ID for an entity being indexed.
 *
 * @param $id
 *   An id number (or string) unique to this site, such as a node ID.
 * @param $entity
 *   A string like 'node', 'file', 'user', or some other Drupal object type.
 *
 * @return
 *   A string combining the parameters with the site hash.
 */
function apachesolr_document_id($id, $entity = 'node') {
  return apachesolr_site_hash() . "/$entity/" . $id;
}

/**
 * Implementation of hook_user().
 *
 * Mark nodes as needing re-indexing if the author name changes.
 */
function apachesolr_user($op, &$edit, &$account) {
  switch ($op) {
    case 'update':
      if (isset($edit['name']) && $account->name != $edit['name']) {
        switch ($GLOBALS['db_type']) {
          case 'mysql':
          case 'mysqli':
            db_query("UPDATE {apachesolr_search_node} asn INNER JOIN {node} n ON asn.nid = n.nid SET asn.changed = %d WHERE n.uid = %d", time(), $account->uid);
            break;
          default:
            db_query("UPDATE {apachesolr_search_node} SET changed = %d WHERE nid IN (SELECT nid FROM {node} WHERE uid = %d)", time(), $account->uid);
            break;
        }
      }
      break;
  }
}

/**
 * Implementation of hook_taxonomy().
 *
 * Mark nodes as needing re-indexing if a term name changes.
 */
function apachesolr_taxonomy($op, $type, $edit) {
  if ($type == 'term' && ($op == 'update')) {
    switch ($GLOBALS['db_type']) {
      case 'mysql':
      case 'mysqli':
        db_query("UPDATE {apachesolr_search_node} asn INNER JOIN {term_node} tn ON asn.nid = tn.nid SET asn.changed = %d WHERE tn.tid = %d", time(), $edit['tid']);
        break;
      default:
        db_query("UPDATE {apachesolr_search_node} SET changed = %d WHERE nid IN (SELECT nid FROM {term_node} WHERE tid = %d)", time(), $edit['tid']);
        break;
    }
  }
  // TODO: the rest, such as term deletion.
}

/**
 * Implementation of hook_comment().
 *
 * Mark nodes as needing re-indexing if comments are added or changed.
 * Like search_comment().
 */
function apachesolr_comment($edit, $op) {
  $edit = (array) $edit;
  switch ($op) {
    // Reindex the node when comments are added or changed
    case 'insert':
    case 'update':
    case 'delete':
    case 'publish':
    case 'unpublish':
      // TODO: do we want to skip this if we are excluding comments
      // from the index for this node type?
      apachesolr_mark_node($edit['nid']);
      break;
  }
}

/**
 * Mark one node as needing re-indexing.
 */
function apachesolr_mark_node($nid) {
  db_query("UPDATE {apachesolr_search_node} SET changed = %d WHERE nid = %d", time(), $nid);
}

/**
 * Implementation of hook_node_type().
 *
 * Mark nodes as needing re-indexing if a node type name changes.
 */
function apachesolr_node_type($op, $info) {
  if ($op != 'delete' && !empty($info->old_type) && $info->old_type != $info->type) {
    // We cannot be sure we are going before or after node module.
    switch ($GLOBALS['db_type']) {
      case 'mysql':
      case 'mysqli':
        db_query("UPDATE {apachesolr_search_node} asn INNER JOIN {node} n ON asn.nid = n.nid SET asn.changed = %d WHERE (n.type = '%s' OR n.type = '%s')", time(), $info->old_type, $info->type);
        break;
      default:
        db_query("UPDATE {apachesolr_search_node} SET changed = %d WHERE nid IN (SELECT nid FROM {node} WHERE type = '%s' OR type = '%s')", time(), $info->old_type, $info->type);
        break;
    }
  }
}

/**
 * Helper function for modules implmenting hook_search's 'status' op.
 */
function apachesolr_index_status($namespace) {
  list($excluded_types, $args, $join_sql, $exclude_sql) = apachesolr_exclude_types($namespace);
  $total = db_result(db_query("SELECT COUNT(asn.nid) FROM {apachesolr_search_node} asn ". $join_sql ."WHERE asn.status = 1 " . $exclude_sql, $excluded_types));
  $remaining = db_result(db_query("SELECT COUNT(asn.nid) FROM {apachesolr_search_node} asn ". $join_sql ."WHERE (asn.changed > %d OR (asn.changed = %d AND asn.nid > %d)) AND asn.status = 1 "  . $exclude_sql, $args));
  return array('remaining' => $remaining, 'total' => $total);
}

/**
 * Returns last changed and last nid for an indexing namespace.
 */
function apachesolr_get_last_index($namespace) {
  $stored = variable_get('apachesolr_index_last', array());
  return isset($stored[$namespace]) ? $stored[$namespace] : array('last_change' => 0, 'last_nid' => 0);
}

/**
 * Clear a specific namespace's last changed and nid, or clear all.
 */
function apachesolr_clear_last_index($namespace = '') {
  if ($namespace) {
    $stored = variable_get('apachesolr_index_last', array());
    unset($stored[$namespace]);
    variable_set('apachesolr_index_last', $stored);
  }
  else {
    variable_del('apachesolr_index_last');
  }
}

/**
 * Truncate and rebuild the apachesolr_search_node table, reset the apachesolr_index_last variable.
 * This is the most complete way to force reindexing, or to build the indexing table for the
 * first time.
 *
 * @param $type
 *   A single content type to be reindexed, leaving the others unaltered.
 */
function apachesolr_rebuild_index_table($type = NULL) {

  if (isset($type)) {
    switch ($GLOBALS['db_type']) {
      case 'mysql':
      case 'mysqli':
        db_query("DELETE FROM {apachesolr_search_node} USING {apachesolr_search_node} asn INNER JOIN {node} n ON asn.nid = n.nid WHERE n.type = '%s'", $type);
        break;
      default:
        db_query("DELETE FROM {apachesolr_search_node} WHERE nid IN (SELECT nid FROM {node} WHERE type = '%s')", $type);
        break;
    }
    // Populate table
    db_query("INSERT INTO {apachesolr_search_node} (nid, status, changed)
              SELECT n.nid, n.status, %d AS changed
              FROM {node} n WHERE n.type = '%s'", time(), $type);
  }
  else {
    db_query("DELETE FROM {apachesolr_search_node}");
    // Populate table
    db_query("INSERT INTO {apachesolr_search_node} (nid, status, changed)
              SELECT n.nid, n.status, GREATEST(n.created, n.changed, COALESCE(c.last_comment_timestamp, 0)) AS changed
              FROM {node} n
              LEFT JOIN {node_comment_statistics} c ON n.nid = c.nid");
    // Make sure no nodes end up with a timestamp that's in the future.
    $time = time();
    db_query("UPDATE {apachesolr_search_node} SET changed = %d WHERE changed > %d", $time, $time);
    apachesolr_clear_last_index();
  }
  cache_clear_all('*', 'cache_apachesolr', TRUE);
}

function apachesolr_exclude_types($namespace) {
  extract(apachesolr_get_last_index($namespace));
  $excluded_types = module_invoke_all('apachesolr_types_exclude', $namespace);
  $args = array($last_change, $last_change, $last_nid);
  $join_sql = '';
  $exclude_sql = '';
  if ($excluded_types) {
    $excluded_types = array_unique($excluded_types);
    $join_sql = "INNER JOIN {node} n ON n.nid = asn.nid ";
    $exclude_sql = "AND n.type NOT IN(". db_placeholders($excluded_types, 'varchar') .") ";
    $args = array_merge($args, $excluded_types);
  }
  return array($excluded_types, $args, $join_sql, $exclude_sql);
}

/**
 * Returns an array of rows from a query based on an indexing namespace.
 */
function apachesolr_get_nodes_to_index($namespace, $limit) {
  $rows = array();
  if (variable_get('apachesolr_read_only', 0)) {
    return $rows;
  }
  list($excluded_types, $args, $join_sql, $exclude_sql) = apachesolr_exclude_types($namespace);
  $result = db_query_range("SELECT asn.nid, asn.changed FROM {apachesolr_search_node} asn ". $join_sql ."WHERE (asn.changed > %d OR (asn.changed = %d AND asn.nid > %d)) AND asn.status = 1 ". $exclude_sql ."ORDER BY asn.changed ASC, asn.nid ASC", $args, 0, $limit);
  while ($row = db_fetch_object($result)) {
    $rows[] = $row;
  }
  return $rows;
}

/**
 * Function to handle the indexing of nodes.
 *
 * The calling function must supply a namespace.
 * Returns FALSE if no nodes were indexed (none found or error).
 */
/**
 * Handles the indexing of nodes.
 *
 * @param array $rows
 *   Each $row in $rows must have:
 *   $row->nid
 *   $row->changed
 * @param string $namespace
 *   Usually the calling module. Is used as a clue for other modules
 *   when they decide whether to create extra $documents, and is used
 *   to track the last_index timestamp.
 * @return timestamp $position
 *   Either a timestamp representing the last value of apachesolr_get_last_index
 *   to be indexed, or FALSE if indexing failed.
 */
function apachesolr_index_nodes($rows, $namespace) {
  if (!$rows) {
    // Nothing to do.
    return FALSE;
  }

  try {
    // Get the $solr object
    $solr = apachesolr_get_solr();
    // If there is no server available, don't continue.
    if (!$solr->ping(variable_get('apachesolr_ping_timeout', 4))) {
      throw new Exception(t('No Solr instance available during indexing.'));
    }
  }
  catch (Exception $e) {
    watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR);
    return FALSE;
  }
  include_once(drupal_get_path('module', 'apachesolr') .'/apachesolr.index.inc');
  $documents = array();
  $old_position = apachesolr_get_last_index($namespace);
  $position = $old_position;

  // Invoke hook_apachesolr_document_handlers to find out what modules build $documents
  // from nodes in this namespace.
  $callbacks = module_invoke_all('apachesolr_document_handlers', 'node', $namespace);
  $callbacks = array_filter($callbacks, 'function_exists');

  // Always build the content for the index as an anonynmous user.
  global $user;
  session_save_session(FALSE);
  $saved_user = $user;
  $user = drupal_anonymous_user();

  foreach ($rows as $row) {
    try {
      // Build node. Set reset = TRUE to avoid static caching of all nodes that get indexed.
      if ($node = node_load($row->nid, NULL, TRUE)) {
        foreach ($callbacks as $callback) {
          // The callback can either return a $document or an array of $documents.
          $documents[] = $callback($node, $namespace);
        }
      }
      // Variables to track the last item changed.
      $position['last_change'] = $row->changed;
      $position['last_nid'] = $row->nid;
    }
    catch (Exception $e) {
      // Something bad happened - log the error.
      watchdog('Apache Solr', 'Error constructing documents to index: <br /> !message', array('!message' => "Node ID: {$row->nid}<br />" . nl2br(strip_tags($e->getMessage()))), WATCHDOG_ERROR);
    }
  }
  // Restore the user.
  $user = $saved_user;
  session_save_session(TRUE);

  // Flatten $documents
  $tmp = array();
  apachesolr_flatten_documents_array($documents, $tmp);
  $documents = $tmp;

  if (count($documents)) {
    try {
      watchdog('Apache Solr', 'Adding @count documents.', array('@count' => count($documents)));
      // Chunk the adds by 20s
      $docs_chunk = array_chunk($documents, 20);
      foreach ($docs_chunk as $docs) {
        $solr->addDocuments($docs);
      }
      // Set the timestamp to indicate an index update.
      apachesolr_index_set_last_updated(time());
    }
    catch (Exception $e) {
      $nids = array();
      if (!empty($docs)) {
        foreach ($docs as $doc) {
          $nids[] = $doc->nid;
        }
      }
      watchdog('Apache Solr', 'Indexing failed on one of the following nodes: @nids <br /> !message', array(
        '@nids' => implode(', ', $nids),
        '!message' => nl2br(strip_tags($e->getMessage())),
      ), WATCHDOG_ERROR);
      return FALSE;
    }
  }

  // Save the new position in case it changed.
  if ($namespace && $position != $old_position) {
    $stored = variable_get('apachesolr_index_last', array());
    $stored[$namespace] = $position;
    variable_set('apachesolr_index_last', $stored);
  }

  return $position;
}

/**
 * Convert date from timestamp into ISO 8601 format.
 * http://lucene.apache.org/solr/api/org/apache/solr/schema/DateField.html
 */
function apachesolr_date_iso($date_timestamp) {
  return gmdate('Y-m-d\TH:i:s\Z', $date_timestamp);
}

/**
 * Function to flatten documents array recursively.
 *
 * @param array $documents
 *   The array of documents being indexed.
 * @param array &$tmp
 *   A container variable that will contain the flattened array.
 */
function apachesolr_flatten_documents_array($documents, &$tmp) {
  foreach ($documents AS $index => $item) {
    if (is_array($item)) {
      apachesolr_flatten_documents_array($item, $tmp);
    }
    else {
      $tmp[] = $item;
    }
  }
}

function apachesolr_delete_node_from_index($node) {
  static $failed = FALSE;
  if ($failed) {
    return FALSE;
  }
  try {
    $solr = apachesolr_get_solr();
    $solr->deleteById(apachesolr_document_id($node->nid));
    apachesolr_index_set_last_updated(time());
    return TRUE;
  }
  catch (Exception $e) {
    watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR);
    // Don't keep trying queries if they are failing.
    $failed = TRUE;
    return FALSE;
  }
}

/**
 * Set the timestamp of the last index update
 * @param $updated
 *   A timestamp or zero. If zero, the variable is deleted.
 */
function apachesolr_index_set_last_updated($updated = 0) {
  if ($updated) {
    variable_set('apachesolr_index_updated', (int) $updated);
  }
  else {
    variable_del('apachesolr_index_updated');
  }
}

/**
 * Get the timestamp of the last index update.
 * @return integer (timestamp)
 */
function apachesolr_index_get_last_updated() {
  return variable_get('apachesolr_index_updated', 0);
}

/**
 * Implementation of hook_cron().
 */
function apachesolr_cron() {
  try {
    $solr = apachesolr_get_solr();

    // Check for unpublished content that wasn't deleted from the index.
    $result = db_query("SELECT n.nid, n.status FROM {apachesolr_search_node} asn INNER JOIN {node} n ON n.nid = asn.nid WHERE asn.status <> n.status");
    while ($node = db_fetch_object($result)) {
      apachesolr_nodeapi_update($node, FALSE);
    }

    // Check for deleted content that wasn't deleted from the index.
    $result = db_query("SELECT asn.nid FROM {apachesolr_search_node} asn LEFT JOIN {node} n ON n.nid = asn.nid WHERE n.nid IS NULL");
    while ($node = db_fetch_object($result)) {
      apachesolr_nodeapi_delete($node, FALSE);
    }

    // Optimize the index (by default once a day).
    $optimize_interval = variable_get('apachesolr_optimize_interval', 60 * 60 * 24);
    $last = variable_get('apachesolr_last_optimize', 0);
    $time = time();
    if ($optimize_interval && ($time - $last > $optimize_interval)) {
      $solr->optimize(FALSE, FALSE);
      variable_set('apachesolr_last_optimize', $time);
      apachesolr_index_set_last_updated($time);
    }

    // Only clear the cache if the index changed.
    // TODO: clear on some schedule if running multi-site.
    $updated = apachesolr_index_get_last_updated();
    if ($updated) {
      $solr->clearCache();
      // Re-populate the luke cache.
      $solr->getLuke();
      // TODO: an admin interface for setting this.  Assume for now 5 minutes.
      if ($time - $updated >= variable_get('apachesolr_cache_delay', 300)) {
        // Clear the updated flag.
        apachesolr_index_set_last_updated(0);
      }
    }
  }
  catch (Exception $e) {
    watchdog('Apache Solr', nl2br(check_plain($e->getMessage())) . ' in apachesolr_cron', NULL, WATCHDOG_ERROR);
  }
}

/**
 * Implementation of hook_flush_caches().
 */
function apachesolr_flush_caches() {
  return array('cache_apachesolr');
}

/**
 * A wrapper for cache_clear_all to be used as a submit handler on forms that
 * require clearing Luke cache etc.
 */
function apachesolr_clear_cache() {
  try {
    $solr = apachesolr_get_solr();
    $solr->clearCache();
  }
  catch (Exception $e) {
    watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR);
    drupal_set_message(nl2br(check_plain($e->getMessage())), 'warning');
  }
}

/**
 * Implementation of hook_nodeapi().
 */
function apachesolr_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
  switch ($op) {
    case 'delete':
      apachesolr_nodeapi_delete($node);
      break;
    case 'insert':
      // Make sure no node ends up with a timestamp that's in the future
      // by using time() rather than the node's changed or created timestamp.
      db_query("INSERT INTO {apachesolr_search_node} (nid, status, changed) VALUES (%d, %d, %d)", $node->nid, $node->status, time());
      break;
    case 'update':
      apachesolr_nodeapi_update($node);
      break;
  }
}

/**
 * Helper function for hook_nodeapi().
 */
function apachesolr_nodeapi_delete($node, $set_message = TRUE) {
  if (apachesolr_delete_node_from_index($node)) {
    // There was no exception, so delete from the table.
    db_query("DELETE FROM {apachesolr_search_node} WHERE nid = %d", $node->nid);
    if ($set_message && user_access('administer search') && variable_get('apachesolr_set_nodeapi_messages', 1)) {
      apachesolr_set_stats_message('Deleted content will be removed from the Apache Solr search index in approximately @autocommit_time.');
    }
  }
}

/**
 * Helper function for hook_nodeapi().
 */
function apachesolr_nodeapi_update($node, $set_message = TRUE) {
  // Check if the node has gone from published to unpublished.
  if (!$node->status && db_result(db_query("SELECT status FROM {apachesolr_search_node} WHERE nid = %d", $node->nid))) {
    if (apachesolr_delete_node_from_index($node)) {
      // There was no exception, so update the table.
      db_query("UPDATE {apachesolr_search_node} SET changed = %d, status = %d WHERE nid = %d", time(), $node->status, $node->nid);
      if ($set_message && user_access('administer search') && variable_get('apachesolr_set_nodeapi_messages', 1)) {
        apachesolr_set_stats_message('Unpublished content will be removed from the Apache Solr search index in approximately @autocommit_time.');
      }
    }
  }
  else {
    db_query("UPDATE {apachesolr_search_node} SET changed = %d, status = %d WHERE nid = %d", time(), $node->status, $node->nid);
  }
}

/**
 * Call drupal_set_message() with the text.
 *
 * The text is translated with t() and substituted using Solr stats.
 */
function apachesolr_set_stats_message($text, $type = 'status', $repeat = FALSE) {
  try {
    $solr = apachesolr_get_solr();
    $stats_summary = $solr->getStatsSummary();
    drupal_set_message(t($text, $stats_summary), $type, FALSE);
  }
  catch (Exception $e) {
    watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR);
  }
}

/**
 * Return the enabled facets from the specified block array.
 *
 * @param $module
 *   The module (optional).
 * @return
 *   An array consisting of info for facets that have been enabled
 *   for the specified module, or all enabled facets.
 */
function apachesolr_get_enabled_facets($module = NULL) {
  $enabled = variable_get('apachesolr_enabled_facets', array());
  if (isset($module)) {
    return isset($enabled[$module]) ? $enabled[$module] : array();
  }
  return $enabled;
}

/**
 * Save the enabled facets for all modules.
 *
 * @param $enabled
 *   An array consisting of info for all enabled facets.
 * @return
 *   The array consisting of info for all enabled facets.
 */
function apachesolr_save_enabled_facets($enabled) {
  variable_set('apachesolr_enabled_facets', $enabled);
  return $enabled;
}

/**
 * Save the enabled facets for one module.
 *
 * @param $module
 *   The module name.
 * @param $facets
 *   Associative array of $delta => $facet_field pairs.  If omitted, all facets
 *   for $module are disabled.
 * @return
 *   An array consisting of info for all enabled facets.
 */
function apachesolr_save_module_facets($module, $facets = array()) {
  $enabled = variable_get('apachesolr_enabled_facets', array());
  if (!empty($facets) && is_array($facets)) {
    $enabled[$module] = $facets;
  }
  else {
    unset($enabled[$module]);
  }
  variable_set('apachesolr_enabled_facets', $enabled);
  return $enabled;
}

/**
 * Implementation of hook_block().
 */
function apachesolr_block($op = 'list', $delta = 0, $edit = array()) {
  static $access;

  switch ($op) {
    case 'list':
      // Get all of the moreLikeThis blocks that the user has created
      $blocks = apachesolr_mlt_list_blocks();
      // Add the sort block.
      $blocks['sort'] = array(
        'info' => t('Apache Solr Core: Sorting'),
        'cache' => BLOCK_CACHE_PER_PAGE,
      );
      return $blocks;

    case 'view':
      if ($delta != 'sort' && ($node = menu_get_object()) && (!arg(2) || arg(2) == 'view')) {
        $suggestions = array();
        // Determine whether the user can view the current node.
        if (!isset($access)) {
          $access = node_access('view', $node);
        }
        $block = apachesolr_mlt_load_block($delta);
        if ($access && $block) {
          $docs = apachesolr_mlt_suggestions($block, apachesolr_document_id($node->nid));
          if (!empty($docs)) {
            $suggestions['subject'] = check_plain($block['name']);
            $suggestions['content'] = theme('apachesolr_mlt_recommendation_block', $docs);
            if (user_access('administer search')) {
              $suggestions['content'] .= l(t('Configure this block'), 'admin/build/block/configure/apachesolr/' . $delta, array('attributes' => array('class' => 'apachesolr-mlt-admin-link')));
            }
          }
        }
        return $suggestions;
      }
      elseif (apachesolr_has_searched() && $delta == 'sort') {
        // Get the query and response. Without these no blocks make sense.
        $response = apachesolr_static_response_cache();
        if (empty($response) || ($response->response->numFound < 2)) {
          return;
        }

        $query = apachesolr_current_query();
        $sorts = $query->get_available_sorts();

        // Get the current sort as an array.
        $solrsort = $query->get_solrsort();

        $sort_links = array();
        $path = $query->get_path();
        $new_query = clone $query;
        $toggle = array('asc' => 'desc', 'desc' => 'asc');
        foreach ($sorts as $name => $sort) {
          $active = $solrsort['#name'] == $name;
          if ($name == 'score') {
            $direction = '';
            $new_direction = 'desc'; // We only sort by descending score.
          }
          elseif ($active) {
            $direction = $toggle[$solrsort['#direction']];
            $new_direction = $toggle[$solrsort['#direction']];
          }
          else {
            $direction = '';
            $new_direction = $sort['default'];
          }
          $new_query->set_solrsort($name, $new_direction);
          $sort_links[$name] = array(
            'title' => $sort['title'],
            'path' => $path,
            'options' => array('query' => $new_query->get_url_queryvalues()),
            'active' => $active,
            'direction' => $direction,
          );
          if ($name == 'score') {
            // Set a solrsort parameter so we can distinguish between a user
            // selecting "score desc" and the page defaulting to "score desc".
            $sort_links[$name]['options']['query']['solrsort'] = 'score desc';
          }
        }
        // Allow other modules to add or remove sorts.
        drupal_alter('apachesolr_sort_links', $sort_links);
        if (!empty($sort_links)) {
          foreach ($sort_links as $name => $link) {
            $themed_links[$name] = theme('apachesolr_sort_link', $link['title'], $link['path'], $link['options'], $link['active'], $link['direction']);
          }
          return array(
            'subject' => t('Sort by'),
            'content' => theme('apachesolr_sort_list', $themed_links),
          );
        }
      }
      break;
    case 'configure':
      if ($delta != 'sort') {
        require_once(drupal_get_path('module', 'apachesolr') .'/apachesolr.admin.inc');
        return apachesolr_mlt_block_form($delta);
      }
      break;
    case 'save':
      if ($delta != 'sort') {
        require_once(drupal_get_path('module', 'apachesolr') .'/apachesolr.admin.inc');
        apachesolr_mlt_save_block($edit, $delta);
      }
      break;
  }
}

/**
 * This code makes a decision whether to show a block or not.
 * @param $query
 *   The current query object.
 * @param string $module
 *   The module's name to whom this block belongs.
 * @param string $delta
 *   The delta string the identifies the block within $module.
 * @return boolean
 *   Whether the block should be visible. Other factors, like
 *   the block system's visibility settings, apply as well.
 */
function apachesolr_block_visibility($query, $module, $delta) {
  // TYPE HIERARCHY.
  // If the block is configured to heed type hierarchy then it looks to see if a
  // suitable type filter has been chosen. If not, the function returns.
  // This variable is not static cached because variable_get() already does that.
  $type_filters = variable_get('apachesolr_type_filter', array());
  if (isset($type_filters[$module][$delta]) && $type_filters[$module][$delta] == TRUE) {
    $facet_info = apachesolr_get_facet_definitions();
    if (isset($facet_info[$module][$delta]['content_types'])) {
      $has_filter = $query->get_filters($facet_info[$module][$delta]['facet_field']);
      $show = count($has_filter);
      foreach ($facet_info[$module][$delta]['content_types'] as $content_type) {
        if ($query->has_filter('type', $content_type)) {
          $show = TRUE;
        }
      }
      if (!$show) {
        return FALSE;
      }
    }
  }
  return TRUE;
}

function apachesolr_get_facet_definitions() {
  static $definitions;
  if (!isset($definitions)) {
    $operator_settings = variable_get('apachesolr_operator', array());
    foreach (module_implements('apachesolr_facets') as $module) {
      $facets = module_invoke($module, 'apachesolr_facets');
      if (!empty($facets)) {
        foreach ($facets as $delta => $info) {
          $definitions[$module][$delta] = $info;
          if (isset($definitions[$module][$delta])) {
            $definitions[$module][$delta]['operator'] = isset($operator_settings[$module][$delta]) ? $operator_settings[$module][$delta] : 'AND';
          }
        }
      }
    }
  }
  return $definitions;
}

/**
 * Returns a member of the facet definitions array if it contains
 * $field as the 'field_name' element.
 *
 * @param string $field
 *   The field_name being sought.
 */
function apachesolr_get_facet_definition_by_field_name($field_name) {
  $definitions = apachesolr_get_facet_definitions();
  foreach ($definitions as $module => $facets) {
    foreach ($facets as $key => $values) {
      if (isset($values['facet_field']) && $values['facet_field'] == $field_name) {
        return $definitions[$module][$key];
      }
    }
  }
}

/**
 * Implementation of hook_form_[form_id]_alter().
 *
 * Make sure to flush cache when content types are changed.
 */
function apachesolr_form_node_type_form_alter(&$form, $form_state) {
  $form['#submit'][] = 'apachesolr_clear_cache';
}

/**
 * Implementation of hook_form_[form_id]_alter().
 *
 * Make sure to flush cache when fields are added.
 */
function apachesolr_form_content_field_overview_form_alter(&$form, $form_state) {
  $form['#submit'][] = 'apachesolr_clear_cache';
}

/**
 * Implementation of hook_form_[form_id]_alter().
 *
 * Make sure to flush cache when fields are updated.
 */
function apachesolr_form_content_field_edit_form_alter(&$form, $form_state) {
  $form['#submit'][] = 'apachesolr_clear_cache';
}

/**
 * Implementation of hook_form_[form_id]_alter().
 *
 * Hide the core 'title' field in favor of our 'name' field.
 *
 * Add a checkbox to enable type-specific visibility.
 */
function apachesolr_form_block_admin_configure_alter(&$form, $form_state) {
  // Hide the core title field.
  if ($form['module']['#value'] == 'apachesolr' && $form['delta']['#value'] != 'sort') {
    $form['block_settings']['title']['#access'] = FALSE;
  }

  // Add a type-specific visibility checkbox.
  $module = $form['module']['#value'];
  $delta = $form['delta']['#value'];
  $enabled_facets = apachesolr_get_enabled_facets();

  // If this block isn't enabled as a facet, get out of here.
  if (!isset($enabled_facets[$module][$delta])) {
    return;
  }

  $facet_info = apachesolr_get_facet_definitions();

  if (isset($facet_info[$module][$delta]['content_types'])) {
    $type_filter_settings = variable_get('apachesolr_type_filter', array());
    // Set up some variables for the verbiage of the form element.
    $count = count($facet_info[$module][$delta]['content_types']);
    $types = format_plural($count, t('type'), t('types'));
    $are = format_plural($count, t('is'), t('are'));
    $content_types = implode(', ', $facet_info[$module][$delta]['content_types']) . '.';
    $form['block_settings']['apachesolr_type_filter'] = array(
      '#type' => 'checkbox',
      '#title' => t('Show this block only when the type filter is selected for: %content_types',  array('%content_types' => $content_types)),
      '#default_value' => isset($type_filter_settings[$module][$delta]) ? $type_filter_settings[$module][$delta] : FALSE,
      '#description' => t('This filter is relevant only for specific content types. Check this to display the block only when the type filter has been selected for one of the relevant content types.'),
      '#weight' => 11,
    );
  }
  $operator_settings = variable_get('apachesolr_operator', array());
  $form['block_settings']['apachesolr_operator'] = array(
    '#type' => 'radios',
    '#title' => t('Operator to use for facets'),
    '#options' => drupal_map_assoc(array('AND', 'OR')),
    '#default_value' => isset($operator_settings[$module][$delta]) ? $operator_settings[$module][$delta] : 'AND',
    '#description' => t('AND filters are exclusive. OR filters are inclusive. Selecting more AND filters narrows the result set. Selecting more OR filters widens the result set.'),
    '#weight' => 12,
  );
  // Add a submit handler to save the value.
  $form['#validate'][] = 'apachesolr_block_admin_configure_submit';
}

function apachesolr_block_admin_configure_submit($form, &$form_state) {
  if (isset($form_state['values']['apachesolr_type_filter'])) {
    $type_filter_settings = variable_get('apachesolr_type_filter', array());
    $module = $form_state['values']['module'];
    $delta = $form_state['values']['delta'];
    unset($type_filter_settings[$module][$delta]);
    $type_filter_settings[$module][$delta] = $form_state['values']['apachesolr_type_filter'];
    variable_set('apachesolr_type_filter', $type_filter_settings);
  }
  if (isset($form_state['values']['apachesolr_operator'])) {
    $operator_settings = variable_get('apachesolr_operator', array());
    $module = $form_state['values']['module'];
    $delta = $form_state['values']['delta'];
    unset($operator_settings[$module][$delta]);
    $operator_settings[$module][$delta] = $form_state['values']['apachesolr_operator'];
    variable_set('apachesolr_operator', $operator_settings);
  }
}

/**
 * Helper function for displaying a facet block.
 */
function apachesolr_facet_block($response, $query, $module, $delta, $facet_field, $filter_by, $facet_callback = FALSE) {
  if (!empty($response->facet_counts->facet_fields->$facet_field)) {
    $facet_query_sorts = variable_get('apachesolr_facet_query_sorts', array());
    $contains_active = FALSE;
    $items = array();
    foreach ($response->facet_counts->facet_fields->$facet_field as $facet => $count) {
      $options = array('delta' => $delta);
      $exclude = FALSE;

      // Solr sends this back if it's empty.
      if ($facet == '_empty_') {
        $exclude = TRUE;
        $facet = '[* TO *]';
        $options['html'] = TRUE;
      }

      if ($facet_callback && function_exists($facet_callback)) {
        $facet_text = $facet_callback($facet, $options);
      }
      elseif ($exclude) {
        $facet_text = theme('placeholder', t('Missing this field'));
      }
      else {
        $facet_text = $facet;
      }

      $active = $query->has_filter($facet_field, $facet);

      if ($active) {
        // '*' sorts before all numbers.
        $sortpre = '*';
      }
      elseif (isset($facet_query_sorts[$module][$delta]) && strpos($facet_query_sorts[$module][$delta], 'index key') === 0) {
        // If this block is to be alphabetically sorted by key, change $sortpre.
        $sortpre = $facet;
      }
      elseif (isset($facet_query_sorts[$module][$delta]) && strpos($facet_query_sorts[$module][$delta], 'index') === 0) {
        // If this block is to be alphabetically/numerically sorted by value, change $sortpre.
        $sortpre = $facet_text;
      }
      elseif ($exclude) {
        // '-' sorts before all numbers, but after '*'.
        $sortpre = '-';
      }
      else {
        $sortpre = 1000000 - $count;
      }

      $new_query = clone $query;
      if ($active) {
        $contains_active = TRUE;
        $new_query->remove_filter($facet_field, $facet);
        $options['query'] = $new_query->get_url_queryvalues();
        $link = theme('apachesolr_unclick_link', $facet_text, $new_query->get_path(), $options);
      }
      else {
        $new_query->add_filter($facet_field, $facet, $exclude);
        $options['query'] = $new_query->get_url_queryvalues();
        $link = theme('apachesolr_facet_link', $facet_text, $new_query->get_path(), $options, $count, FALSE, $response->response->numFound);
      }

      if ($count || $active) {
        $items[$sortpre . '*' . $facet_text] = $link;
      }
    }
    // Unless a facet is active only display 2 or more.
    if ($items && ($response->response->numFound > 1 || $contains_active)) {
      switch ($facet_query_sorts[$module][$delta]) {
        case 'index numeric asc':
          ksort($items, SORT_NUMERIC);
          break;
        case 'index numeric desc':
          krsort($items, SORT_NUMERIC);
          break;
        case 'index desc':
        case 'index key desc':
          krsort($items, SORT_STRING);
          break;
        case 'index asc':
        case 'index key asc':
        default:
          ksort($items, SORT_STRING);
          break;
      }
      // Get information needed by the rest of the blocks about limits.
      $initial_limits = variable_get('apachesolr_facet_query_initial_limits', array());
      $limit = isset($initial_limits[$module][$delta]) ? $initial_limits[$module][$delta] : variable_get('apachesolr_facet_query_initial_limit_default', 10);
      $output = theme('apachesolr_facet_list', $items, $limit, $delta);
      return array('subject' => $filter_by, 'content' => $output);
    }
  }
  return NULL;
}

/**
 * Helper function for displaying a date facet block.
 *
 * TODO: Refactor with apachesolr_facet_block().
 */
function apachesolr_date_facet_block($response, $query, $module, $delta, $facet_field, $filter_by, $facet_callback = FALSE) {
  $items = array();

  $new_query = clone $query;
  foreach (array_reverse($new_query->get_filters($facet_field)) as $filter) {
    $options = array();
    // Iteratively remove the date facets.
    $new_query->remove_filter($facet_field, $filter['#value']);
    if ($facet_callback && function_exists($facet_callback)) {
      $facet_text = $facet_callback($filter['#start'], $options);
    }
    else {
      $facet_text = apachesolr_date_format_iso_by_gap(apachesolr_date_find_query_gap($filter['#start'], $filter['#end']), $filter['#start']);
    }
    $options['query'] = $new_query->get_url_queryvalues();
    array_unshift($items, theme('apachesolr_unclick_link', $facet_text, $new_query->get_path(), $options));
  }
  // Add links for additional date filters.
  if (!empty($response->facet_counts->facet_dates->$facet_field)) {
    $field = clone $response->facet_counts->facet_dates->$facet_field;

    $end = $field->end;
    unset($field->end);

    $gap = $field->gap;
    unset($field->gap);

    // Treat each date facet as a range start, and use the next date
    // facet as range end.  Use 'end' for the final end.
    $range_end = array();
    foreach ($field as $facet => $count) {
      if (isset($prev_facet)) {
        $range_end[$prev_facet] = $facet;
      }
      $prev_facet = $facet;
    }
    $range_end[$prev_facet] = $end;

    foreach ($field as $facet => $count) {
      $options = array();
      // Solr sends this back if it's empty.
      if ($facet == '_empty_' || $count == 0) {
        continue;
      }
      if ($facet_callback && function_exists($facet_callback)) {
        $facet_text = $facet_callback($facet, $options);
      }
      else {
        $facet_text = apachesolr_date_format_iso_by_gap(substr($gap, 2), $facet);
      }
      $new_query = clone $query;
      $new_query->add_filter($facet_field, '['. $facet .' TO '. $range_end[$facet] .']');
      $options['query'] = $new_query->get_url_queryvalues();
      $items[] = theme('apachesolr_facet_link', $facet_text, $new_query->get_path(), $options, $count, FALSE, $response->response->numFound);
    }
  }
  if (count($items) > 0) {
    // Get information needed by the rest of the blocks about limits.
    $initial_limits = variable_get('apachesolr_facet_query_initial_limits', array());
    $limit = isset($initial_limits[$module][$delta]) ? $initial_limits[$module][$delta] : variable_get('apachesolr_facet_query_initial_limit_default', 10);
    $output = theme('apachesolr_facet_list', $items, $limit, $delta);
    return array('subject' => $filter_by, 'content' => $output);
  }
  return NULL;
}

/**
 * Determine the gap in a date range query filter that we generated.
 *
 * This function assumes that the start and end dates are the
 * beginning and end of a single period: 1 year, month, day, hour,
 * minute, or second (all date range query filters we generate meet
 * this criteria).  So, if the seconds are different, it is a second
 * gap.  If the seconds are the same (incidentally, they will also be
 * 0) but the minutes are different, it is a minute gap.  If the
 * minutes are the same but hours are different, it's an hour gap.
 * etc.
 *
 * @param $start
 *   Start date as an ISO date string.
 * @param $end
 *   End date as an ISO date string.
 * @return
 *   YEAR, MONTH, DAY, HOUR, MINUTE, or SECOND.
 */
function apachesolr_date_find_query_gap($start_iso, $end_iso) {
  $gaps = array('SECOND' => 6, 'MINUTE' => 5, 'HOUR' => 4, 'DAY' => 3, 'MONTH' => 2, 'YEAR' => 1);
  $re = '@(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})@';
  if (preg_match($re, $start_iso, $start) && preg_match($re, $end_iso, $end)) {
    foreach ($gaps as $gap => $idx) {
      if ($start[$idx] != $end[$idx]) {
        return $gap;
      }
    }
  }
  // can't tell
  return 'YEAR';
}

/**
 * Format an ISO date string based on the gap used to generate it.
 *
 * This function assumes that gaps less than one day will be displayed
 * in a search context in which a larger containing gap including a
 * day is already displayed.  So, HOUR, MINUTE, and SECOND gaps only
 * display time information, without date.
 *
 * @param $gap
 *   A gap.
 * @param $iso
 *   An ISO date string.
 * @return
 *   A gap-appropriate formatted date.
 */
function apachesolr_date_format_iso_by_gap($gap, $iso) {
  // TODO: If we assume that multiple search queries are formatted in
  // order, we could store a static list of all gaps we've formatted.
  // Then, if we format an HOUR, MINUTE, or SECOND without previously
  // having formatted a DAY or later, we could include date
  // information.  However, we'd need to do that per-field and I'm not
  // our callers always have field information handy.
  $unix = strtotime($iso);
  if ($unix !== FALSE) {
    switch ($gap) {
      case 'YEAR':
        return format_date($unix, 'custom', 'Y', 0);
      case 'MONTH':
        return format_date($unix, 'custom', 'F Y', 0);
      case 'DAY':
        return format_date($unix, 'custom', 'F j, Y', 0);
      case 'HOUR':
        return format_date($unix, 'custom', 'g A', 0);
      case 'MINUTE':
        return format_date($unix, 'custom', 'g:i A', 0);
      case 'SECOND':
        return format_date($unix, 'custom', 'g:i:s A', 0);
    }
  }

  return $iso;
}

/**
 * Format the beginning of a date range query filter that we
 * generated.
 *
 * @param $start_iso
 *   The start date.
 * @param $end_iso
 *   The end date.
 * @return
 *   A display string reprepsenting the date range, such as "January
 * 2009" for "2009-01-01T00:00:00Z TO 2009-02-01T00:00:00Z"
 */
function apachesolr_date_format_range($start_iso, $end_iso) {
  $gap = apachesolr_date_find_query_gap($start_iso, $end_iso);
  return apachesolr_date_format_iso_by_gap($gap, $start_iso);
}

/**
 * Determine the best search gap to use for an arbitrary date range.
 *
 * Generally, we the maximum gap that fits between the start and end
 * date.  If they are more than a year apart, 1 year; if they are more
 * than a month apart, 1 month; etc.
 *
 * This function uses Unix timestamps for its computation and so is
 * not useful for dates outside that range.
 *
 * @param $start
 *   Start date as an ISO date string.
 * @param $end
 *   End date as an ISO date string.
 * @return
 *   YEAR, MONTH, DAY, HOUR, MINUTE, or SECOND depending on how far
 *   apart $start and $end are.
 */
function apachesolr_date_determine_gap($start, $end) {
  $start = strtotime($start);
  $end = strtotime($end);

  if ($end - $start >= 86400 * 365) {
    return 'YEAR';
  }
  if (date('Ym', $start) != date('Ym', $end)) {
    return 'MONTH';
  }
  if ($end - $start > 86400) {
    return 'DAY';
  }
  return 'HOUR';
}

/**
 * Return the next smaller date gap.
 *
 * @param $gap
 *   A gap.
 * @return
 *   The next smaller gap, or NULL if there is no smaller gap.
 */
function apachesolr_date_gap_drilldown($gap) {
  $drill = array(
    'YEAR' => 'MONTH',
    'MONTH' => 'DAY',
    'DAY' => 'HOUR',
    'HOUR' => 'MINUTE',
  );
  return isset($drill[$gap]) ? $drill[$gap] : NULL;
}

/**
 * Used by the 'configure' $op of hook_block so that modules can generically set
 * facet limits on their blocks.
 */
function apachesolr_facetcount_form($module, $delta) {
  $initial = variable_get('apachesolr_facet_query_initial_limits', array());
  $limits = variable_get('apachesolr_facet_query_limits', array());
  $sorts = variable_get('apachesolr_facet_query_sorts', array());
  $facet_missing = variable_get('apachesolr_facet_missing', array());

  $limit = drupal_map_assoc(array(50, 40, 30, 20, 15, 10, 5, 3));

  $form['apachesolr_facet_query_initial_limit'] = array(
    '#type' => 'select',
    '#title' => t('Initial filter links'),
    '#options' => $limit,
    '#description' => t('The initial number of filter links to show in this block.'),
    '#default_value' => isset($initial[$module][$delta]) ? $initial[$module][$delta] : variable_get('apachesolr_facet_query_initial_limit_default', 10),
  );
  $limit = drupal_map_assoc(array(100, 75, 50, 40, 30, 20, 15, 10, 5, 3));
  $form['apachesolr_facet_query_limit'] = array(
    '#type' => 'select',
    '#title' => t('Maximum filter links'),
    '#options' => $limit,
    '#description' => t('The maximum number of filter links to show in this block.'),
    '#default_value' => isset($limits[$module][$delta]) ? $limits[$module][$delta] : variable_get('apachesolr_facet_query_limit_default', 20),
  );

  // TODO: Generalize how we know what type a facet block is by putting field
  // type into the facet definition. 'created' and 'changed' are date blocks.
  if ($delta != 'created' && $delta != 'changed') {
    $form['apachesolr_facet_query_sort'] = array(
      '#type' => 'radios',
      '#title' => t('Sort order of facet links'),
      '#options' => array(
        'count' => t('Count'),
        'index asc' => t('Alphabetical, ascending'),
        'index desc' => t('Alphabetical, descending'),
        'index numeric asc' => t('Numeric, ascending'),
        'index numeric desc' => t('Numeric, descending'),
        'index key asc' => t('Key sort, ascending'),
        'index key desc' => t('Key sort, descending'),
      ),
      '#description' => t('The sort order of facet links in this block. %Count, which is the default, will show facets with the most results first. %Alphabetical will sort alphabetically, and %Numeric numerically, either ascending or descending.', array(
        '%Count' => t('Count'),
        '%Alphabetical' => t('Alphanumeric'),
        '%Numeric' => t('Numeric'),
      )),
      '#default_value' => isset($sorts[$module][$delta]) ? $sorts[$module][$delta] : 'count',
    );
  }
  $form['apachesolr_facet_missing'] = array(
    '#type' => 'radios',
    '#title' => t('Include a facet for missing'),
    '#options' => array(0 => t('No'), 1 => t('Yes')),
    '#description' => t('A facet can be generated corresponding to all documents entirely missing this field.'),
    '#default_value' => isset($facet_missing[$module][$delta]) ? $facet_missing[$module][$delta] : 0,
  );
  return $form;
}

/**
 * Used by the 'save' $op of hook_block so that modules can generically set
 * facet limits on their blocks.
 */
function apachesolr_facetcount_save($edit) {
  // Save query limits
  $module = $edit['module'];
  $delta = $edit['delta'];
  $limits = variable_get('apachesolr_facet_query_limits', array());
  $limits[$module][$delta] = (int)$edit['apachesolr_facet_query_limit'];
  variable_set('apachesolr_facet_query_limits', $limits);
  $sorts = variable_get('apachesolr_facet_query_sorts', array());
  $sorts[$module][$delta] = $edit['apachesolr_facet_query_sort'];
  variable_set('apachesolr_facet_query_sorts', $sorts);
  $initial = variable_get('apachesolr_facet_query_initial_limits', array());
  $initial[$module][$delta] = (int)$edit['apachesolr_facet_query_initial_limit'];
  variable_set('apachesolr_facet_query_initial_limits', $initial);
  $facet_missing = variable_get('apachesolr_facet_missing', array());
  $facet_missing[$module][$delta] = (int)$edit['apachesolr_facet_missing'];
  variable_set('apachesolr_facet_missing', $facet_missing);
}

/**
 * Initialize a pager for theme('pager') without running an SQL query.
 *
 * @see pager_query()
 *
 * @param $total
 *  The total number of items found.
 * @param $limit
 *  The number of items you will display per page.
 * @param $element
 *  An optional integer to distinguish between multiple pagers on one page.
 *
 * @return
 *  The current page for $element. 0 by default if $_GET['page'] is empty.
 */
function apachesolr_pager_init($total, $limit = 10, $element = 0) {
  global $pager_page_array, $pager_total, $pager_total_items;
  $page = isset($_GET['page']) ? $_GET['page'] : '';

  // Convert comma-separated $page to an array, used by other functions.
  $pager_page_array = explode(',', $page);

  // We calculate the total of pages as ceil(items / limit).
  $pager_total_items[$element] = $total;
  $pager_total[$element] = ceil($pager_total_items[$element] / $limit);
  $pager_page_array[$element] = max(0, min((int)$pager_page_array[$element], ((int)$pager_total[$element]) - 1));
  return $pager_page_array[$element];
}

/**
 * This hook allows modules to modify the query and params objects.
 *
 * Example:
 *
 * function my_module_apachesolr_modify_query(&$query, &$params, $caller) {
 *   // I only want to see articles by the admin!
 *   $query->add_filter("uid", 1);
 *
 * }
 */
function apachesolr_modify_query(&$query, &$params, $caller) {
  if (empty($query)) {
    // This should only happen if Solr is not set up - avoids fatal errors.
    return;
  }

  // Call the hooks first because otherwise any modifications to the
  // $query object don't end up in the $params.
  foreach (module_implements('apachesolr_modify_query') as $module) {
    $function_name = $module . '_apachesolr_modify_query';
    $function_name($query, $params, $caller);
  }

  // TODO: The query object should hold all the params.
  // Add array of fq parameters.
  if ($query && ($fq = $query->get_fq())) {
    foreach ($fq as $delta => $values) {
      if (is_array($values) || is_object($values)) {
        foreach ($values as $value) {
          $params['fq'][$delta][] = $value;
        }
      }
    }
  }
  // Add sort if present.
  if ($query) {
    $sort = $query->get_solrsort();
    $sortstring = $sort['#name'] .' '. $sort['#direction'];
    // We don't bother telling Solr to do its default sort.
    if ($sortstring != 'score desc') {
      $params['sort'] = $sortstring;
    }
  }
  $ors = array();
  $facet_info = apachesolr_get_facet_definitions();
  foreach ($facet_info as $infos) {
    foreach ($infos as $delta => $facet) {
      if ($facet['operator'] == 'OR') {
        $ors[] = $delta;
      }
    }
  }

  if (isset($params['fq'])) {
    $filter_queries = $params['fq'];
    foreach ($filter_queries as $delta => $values) {
      $fq = $tag = '';
      $op = 'AND';
      $fields = array($delta);
      // CCK and taxonomy facet field block deltas are not the same as their Solr index field names.
      if (strpos($delta, '_cck_') !== FALSE) {
        $fields[] = trim(drupal_substr($delta, 7, drupal_strlen($delta)));
      }
      elseif ($delta == 'tid') {
        if (function_exists('taxonomy_get_term')) {
          foreach ($values as $value) {
            $filters = $query->filter_extract($value, 'tid');
            $term = taxonomy_get_term($filters[0]['#value']);
            $fields[] = 'im_vid_' . $term->vid;
          }
        }
      }

      if (array_intersect($fields, $ors)) {
        $tag = "{!tag=$delta}";
        $op = 'OR';
      }
      $fq = implode(" $op ", $params['fq'][$delta]);
      $params['fq'][] = $tag . $fq;
      unset($params['fq'][$delta]);
    }
  }
}

/**
 * Semaphore that indicates whether a search has been done. Blocks use this
 * later to decide whether they should load or not.
 *
 * @param $searched
 *   A boolean indicating whether a search has been executed.
 *
 * @return
 *   TRUE if a search has been executed.
 *   FALSE otherwise.
 */
function apachesolr_has_searched($searched = NULL) {
  static $_searched = FALSE;
  if (is_bool($searched)) {
    $_searched = $searched;
  }
  return $_searched;
}

/**
 * Factory method for solr singleton object. Structure allows for an arbitrary
 * number of solr objects to be used based on the host, port, path combination.
 * Get an instance like this:
 *   $solr = apachesolr_get_solr();
 *
 * @throws Exception
 */
function apachesolr_get_solr($host = NULL, $port = NULL, $path = NULL) {
  static $solr_cache;

  if (empty($host)) {
    $host = variable_get('apachesolr_host', 'localhost');
  }
  if (empty($port)) {
    $port = variable_get('apachesolr_port', '8983');
  }
  if (empty($path)) {
    $path = variable_get('apachesolr_path', '/solr');
  }

  if (empty($solr_cache[$host][$port][$path])) {
    list($module, $filepath, $class) = variable_get('apachesolr_service_class', array('apachesolr', 'Drupal_Apache_Solr_Service.php', 'Drupal_Apache_Solr_Service'));
    include_once(drupal_get_path('module', $module) .'/'. $filepath);
    $solr = new $class($host, $port, $path);
    // Set a non-default behavior.
    $solr->setCollapseSingleValueArrays(FALSE);
    $solr_cache[$host][$port][$path] = $solr;
  }
  return $solr_cache[$host][$port][$path];
}

/**
 * Checks if a specific Apache Solr server is available.
 *
 * @return boolean TRUE if the server can be pinged, FALSE otherwise.
 */
function apachesolr_server_status($host = NULL, $port = NULL, $path = NULL) {
  if (empty($host)) {
    $host = variable_get('apachesolr_host', 'localhost');
  }
  if (empty($port)) {
    $port = variable_get('apachesolr_port', '8983');
  }
  if (empty($path)) {
    $path = variable_get('apachesolr_path', '/solr');
  }

  $ping = FALSE;
  try {
    $solr = apachesolr_get_solr($host, $port, $path);
    $ping = @$solr->ping(variable_get('apachesolr_ping_timeout', 4));
  }
  catch (Exception $e) {
    watchdog('Apache Solr', check_plain($e->getMessage()), NULL, WATCHDOG_ERROR);
  }
  return $ping;
}

/**
 * Execute a search based on a query object.
 *
 * Normally this function is used with the default (dismax) handler for keyword
 * searches. The $final_query that's returned will have been modified by
 * both hook_apachesolr_prepare_query() and hook_apachesolr_modify_query().
 *
 * @param $caller
 *   String, name of the calling module or function for use as a cache namespace.
 * @param $current_query
 *   A query object from apachesolr_drupal_query().  It will be modified by
 *   hook_apachesolr_prepare_query() and then cached in apachesolr_current_query().
 * @param $params
 *   Array of parameters to pass to Solr.  Must include at least 'rows'.
 * @param $page
 *   For paging into results, using $params['rows'] results per page.
 *
 * @return array($final_query, $response)
 *
 * @throws Exception
 */
function apachesolr_do_query($caller, $current_query, &$params = array('rows' => 10), $page = 0) {
  // Allow modules to alter the query prior to statically caching it.
  // This can e.g. be used to add available sorts.
  foreach (module_implements('apachesolr_prepare_query') as $module) {
    $function_name = $module . '_apachesolr_prepare_query';
    $function_name($current_query, $params, $caller);
  }

  // Cache the original query. Since all the built queries go through
  // this process, all the hook_invocations will happen later
  $query = apachesolr_current_query($current_query, $caller);

  // This hook allows modules to modify the query and params objects.
  apachesolr_modify_query($query, $params, $caller);
  $params['start'] = $page * $params['rows'];

  if (!$query) {
    return array(NULL, array());
  }
  // Final chance for the caller to modify the query and params. The signature
  // is: CALLER_finalize_query(&$query, &$params);
  $function = $caller . '_finalize_query';
  if (function_exists($function)) {
    $function($query, $params);
  }

  $keys = $query->get_query_basic();
  if ($keys == '' && isset($params['fq'])) {
    // Move the fq params to q.alt for better performance.
    $qalt = array();
    foreach ($params['fq'] as $delta => $value) {
      // Move the fq param if it has no local params and is not negative.
      if (!preg_match('/^(?:\{!|-)/', $value)) {
        $qalt[] = '(' . $value . ')';
        unset($params['fq'][$delta]);
      }
    }
    if ($qalt) {
      $params['q.alt'] = implode(' ', $qalt);
    }
  }
  // This is the object that does the communication with the solr server.
  $solr = apachesolr_get_solr();
  // We must run htmlspecialchars() here since converted entities are in the index.
  // and thus bare entities &, > or < won't match.
  $response = $solr->search(htmlspecialchars($keys, ENT_NOQUOTES, 'UTF-8'), $params['start'], $params['rows'], $params);
  // The response is cached so that it is accessible to the blocks and anything
  // else that needs it beyond the initial search.
  apachesolr_static_response_cache($response, $caller);
  return array($query, $response);
}

/**
 * It is important to hold on to the Solr response object for the duration of the
 * page request so that we can use it for things like building facet blocks.
 *
 * @todo reverse the order of parameters in future branches.
 */
function apachesolr_static_response_cache($response = NULL, $namespace = 'apachesolr_search') {
  static $_response = array();

  if (is_object($response)) {
    $_response[$namespace] = clone $response;
  }
  if (!isset($_response[$namespace])) {
    $_response[$namespace] = NULL;
  }
  return $_response[$namespace];
}

/**
 * Factory function for query objects.
 *
 * @param $keys
 *   The string that a user would type into the search box. Suitable input
 *   may come from search_get_keys().
 *
 * @param $filters
 *   Key and value pairs that are applied as a filter query.
 *
 * @param $solrsort
 *   Visible string telling solr how to sort.
 *
 * @param $base_path
 *   The search base path (without the keywords) for this query.
 *
 * @param $solr
 *   An instance of Drupal_Apache_Solr_Service.
 */
function apachesolr_drupal_query($keys = '', $filters = '', $solrsort = '', $base_path = '', $solr = NULL) {
  list($module, $class) = variable_get('apachesolr_query_class', array('apachesolr', 'Solr_Base_Query'));
  include_once drupal_get_path('module', $module) .'/'. $class .'.php';

  if (empty($solr)) {
    $solr = apachesolr_get_solr();
  }
  try {
    $query = new $class($solr, $keys, $filters, $solrsort, $base_path);
  }
  catch (Exception $e) {
    watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR);
    $query = NULL;
  }

  return $query;
}

/**
 * Static getter/setter for the current query
 *
 * @todo reverse the order of parameters in future branches.
 */
function apachesolr_current_query($query = NULL, $namespace = 'apachesolr_search') {
  static $saved_query = array();

  if (is_object($query)) {
    $saved_query[$namespace] = clone $query;
  }

  return is_object($saved_query[$namespace]) ? clone $saved_query[$namespace] : NULL;
}

/**
 * array('index_type' => 'integer',
 *        'multiple' => TRUE,
 *        'name' => 'fieldname',
 *        ),
 */
function apachesolr_index_key($field) {
  switch ($field['index_type']) {
    case 'text':
      $type_prefix = 't';
      break;
    case 'string':
      $type_prefix = 's';
      break;
    case 'integer':
      $type_prefix = 'i';
      break;
    case 'sint':
      $type_prefix = 'si';
      break;
    case 'double':
      $type_prefix = 'p'; // reserve d for date
      break;
    case 'boolean':
      $type_prefix = 'b';
      break;
    case 'date':
      $type_prefix = 'd';
      break;
    case 'float':
      $type_prefix = 'f';
      break;
    case 'tdate':
      $type_prefix = 'td';
      break;
    case 'tint':
      $type_prefix = 'ti';
      break;
    case 'tlong';
      $type_prefix = 'tl';
      break;
    case 'tfloat':
      $type_prefix = 'tf';
      break;
    case 'tdouble':
      $type_prefix = 'tp';
      break;
    default:
      $type_prefix = 's';
  }
  $sm = $field['multiple'] ? 'm_' : 's_';
  return $type_prefix . $sm . $field['name'];
}

/**
 * Try to map a schema field name to a human-readable description.
 */
function apachesolr_field_name_map($field_name) {
  static $map;

  if (!isset($map)) {
    $map = array(
      'body' => t('Body text - the full, rendered content'),
      'title' => t('Title'),
      'teaser' => t('Teaser'),
      'name' => t('Author name'),
      'path_alias' => t('Path alias'),
      'taxonomy_names' => t('All taxonomy term names'),
      'tags_h1' => t('Body text inside H1 tags'),
      'tags_h2_h3' => t('Body text inside H2 or H3 tags'),
      'tags_h4_h5_h6' => t('Body text inside H4, H5, or H6 tags'),
      'tags_inline' => t('Body text in inline tags like EM or STRONG'),
      'tags_a' => t('Body text inside links (A tags)'),
      'tid' => t('Taxonomy term IDs'),
    );
    if (module_exists('taxonomy')) {
      foreach (taxonomy_get_vocabularies() as $vocab) {
        $map['ts_vid_'. $vocab->vid .'_names'] = t('Taxonomy term names only from the %name vocabulary', array('%name' => $vocab->name));
        $map['im_vid_'. $vocab->vid] = t('Taxonomy term IDs from the %name vocabulary', array('%name' => $vocab->name));
      }
    }
    foreach (apachesolr_cck_fields() as $name => $field) {
      $map[apachesolr_index_key($field)] = t('CCK @type field %label', array('@type' => $field['index_type'], '%label' => $field['label']));
    }
    drupal_alter('apachesolr_field_name_map', $map);
  }
  return isset($map[$field_name]) ? $map[$field_name] : $field_name;
}

 /**
 * Implementation of hook_content_fieldapi().
 */
function apachesolr_content_fieldapi($op, $field) {
  switch ($op) {
    case 'delete instance':
      cache_clear_all('*', 'cache_apachesolr', TRUE);
      apachesolr_mark_node_type($field['type_name']);
      break;
    case 'update instance':
      cache_clear_all('*', 'cache_apachesolr', TRUE);
      // Get the previous value from the table.
      $previous = content_field_instance_read(array('field_name' => $field['field_name'], 'type_name' => $field['type_name']));
      $prev_field = array_pop($previous);
      if ($field['display_settings'][NODE_BUILD_SEARCH_INDEX]['exclude'] != $prev_field['display_settings'][NODE_BUILD_SEARCH_INDEX]['exclude']) {
        apachesolr_mark_node_type($field['type_name']);
      }
      elseif ($field['multiple'] != $prev_field['multiple']) {
        apachesolr_mark_node_type($field['type_name']);
      }
      break;
  }
}

/**
 * Mark all nodes of one type as needing re-indexing.
 */
function apachesolr_mark_node_type($type_name) {
  switch ($GLOBALS['db_type']) {
    case 'mysql':
    case 'mysqli':
      db_query("UPDATE {apachesolr_search_node} asn INNER JOIN {node} n ON asn.nid = n.nid SET asn.changed = %d WHERE n.type = '%s'", time(), $type_name);
      break;
    default:
      db_query("UPDATE {apachesolr_search_node} SET changed = %d WHERE nid IN (SELECT nid FROM {node} WHERE type = '%s')", time(), $type_name);
      break;
  }
}

/**
 * Invokes hook_apachesolr_cck_field_mappings to find out how to handle CCK fields.
 */
function apachesolr_cck_fields() {
  static $fields;

  if (!isset($fields)) {
    $fields = array();
    // If CCK isn't enabled, do nothing.
    if (module_exists('content')) {
      module_load_include('inc', 'content', 'includes/content.crud');
      $cck_field_instances = content_field_instance_read();
      // A single default mapping for all text fields.
      $mappings['text'] = array(
        'optionwidgets_select' => array(
          'display_callback' => 'apachesolr_cck_text_field_callback',
          'indexing_callback' => 'apachesolr_cck_text_indexing_callback',
          'index_type' => 'string',
          'facets' => TRUE,
        ),
        'optionwidgets_buttons' => array(
          'display_callback' => 'apachesolr_cck_text_field_callback',
          'indexing_callback' => 'apachesolr_cck_text_indexing_callback',
          'index_type' => 'string',
          'facets' => TRUE,
        ),
      );
      $mappings['nodereference'] = array(
        'nodereference_buttons' => array(
          'display_callback' => 'apachesolr_cck_nodereference_field_callback',
          'indexing_callback' => 'apachesolr_cck_nodereference_indexing_callback',
          'index_type' => 'integer',
          'facets' => TRUE,
        ),
        'nodereference_select' => array(
          'display_callback' => 'apachesolr_cck_nodereference_field_callback',
          'indexing_callback' => 'apachesolr_cck_nodereference_indexing_callback',
          'index_type' => 'integer',
          'facets' => TRUE,
        ),
        'nodereference_autocomplete' => array(
          'display_callback' => 'apachesolr_cck_nodereference_field_callback',
          'indexing_callback' => 'apachesolr_cck_nodereference_indexing_callback',
          'index_type' => 'integer',
          'facets' => TRUE,
        ),
      );
      $mappings['userreference'] = array(
        'userreference_buttons' => array(
          'display_callback' => 'apachesolr_cck_userreference_field_callback',
          'indexing_callback' => 'apachesolr_cck_userreference_indexing_callback',
          'index_type' => 'integer',
          'facets' => TRUE,
        ),
        'userreference_select' => array(
          'display_callback' => 'apachesolr_cck_userreference_field_callback',
          'indexing_callback' => 'apachesolr_cck_userreference_indexing_callback',
          'index_type' => 'integer',
          'facets' => TRUE,
        ),
        'userreference_autocomplete' => array(
          'display_callback' => 'apachesolr_cck_userreference_field_callback',
          'indexing_callback' => 'apachesolr_cck_userreference_indexing_callback',
          'index_type' => 'integer',
          'facets' => TRUE,
        ),
      );
      // Allow other modules to add or alter mappings.
      drupal_alter('apachesolr_cck_fields', $mappings);

      foreach ($cck_field_instances as $instance) {
        $field_type = $instance['type'];
        $field_name = $instance['field_name'];
        $widget_type = $instance['widget']['type'];
        if (!isset($mappings[$field_type][$widget_type]) && !isset($mappings['per_field'][$field_name]) && isset($mappings[$field_type]['default'])) {
          $widget_type = 'default';
        }
        // Only deal with fields that have index mappings and that have not been marked for exclusion.
       if ((isset($mappings[$field_type][$widget_type]) ||
             isset($mappings['per-field'][$field_name])) &&
             empty($instance['display_settings'][NODE_BUILD_SEARCH_INDEX]['exclude'])) {
          if (isset($mappings['per-field'][$field_name])) {
            $instance['index_type'] = $mappings['per-field'][$field_name]['index_type'];
            $instance['indexing_callback'] = $mappings['per-field'][$field_name]['indexing_callback'];
            $instance['display_callback'] = $mappings['per-field'][$field_name]['display_callback'];
            $instance['facets'] = $mappings['per-field'][$field_name]['facets'];
          }
          else {
            $instance['index_type'] = $mappings[$field_type][$widget_type]['index_type'];
            $instance['indexing_callback'] = $mappings[$field_type][$widget_type]['indexing_callback'];
            $instance['display_callback'] = $mappings[$field_type][$widget_type]['display_callback'];
            $instance['facets'] = $mappings[$field_type][$widget_type]['facets'];
          }
          $instance['multiple'] = (bool) $instance['multiple'];
          $instance['name'] = 'cck_' . $field_name;
          if (isset($fields[$field_name]) && is_array($fields[$field_name])) {
            // Merge together settings when used for multiple node types.
            $fields[$field_name] = array_merge($fields[$field_name], $instance);
          }
          else {
            $fields[$field_name] = $instance;
          }
          $fields[$field_name]['content_types'][] = $instance['type_name'];
          unset($fields[$field_name]['type_name']);
        }
      }
    }
  }
  return $fields;
}


/**
 * Use the content.module's content_format() to format the
 * field based on its value ($facet).
 *
 *  @param $facet string
 *    The indexed value
 *  @param $options
 *    An array of options including the hook_block $delta.
 */
function apachesolr_cck_text_field_callback($facet, $options) {
  if (function_exists('content_format')) {
    return content_format($options['delta'], array('value' => $facet));
  }
  else {
    return $facet;
  }
}

/**
 * Use the content.module's content_format() to format the
 * field based on its nid ($facet).
 *
 *  @param $facet string
 *    The indexed value
 *  @param $options
 *    An array of options including the hook_block $delta.
 */
function apachesolr_cck_nodereference_field_callback($facet, $options) {
  if (function_exists('content_format')) {
    return strip_tags(content_format($options['delta'], array('nid' => $facet)));
  }
  else {
    return $facet;
  }
}

/**
 * Use the content.module's content_format() to format the
 * field based on its uid ($facet).
 *
 *  @param $facet string
 *    The indexed value
 *  @param $options
 *    An array of options including the hook_block $delta.
 */
function apachesolr_cck_userreference_field_callback($facet, $options) {
  if (function_exists('content_format')) {
    return strip_tags(content_format($options['delta'], array('uid' => $facet)));
  }
  else {
    return $facet;
  }
}

/**
 * Implementation of hook_theme().
 */
function apachesolr_theme() {
  return array(
    /**
     * Returns a link for a facet term, with the number (count) of results for that term
     */
    'apachesolr_facet_link' => array(
      'arguments' => array('facet_text' => NULL, 'path' => NULL, 'options' => NULL, 'count' => NULL, 'active' => FALSE, 'num_found' => NULL),
    ),
    /**
     * Returns a link to remove a facet filter from the current search.
     */
    'apachesolr_unclick_link' => array(
      'arguments' => array('facet_text' => NULL, 'path' => NULL, 'options' => NULL),
    ),
    /**
     * Returns a list of links from the above functions (apachesolr_facet_item and apachesolr_unclick_link)
     */
    'apachesolr_facet_list' => array(
      'arguments' => array('items' => NULL, 'display_limit' => 0, 'delta' => ''),
    ),
    /**
     * Returns a list of links generated by apachesolr_sort_link
     */
    'apachesolr_sort_list' => array(
      'arguments' => array('items' => NULL),
    ),
    /**
     * Returns a link which can be used to search the results.
     */
    'apachesolr_sort_link' => array(
      'arguments' => array('text' => NULL, 'path' => NULL, 'options' => NULL, 'active' => FALSE, 'direction' => ''),
    ),
    /**
     * Returns a list of results (docs) in content recommendation block
     */
    'apachesolr_mlt_recommendation_block' => array(
      'arguments' => array('docs' => NULL),
    ),
  );
}

/**
 * Performs a moreLikeThis query using the settings and retrieves documents.
 *
 * @param $settings
 *   An array of settings.
 * @param $id
 *   The Solr ID of the document for which you want related content.
 *   For a node that is apachesolr_document_id($node->nid)
 *
 * @return An array of response documents, or NULL
 */
function apachesolr_mlt_suggestions($settings, $id) {

  try {
    $solr = apachesolr_get_solr();
    $fields = array(
      'mlt_mintf' => 'mlt.mintf',
      'mlt_mindf' => 'mlt.mindf',
      'mlt_minwl' => 'mlt.minwl',
      'mlt_maxwl' => 'mlt.maxwl',
      'mlt_maxqt' => 'mlt.maxqt',
      'mlt_boost' => 'mlt.boost',
      'mlt_qf' => 'mlt.qf',
    );

    $params = array(
      'qt' => 'mlt',
      'fl' => 'nid,title,path,url',
      'mlt.fl' => implode(',', $settings['mlt_fl']),
    );

    foreach ($fields as $form_key => $name) {
      if (!empty($settings[$form_key])) {
        $params[$name] = $settings[$form_key];
      }
    }
    $query = apachesolr_drupal_query('id:' . $id);

    $type_filters = array();
    if (is_array($settings['mlt_type_filters'])) {
      foreach ($settings['mlt_type_filters'] as $type_filter) {
        $type_filters[] = "type:{$type_filter}";
      }
      $params['fq']['mlt'][] = '(' . implode(' OR ', $type_filters) . ') ';
    }

    if ($custom_filters = $settings['mlt_custom_filters']) {
      $params['fq']['mlt'][] = $custom_filters;
    }

    // This hook allows modules to modify the query and params objects.
    apachesolr_modify_query($query, $params, 'apachesolr_mlt');
    if (empty($query)) {
      return;
    }

    $response = $solr->search($query->get_query_basic(), 0, $settings['num_results'], $params);

    if ($response->response) {
      $docs = (array) end($response->response);
      return $docs;
    }
  }
  catch (Exception $e) {
    watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR);
  }
}

/**
 * Implementation of hook_form_[form_id]_alter
 */
function apachesolr_form_block_admin_display_form_alter(&$form) {
  foreach ($form as $key => $block) {
    if ((strpos($key, "apachesolr_mlt-") === 0) && $block['module']['#value'] == 'apachesolr') {
      $form[$key]['delete'] = array('#value' => l(t('delete'), 'admin/settings/apachesolr/mlt/delete_block/'. $block['delta']['#value']));
    }
  }
}

/**
 * Returns a list of blocks. Used by hook_block
 */
function apachesolr_mlt_list_blocks() {
  $blocks = variable_get('apachesolr_mlt_blocks', array());
  foreach ($blocks as $delta => $settings) {
    $blocks[$delta] += array('info' => t('Apache Solr recommendations: !name', array('!name' => $settings['name'])) , 'cache' => BLOCK_CACHE_PER_PAGE);
  }
  return $blocks;
}

function apachesolr_mlt_load_block($delta) {
  $blocks = variable_get('apachesolr_mlt_blocks', array());
  return isset($blocks[$delta]) ? $blocks[$delta] : FALSE;
}

function theme_apachesolr_mlt_recommendation_block($docs) {
  $links = array();
  foreach ($docs as $result) {
    // Suitable for single-site mode.
    $links[] = l($result->title, $result->path, array('html' => TRUE));
  }
  return theme('item_list', $links);
}

function theme_apachesolr_facet_link($facet_text, $path, $options = array(), $count, $active = FALSE, $num_found = NULL) {
  $options['attributes']['class'][] = 'apachesolr-facet';
  if ($active) {
    $options['attributes']['class'][] = 'active';
  }
  $options['attributes']['class'] = implode(' ', $options['attributes']['class']);
  return apachesolr_l($facet_text ." ($count)",  $path, $options);
}

/**
 * A replacement for l()
 *  - doesn't add the 'active' class
 *  - retains all $_GET parameters that ApacheSolr may not be aware of
 *  - if set, $options['query'] MUST be an array
 *
 * @see http://api.drupal.org/api/function/l/6 for parameters and options.
 *
 * @return
 *   an HTML string containing a link to the given path.
 */
function apachesolr_l($text, $path, $options = array()) {
  // Merge in defaults.
  $options += array(
    'attributes' => array(),
    'html' => FALSE,
    'query' => array(),
  );

  // Don't need this, and just to be safe.
  unset($options['attributes']['title']);
  // Double encode + characters for clean URL Apache quirks.
  if (variable_get('clean_url', '0')) {
    $path = str_replace('+', '%2B', $path);
  }

  // Retain GET parameters that ApacheSolr knows nothing about.
  $query = apachesolr_current_query();
  $get = array_diff_key($_GET, array('q' => 1, 'page' => 1), $options['query'], $query->get_url_queryvalues());
  $options['query'] += $get;

  return '<a href="'. check_url(url($path, $options)) .'"'. drupal_attributes($options['attributes']) .'>'. ($options['html'] ? $text : check_plain($text)) .'</a>';
}

function apachesolr_js() {
  static $settings;
  // Only add the js stuff once.
  if (is_null($settings)) {
    $settings['apachesolr_facetstyle'] = variable_get('apachesolr_facetstyle', 'checkboxes');
    // This code looks for enabled facet blocks and injects the block #ids into
    // Drupal.settings as jQuery selectors to add the Show more links.
    $show_more_blocks = array();
    $facet_map = array();
    foreach (apachesolr_get_facet_definitions() as $module => $definitions) {
      foreach ($definitions as $facet => $facet_definition) {
        $facet_map[$facet_definition['facet_field']] = $facet;
      }
    }
    $show_more_selector = array();
    foreach (apachesolr_get_enabled_facets() as $module => $blocks) {
      foreach ($blocks as $block) {
        $show_more_selector[] = "#block-{$module}-{$facet_map[$block]}:has(.apachesolr-hidden-facet)";
      }
    }
    $settings['apachesolr_show_more_blocks'] = implode(', ', $show_more_selector);
    drupal_add_js($settings, 'setting');
    drupal_add_js(drupal_get_path('module', 'apachesolr') . '/apachesolr.js');
  }
}

function theme_apachesolr_unclick_link($facet_text, $path, $options = array()) {
  apachesolr_js();
  if (empty($options['html'])) {
    $facet_text = check_plain($facet_text);
  }
  else {
    // Don't pass this option as TRUE into apachesolr_l().
    unset($options['html']);
  }
  $options['attributes']['class'] = 'apachesolr-unclick';
  return apachesolr_l("(-)", $path, $options) . ' '. $facet_text;
}

function theme_apachesolr_sort_link($text, $path, $options = array(), $active = FALSE, $direction = '') {
  $icon = '';
  if ($direction) {
    $icon = ' '. theme('tablesort_indicator', $direction);
  }
  if ($active) {
    if (isset($options['attributes']['class'])) {
      $options['attributes']['class'] .= ' active';
    }
    else {
      $options['attributes']['class'] = 'active';
    }
  }
  return $icon . apachesolr_l($text, $path, $options);
}

function theme_apachesolr_facet_list($items, $display_limit = 0, $delta = '') {
  apachesolr_js();
  // theme('item_list') expects a numerically indexed array.
  $items = array_values($items);
  // If there is a limit and the facet count is over the limit, hide the rest.
  if (($display_limit > 0) && (count($items) > $display_limit)) {
    // Split items array into displayed and hidden.
    $hidden_items = array_splice($items, $display_limit);
    foreach ($hidden_items as $hidden_item) {
      if (!is_array($hidden_item)) {
        $hidden_item = array('data' => $hidden_item);
      }
      $hidden_item['class'] = isset($hidden_item['class']) ? $hidden_item['class'] . ' apachesolr-hidden-facet' : 'apachesolr-hidden-facet';
      $items[] = $hidden_item;
    }
  }
  $admin_link = '';
  if (user_access('administer search')) {
    $admin_link = l(t('Configure enabled filters'), 'admin/settings/apachesolr/enabled-filters');
  }
  return theme('item_list', $items) . $admin_link;
}

function theme_apachesolr_sort_list($items) {
  // theme('item_list') expects a numerically indexed array.
  $items = array_values($items);
  return theme('item_list', $items);
}

/**
 * The interface for all 'query' objects.
 */
interface Drupal_Solr_Query_Interface {
  /**
   * Get all filters, or the subset of filters for one field.
   *
   * @param $name
   *   Optional name of a Solr field.
   */
  function get_filters($name = NULL);

  /**
   * Checks to see if a specific filter is already present.
   *
   * @param string $field
   * the facet field to check
   *
   * @param string $value
   * The facet value to check against
   */
  function has_filter($field, $value);

  /**
   * Add a filter to a query.
   *
   * @param string $field
   *   the facet field to apply to this query
   *
   * @param string value
   *   the value of the facet to apply
   *
   * @param boolean $exclude
   *   Optional paramter.  If TRUE, the filter will be negative,
   *   meaning that matching values will be excluded from the
   *   result set.
   */
  function add_filter($field, $value, $exclude = FALSE);

  /**
   * Remove a filter from the query.
   *
   * @param string $field
   * the facet field to remove
   *
   * @param string $value
   * The facet value to remove
   * This value can be NULL
   */
  function remove_filter($field, $value = NULL);

  /**
   * Get the query's keywords.
   */
  function get_keys();

  /**
   * Set the query's keywords.
   *
   * @param string $keys
   *   The new keywords.
   */
  function set_keys($keys);

  /**
   * Removes the query's keywords.
   */
  function remove_keys();

  /**
   * Return the search path (including the search keywords).
   */
  function get_path();

  /**
   * Return filters and sort in a form suitable for a query param to url().
   */
  function get_url_queryvalues();

  /**
   * Return the basic string query.
   */
  function get_query_basic();

  /**
   * Return the sorts that are provided by the query object.
   *
   * @return array all the sorts provided
   */
  function get_available_sorts();

  /**
   * Make a sort available.
   */
  function set_available_sort($field, $sort);

  /**
   * Get the solrsort.
   *
   * Returns the non-urlencode, non-aliased sort field and direction.
   * as an array keyed with '#name' and '#direction'.
   */
  function get_solrsort();

  /**
   * Set the solrsort.
   *
   * @param $field
   *  The name of a field in the solr index that's an allowed sort.
   *
   * @param $direction
   *  'asc' or 'desc'
   */
  function set_solrsort($field, $direction);

  /**
   * Add a subquery to the query.
   *
   * @param Drupal_Solr_Query_Interface $query
   *   The query to add to the orginal query - may have keywords or filters.
   *
   * @param string $fq_operator
   *   The operator to use within the filter part of the subquery
   *
   * @param string $q_operator
   *   The operator to use in joining the subquery to
   *   the main keywords.  Note - this is unlikely to work
   *   with the Dismax handler when the main query is only
   *   keywords.
   */
  function add_subquery(Drupal_Solr_Query_Interface $query, $fq_operator = 'OR', $q_operator = 'AND');

  /**
   * Remove a specific subquery.
   *
   * @param Drupal_Solr_Query_Interface $query
   * the query to remove
   */
  function remove_subquery(Drupal_Solr_Query_Interface $query);

  /**
   * Remove all subqueries.
   */
  function remove_subqueries();
}

/**
 * Wrapper function for tt() if i18nstrings enabled.
 */
function apachesolr_tt($name, $string, $langcode = NULL, $update = FALSE) {
  if (module_exists('i18nstrings')) {
    return tt($name, $string, $langcode, $update);
  }
  else {
    return $string;
  }
}
