<?php

/**
 * @file
 * Location search interface.
 */

/**
 * Implements hook_menu().
 */
function location_search_menu() {
  $items['admin/settings/location/search'] = array(
    'title' => 'Search options',
    'description' => 'Settings for Location Search module',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('location_search_admin_settings'),
    'file' => 'location_search.admin.inc',
    'access arguments' => array('administer site configuration'),
    'type' => MENU_LOCAL_TASK,
    'weight' => 5,
  );

  return $items;
}

/**
 * Implements hook_theme().
 */
function location_search_theme() {
  return array(
    'search_result_location' => array(
      'variables' => array(
        'result' => NULL,
        'module' => NULL,
      ),
      'template' => 'search_result_location',
    ),
  );
}

/**
 * Implements hook_search_info().
 */
function location_search_search_info() {
  return array(
    'title' => 'Locations',
    'path' => 'location',
  );
}

/**
 * Implements hook_search_access().
 */
function location_search_search_access() {
  return user_access('access content');
}

/**
 * Implements hook_search_reset().
 */
function location_search_search_reset() {
  db_delete('location_search_work')->execute();
  $insert_select = db_select('location', 'l');
  $insert_select->addField('l', 'lid');

  db_insert('location_search_work')
    ->fields(array('lid'))
    ->from($insert_select)
    ->execute();
}

/**
 * Implements hook_search_status().
 */
function location_search_search_status() {
  $total = db_query('SELECT COUNT(lid) FROM {location}')->fetchField();
  $remaining = db_query('SELECT COUNT(lid) FROM {location_search_work}')->fetchField();
  return array('remaining' => $remaining, 'total' => $total);
}


/**
 * Implements hook_search_execute().
 */
function location_search_search_execute($keys = NULL, $conditions = NULL) {
  $proximity = FALSE;

  $lids = array();

  // Determine whether this is a fulltext search or not.
  $fulltext = $keys;
  $fulltext = search_expression_insert($fulltext, 'country');
  $fulltext = search_expression_insert($fulltext, 'province');
  $fulltext = search_expression_insert($fulltext, 'city');
  $fulltext = search_expression_insert($fulltext, 'from');
  $fulltext = empty($fulltext) ? FALSE : TRUE;

  if ($fulltext) {
    // Fuzzy search -- Use the fulltext routines against the indexed locations.
    $query = db_select('search_index', 'i')->extend('SearchQuery')->extend('PagerDefault');
    $query->join('location', 'l', 'l.lid = i.sid');
    // Add access control.
    if (!location_search_add_access_control($query)) {
      return array();
    }

    $query->searchExpression($keys, 'node');

    // Insert special keywords.
    $query->setOption('country', 'l.country');
    $query->setOption('province', 'l.province');
    $query->setOption('city', 'l.city');

    // setOption() can't handle the complexity of this so do it manually.
    if ($value = search_expression_extract($keys, 'from')) {
      // Set up a proximity search.
      $proximity = TRUE;
      list($lat, $lon, $dist, $unit) = explode(',', $value);
      $distance_meters = _location_convert_distance_to_meters($dist, $unit);

      // MBR query to make it easier on the database.
      $latrange = earth_latitude_range($lon, $lat, $distance_meters);
      $lonrange = earth_longitude_range($lon, $lat, $distance_meters);
      $query->condition('l.latitude', array($latrange[0], $latrange[1]), 'BETWEEN');
      $query->condition('l.longitude', array($lonrange[0], $lonrange[1]), 'BETWEEN');

      // Distance query to finish the job.
      $query->where(earth_distance_sql($lon, $lat) . ' < ' . $distance_meters);

      // Override the scoring mechanism to use calculated distance
      // as the scoring metric.
      $query->addExpression(earth_distance_sql($lon, $lat, 'l'), 'distance');
      $query->orderBy('distance', 'DESC');

      $query->searchExpression = search_expression_insert($query->searchExpression, 'from');
    }

    // Only continue if the first pass query matches.
    if (!$query->executeFirstPass()) {
      return array();
    }
  }
  else {
    $query = db_select('location', 'l')->extend('PagerDefault');
    // sid is the alias so that our results match the fulltext search results.
    $query->addField('l', 'lid', 'sid');
    // Add access control.
    if (!location_search_add_access_control($query)) {
      return array();
    }

    // Insert special keywords.
    if ($value = search_expression_extract($keys, 'country')) {
      $query->condition('l.country', $value);
    }
    if ($value = search_expression_extract($keys, 'province')) {
      $query->condition('l.province', $value);
    }
    if ($value = search_expression_extract($keys, 'city')) {
      $query->condition('l.city', $value);
    }
    // This is almost duplicated from the fulltext search above because if it
    // were refactored out it would make the code a little less clean and a
    // little harder to understand.
    if ($value = search_expression_extract($keys, 'from')) {
      // Set up a proximity search.
      $proximity = TRUE;
      list($lat, $lon, $dist, $unit) = explode(',', $value);
      $distance_meters = _location_convert_distance_to_meters($dist, $unit);

      // MBR query to make it easier on the database.
      $latrange = earth_latitude_range($lon, $lat, $distance_meters);
      $lonrange = earth_longitude_range($lon, $lat, $distance_meters);
      $query->condition('l.latitude', array($latrange[0], $latrange[1]), 'BETWEEN');
      $query->condition('l.longitude', array($lonrange[0], $lonrange[1]), 'BETWEEN');

      // Distance query to finish the job.
      $query->where(earth_distance_sql($lon, $lat) . ' < ' . $distance_meters);

      // Override the scoring mechanism to use calculated distance
      // as the scoring metric.
      $query->addExpression(earth_distance_sql($lon, $lat, 'l'), 'distance');
      $query->orderBy('distance', 'DESC');
    }
  }

  // Load results.
  $found = $query
    ->limit(10)
    ->execute();

  foreach ($found as $item) {
    $lids[] = $item->sid;
  }

  $results = array();
  foreach ($lids as $lid) {
    $loc = location_load_location($lid);

    $result = db_query('SELECT nid, uid, genid FROM {location_instance} WHERE lid = :lid', array(':lid' => $lid), array('fetch' => PDO::FETCH_ASSOC));
    $instance_links = array();
    foreach ($result as $row) {
      $instance_links[] = $row;
    }
    location_invoke_locationapi($instance_links, 'instance_links');

    $results[] = array(
      'links' => $instance_links,
      'location' => $loc,
    );

  }
  return $results;
}

/**
 * Add access control for users & nodes to the search query.
 *
 * @param $query
 *   The query object.
 *
 * @return
 *   Boolean for whether or not to run a query at all.
 */
function location_search_add_access_control(&$query) {
  // Add access control.
  $has_user_location_access = user_access('access user profiles') && user_access('view all user locations');
  $has_content_access = user_access('access content');

  if (!$has_user_location_access && !$has_content_access) {
    // The user has access to no locations.
    return FALSE;
  }
  elseif ($has_user_location_access && !$has_content_access) {
    // The user doesn't have access to nodes, so include only locations
    // that don't belong to nodes.
    $query->join('location_instance', 'li', 'l.lid = li.lid AND li.nid = 0');
  }
  elseif (!$has_user_location_access && $has_content_access) {
    // The user doesn't have access to user locations, so include only
    // locations that don't belong to users.
    // This also means we'll need to enforce node access.
    $query->join('location_instance', 'li', 'l.lid = li.lid AND li.uid = 0');
    $query->join('node', 'n', 'li.nid = n.nid AND n.status = 1');
    $query->addMetaData('base_table', 'node');
    $query->addTag('node_access');
  }
  else {
    // The user has access to both.  However, for the locations that
    // belong to nodes, we need to use node access. These are the ones with
    // location_instance.nid != 0
    // location_instance.nid = 0 means the location instance belongs
    // to a user record.
    $query->join('location_instance', 'li', 'l.lid = li.lid');
    $query->leftjoin('node', 'n', 'li.nid = n.nid AND n.status = 1');

    // The node part must use a sub-select because if the node access
    // rewrites get added to the main query there will never be any user
    // results because location instance will be inner joined to node access.
    $subselect = db_select('node', 'subnode');
    $subselect->addField('subnode', 'nid');
    $subselect->addMetaData('base_table', 'node');
    $subselect->addTag('node_access');

    // If we pass in the subselect query object then when the pager query is
    // run later on the node_access alterations don't get applied to the
    // subselect. So instead we pass in the array result set.
    $query->condition(db_or()
      ->condition('li.uid', 0, '<>')
      ->condition(db_and()
        ->condition('li.nid', 0, '<>')
        ->condition('n.nid', $subselect->execute()->fetchCol(), 'IN')));
  }
  return TRUE;
}

function location_search_form_alter(&$form, &$form_state, $form_id) {
  if ($form_id == 'search_form' && arg(1) == 'location' && user_access('use advanced search')) {
    // @@@ Cache this.
    $result = db_query('SELECT DISTINCT country FROM {location}', array(), array('fetch' => PDO::FETCH_ASSOC));
    $countries = array('' => '');
    foreach ($result as $row) {
      if (!empty($row['country'])) {
        $country = $row['country'];
        location_standardize_country_code($country);
        $countries[$country] = location_country_name($country);
      }
    }
    ksort($countries);

    drupal_add_js(drupal_get_path('module', 'location') .'/location_autocomplete.js');

    // Keyword boxes:
    $form['advanced'] = array(
      '#type' => 'fieldset',
      '#title' => t('Advanced search'),
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
      '#attributes' => array('class' => array('search-advanced')),
    );

    $form['advanced']['country'] = array(
      '#type' => 'select',
      '#title' => t('Country'),
      '#options' => $countries,
      // Used by province autocompletion js.
      '#attributes' => array('class' => array('location_auto_country')),
    );

    $form['advanced']['province'] = array(
      '#type' => 'textfield',
      '#title' => t('State/Province'),
      '#autocomplete_path' => 'location/autocomplete/'. variable_get('location_default_country', 'us'),
      // Used by province autocompletion js.
      '#attributes' => array('class' => array('location_auto_province')),
    );

    $form['advanced']['city'] = array(
      '#type' => 'textfield',
      '#title' => t('City'),
    );

    $form['advanced']['proximity'] = array(
      '#type' => 'fieldset',
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
      '#title' => t('Proximity'),
    );

    $form['advanced']['proximity']['map'] = array();

    if (variable_get('location_search_map_address', 1)) {
      $form['advanced']['proximity']['locpick_address'] = array(
        '#type' => 'textfield',
        '#title' => t('Locate Address'),
      );
    }

    $form['advanced']['proximity']['latitude'] = array(
      '#type' => 'textfield',
      '#title' => t('Latitude'),
    );
    $form['advanced']['proximity']['longitude'] = array(
      '#type' => 'textfield',
      '#title' => t('Longitude'),
    );
    $form['advanced']['proximity']['distance'] = array(
      '#type' => 'fieldset',
      '#title' => t('Distance'),
    );
    $form['advanced']['proximity']['distance']['distance'] = array(
      '#type' => 'textfield',
      '#size' => 5,
      '#maxlength' => 5,
    );
    $form['advanced']['proximity']['distance']['units'] = array(
      '#type' => 'select',
      '#options' => array(
        'mi' => t('mi'),
        'km' => t('km'),
      ),
    );

    if (variable_get('location_search_map', 1)) {
      $map_fields = array(
        'latitude' => 'latitude',
        'longitude' => 'longitude',
      );
      if (variable_get('location_search_map_address', 1)) {
        $map_fields['address'] = 'locpick_address';
      }
      if (module_exists('gmap')) {
        $form['advanced']['proximity']['map']['#value'] = gmap_set_location(variable_get('location_search_map_macro', '[gmap |behavior=+collapsehack]'), $form['advanced']['proximity'], $map_fields);
      }
    }

    $form['advanced']['submit'] = array(
      '#type' => 'submit',
      '#value' => t('Advanced search'),
      '#prefix' => '<div class="action">',
      '#suffix' => '</div>',
    );

    $form['#validate'][] = 'location_search_validate';
  }
}

function location_search_validate($form, &$form_state) {
  $values = $form_state['values'];

  // Initialise using any existing basic search keywords.
  $keys = $values['processed_keys'];

  if (!empty($values['country'])) {
    $keys = search_expression_insert($keys, 'country', $values['country']);

    if (!empty($values['province'])) {
      $keys = search_expression_insert($keys, 'province', location_province_code($values['country'], $values['province']));
    }
  }

  if (!empty($values['city'])) {
    $keys = search_expression_insert($keys, 'city', $values['city']);
  }

  if (!empty($values['latitude']) && !empty($values['longitude']) && !empty($values['distance'])) {
    $keys = search_expression_insert($keys, 'from', $values['latitude'] .','. $values['longitude'] .','. $values['distance'] .','. $values['units']);
  }

  if (!empty($keys)) {
    form_set_value($form['basic']['processed_keys'], trim($keys), $form_state);
  }
}

/**
 * Implements hook_update_index().
 */
function location_update_index() {
  $limit = (int)variable_get('search_cron_limit', 100);

  $result = db_query_range('SELECT lid FROM {location_search_work}', 0, $limit);
  foreach ($result as $row) {
    $loc = location_load_location($row->lid);
    $text = theme('location', array('location' => $loc, 'hide' => array())); // @@@ hide?

    search_index($row->lid, 'location', $text);
    db_delete('location_search_work')
      ->condition('lid', $row->lid)
      ->execute();
  }
}

/**
 * Implements hook_locationapi().
 */
function location_search_locationapi(&$obj, $op, $a3 = NULL, $a4 = NULL, $a5 = NULL) {
  if ($op == 'save') {
    // Ensure the changed location is in our work list.
    db_delete('location_search_work')
      ->condition('lid', $obj['lid'])
      ->execute();
    db_insert('location_search_work')
      ->fields(array(
        'lid' => $obj['lid'],
      ))
      ->execute();
  }
  if ($op == 'delete') {
    search_reindex($obj['lid'], 'location');
  }
}

/**
 * Implements hook_search_page().
 */
function location_search_search_page($results) {
  $output['prefix']['#markup'] = '<ol class="search-results">';

  foreach ($results as $entry) {
    $output[] = array(
      '#theme' => 'search_result_location',
      '#result' => $entry,
      '#module' => 'location',
    );
  }
  $output['suffix']['#markup'] = '</ol>' . theme('pager');

  return $output;
}

function template_preprocess_search_result_location(&$variables) {
  $result = $variables['result'];

  $variables['links_raw'] = array();
  foreach ($result['links'] as $link) {
    if (isset($link['title']) && isset($link['href'])) {
      $variables['links_raw'][] = $link;
    }
  }
  $variables['location_raw'] = $result['location'];

  $variables['location'] = theme('location', array('location' => $result['location'], 'hide' => array())); // @@@ hide?
  $variables['links'] = theme('links', array('links' => $variables['links_raw']));

  // Provide alternate search result template.
  $variables['theme_hook_suggestions'][] = 'search_result_' . $variables['module'];
}
