<?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) {
  // fix for Drupal 7.33+
  if(isset($build['theme_hook_original'])) {
    unset($build['theme_hook_original']);
  }

  // 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' => search_help('search#noresults', drupal_help_arg()));
  }

  $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, files and maybe others later.
 */
function ds_entity_view_fallback($entity_type, $entities, $view_mode = 'full', $langcode = NULL, $page = NULL) {

  // Use the entity module in case it's enabled.
  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' && function_exists('file_view_multiple')) {
      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) {
      // Use default search result theming for file in case it's configured.
      if ($result->entity_type == 'file' && variable_get('ds_search_file_render', FALSE)) {
        // Get the file type from the file entity module. We'll overwrite
        // the bundle here then as that makes more sense as a suggestion.
        if (function_exists('file_get_type')) {
          $type = file_get_type($result);
          $result->original_result['bundle'] = $type;
        }
        $build['search_results'][] = array(
          '#weight' => $i++,
          '#markup' => theme('search_result', array('result' => $result->original_result, 'module' => 'apachesolr_search')),
        );
        continue;
      }
      $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'));
      // Check that we got an actual result back.
      if ($data) {
        $data = reset($data);
        $data[$result->entity_id]['#weight'] = $i++;
        $build['search_results'][] = $data[$result->entity_id];
      }
    }
  }

  // Group by type.
  if (variable_get('ds_search_group_by_type') && variable_get('ds_search_group_by_type_settings') && !empty($build['search_results'])) {
    _ds_search_group_by_type($build);
  }
  else {
    // Provide zebra striping for results that are not grouped.
    $parity = 'odd';
    foreach ($build['search_results'] as $id => $result) {
      // We need to check on the entity type, as the container
      // where the object is stored in doesn't necessarily reflect
      // the name of the entity type.
      if (!empty($build['search_results'][$id]['#entity_type'])) {
        switch ($build['search_results'][$id]['#entity_type']) {
          case 'taxonomy_term':
            $key = '#term';
            break;

           default:
             $key = '#' . $build['search_results'][$id]['#entity_type'];
             break;
        }

        $build['search_results'][$id][$key]->ds_search_zebra = $parity;
      }

      // Let parity change always.
      $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') {
    _ds_search_group_by_type_multisearch($build);
  }

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

/**
 * Helper function to group by type.
 */
function _ds_search_group_by_type(&$build) {
  $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';
    }
  }
}

/**
 * Helper function to perform grouping on Apache Solr multisearch.
 */
function _ds_search_group_by_type_multisearch(&$build) {
  $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;
    }
  }
}

/**
 * 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 .= ' ' . implode('', $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' && isset($form['module']) && $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);
  }
}

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

  // 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) {

      $load = entity_load($result['fields']['entity_type'], array($result['fields']['entity_id']));
      $entity = reset($load);
      if (!$entity) {
        watchdog('ds_search', "Empty entity loaded from results, possibly corrupt solr index? (@entity_type, @id)", array('@entity_type' => $result['fields']['entity_type'], '@id' => $result['fields']['entity_id']), WATCHDOG_DEBUG);
        continue;
      }

      // 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'];

      // Add the original result on the entity too in case this is a file
      // entity. Attachments have brittle support as the file entity only
      // exists in media 1.x or file entity 2.x. Because of that, we're
      // most likely will render files through theme('search_result').
      if ($result['fields']['entity_type'] == 'file') {
        $entity->original_result = $result;
      }

      // 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 = ds_search_process_results($build['search_results']['#results']);
    $build['search_results'] = ds_search_search_page($results);
  }
}
