<?php

// $Id: menu_node.module,v 1.4 2009/09/14 14:26:16 agentken Exp $

/**
 * @file
 * Menu Node API
 * Manages relationships between the {node} and {menu_links} table.
 */

/**
 * Implements hook_nodeapi().
 */
function menu_node_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
  $watch = array('insert', 'update', 'delete');

  // Do we care about this node?
  if (!in_array($op, $watch)) {
    return;
  }

  $mlid = (isset($node->menu['mlid'])) ? $node->menu['mlid'] : NULL;
  // If the node is being deleted, remove all records.
  if ($op == 'delete') {
    menu_node_delete($node->nid);
  }
  else {
    // If we have a record to insert, then do so now.
    if (empty($node->menu['delete']) && !empty($mlid)) {
      menu_node_save($node->nid, $mlid);
    }
    // If the menu item is being deleted, do so.
    else if (!empty($mlid)) {
      menu_node_delete($node->nid, $mlid);
    }
  }
}

/**
 * Get the relevant node object for a menu item.
 *
 * @param $mlid
 *   The menu link id.
 * @param $load
 *   Boolean value that indicates whether to return the node id or a full $node.
 * @return
 *   A node id, a complete node object or FALSE on failure.
 */
function menu_node_get_node($mlid, $load = TRUE) {
  $nid = db_result(db_query("SELECT n.nid FROM {node} n INNER JOIN {menu_node} mn ON n.nid = mn.nid WHERE mn.mlid = %d", $mlid));
  if (empty($nid)) {
    return FALSE;
  }
  if ($load) {
    return node_load($nid);
  }
  return $nid;
}

/**
 * Get the relevant menu links for a node.
 * @param $nid
 *   The node id.
 * @param $router
 *   Boolean flag indicating whether to attach the menu router item to the $item object.
 *   If set to TRUE, the router will be set as $item->menu_router.
 * @return
 *   An array of complete menu_link objects or an empy array on failure.
 */
function menu_node_get_links($nid, $router = FALSE) {
  $result = db_query("SELECT * FROM {menu_links} WHERE link_path = '%s'", 'node/'. $nid);
  $items = array();
  while ($data = db_fetch_object($result)) {
    if ($router) {
      $data->menu_router = menu_get_item('node/'. $nid);
    }
    $items[$data->mlid] = $data;
  }
  return $items;
}

/**
 * Get all menu links assigned to a specific menu.
 *
 * @param $menu_name
 *   The machine name of the menu, e.g. 'navigation'.
 * @return
 *   A simple array of menu link ids.
 */
function menu_node_get_links_by_menu($menu_name) {
  $links = array();
  $result = db_query("SELECT mlid FROM {menu_links} WHERE menu_name = '%s'", $menu_name);
  while ($data = db_fetch_object($result)) {
    $links[] = $data->mlid;
  }
  return $links;
}

/**
 * Get all nodes assigned to a specific menu.
 *
 * @param $menu_name
 *   The machine name of the menu, e.g. 'navigation'.
 * @param $load
 *   Boolean flag that indicates whether to load the node object or not.
 *   NOTE: This can be resource intensive!
 * @return
 *   A simple array of node ids.
 */
function menu_node_get_nodes_by_menu($menu_name, $load = FALSE) {
  $links = array();
  $result = db_query("SELECT mn.nid FROM {menu_node} mn INNER JOIN {menu_links} ml ON mn.mlid = ml.mlid WHERE ml.menu_name = '%s'", $menu_name);
  while ($data = db_fetch_object($result)) {
    if ($load) {
      $nodes[$data->nid] = node_load($data->nid);
    }
    else {
      $nodes[] = $data->nid;
    }
  }
  return $nodes;
}

/**
 * Public function for generating a tree representation of nodes in a menu.
 *
 * This function is useful for showing the relationship between nodes within
 * a given menu tree. It can be used to build options lists for forms and other
 * user interface elements.
 *
 * @param $tree
 *   The parent menu tree, generated by menu_tree_all_data().
 * @param $menu
 *   The name of the menu for which to return data.
 * @param $filter
 *   An array of menu links ids that indicate the only children to return.
 *   That is, if this array is populated, only its members and their children will
 *   be returned by this function.
 * @param $options
 *   An array of processing options. The valid options are 'marker' and 'spacer'.
 *   -- 'marker' indicates a text mark to indicate menu depth for a menu link.
 *   -- 'spacer' indicates the text string to insert betwen a marker and its link title.
 * @return
 *   A nested array of menu data.
 */
function menu_node_tree($tree, $menu = NULL, $filter = array(), $options = array()) {
  $options += array('marker' => '-', 'spacer' => ' ');
  $data = array();
  if (!empty($menu)) {
    _menu_node_tree($data, $menu, $value, $filter, $options['marker'], $options['spacer']);
    return $data[$menu];
  }
  else {
    foreach ($tree as $key => $value) {
      _menu_node_tree($data, $key, $value, $filter, $options['marker'], $options['spacer']);
    }
  }
  return $data;
}

/**
 * A private recursive sort function.
 *
 * Given a menu tree, return its child node items.
 *
 * @param $tree
 *   The recursive tree data.
 * @param $menu
 *   The menu that this data belongs to.
 * @param $data
 *   The tree data for this menu element.
 * @param $parents
 *   An array of menu link ids indicating the tree elements to return.
 * @param $marker
 *   A string (or NULL) to prepend to the menu link title, indicating relative depth.
 * @param $spacer
 *   A string (or NULL) to place between the marker and the title.
 * @return
 *   No return. Modify $tree by reference.
 */
function _menu_node_tree(&$tree, $menu, $data, $parents, $marker = NULL, $spacer = NULL) {
  if (empty($tree)) {
    $tree = array();
  }
  if (empty($parents)) {
    $parents = array();
  }
  if (in_array($data['link']['mlid'], $parents)) {
    $parent = menu_node_get_parent($data['link']);
    $tree[$parent][$data['link']['mlid']] = str_repeat($marker, $data['link']['depth']) . $spacer . $data['link']['title'] . ' ';
  }
  if (!empty($data['below'])) {
    // Recursive processing joy!
    foreach ($data['below'] as $value) {
      _menu_node_tree($tree, $menu, $value, $parents, $marker, $spacer);
    }
  }
}

/**
 * Return the parent item of a menu element.
 *
 * @param $item
 *   The menu item.
 * @param $return
 *   Indicates the value to return, options are:
 *   -- 'title' returns the name of the parent menu item.
 *   -- 'item' returns the entire parent, as loaded by menu_get_item().
 * @return
 *   A string, representing the parent name or a menu object.
 */
function menu_node_get_parent($item, $return = 'title') {
  if ($return == 'title') {
    return db_result(db_query("SELECT link_title FROM {menu_links} WHERE mlid = %d", $item['p1']));
  }
  $path = db_result(db_query("SELECT link_path FROM {menu_links} WHERE mlid = %d", $item['p1']));
  return menu_get_item($path);
}

/**
 * Implements hook_form_alter().
 *
 * React to the editing of custom menu items.
 */
function menu_node_form_menu_edit_item_alter(&$form, $form_state) {
  $form['#submit'][] = 'menu_node_edit_form_submit';
}

/**
 * Implements hook_form_alter().
 *
 * React to the deletion of custom menu items.
 */
function menu_node_form_menu_item_delete_form_alter(&$form, $form_state) {
  $form['mlid'] = array('#type' => 'value', '#value' => $form['#item']['mlid']);
  $form['#submit'][] = 'menu_node_delete_form_submit';
  // Our submit _must_ be run first.
  $form['#submit'] = array_reverse($form['#submit']);
}

/**
 * Implements hook_form_alter().
 *
 * React to the deletion of entire menus.
 */
function menu_node_form_menu_delete_menu_confirm_alter(&$form, $form_state) {
  $form['menu_name'] = array('#type' => 'value', '#value' => $form['#menu']['menu_name']);
  $form['#submit'][] = 'menu_node_delete_menu_form_submit';
  // Our submit _must_ be run first.
  $form['#submit'] = array_reverse($form['#submit']);
}

/**
 * Custom form handler to react to menu changes.
 */
function menu_node_edit_form_submit($form, &$form_state) {
  $menu = $form_state['values']['menu'];
  // Is this a node item?
  if (count($menu['parts']) == 2 && $menu['parts'][0] = 'node' && is_numeric($menu['parts'][1])) {
    menu_node_save($menu['parts'][1], $menu['mlid']);
  }
}

/**
 * Save records to the {menu_node} table.
 *
 * After saving, we fire the appropriate menu_node hook,
 * either 'insert' or 'update'.
 *
 * @param $nid
 *   The node id.
 * @param $mlid
 *   The menu link id.
 */
function menu_node_save($nid, $mlid, $hook = 'update') {
  $new = menu_node_exists($nid, $mlid);
  // We only save if the record does not exist, otherwise, call the update hook.
  if (empty($new)) {
    $record = array('nid' => $nid, 'mlid' => $mlid);
    drupal_write_record('menu_node', $record);
    $hook = 'insert';
  }
  _menu_node_invoke($nid, $mlid, $hook);
}

/**
 * Wrapper function for module hooks.
 *
 * @param $nid
 *   The node id.
 * @param $mlid
 *   The menu link id.
 * @param $hook
 *   The hook to invoke ('insert', 'update', or 'delete').
 */
function _menu_node_invoke($nid, $mlid, $hook) {
  // Use our internal lookup fuinctions.
  $node = menu_node_get_node($mlid);
  $items = menu_node_get_links($nid);
  module_invoke_all('menu_node_'. $hook, $items[$mlid], $node);
}

/**
 * Check to see if a specific nid/mlid combination exists.
 *
 * @param $nid
 *   The node id.
 * @param $mlid
 *   The menu link id.
 * @return
 *   The count of matches (which should be 1 or 0).
 */
function menu_node_exists($nid, $mlid) {
  return db_result(db_query("SELECT COUNT(nid) FROM {menu_node} WHERE nid = %d AND mlid = %d", $nid, $mlid));
}

/**
 * Custom form handler to react to menu item changes.
 */
function menu_node_delete_form_submit($form, &$form_state) {
  $mlid = $form_state['values']['mlid'];
  $nid = menu_node_get_node($mlid, FALSE);
  // Is this a node item?
  if (!empty($nid)) {
    menu_node_delete($nid, $mlid);
  }
}

/**
 * Custom form handler to react to custom menu changes.
 */
function menu_node_delete_menu_form_submit($form, &$form_state) {
  $menu_name= $form_state['values']['menu_name'];
  $items = menu_node_get_links_by_menu($menu_name);
  // We pass these individually in case any hook implementations care.
  foreach ($items as $mlid) {
    $nid = menu_node_get_node($mlid, FALSE);
    menu_node_delete($nid, $mlid);
  }
}

/**
 * Delete a record from {menu_node} and run hook_menu_node_delete().
 *
 * We deliberately run the hook before the delete, in case any module
 * wishes to run a JOIN on the {menu_node} table.
 *
 * @param $nid
 *   The node id.
 * @param $mlid
 *   The menu link id.
 * @return
 *   No return. hook_menu_node_delete() is invoked.
 */
function menu_node_delete($nid, $mlid = NULL) {
  if (!empty($mlid)) {
    _menu_node_invoke($nid, $mlid, 'delete');
    db_query("DELETE FROM {menu_node} WHERE nid = %d AND mlid = %d", $nid, $mlid);
    return;
  }
  $result = db_query("SELECT mlid FROM {menu_node} WHERE nid = %d", $nid);
  while($data = db_fetch_object($result)) {
    _menu_node_invoke($nid, $data->mlid, 'delete');
    // Run the deletes one at a time, to perserve accurate JOINs.
    db_query("DELETE FROM {menu_node} WHERE nid = %d AND mlid = %d", $nid, $data->mlid);
  }
}
