<?php

define ('SEARCH_API_RANGES_ENDPOINT_INFINITE', '*');

/**
 * @file
 * Performs min/max queries through Search API
 * and provides UI Slider display widget for Facet API
 */

/**
 * Implements hook_theme().
 */
function search_api_ranges_theme() {
  $themes = array(
    'search_api_ranges_slider' => array(
      'variables' => array('slider' => ''),
      'file' => 'search_api_ranges.theme.inc',
    ),
    'search_api_ranges_block_slider_view_form' => array(
      'template' => 'search-api-ranges-block-slider-view-form',
      'render element' => 'form',
    ),
  );
  return $themes;
}

/**
 * Implements hook_facetapi_widgets().
 */
function search_api_ranges_facetapi_widgets() {
  return array(
    'search_api_ranges_ui_slider' => array(
      'handler' => array(
        'label' => t('Ranges: Min/Max UI Slider'),
        'class' => 'SearchApiRangesWidgetUISlider',
        'query types' => array('term'),
      ),
    ),
    'search_api_ranges_ui_links' => array(
      'handler' => array(
        'label' => t('Ranges: Text links'),
        'class' => 'SearchApiRangesWidgetLinks',
        'query types' => array('term'),
      ),
    ),
    'search_api_ranges_ui_checkbox_links' => array(
      'handler' => array(
        'label' => t('Ranges: Text links with checkboxes'),
        'class' => 'SearchApiRangesWidgetCheckboxLinks',
        'query types' => array('term'),
      ),
    ),
    'search_api_ranges_ui_select' => array(
      'handler' => array(
        'label' => t('Ranges: Drop down list'),
        'class' => 'SearchApiRangesWidgetSelect',
        'query types' => array('term'),
      ),
    ),
  );
}

/**
 * Implements hook_facetapi_facet_info().
 */
function search_api_ranges_facetapi_facet_info_alter(array &$facet_info, array $searcher_info) {
  $adapter = facetapi_adapter_load($searcher_info['name']);
  foreach ($facet_info as &$facet) {
    $facet_settings = $adapter->getFacet($facet)->getSettings('block');

    if (isset($facet_settings->settings['widget'])) {
      switch ($facet_settings->settings['widget']) {
        case 'search_api_ranges_ui_slider':
          $facet['facetapi pretty paths coder'] = 'default';
          break;

        case 'search_api_ranges_ui_links':
          $facet['map options']['map callback'] = 'search_api_ranges_map_label';
          break;
      }
    }
  }
}

/**
 * Find the lowest/highest valuse for the active facets
 *
 * @param array $variables
 *   An array with at least the following keys => values:
 *   - (string) range_field: the name of the slider range field
 *   - (array) query: the Search API query object
 *
 * @param string $order
 *   Either ASC (for min) or DESC (for max)
 *
 * @return string
 *   The rounded integer value for min/max
 *
 */
function search_api_ranges_minmax($variables, $order = 'ASC') {

  // Allow the other modules to modify the query parameters.
  drupal_alter('search_api_ranges_minmax', $variables, $order);

  // If query is empty, ignore sending the query.
  // This allows other modules to decide
  // whenever we want to perform the query.
  if (empty($variables['query'])) {
    return isset($variables['result']) ? $variables['result'] : NULL;
  }

  $query = $variables['query'];
  $index = $variables['index'];
  $order_lower = strtolower($order);

  // Check if min or max values are indexed for multiple fields.
  if (search_api_is_list_type($index->options['fields'][$variables['range_field']]['type'])) {
    $field_name = str_replace(':', '_', $variables['range_field']) . '_' . $order_lower;
    if (isset($index->options['fields'][$field_name])) {
      $variables['range_field'] = $field_name;
    }
  }

  // Alter sort.
  $sort = &$query->getSort();
  $sort = array($variables['range_field'] => $order);

  // Alter options.
  $options = &$query->getOptions();
  $options['limit'] = 1;
  $options['search id'] = 'search_api_ranges:' . $variables['range_field'] . ':minmax/' . $order;

  // For performance, we don't need to return any facets.
  $options['search_api_facets'] = array();

  // Do not take into account pager query.
  $options['offset'] = 0;

  // Set some metadata to allow modules to alter based on that information.
  $query->setOption('search_api_ranges', array('range_field' => $variables['range_field']));

  // Remove current range field from the filters
  // otherwise our min/max would always equal user input.
  $filters = &$query->getFilter()->getFilters();
  foreach ($filters as $key => $filter) {

    // Check for array: old style filters are objects which we can skip.
    if (is_array($filter)) {
      if ($filter[0] == $variables['range_field'] || ($filter[0] != $variables['range_field'] && $filter[2] == '<>')) {
        $current_filter = $filters[$key];
        unset($filters[$key]);
      }
    }
  }

  // Filter out results with no values for that field.
  $query->condition($variables['range_field'], NULL, '<>');

  // Execute the query and process results.
  $results = search_api_ranges_minmax_execute($query);
  foreach ($filters as $key => $filter) {

    // Remove $query->condition($variables['range_field'], NULL, '<>'); to prevent this condition influence on other facets
    if (is_array($filter)) {
      if ($filter[0] == $variables['range_field']  ) {
        unset($filters[$key]);
      }
    }
  }

  // Return current filter with digital ranges to the query
  if (isset($current_filter) && is_array($current_filter)) {
    if ($current_filter[1] != NULL && $current_filter[2] !='<>') {
      $query->condition($variables['range_field'], $current_filter[1], $current_filter[2]);
    }
    if ($current_filter[1] != NULL && $current_filter[2] !='<>') {
      $query->condition($variables['range_field'], $current_filter[1], $current_filter[2]);
    }
  }
  if (!$results['results']) {
    return;
  }

  foreach ($results['results'] as $result) {
    $entity = entity_load($variables['index']->item_type, array($result['id']));
    $variables['index']->dataAlter($entity);
    $wrapper = $variables['index']->entityWrapper($entity[$result['id']]);
  }
  $fields[$variables['range_field']]['type'] = 'integer';
  $fields = search_api_extract_fields($wrapper, $fields);

  // We have to round because jQuery slider cannot handle decimals.
  $return = $fields[$variables['range_field']]['value'];
  switch ($order) {
    case 'DESC':
      $return = ceil($return);
      break;

    default:
      $return = floor($return);
  }
  return $return;
}

/**
 * Executes the min/max search query.
 *
 * @return array
 *   An associative array containing the search results. The following keys
 *   are standardized:
 *   - 'result count': The overall number of results for this query, without
 *     range restrictions. Might be approximated, for large numbers.
 *   - results: An array of results, ordered as specified. The array keys are
 *     the items' IDs, values are arrays containing the following keys:
 *     - id: The item's ID.
 *     - score: A float measuring how well the item fits the search.
 *     - entity (optional): If set, the fully loaded entity. This field should
 *       always be used by modules using search results, to avoid duplicate
 *       entity loads.
 *     - excerpt (optional): If set, an HTML text containing highlighted
 *       portions of the fulltext that match the query.
 *   - warnings: A numeric array of translated warning messages that may be
 *     displayed to the user.
 *   - ignored: A numeric array of search keys that were ignored for this
 *     search (e.g., because of being too short or stop words).
 *   - performance: An associative array with the time taken (as floats, in
 *     seconds) for specific parts of the search execution:
 *     - complete: The complete runtime of the query.
 *     - hooks: Hook invocations and other server-independent processing.
 *     - preprocessing: Preprocessing of the service class.
 *     - execution: The actual query to the search server, in whatever form.
 *     - postprocessing: Preparing the results for returning.
 *   Additional metadata may be returned in other keys. Only 'result count'
 *   and 'results' always have to be set, all other entries are optional.
 */
function search_api_ranges_minmax_execute(SearchApiQuery $query) {
  $start = microtime(TRUE);

  // Prepare the query for execution by the server.
  $query->preExecute();

  $pre_search = microtime(TRUE);

  // Execute query.
  $response = $query->getIndex()->server()->search($query);

  $post_search = microtime(TRUE);

  // Postprocess the search results.
  $query->postExecute($response);

  $end = microtime(TRUE);
  $response['performance']['complete'] = $end - $start;
  $response['performance']['hooks'] = $response['performance']['complete'] - ($post_search - $pre_search);

  return $response;
}

/**
 * Implements hook_forms().
 */
function search_api_ranges_forms($form_id, $args) {
  // Map all form IDs starting with search_api_ranges_block_slider_view_form
  // to our callback.
  if (strpos($form_id, 'search_api_ranges_block_slider_view_form') === 0) {
    $forms[$form_id] = array(
      'callback' => 'search_api_ranges_block_slider_view_form',
    );
    return $forms;
  }
}

/**
 * Generates the jQuery range slider form for range facet blocks.
 *
 * @see search_api_ranges_forms()
 */
function search_api_ranges_block_slider_view_form($form, &$form_state, $variables) {
  $form = array();

  // For compatibility with Search API ajax,
  // we generate the 'ajax target URL' as a hidden field.
  $params = drupal_get_query_parameters($_GET, array('q'));
  foreach ($variables['active_items'] as $key => $active_item) {
    if ($active_item['field alias'] == $variables['range_field']) {
      $pos = $active_item['pos'];
      unset($params['f'][$pos]);
    }
  }

  // Get path or facetapi_pretty_paths.
  $path = $variables['target'];
  if (module_exists('facetapi_pretty_paths')) {
    $path = request_path();
    unset($_GET['f']);
  }

  $form['text-range'] = array(
    '#markup' => '<p class="text-range">'
      . t(
        '!field_name ranges from !prefix_' . $variables['range_field']
        . '@from!suffix_' . $variables['range_field'] . ' to !prefix_'
        . $variables['range_field'] . '@to!suffix_' . $variables['range_field'],
        array(
          '!field_name' => t($variables['name']),
          '@from' => number_format($variables['min'], 0),
          '@to' => number_format($variables['max'], 0),
          '!prefix_' . $variables['range_field'] => $variables['prefix'],
          '!suffix_' . $variables['range_field'] => $variables['suffix'],
         )
      ) . '</p>',
    );
  $form['range-from'] = array(
    '#type' => 'textfield',
    '#title' => t('From'),
    '#size' => 10,
    '#default_value' => $variables['from'],
  );
  $form['range-slider'] = array('#markup' => '<div class="range-slider"></div>');
  $form['range-to'] = array(
    '#type' => 'textfield',
    '#title' => t('To'),
    '#size' => 10,
    '#default_value' => $variables['to'],
  );
  $form['range-min'] = array(
    '#type' => 'hidden',
    '#value' => $variables['min'],
  );
  $form['range-max'] = array(
    '#type' => 'hidden',
    '#value' => $variables['max'],
  );
  $form['path'] = array(
    '#type' => 'hidden',
    '#value' => $path,
  );
  $form['range-field'] = array(
    '#type' => 'hidden',
    '#value' => $variables['range_field'],
  );
  if ($variables['auto_submit_delay']) {
    $form['delay'] = array(
      '#type' => 'hidden',
      '#value' => $variables['auto_submit_delay'],
    );
  }
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Go'),
  );

  return $form;
}

/**
 * Handle slider block submit
 */
function search_api_ranges_block_slider_view_form_submit($form, &$form_state) {
  $values = $form_state['values'];
  $range_field = $form_state['input']['range-field'];

  // Prepare params and existing filter $pos (if any)
  $params = drupal_get_query_parameters($_GET, array('q', 'page'));

  // Get pretty path path and goto()
  if (drupal_multilingual() && variable_get('locale_language_negotiation_url_part') == LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX) {
    list($language, $path) = language_url_split_prefix(request_path(), language_list());
    $language = $language ? $language : NULL;
  }
  else {
    $path = request_path();
    $language = NULL;
  }
  if (module_exists('facetapi_pretty_paths')) {
    $exists = FALSE;
    $split_path = explode('/', $path);
    foreach ($split_path as $key => $value) {
      if (!($key % 2) && $value == $range_field) {
        $exists = $split_path[$key + 1];
      }
    }

    // Decision: replace existing range or add new
    $new_range = '[' . $values['range-from'] . ' TO ' . $values['range-to'] . ']';
    if ($exists) {
      $path = str_replace($exists, $new_range, $path);
    }
    else {
      $path .= '/' . $range_field . '/' . $new_range;
    }

    // Unset non-pretty query
    unset($params['f']);
  }
  else {
    // Not pretty path logic
    $query = $range_field . ':' . '[' . $values['range-from'] . ' TO ' . $values['range-to'] . ']';

    $pos = -1;
    if (isset($params['f'])) {
      foreach ($params['f'] as $key => $param) {
        if (strpos($param, $range_field . ':') !== FALSE) {
          $pos = $key;
        }
      }
    }

    if ($pos != -1) {
      $params['f'][$pos] = $query;
    }
    else {
      $params['f'][] = $query;
    }
  }

  drupal_goto($path, array('query' => array($params), 'language' => $language));
}

/**
 * Implements hook_search_api_alter_callback_info().
 */
function search_api_ranges_search_api_alter_callback_info() {
  $callbacks['search_api_ranges_alter'] = array(
    'name' => t('Search API ranges'),
    'description' => t('Adds the minimum and maximum values of selected numeric fields.'),
    'class' => 'SearchApiRangesAlter',
    'weight' => 100,
  );
  return $callbacks;
}

/**
 * Generate the available ranges given the active facets
 *
 * @param array $variables
 *   An array with at least the following keys => values:
 *   - (string) range_field: the name of the slider range field
 *   - (array) query: the Search API query object
 *
 * @param integer $step
 *   used as the increment between elements in the range sequence.
 *
 * @return array
 *   An array of text link arrays, each with the following keys => values:
 *   - (string) text: A textrual representation of the range,
 *   - (integer) min: The minimum vaule for the range.
 *   - (integer) max: The maximum value for the range.
 */
function search_api_ranges_generate_ranges_simple($variables, $step) {
  $element = $variables['element'];
  $params = drupal_get_query_parameters($_GET, array('q', 'page'));
  $ranges = array();

  if (count($element)) {
    $values = array_keys($element);
    sort($values, SORT_NUMERIC);
    $min = floor($values[0] / $step) * $step;
    $max = $min + $step;
    $result_count = 0;

    foreach ($values as $val) {
      if ($val >= $max) {
        $ranges[$min . ' - ' . $max] = _search_api_generate_range($min, $max, $result_count, $variables, $params);
        $min = floor($val / $step) * $step;
        $max = $min + $step;
        $result_count = 0;
      }
      $result_count += $element[$val]['#count'];
    }
    if ($result_count) {
      $ranges[$min . ' - ' . $max] = _search_api_generate_range($min, $max, $result_count, $variables, $params);
    }
  }

  return $ranges;
}

function search_api_ranges_generate_ranges_advanced($variables, $steps) {
  $element = $variables['element'];
  // Calculate the min and max of the ranges.
  $values = array_keys($element);
  sort($values, SORT_NUMERIC);
  $min_all = $values[0];
  $max_all = $values[count($values) - 1];

  $adv_ranges = _search_api_ranges_parse_advanced_range_settings($steps);
  $tmp_ranges = array();
  $ret_ranges = array();

  if (count($adv_ranges)) {
    foreach ($adv_ranges as $range) {
      if (substr_count($range['value'], '-') == 1) {
        $maxmin = explode("-", $range['value']);
        $min = trim($maxmin[0]);
        $max = trim($maxmin[1]);

        if (strlen($min) == 0) {
          $min = $min_all;
        }
        if (strlen($max) == 0) {
          $max = $max_all;
        }
        $tmp_ranges[] = array(
          'min' => $min,
          'max' => $max,
          'label' => $range['label'],
        );
      }
    }
  }

  if (count($tmp_ranges)) {
    $params = drupal_get_query_parameters($_GET, array('q', 'page'));
    $range_step = 0;

    foreach ($tmp_ranges as $tmp_range_id => $tmp_range) {
      $min = $tmp_range['min'];
      $max = $tmp_range['max'];
      $label = $tmp_range['label'];
      $result_count = 0;

      foreach ($values as $val) {
        if (($max != SEARCH_API_RANGES_ENDPOINT_INFINITE) && ($val > $max)) {
          break;
        }
        elseif ($min != SEARCH_API_RANGES_ENDPOINT_INFINITE && ($val < $min)) {
          continue;
        }
        $result_count += $element[$val]['#count'];
      }
      if ($result_count) {
        $ret_ranges[$min . ' - ' . $max] = _search_api_generate_range($min, $max, $result_count, $variables, $params, $label);
      }
    }
  }

  return $ret_ranges;
}

/**
 * Parse user submitted advanced range settings.
 *
 * @param $settings_string
 * @return array $ranges Each consisting of a 'value' and 'label' element.
 */
function _search_api_ranges_parse_advanced_range_settings($settings_string) {
  // This parsing code was lifted from core list.module
  $list = explode("\n", $settings_string);
  $list = array_map('trim', $list);
  $list = array_filter($list, 'strlen');

  $ranges = array();
  foreach ($list as $range) {
    // Check for an explicit key.
    $matches = array();
    if (preg_match('/(.*)\|(.*)/', $range, $matches)) {
      $ranges[] = array(
        'value' => trim($matches[1]),
        'label' => trim($matches[2]),
      );
    }
    else {
      $ranges[] = array(
        'value' => trim($range),
        'label' => '',
      );
    }
  }
  return $ranges;
}

function _search_api_generate_range($min, $max, $count, $variables, $params, $label = '') {
  // Generate the new query.
  $query = urlencode($variables['range_field']) . ":[$min TO $max]";
  $active = FALSE;

  // Add the new query or remove it if the range is already active.
  if (empty($params['f'])) {
    $params['f'] = array($query);
  }
  else {
    $key = array_search($query, $params['f']);
    if ($key !== FALSE) {
      unset($params['f'][$key]);
      $active = TRUE;
    }
    else {
      $params['f'][] = $query;
    }
  }

  if (empty($label)) {
    $label = $variables['prefix'] . number_format($min, 0)
      . $variables['suffix'] . '–' . $variables['prefix']
      . number_format($max, 0) . $variables['suffix'];
  }
  // Build up a render array.
  return array(
    '#markup' => $label,
    '#path' => $variables['target'],
    '#html' => FALSE,
    '#indexed_value' => 'TODO: what to put here?',
    '#count' => $count,
    '#query' => $params,
    '#active' => $active,
  );
}

/**
 * Maps facet ranges to their human readable label.
 *
 * @param $values
 *   An array of ranges being mapped.
 * @param $options
 *   An associative array of map options.
 *
 * @return
 *   An array mapping the ranges to their human readable label.
 */
function search_api_ranges_map_label(array $values, array $options) {
  $adapter = facetapi_adapter_load('search_api@' . $options['index id']);
  $facet_settings = $adapter->getFacet(array('name' => $options['field']['key']))->getSettings('block');

  $map = array();
  if (!empty($facet_settings->settings['range_advanced'])) {
    $ranges = preg_split('/[\r\n]+/', $facet_settings->settings['range_advanced']);

    foreach ($ranges as $item) {
      $item = explode('|', $item, 2);
      if (isset($item[1])) {
        $item[0] = '[' . str_replace('-', ' TO ', $item[0]) . ']';
        $map[$item[0]] = $item[1];
      }
    }
  }

  return $map;
}