<?php

/**
 * @file
 * Display Suite search.
 */

/**
 * Implements hook_help().
 */
function ds_search_help($path, $arg) {
  switch ($path) {
    case 'admin/structure/ds/list/search':
      $output = '<dl>';
      $output .= '<dt>' . t('Display Suite defines its own search type for search. You need to enable it at !url when you are going to use Drupal core search. You do not have to enable and use it when using the Apachesolr module. Search results will be themed on the default Apachesolr pages.', array('!url' => l('search settings', 'admin/config/search/settings'))) . '</dt>';
      $output .= '</dl>';
      return $output;
  }
}

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

  $items['admin/structure/ds/list/search'] = array(
    'title' => 'Search',
    'description' => 'Configure search settings.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('ds_search_settings'),
    'access arguments' => array('admin_display_suite'),
    'file' => 'includes/ds_search.admin.inc',
    'type' => MENU_LOCAL_TASK,
  );

  return $items;
}

/**
 * Implements hook_theme().
 */
function ds_search_theme() {
  return array(
    'ds_search_page' => array(),
    'ds_search_group_by_type_settings' => array(
      'render element' => 'element',
      'file' => 'includes/ds_search.admin.inc',
    ),
  );
}

/**
 * Search page theming.
 */
function theme_ds_search_page($build) {

  // Check on empty search results.
  if (empty($build['search_results'])) {

    // Alter the title and extra variables.
    if (!empty($build['search_title'])) {
      $build['search_title']['#markup'] = '<h2>' . t('Your search yielded no results') . '</h2>';
      unset($build['search_extra']);
    }

    $build['search_empty'] = array('#markup' => t('<ul><li>Check if your spelling is correct.</li><li>Remove quotes around phrases to search for each word individually. <em>bike shed</em> will often show more results than <em>&quot;bike shed&quot;</em>.</li><li>Consider loosening your query with <em>OR</em>. <em>bike OR shed</em> will often show more results than <em>bike shed</em>.</li></ul>'));
  }

  $build['search_results']['#sorted'] = TRUE;

  return $build;
}

/**
 * Implements hook_ds_fields_info().
 */
function ds_search_ds_fields_info($entity_type) {
  $fields = array();

  if ($entity_type == 'node') {
    $fields['node']['search_snippet'] = array(
      'title' => t('Search snippet'),
      'field_type' => DS_FIELD_TYPE_FUNCTION,
      'function' => 'ds_search_snippet',
      'ui_limit' => array('*|' . variable_get('ds_search_view_mode', 'search_result')),
    );
    $fields['node']['search_info'] = array(
      'title' => t('Search info'),
      'field_type' => DS_FIELD_TYPE_FUNCTION,
      'function' => 'ds_search_extra_info',
      'ui_limit' => array('*|' . variable_get('ds_search_view_mode', 'search_result')),
    );
  }

  if (isset($fields[$entity_type])) {
    return array($entity_type => $fields[$entity_type]);
  }

  return;
}

/**
 * Returns the snippet field.
 */
function ds_search_snippet($field) {
  // Apache Solr
  if (isset($field['entity']->search_snippet)) {
    return $field['entity']->search_snippet;
  }
  // Original node snippet
  elseif (isset($field['entity']->snippet)) {
    return $field['entity']->snippet;
  }
}

/**
 * Returns the info field, just like default search.
 */
function ds_search_extra_info($field) {
  $info = array();
  $info['user'] = theme('username', array('account' => $field['entity']));
  $info['date'] = format_date($field['entity']->changed, 'short');
  if (isset($field['entity']->search_extra) && is_array($field['entity']->search_extra)) {
    $info = array_merge($info, $field['entity']->search_extra);
  }
  return implode(' - ', $info);
}

/**
 * Implements hook_search_info().
 */
function ds_search_search_info() {
  return array(
    'title' => 'Content',
    'path' => variable_get('ds_search_path', 'content'),
  );
}

/**
 * Implements hook_node_update_index().
 */
function ds_search_update_index() {
  ds_search_invoke_node_search('update_index');
}

/**
 * Implements hook_search_status().
 */
function ds_search_search_status() {
  return ds_search_invoke_node_search('search_status');
}

/**
 * Implements hook_search_execute().
 */
function ds_search_search_execute($keys = NULL, $conditions = NULL) {
  // Save the keys in case we need them later on.
  ds_search_get_keys($keys);

  // We will call an extra function which handles the actual search.
  // In some cases, we simply copied a lot from the original hook,
  // because some modules already called drupal_render and were unsetting
  // the #theme key. By using our own search info type, we can call
  // hook_search_page ourselves and be as flexible as we need to be.
  $ds_search_type = variable_get('ds_search_type', 'node') . '_ds_search_execute';

  // Make sure the function exists.
  if (function_exists($ds_search_type)) {
    return $ds_search_type($keys, $conditions);
  }
}

/**
 * Save or get the search keys.
 */
function ds_search_get_keys($keys = NULL) {
  static $run, $saved_keys = FALSE;

  if (!$run) {
    $run = TRUE;
    $saved_keys = $keys;
  }
  else {
    return $saved_keys;
  }
}

/**
 * Invoke a given search hook on the node module.
 *
 * @param $hook
 *   Hook to invoke.
 */
function ds_search_invoke_node_search($hook) {

  $enabled_search_modules = variable_get('search_active_modules', array());

  // If node search is enabled, core is invoking it.
  if (isset($enabled_search_modules['node']) && $enabled_search_modules['node'] === 'node') {
    return;
  }
  else {
    $ds_search_type = variable_get('ds_search_type', 'node');
    if ($ds_search_type != 'node') {
      return;
    }
  }

  return module_invoke('node', $hook);
}

/**
 * DS entity view callback. Straight copy from Entity API module with fallback
 * to Drupal core view callbacks for nodes .
 */
function ds_entity_view_fallback($entity_type, $entities, $view_mode = 'full', $langcode = NULL, $page = NULL) {

  // Entity support for entity search.
  if (module_exists('entity')) {
    return entity_view($entity_type, $entities, $view_mode, $langcode, $page);
  }
  else {
    if ($entity_type == 'node') {
      return node_view_multiple($entities, $view_mode);
    }
    elseif ($entity_type == 'file') {
      return file_view_multiple($entities, $view_mode);
    }
  }
}

/**
 * Implements hook_search_page().
 */
function ds_search_search_page($results) {

  // Build shared variables.
  $build = array('#type' => 'node');
  ds_build_shared_page_variables($build);

  $i = 0;
  // Multi site Apache Solr support.
  if (variable_get('ds_search_apachesolr_multisite') && variable_get('ds_search_type', 'node') == 'apachesolr_search') {
    $build['search_results'] = $results;
  }
  else {
    foreach ($results as $id => $result) {
      $entity_type = isset($result->entity_type) ? $result->entity_type : 'node';
      $data = ds_entity_view_fallback($entity_type, array($result->entity_id => $result), variable_get('ds_search_view_mode', 'search_result'));
      $data = reset($data);
      $data[$result->entity_id]['#weight'] = $i++;
      $build['search_results'][] = $data[$result->entity_id];
      unset($results[$result->entity_id]);
      unset($result);
    }
  }

  // Group by type.
  if (variable_get('ds_search_group_by_type') && variable_get('ds_search_group_by_type_settings') && !empty($build['search_results'])) {

    $settings = variable_get('ds_search_group_by_type_settings');
    foreach ($build['search_results'] as $id => $result) {
      if ($settings[$result['#bundle']]['status']) {

        // Type group.
        if (!isset($build['search_results'][$result['#bundle']])) {
          $type = $settings[$result['#bundle']]['wrapper'];
          $title = check_plain(t($settings[$result['#bundle']]['label']));
          $class = 'group-result group-result-' . strtr($result['#bundle'], '_', '-');
          $parity[$result['#bundle']] = 'odd';
          $build['search_results'][$result['#bundle']] = array(
            '#type' => $type,
            '#title' => $title,
            '#weight' => $settings[$result['#bundle']]['weight'],
            '#attributes' => array(
              'class' => array($class),
            ),
          );

          if ($type == 'markup') {
            $build['search_results'][$result['#bundle']]['#prefix'] = '<div class="' . $class . '">' . ((!empty($title)) ? ' <h2>' . $title . '</h2>' : '');
            $build['search_results'][$result['#bundle']]['#suffix'] = '</div>';
          }
        }

        // Move result into the wrapper of its type and unset previous.
        $build['search_results'][$result['#bundle']][$id] = $result;
        unset($build['search_results'][$id]);

        // Add the parity to the result to enable correct zebra striping.
        $build['search_results'][$result['#bundle']][$id]['#node']->ds_search_zebra = $parity[$result['#bundle']];
        $parity[$result['#bundle']] = $parity[$result['#bundle']] == 'odd' ? 'even' : 'odd';
      }
      else {

        // Other group.
        if (!isset($build['search_results']['ds-other'])) {
          $title = check_plain(t(variable_get('ds_search_group_by_type_other', 'Other')));
          $type = variable_get('ds_search_group_by_type_other_wrapper', 'fieldset');
          $class = 'group-result group-result-other';
          $parity['ds-other'] = 'odd';
          $build['search_results']['ds-other'] = array(
            '#type' => $type,
            '#title' => $title,
            '#weight' => 100,
            '#attributes' => array(
              'class' => array($class),
            ),
          );

          if ($type == 'markup') {
            $build['search_results']['ds-other']['#prefix'] = '<div class="' . $class . '">' . ((!empty($title)) ? '<h2>' . $title . '</h2>' : '');
            $build['search_results']['ds-other']['#suffix'] = '</div>';
          }
        }

        // Move result into other wrapper and unset previous.
        $build['search_results']['ds-other'][$id] = $result;
        unset($build['search_results'][$id]);

        // Add the parity to the result to enable correct zebra striping.
        $build['search_results']['ds-other'][$id]['#node']->ds_search_parity = $parity['ds-other'];
        $parity['ds-other'] = $parity['ds-other'] == 'odd' ? 'even' : 'odd';
      }
    }
  }
  else {
    // Provide zebra striping for results that are not grouped.
    $parity = 'odd';
    foreach ($build['search_results'] as $id => $result) {
      $build['search_results'][$id]['#node']->ds_search_zebra = $parity;
      $parity = $parity == 'odd' ? 'even' : 'odd';
    }
  }

  // Apache Solr multisearch grouping.
  if (variable_get('ds_search_apachesolr_multisite') && variable_get('ds_search_apachesolr_multisite_group') && variable_get('ds_search_type', 'node') == 'apachesolr_search') {
    $site_counter = array();
    $conf_array = array();
    $config = explode("\n", variable_get('ds_search_apachesolr_multisite_group_config'));
    foreach ($config as $weight => $conf) {
      $conf = trim($conf);
      if (empty($conf)) {
        continue;
      }
      $site_conf = explode('|', $conf);
      $conf_array[$site_conf[0]] = array(
        'label' => $site_conf[1],
        'wrapper' => $site_conf[2],
        'weight' => $weight,
      );
    }

    // Iterate over results.
    foreach ($build['search_results'] as $id => $result) {
      if (!isset($build['search_results'][$result['#site_hash']])) {
        $class = 'group-result group-result-' . strtr($result['#site_hash'], '_', '-');
        $build['search_results'][$result['#site_hash']] = array(
          '#type' => 'fieldset',
          '#weight' => $conf_array[$result['#site_hash']]['weight'],
          '#attributes' => array(
            'class' => array($class),
          ),
        );

        // Create site counter.
        $site_counter[$result['#site_hash']] = array(
          'counter' => 0,
          'title' => $conf_array[$result['#site_hash']]['label'],
          'type' => $conf_array[$result['#site_hash']]['wrapper'],
          'class' => $class,
        );
      }

      // Move result into other wrapper and unset previous. Also count for
      // every site so we can populate @total_per_site later on.
      $site_counter[$result['#site_hash']]['counter']++;
      $build['search_results'][$result['#site_hash']][$id] = $result;
      unset($build['search_results'][$id]);
    }

    // Site counter.
    foreach ($site_counter as $hash => $values) {
      $title = check_plain(t($values['title'], array('!total_per_site' => format_plural($values['counter'], '1 result', '@count results'))));
      if ($values['type'] == 'div') {
        $build['search_results'][$hash]['#prefix'] = '<div class="' . $values['class'] . '">' . ((!empty($title)) ? '<h2>' . $title . '</h2>' : '');
        $build['search_results'][$hash]['#suffix'] = '</div>';
      }
      else {
        $build['search_results'][$hash]['#title'] = $title;
      }
    }
  }

  return theme('ds_search_page', $build);
}

/**
 * Search on behalf of Drupal Core.
 */
function node_ds_search_execute($keys = NULL, $conditions = NULL) {
  // Build matching conditions
  $query = db_select('search_index', 'i', array('target' => 'slave'))->extend('SearchQuery')->extend('PagerDefault');;
  $query->join('node', 'n', 'n.nid = i.sid');
  $query
    ->condition('n.status', 1)
    ->addTag('node_access')
    ->searchExpression($keys, 'node');

  // Language.
  if (variable_get('ds_search_language', FALSE)) {
    global $language;
    $query->condition('n.language', $language->language);
  }

  // Insert special keywords.
  $query->setOption('type', 'n.type');
  $query->setOption('language', 'n.language');
  if ($query->setOption('term', 'ti.tid')) {
    $query->join('taxonomy_index', 'ti', 'n.nid = ti.nid');
  }
  // Only continue if the first pass query matches.
  if (!$query->executeFirstPass()) {
    return array();
  }

  // Add the ranking expressions.
  _node_rankings($query);

  $limit = variable_get('ds_search_node_limit', 10);
  $query->limit($limit);

  // Load results.
  $find = $query->execute();
  $results = array();
  foreach ($find as $item) {
    $node = node_load($item->sid);
    $node->entity_type = 'node';
    $node->entity_id = $item->sid;
    $node->search_extra = module_invoke_all('node_search_result', $node);
    // Only build a node search snippet if this field is actually being used.
    $fields = ds_get_field_settings($node->entity_type, $node->type, 'search_result');
    if (!empty($fields) && isset($fields['search_snippet'])) {
      // Because the 'search_result' display is being built right now (and because it is being overridden by Display Suite),
      // it is necessary to use the 'search_index' display for rendered field content.
      $build = node_view($node, 'search_index');
      unset($build['#theme']);
      // Render the node.
      $rendered = drupal_render($build);
      // Attach extra information to the rendered output.
      $rendered .= ' ' . $node->search_extra;
      // Generate the snippet based on rendered content.
      $node->snippet = search_excerpt($keys, $rendered);
    }
    $results[$item->sid] = $node;
  }
  return $results;
}

/**
 * Override search results page for users.
 */
if (variable_get('ds_user_override_search_page', FALSE)) {
  function user_search_page($results) {
    $build = array('#type' => 'user');
    global $base_url;

    ds_build_shared_page_variables($build);

    $uids = array();
    foreach ($results as $key => $result) {
      $uid = FALSE;

      // Try to get the uid from the $result['link'];
      $path = explode('/', $result['link']);
      $uid = end($path);

      // Lookup drupal path, we are most likely having an alias.
      if (!is_numeric($uid)) {
        $path = str_replace($base_url . '/', '', $result['link']);
        $alias = drupal_get_normal_path($path);
        $path = explode('/', $alias);
        $uid = end($path);
      }

      if (is_numeric($uid)) {
        $uids[] = $uid;
      }

      // Return all uids.
      if (!empty($uids)) {
        $accounts = user_load_multiple($uids);
        foreach ($accounts as $account) {
          $build['search_results'][$account->uid] = user_view($account, variable_get('ds_search_view_mode', 'search_result'));
        }
      }
    }

    // Return output.
    return theme('ds_search_page', $build);
  }
}

/**
 * Build shared page variables.
 *
 * @param $build
 *   The build array.
 */
function ds_build_shared_page_variables(&$build) {
  // Search results title.
  if (variable_get('ds_search_show_title', FALSE)) {
    $build['search_title'] = array('#markup' => '<h2>' . t('Search results') . '</h2>');
  }

  // Extra variables.
  if (variable_get('ds_search_variables', 'none') != 'none') {
    $build['search_extra'] = array('#markup' => '<div class="ds-search-extra">' . ds_search_extra_variables(arg(2)) . '</div>');
  }

  // Search results.
  $build['search_results'] = array();

  // Pager.
  $build['search_pager'] = array('#markup' => theme('pager', array('tags' => NULL)));

  // CSS and JS.
  if (variable_get('ds_search_highlight', FALSE)) {
    drupal_add_css(drupal_get_path('module', 'ds_search') . '/css/ds_search.theme.css');
    drupal_add_js(drupal_get_path('module', 'ds_search') . '/js/ds_search.js');
    drupal_add_js(array(
      'ds_search' => array(
        'selector' => check_plain(variable_get('ds_search_highlight_selector', '.view-mode-search_result')),
        'search' => check_plain(arg(2)),
      ),
    ), 'setting');
  }
}

/**
 * Return the extra variables.
 */
function ds_search_extra_variables($arg_keys = NULL) {
  $type = variable_get('ds_search_variables', 'none');

  // Define the number of results being shown on a page.
  // We rely on the apache solr rows for now.
  $items_per_page = variable_get('apachesolr_rows', 10);

  // Get the current page.
  $current_page = isset($_REQUEST['page']) ? $_REQUEST['page']+1 : 1;

  // Get the total number of results from the $GLOBALS.
  $total = isset($GLOBALS['pager_total_items'][0]) ? $GLOBALS['pager_total_items'][0] : 0;

  // Perform calculation
  $start = $items_per_page * $current_page - ($items_per_page - 1);
  $end = $items_per_page * $current_page;
  if ($end > $total) $end = $total;

  // Get the search keys.
  $keys = empty($arg_keys) ? trim(ds_search_get_keys()) : $arg_keys;

  // Send the right extra variable.
  switch ($type) {
    case 'search_totals':
      return format_plural($total, 'One result', 'Total results: @total.', array('@total' => $total));
      break;

    case 'search_totals_plus_keywords':
      return format_plural($total, 'Your search for "<strong>@search</strong>" gave back 1 result.',
                                   'Your search for "<strong>@search</strong>" gave back @count results.',
                                   array('@search' => $keys));
      break;

    case 'search_totals_from_to_end':
      return format_plural($total, 'Displaying @start - @end of 1 result.',
                                   'Displaying @start - @end of @count results.',
                                   array('@start' => $start, '@end' => $end));
      break;
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function ds_search_form_search_form_alter(&$form, $form_state) {
  if (variable_get('ds_search_type', 'node') == 'node' && $form['module']['#value'] == 'ds_search') {
    if (variable_get('ds_search_node_form_alter', FALSE)) {
      $form['module']['#value'] = 'node';
      node_form_search_form_alter($form, $form_state);
    }
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function ds_search_form_apachesolr_search_custom_page_search_form_alter(&$form, $form_state) {
  if (variable_get('ds_search_apachesolr_hide_current_filters', FALSE)) {
    $form['basic']['retain-filters']['#type'] = 'value';
    $form['basic']['retain-filters']['#value'] = variable_get('ds_search_apachesolr_current_filters_default', FALSE);
  }
}

/**
 * Implements hook_apachesolr_index_document_build().
 */
function ds_search_apachesolr_index_document_build(ApacheSolrDocument $document, $entity) {
  // Apache Solr multisite support. Render the node already here.
  if (variable_get('ds_search_apachesolr_multisite')) {
    ob_start();
    $element = node_view($entity, variable_get('ds_search_view_mode', 'search_result'));
    print drupal_render($element);
    $output = ob_get_contents();
    ob_end_clean();
    $document->addField('tm_ds_search_result', $output);
  }
  // Creme de la creme: Put the full node object in the index,
  // so no node_loads are needed for results in the Apache Solr engine.
  // We are using zs_entity because this field is not indexed by apachesolr and it
  // is a single.
  $entity_type = $document->getField('entity_type');
  $entity_id = $document->getField('entity_id');
  $entity_type = $entity_type['value'];
  $entity_id = $entity_id['value'];
  $entities = entity_load($entity_type, array($entity_id));
  $entity = reset($entities);
  $json = drupal_json_encode($entity);
  $document->addField('zs_entity', $json);
}

/**
 * Implements hook_apachesolr_query_alter().
 */
function ds_search_apachesolr_query_alter($query) {

  // Get the node from the index.
  $query->addParam('fl', 'zs_entity');

  // Apache Solr multisite support.
  if (variable_get('ds_search_apachesolr_multisite') && variable_get('ds_search_type', 'node') == 'apachesolr_search') {
    // Site hash.
    $query->addParam('fl', 'hash');
    // Rendered search result.
    $query->addParam('fl', 'tm_ds_search_result');

    // Make sure this site's search results are first.
    if (variable_get('ds_search_apachesolr_multisite_boost')) {
      $hash = apachesolr_site_hash();
      $query->addParam('bq', 'hash:' . $hash . '^' . variable_get('ds_search_apachesolr_multisite_boost_nr', 100));
    }
  }

  // Search per language.
  if (variable_get('ds_search_language', FALSE)) {
    global $language;
    $query->addFilter('ss_language', $language->language);
  }
}

/**
 * Process results on behalf of Apache Solr.
 */
function ds_search_process_results($results) {

  $processed_results = array();

  if (is_array($results) && !empty($results)) {
    foreach ($results as $result) {

      // Json decode zs_entity field.
      $entity = @drupal_json_decode($result['fields']['zs_entity']);
      if (empty($entity)) {
        $load = entity_load($result['fields']['entity_type'], array($result['fields']['entity_id']));
        $entity = reset($load);
      }
      else {
        $entity = (object) $entity;
      }

      // Add the snippet, url and extra info on the object.
      $entity->search_snippet = $result['snippet'];
      $entity->search_extra = $result['extra'];
      $entity->search_as_url = $result['fields']['url'];
      $entity->entity_type = $result['fields']['entity_type'];
      $entity->entity_id = $result['fields']['entity_id'];

      // Apache Solr multisite support.
      if (variable_get('ds_search_apachesolr_multisite')) {

        // Pass along the uri path in case some people want to
        // do cool stuff themselves.
        $entity->uri['path'] = $entity->search_as_url;
        $entity->uri['options'] = array();

        // Prefix with site hash so we don't override same id's.
        $markup = $result['fields']['tm_ds_search_result'][0];
        $processed_results[$result['fields']['id'] . '-' . $result['fields']['entity_id']] = array(
          '#markup' => $markup,
          '#site_hash' => $result['fields']['hash'],
        );
      }
      else {
        $processed_results[$result['fields']['id'] . '-' . $result['fields']['entity_id']] = $entity;
      }
    }
  }

  return $processed_results;
}

/**
 * Implements hook_apachesolr_search_page_alter(&$build, $search_page).
 */
function ds_search_apachesolr_search_page_alter(&$build, $search_page) {
  if (!empty($build['search_results']['#results'])) {
    $results = $build['search_results']['#results'];
    $results = ds_search_process_results($results);
    $build['search_results'] = ds_search_search_page($results);
  }
}
