<?php
// $Id: relativity_query.module,v 1.2 2006/09/22 06:45:34 darius Exp $

/**
 * @file
 * Relativity query node type
 */

/**
 * Implementation of hook_help().
 */
function relativity_query_help($section) {
  switch ($section) {
    case 'admin/modules#description':
      // This description is shown in the listing at admin/modules.
      return t('Used to query nodes related using Node Relativity module. <em>Note: requires relativity.module</em>.');
    case 'node/add#relativity_query':
      // This description shows up when users click "create content."
      return t('Node Relativity query.');
  }
}

function relativity_query_menu($may_cache) {
  $items = array();
  if ($may_cache) {
    $items[] = array('path' => 'node/add/relativity_query', 
                     'title' => t('node relativity query'),
                     'access' => user_access('create node relativity queries'),
                     'weight' => 1
                    );
  }
  return $items;
}

/**
 * Implementation of hook_perm().
 */
function relativity_query_perm() {
  return array('create node relativity queries', 'edit own node relativity queries');
}

/**
 * Executes a relativity_query node using the default target_nid or one specified
 * as an optional parameter.
 *
 * @param $querynode   a relativity_query type node containing query info
 * @param $target_nid  optionally specify a starting node
 * @return  an array of nodes indexed by nid that were found while searching.
 */
function relativity_execute_query($querynode, $keywords=NULL, $target_nid=NULL) {
  if (!$target_nid) {
    $target_nid = $querynode->target_nid;
  }
  $search_results = relativity_list_relatives($target_nid, $querynode->follow_parents, $querynode->follow_children, $querynode->recursion_depth, $querynode->unique_types, $querynode->max_results, $querynode->search_types, $querynode->end_pts, $querynode->avoid_pts, $querynode->options, $querynode->search_algorithm);
  if ($keywords) {
    $search_results = relativity_filter_keywords($search_results, $keywords);
  }
  //drupal_set_message("running query for ".print_r($querynode,1)." found ".count($search_results)." results");
  return $search_results;
}

/**
 * A wrapper function that identifies the correct search algorithm function and
 * invokes it.
 *
 * @param $nid   the node id of the node to search from
 * @param $follow_parent_paths  if true, traverse parent paths of the graph
 * @param $follow_child_paths   if true, traverse child paths of the graph
 * @param $recurse  Specify the maximum neighbor distance to search through (-1 for $max_recursion)
 * @param $unique_node_types  if true, only follow a node path if no node currently exists in the results of that node's type
 * @param $max_results  discontinue searching when this many results have been found
 * @param $search_types  array of node types that are the only types allowed in the results.
 * @param $end_types  array of node types that are considered an end point, with no searching going beyond that node.
 * @param $avoid_types  array of node types that will not be included in the results and will not be searched through.
 * @param $search_algorithm  which search algorithm to use (currently, only 'dfs', or Depth-First Search is supported).
 *
 * @return  an array of nodes indexed by nid that were found while searching.
 */
function relativity_list_relatives($nid, $follow_parent_paths=1, $follow_child_paths=1, $recurse=0, $unique_node_types=0, $max_results=50, $search_types = NULL, $end_types = NULL, $avoid_types = NULL, $options=NULL, $search_algorithm='dfs') {
  $search_algorithm = $search_algorithm ? $search_algorithm : 'dfs';
  $search_func = 'relativity_list_relatives_'.$search_algorithm;
  if (function_exists($search_func)) {
    return $search_func($nid, $follow_parent_paths, $follow_child_paths, $recurse, $unique_node_types, $max_results, $search_types, $end_types, $avoid_types, $options);
  }
  drupal_set_message(t('Search algorithm \'%algo\' not supported', array('%algo'=>$search_algorithm)));
  return array();
}

/**
 * Navigate DiGraph of nodes upwards (and downwards, too if $follow_child_paths=1)
 * and return an array of distinct nids whose values are the node type.
 
 * logic:
 * start with a nid.
 * if follow_parent_paths, find all parents of this nid and add them to the nids array
 *   if the nid is already in the nodes array, skip it
 *   otherwise, recurse with the parent nodes that were just found
 * if follow_child_paths, find all children and add them to the nids array
 *   if the nid is already in the nodes array, skip it
 *   otherwise, recurse with the child nodes that were found.
 * return the nodes array.
 *
 * @param $nid   the node id of the node to search from
 * @param $follow_parent_paths  if true, traverse parent paths of the graph
 * @param $follow_child_paths   if true, traverse child paths of the graph
 * @param $recurse  Specify the maximum neighbor distance to search through (-1 for $max_recursion)
 * @param $unique_node_types  if true, only follow a node path if no node currently exists in the results of that node's type
 * @param $end_types  array of node types that are considered an end point, with no searching going beyond that node.
 * @param $avoid_types  array of node types that will not be included in the results and will not be searched through.
 * @param $init  if true, reset static variables to initial state - typically true when called but false upon recursion.
 *
 * @return  an array of nodes indexed by nid that were found while searching.
 */
function relativity_list_relatives_dfs($nid, $follow_parent_paths=1, $follow_child_paths=1, $recurse=0, $unique_node_types=0, $max_results=50, $search_types = NULL, $end_types = NULL, $avoid_types = NULL, $options=NULL, $init=1) {
  global $user;
  static $nodes      = array();
  static $node_types = array();
  static $visited    = array();
  static $trace      = array();
  static $depth      = 0;
  $search_max        = 500; //sanity check
  $max_recursion     = 500; //sanity check
  $search_sql        = array();

  // NOTE: this may be dangerous here, as we're importing items into the namespace
  // of this function.  Current known items: do_trace, my_nodes_only, display_format
  if (is_array($options)) {
    extract($options);
  }

  if ($init) {
    $nodes = array();
    $node_types = array();
    $trace = array();
    $depth = 0;
    if ($recurse == -1) {
      $recurse = $max_recursion;
    }
    if (!$max_results) {
      $max_results = 50;
    }
    $visited = array();
    //drupal_set_message("init: list_relatives_dfs($nid, follow_parent_paths: $follow_parent_paths, follow_child_paths: $follow_child_paths, recurse: $recurse, unique_node_types: $unique_node_types, max_results: $max_results, search_types: $search_types(".count($search_types)."), end_types: $end_types(".count($end_types)."), avoid_types: $avoid_types(".count($avoid_types)."), options: ".print_r($options,1).", init: $init, depth: $depth");

  }
    
  // don't traverse the same node multiple times.
  if (in_array($nid, $visited)) {
    //drupal_set_message("already visited this node ($nid)");
    return $nodes;
  }
  
  //drupal_set_message("list_relatives_dfs($nid, $follow_parent_paths, $follow_child_paths, $recurse, $unique_node_types, $max_results, $search_types(".count($search_types)."), $end_types(".count($end_types)."), $avoid_types(".count($avoid_types)."), $init), options: ".print_r($options,1).", depth: $depth");
  
  if ($follow_parent_paths) {
    $search_sql['parent'] = "SELECT parent_nid as the_nid from {relativity} where nid=%d";
  }
  
  if ($follow_child_paths) {
    $search_sql['child'] = "SELECT nid as the_nid from {relativity} where parent_nid=%d";
  }
  
  if (!is_array($search_types) || !count($search_types)) {
    $search_types = relativity_node_list();
  }
  
  if (!is_array($end_types)) {
    $end_types = array();
  }
  
  if (!is_array($avoid_types)) {
    $avoid_types = array();
  }
  
  if (count($search_types)) {
    $restrict_search_types = TRUE;
  }
  else {
    $restrict_search_types = FALSE;
  }
  
  if (is_numeric($nid)) {
    $visited[] = $nid; // log the fact that we've been here
    $cnt = 0;

    //
    // there's an odd case where the starting node needs to be included in the results
    // This code only gets executed during the initial pass
    if ($init) {
      $relative = node_load($nid);
      $relative->relativity_depth = $depth;

      //drupal_set_message("init loop, considering starting node: $nid..." . print_r($relative, 1));
      if ($relative->type && (!$my_nodes_only || $user->uid == $relative->uid) && !in_array($relative->type, $avoid_types) && node_access('view', $relative) && !($unique_node_types && isset($node_types[$relative->type]))) {
        $relative->recursion_depth = $depth;
        
        if ($restrict_search_types && isset($search_types[$relative->type])) {
          //drupal_set_message("starting node matched all the search rules, so add it to the results");
          $nodes[$obj->the_nid] = $relative;
          $node_types[$relative->type] = 1;
        }
      }
      else {
        //drupal_set_message("starting node did NOT match the search filter, so don't add it to the results - currently size ".count($nodes));
      }
    }
    
    $depth++;
    
    //drupal_set_message("following nid $nid...");
    
    foreach($search_sql as $direction=>$sql) {
      $result = db_query($sql, $nid);
      while($obj = db_fetch_object($result)) {
        
        // we've reached our maximum result set size, bail out now
        if (count($nodes) >= $max_results) {
          return $nodes;
        }

        // quick efficiency break here:
        // if unique_node_types are required, and $nodes is the same size as $search_types, we're done
        if ($unique_node_types && count($nodes) >= count($search_types)) {
          return $nodes;
        }
        
        // make sure we're not pursuing a circular path and that we stop at $search_max parents
        if ($obj->the_nid && !isset($nodes[$obj->the_nid]) && $cnt++ < $search_max) {
          $relative = node_load($obj->the_nid);
          $relative->relativity_depth = $depth;

          if ($relative->type && (!$my_nodes_only || $user->uid == $relative->uid) && !in_array($relative->type, $avoid_types) && node_access('view', $relative) && !($unique_node_types && isset($node_types[$relative->type]))) {
            $relative->recursion_depth = $depth;
            //$nodes[$obj->the_nid] = $relative->type.": ".$relative->title; // for debugging
            if ($do_trace) {
              array_push($trace, $nid);
              //drupal_set_message("list_relatives_dfs: depth: $depth, trace: ".print_r($trace, 1));
            }

            if ($restrict_search_types && isset($search_types[$relative->type])) {
              if ($do_trace) {
                $relative->relativity_trace = $trace;
              }
              $nodes[$obj->the_nid] = $relative;
              $node_types[$relative->type] = 1;
            }
            else {
              //drupal_set_message("skipping $relative->nid (type: $relative->type) as it was not in search_types: " . print_r($search_types,1));
            }
            
            if ($depth < $recurse && !in_array($relative->type, $end_types)) {
              //drupal_set_message("recursing for parent ".$obj->the_nid);
              if ($direction == 'child') {
                relativity_list_relatives_dfs($obj->the_nid, 0, 1, $recurse, $unique_node_types, $max_results, $search_types, $end_types, $avoid_types, $options, 0);
              } else {
                relativity_list_relatives_dfs($obj->the_nid, 1, 0, $recurse, $unique_node_types, $max_results, $search_types, $end_types, $avoid_types, $options, 0);
              }
            }
            if ($do_trace) {
              array_pop($trace);
            }
          }
        }
      }
    }
    $depth--; // step back up the search path
  }
  return $nodes;
}

function relativity_list_children($nid, $recurse=0, $unique_node_types=0, $max_results=50, $search_types = NULL, $end_types = NULL, $avoid_types = NULL, $do_trace=NULL) {
  return relativity_list_relatives($nid, 0, 1, $recurse, $unique_node_types, $max_results, $search_types, $end_types, $avoid_types, $do_trace);
}

function relativity_list_parents($nid, $recurse=0, $unique_node_types=0, $max_results=50, $search_types = NULL, $end_types = NULL, $avoid_types = NULL, $do_trace=NULL) {
  return relativity_list_relatives($nid, 1, 0, $recurse, $unique_node_types, $max_results, $search_types, $end_types, $avoid_types, $do_trace);
}

/**
 * Takes an array of nodes (keyed with nid) and a keyword string and checks to
 * see if any of them match search words in the search_index table
 */
function relativity_filter_keywords($search_results, $keyword_string) {
  //$keywords = preg_split("/[^A-Za-z0-9_\-\+\$\@\#\']+/", $keyword_string); // split strings on any non alpha-num char
  $keywords = preg_split("/[\s,]+/", $keyword_string); // split strings on any whitespace or comma
  
  // no valid keywords were specified, so just disregard keyword search filtering
  if (!count($keywords)) {
    return $search_results;
  }
  
  if (is_array($search_results) && count($search_results)) {
    $placeholders = array();
    foreach($keywords as $k=>$val) {
      // Remove punctuation/special characters (same rule as update_index()).
      $keywords[$k] = preg_replace("'(!|%|,|:|;|\(|\)|\&|\"|\'|\.|-|\/|\?|\\\)'", '', $val);
      $kplaceholders[] = "'%s'";
    }
    unset($search_results['']); // for some reason, there's an empty key in here that's messing things up
    
    $nids = implode(',', array_keys($search_results));
    $results = db_query("SELECT lno AS nid, SUM(count) AS rating FROM {search_index} WHERE type='node' AND lno IN ($nids) AND word IN (".implode(',', $kplaceholders).") GROUP BY lno ORDER BY rating DESC;", $keywords);
    while($obj = db_fetch_object($results)) {
      $node = $search_results[$obj->nid];
      $node->relativity_keyword_rating = $obj->rating;
      $filtered_results[$node->nid] = $node;
    }
  }
  if ($filtered_results) {
    return $filtered_results;
  }
  return array();
}

/**
 * Convenience function to load just a node's type and title for display purposes
 */
function relativity_load_nodeinfo($nid) {
  static $node_data = array();
  //drupal_set_message("loading node info for nid $nid");
  if (!$node_data[$nid]) {
    $obj = db_fetch_object(db_query('SELECT n.type, n.title FROM {node} n WHERE nid=%d', $nid));
    $node_data[$nid] = array('type'=>$obj->type, 'title'=>$obj->title);
  }
  return $node_data[$nid];
}


// *query hooks*
// queries have the following fields: 
// nid, title, target_nid, follow_parents, follow_children, recursion_depth, end_pts, avoid_pts

/**
 * implementation of hook_node_info()
 */
function relativity_query_node_info() {
  return array('relativity_query' => array('name' => t('node relativity query'), 'base' => 'relativity_query'));
}

/**
 * implementation of hook_access()
 *
 * New usage:
 * Without the flexinode hack pointing back here for access privileges, I've
 * removed the old code and now manage access to relativity_query node types.
 *
 * Old usage:
 * Iterate through all node types and disable the creation of nodes that require
 * a parent unless a $_GET['parent_node'] param is present.
 * This doesn't work using current system.  This is being called explicitly by flexinode_access.
 *
 */
function relativity_query_access($op, $node) {
  global $user;
  switch($op) {
    case 'create':
      if (user_access('create node relativity queries')) {
        return TRUE;
      }
      // what does this do? Darius 
      elseif(relativity_requires_parent($node) && !isset($_GET['parent_node'])) {
        return FALSE;
      }
      
      break;
      
    case 'update':
    case 'delete':
      if ($node->uid == $user->uid) {
        return user_access('edit own node relativity queries');
      }
      break;
    
  }
}

/**
 * Implementation of hook_form().
 */
function relativity_query_form(&$node, &$error) {
  $output = '';
  $max_search=100;

  $form['title'] = array(
    '#type' => 'textfield', 
    '#title' => t('Title'), 
    '#required' => TRUE, 
    '#default_value' => $node->title, 
    '#weight' => -5,
  );

  if ($_GET['edit']['target_nid'] && is_numeric($_GET['edit']['target_nid'])) {
    $form['target_nid'] = array(
      '#type' => 'hidden',
      '#value' => $_GET['edit']['target_nid'],
    );
  } 
  else {
    $form['target_nid'] = array(
      '#type' => 'textfield',
      '#title' => t('Specify the node id (nid) of the starting node'),
      '#default_value' => $node->target_nid,
      '#size' => 10,
      '#maxlength' => 25,
    );
  }

  // Now we define the form elements specific to our node type.
  $form['follow_parents'] = array(
    '#type' => 'checkbox',
    '#title' => t('Follow Parent Paths'),
    '#return_value' => 1,
    '#default_value' => $node->follow_parents,
    '#description' => t('If checked, the search engine will follow parent-oriented paths while searching for results.'),
  );
  $form['follow_children'] = array(
    '#type' => 'checkbox',
    '#title' => t('Follow Child Paths'),
    '#return_value' => 1,
    '#default_value' => $node->follow_children,
    '#description' => t('If checked, the search engine will follow child-oriented paths while searching for results.'),
  );
  $form['unique_types'] = array(
    '#type' => 'checkbox',
    '#title' => t('Require Unique Types'),
    '#return_value' => 1,
    '#default_value' => $node->unique_types,
    '#description' => t('If checked, the search engine will return only the first instance of each node type found.'),
  );


  $options = array();
  // $options[-1] = t('use max'); // for some reason, this is not working (and I don't care enough to fix it ;)
  foreach(range(1,$max_search) as $i) {
    $options[$i] = $i;
  }
  $form['max_results'] = array(
    '#type' => 'select',
    '#title' => t('Max Search Results'),
    '#default_value' => isset($node->max_results) ? $node->max_results : 10,
    '#options' => $options,
    '#description' => t('The maximum number of results to return.'),
  );
  $form['recursion_depth'] = array(
    '#type' => 'select',
    '#title' => t('Search Distance'),
    '#default_value' => isset($node->recursion_depth) ? $node->recursion_depth : 10,
    '#options' => $options,
    '#description' => t('How many levels away from the starting node should be searched (recursion depth)?'),
  );
  $form['search_algorithm'] = array(
    '#type' => 'select',
    '#title' => t('Search Algorithm'),
    '#default_value' => $node->search_algorithm,
    '#options' => array('dfs' => t('Depth-First Search')),
    '#description' => t('Specify the search algorithm to use for searching the network of nodes (currently, only DFS is supported).'),
  );
  $form['search_types'] = array(
    '#type' => 'select',
    '#title' => t('Search Node types'),
    '#default_value' => $node->search_types,
    '#options' => relativity_node_list(),
    '#description' => t('Specify which node types to search for.  Only include nodes of this type in the results (leave blank for all).'),
    '#multiple' => TRUE,
  );
  $form['end_pts'] = array(
    '#type' => 'select',
    '#title' => t('End Point Node types'),
    '#default_value' => $node->end_pts,
    '#options' => relativity_node_list(),
    '#description' => t('Specify which node types to stop searching beyond, but include in the results.'),
    '#multiple' => TRUE,
  );
  $form['avoid_pts'] = array(
    '#type' => 'select',
    '#title' => t('Avoid Node types'),
    '#default_value' => $node->avoid_pts,
    '#options' => relativity_node_list(),
    '#description' => t('Specify which node types to avoid completely. These will not be included in the results.'),
    '#multiple' => TRUE,
  );
  $form['display_format'] = array(
    '#type' => 'select',
    '#title' => t('Display Format'),
    '#default_value' => $node->options['display_format'] ? $node->options['display_format'] : 'title',
    '#options' => array('title'=>t('title only'),'teaser'=>t('node teaser'),'body'=>t('node body')),
    '#description' => t('Specify the way in which the results should be formatted.'),
  );
  $form['do_trace'] = array(
    '#type' => 'checkbox',
    '#title' => t('Perform Search Trace'),
    '#return_value' => 1,
    '#default_value' => $node->options['do_trace'],
    '#description' => t('If checked, the search engine will log each node that was found leading up to each result.'),
  );
  $form['my_nodes_only'] = array(
    '#type' => 'checkbox',
    '#title' => t('Only Search My Nodes'),
    '#return_value' => 1,
    '#default_value' => $node->options['my_nodes_only'],
    '#description' => t('If checked, the search engine will only consider noded created by the current user when it is searching for nodes.'),
  );
  $form['show_search_node_field'] = array(
    '#type' => 'checkbox',
    '#title' => t('Display Search Node Form Field'),
    '#return_value' => 1,
    '#default_value' => $node->options['show_search_node_field'],
    '#description' => t('If checked, the search page will display a form field allowing viewers to specify an alternate starting node.'),
  );
  $form['show_keyword_field'] = array(
    '#type' => 'checkbox',
    '#title' => t('Display Search Keyword Form Field'),
    '#return_value' => 1,
    '#default_value' => $node->options['show_keyword_field'],
    '#description' => t('If checked, the search page will display a form field allowing viewers to specify a keyword to filter the results on.'),
  );

  return $form;
}

/**
 * Implementation of hook_validate().
 *
 * This hook lets us ensure that the user entered an appropriate 
 * value before we try inserting anything into the database.
 */
function relativity_query_validate($node) {
  if ($_POST['edit'] && !$node->follow_parents && !$node->follow_children) {
    form_set_error('follow_parents', t('Either "Follow Parent Paths" or "Follow Child Paths" must be selected.'));
  }
}

/**
 * Implementation of hook_submit().
 *
 * This hook lets us ensure that the user entered 
 * an appropriate value before we try inserting anything into the database.
 */
function relativity_query_submit(&$node) {
  if ($_POST['edit'] && !$node->follow_parents && !$node->follow_children) {
    form_set_error('follow_parents', t('Either "Follow Parent Paths" or "Follow Child Paths" must be selected.'));
  }
  if (!$node->search_algorithm) {
    $node->search_algorithm = 'dfs';
  }
  if (!$node->max_results) {
    $node->max_results = 50;
  }
  // squiggle around the additional db fields
  if ($node->display_format) {
    $node->options['display_format'] = $node->display_format;
  }
  if ($node->do_trace) {
    $node->options['do_trace'] = $node->do_trace;
  }
  if ($node->my_nodes_only) {
    $node->options['my_nodes_only'] = $node->my_nodes_only;
  }
  if ($node->show_search_node_field) {
    $node->options['show_search_node_field'] = $node->show_search_node_field;
  }
  if ($node->show_keyword_field) {
    $node->options['show_keyword_field'] = $node->show_keyword_field;
  }
}

/**
 * Implementation of hook_insert().
 */
function relativity_query_insert($node) {
  db_query("INSERT INTO {relativity_query} (nid, target_nid, follow_parents, follow_children, recursion_depth, unique_types, max_results, search_algorithm, search_types, end_pts, avoid_pts, options) VALUES (%d, %d, %d, %d, %d, %d, %d, '%s', '%s', '%s', '%s', '%s')", $node->nid, $node->target_nid, $node->follow_parents, $node->follow_children, $node->recursion_depth, $node->unique_types, $node->max_results, $node->search_algorithm, serialize($node->search_types), serialize($node->end_pts), serialize($node->avoid_pts), serialize($node->options));
}

/**
 * Implementation of hook_update().
 */
function relativity_query_update($node) {
  db_query("UPDATE {relativity_query} SET target_nid = %d, follow_parents = %d, follow_children = %d, recursion_depth = %d, unique_types = %d, max_results = %d, search_algorithm = '%s', search_types = '%s', end_pts = '%s', avoid_pts = '%s', options = '%s' WHERE nid = %d", $node->target_nid, $node->follow_parents, $node->follow_children, $node->recursion_depth, $node->unique_types, $node->max_results, $node->search_algorithm, serialize($node->search_types),  serialize($node->end_pts), serialize($node->avoid_pts), serialize($node->options), $node->nid);
}

/**
 * Implementation of hook_delete().
 *
 * When a node is deleted, we need to clean up related tables.
 */
function relativity_query_delete($node) {
  db_query('DELETE FROM {relativity_query} WHERE nid = %d', $node->nid);
}

/**
 * Implementation of hook_load().
 *
 * Now that we've defined how to manage the node data in the database, we
 * need to tell Drupal how to get the node back out. This hook is called
 * every time a node is loaded, and allows us to do some loading of our own.
 */
function relativity_query_load($node) {
  $additions = db_fetch_object(db_query('SELECT target_nid, follow_parents, follow_children, recursion_depth, unique_types, max_results, search_algorithm, search_types, end_pts, avoid_pts, options FROM {relativity_query} WHERE nid = %d', $node->nid));
  $additions->search_types = unserialize($additions->search_types);
  $additions->end_pts = unserialize($additions->end_pts);
  $additions->avoid_pts = unserialize($additions->avoid_pts);
  $additions->options = unserialize($additions->options);
  return $additions;
}

/**
 * Implementation of hook_view().
 *
 * The query node is executed and results are themed.
 */
function relativity_query_view(&$node, $teaser = FALSE, $page = FALSE) {
  $node->teaser = theme('relativity_query_form',$node);
  $node->body = $node->teaser;

  if ($_GET['edit']['target_nid']) {
    $node->body .= theme('relativity_results', relativity_execute_query($node, $_GET['edit']['keywords'], $_GET['edit']['target_nid']), t('Search Results'), $node->options);
  }
  elseif($node->target_nid) {
    $node->body .= theme('relativity_results', relativity_execute_query($node, $_GET['edit']['keywords']), t('Search Results'), $node->options);
  }
}
// End of relativity_query node type hooks


/**
 * themes the output for the query search form using options within the node
 */
function theme_relativity_query_form($node) {
  if (!$node) {
    return '';
  }
  if (is_array($node->options)) {
    extract($node->options);
  }

  if ($show_search_node_field) {
    $form['target_nid'] = array(
      '#type' => 'textfield',
      '#title' => t('Specify the node id (nid) of the starting node'),
      '#default_value' => $_GET['edit']['target_nid']? $_GET['edit']['target_nid'] : $node->target_nid,
      '#size' => 10,
      '#maxlength' => 25,
    );
  }
  if ($show_keyword_field) {
    $form['keywords'] = array(
      '#type' => 'textfield',
      '#title' => t('Search Words (optional)'),
      '#default_value' => $_GET['edit']['keywords'],
      '#size' => 20,
      '#maxlength' => 255,
    );
  }
  if ($show_search_node_field || $show_keyword_field) {
    $form['submit'] = array(
      '#type' => 'submit',
      '#value' => t('Search'),
    );

    $form['#method'] = 'GET';
    $form['#action'] = url("node/$node->nid");

    return drupal_get_form('relativity_query_form', $form);
  }
  return '';
}

function theme_relativity_trace($trace="") {
  $output = "";
  if (!$trace || !is_array($trace)) {
    return "";
  }
  foreach($trace as $nid) {
    $info = relativity_load_nodeinfo($nid); // returns nid, type and title for node
    $links[] = l($info['title'], 'node/'.$nid);
  }
  return "<small>[".implode(" &gt;&gt; ", $links)."]</small>";
}

function theme_relativity_results($nodes, $title, $options) {
  if (is_array($options)) {
    extract($options);
  }
  $display_format = $display_format ? $display_format : 'title';

  //drupal_set_message("nodes: ".print_r(array_keys($nodes)));
  // sort output by node type
  foreach(relativity_sort_types() as $type) {
    foreach($nodes as $nid=>$node) {
      if ($node->type == $type) {
        switch($display_format) {
          case 'title':
            $number = module_invoke('comment', 'num_all', $node->nid);
            $items[] = "<div class='relativity_result'>".l(node_get_name($node->type).": ".$node->title, 'node/'. $node->nid, $number ? array('title' => format_plural($number, '1 comment', '%count comments')) : '') .' '. t('(Distance: %depth)', array('%depth'=>$node->recursion_depth)).theme_relativity_trace($node->relativity_trace).'</div>';
            break;
          case 'teaser':
            $items[] = "<div class='relativity_result'>".theme('fieldset', 
                       array('#title' => node_get_name($node->type). t(' (Distance: %depth)', array('%depth'=>$node->recursion_depth)).": ", 
                             '#children' => theme_relativity_trace($node->relativity_trace).node_view($node, TRUE)
                            )).'</div>';
            break;
          case 'body':
            $items[] = "<div class='relativity_result'>" . theme('fieldset', 
                       array('#title' => node_get_name($node->type). t(' (Distance: %depth)', array('%depth'=>$node->recursion_depth)).": ", 
                             '#children' => theme_relativity_trace($node->relativity_trace).node_view($node, FALSE),
                            )).'</div>';
            break;
        }
      }
    }
  }

  drupal_set_title($title);
  return $items? implode("\n", $items) : "";
}
