<?php

/**
 * @file
 *   Provides an API for building breadcrumbs.
 */

// Register the module-provided autoloader if xautoload is missing.
if (!module_exists('xautoload')) {
  spl_autoload_register('_crumbs_autoload');
}

/**
 * Implements hook_permission().
 */
function crumbs_permission() {
  return array(
    'administer crumbs' => array(
      'title' => t('Administer Crumbs'),
    ),
  );
}

/**
 * Implements hook_menu().
 */
function crumbs_menu() {
  $items = array();
  $items['admin/structure/crumbs'] = array(
    'title' => 'Crumbs',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('crumbs_admin_form'),
    'access arguments' => array('administer crumbs'),
    'file' => 'admin/crumbs.admin.inc',
  );
  $items['admin/structure/crumbs/settings'] = array(
    'title' => 'Settings',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => 0,
  );
  $items['admin/structure/crumbs/node-parent'] = array(
    'title' => 'Node parent',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('crumbs_admin_node_parent_form'),
    'access arguments' => array('administer crumbs'),
    'file' => 'admin/crumbs.node_parent.inc',
    'weight' => 1,
    'type' => MENU_LOCAL_TASK,
  );
  $items['admin/structure/crumbs/debug'] = array(
    'title' => 'Debug',
    'page callback' => 'crumbs_debug_page',
    'access arguments' => array('administer crumbs'),
    'file' => 'admin/crumbs.debug.inc',
    'weight' => 2,
    'type' => MENU_LOCAL_TASK,
  );

  return $items;
}

/**
 * Implements hook_theme().
 */
function crumbs_theme($existing, $type, $theme, $path) {
  return array(
    'crumbs_breadcrumb_link' => array(
      'item' => NULL,
    ),
    'crumbs_breadcrumb_current_page' => array(
      'item' => NULL,
    ),
  );
}

/**
 * Default theme implementation for theme('crumbs_breadcrumb_link').
 */
function theme_crumbs_breadcrumb_link(array $item) {
  $options = isset($item['localized_options']) ? $item['localized_options'] : array();
  return l($item['title'], $item['href'], $options);
}

/**
 * Default theme implementation for theme('crumbs_breadcrumb_current_page').
 */
function theme_crumbs_breadcrumb_current_page(array $item) {
  return check_plain($item['title']);
}

/**
 * Implements hook_block_info()
 */
function crumbs_block_info() {

  return array(
    'breadcrumb' => array(
      'info' => t('Breadcrumb (Crumbs)'),
      'cache' => DRUPAL_NO_CACHE,
    ),
  );
}

/**
 * Implements hook_block_view()
 */
function crumbs_block_view($delta = '') {

  $block = array();

  switch ($delta) {
    case 'breadcrumb':
      $breadcrumb_data = crumbs_get_breadcrumb_data();
      if ($breadcrumb_data) {
        $block['content'] = $breadcrumb_data['html'];
      }
      break;
  }

  return $block;
}

/**
 * Implements hook_preprocess_page().
 */
function crumbs_preprocess_page(&$vars) {
  $vars['crumbs_trail'] = array();
  $vars['breadcrumb'] = '';

  $breadcrumb_data = crumbs_get_breadcrumb_data();
  if ($breadcrumb_data) {
    $vars['crumbs_trail'] = $breadcrumb_data['trail'];
    $vars['breadcrumb'] = $breadcrumb_data['html'];
  }
}

/**
 * Implements hook_themekey_properties().
 */
function crumbs_themekey_properties() {
  $attributes = array();
  $attributes['crumbs:trail_paths'] = array(
    'description' => t('Crumbs trail paths'),
    'validator' => '',
    'page cache' => THEMEKEY_PAGECACHE_SUPPORTED,
  );

  $maps = array();
  $maps[] = array(
    'src' => 'drupal:get_q',
    'dst' => 'crumbs:trail_paths',
    'callback' => '_crumbs_themekey_path2trailpaths',
  );

  return array('attributes' => $attributes, 'maps' => $maps);
}

/**
 * Callback for themekey integration.
 */
function _crumbs_themekey_path2trailpaths($path) {
  $trail = crumbs_get_trail($path);
  $paths = array();
  foreach ($trail as $k => $item) {
    $paths[$item['alias']] = TRUE;
    $paths[$item['route']] = TRUE;
    $paths[$k] = TRUE;
  }
  return array_keys($paths);
}

/**
 * Returns the breadcrumb data for the current page.
 *
 * Gets the menu trail for the current page, and then uses it to build the
 * breadcrumb. Each link is themed separately, and then the links are passed
 * to theme('breadcrumb'), which returns the final rendered breadcrumb.
 *
 * Note: If the existing Drupal-provided breadcrumb is empty, then Crumbs
 * makes no effort to calculate its own, since it means that a module has
 * intentionally removed it.
 *
 * Breadcrumbs with one item are also ignored, to prevent the breadcrumb
 * from being shown on the frontpage.
 *
 * @return
 *   An associative array with the following keys:
 *   - trail: An array containing the menu trail of the current page.
 *   - items: An array containing the built breadcrumb.
 *   - html: The rendered breadcrumb received from theme('breadcrumb').
 *   or FALSE if the breadcrumb could not be determined.
 */
function crumbs_get_breadcrumb_data() {
  static $data;

  if (!isset($data)) {
    $existing_breadcrumb = drupal_get_breadcrumb();
    // If the existing breadcrumb is empty, that means a module has
    // intentionally removed it. Honor that, and stop here.
    if (empty($existing_breadcrumb)) {
      $data = FALSE;
      return $data;
    }

    $show_current_page = variable_get('crumbs_show_current_page', FALSE);
    $trail = crumbs_get_trail();
    if ($show_current_page) {
      // Do not show the breadcrumb if it contains only 1 item. This prevents
      // the breadcrumb from showing up on the frontpage.
      if (count($trail) == 1) {
        $data = FALSE;
        return $data;
      }
    }
    else {
      // The current page should not be shown, remove it from the trail so that
      // the plugins don't need to waste time loading its title.
      array_pop($trail);
    }

    $breadcrumb_items = crumbs_build_breadcrumb($trail);
    $router_item = crumbs_get_router_item($_GET['q']);

    // Allow modules to alter the breadcrumb, if possible, as that is much
    // faster than rebuilding an entirely new active trail.
    drupal_alter('menu_breadcrumb', $breadcrumb_items, $router_item);

    $links = array();
    $last_item = end($breadcrumb_items);
    foreach ($breadcrumb_items as $i => $item) {
      if ($show_current_page && $item == $last_item) {
        // The current page should be styled differently (no link).
        $links[$i] = theme('crumbs_breadcrumb_current_page', $item);
      }
      else {
        $links[$i] = theme('crumbs_breadcrumb_link', $item);
      }
    }

    $html = theme('breadcrumb', array(
      'breadcrumb' => $links,
      'crumbs_breadcrumb_items' => $breadcrumb_items,
      'crumbs_trail' => $trail,
    ));
    $data = array(
      'trail' => $trail,
      'items' => $breadcrumb_items,
      'html' => $html,
    );
  }

  return $data;
}

/**
 * Returns the trail for the provided path.
 *
 * @param $path
 *   The path for which the trail is built. If NULL, the url of the
 *   current page is assumed.
 *
 * @return
 *   An associative array containing the trail, with the paths as the keys, and
 *   the router items (as received from crumbs_get_router_item()) as the values.
 *
 * @see crumbs_TrailFinder
 */
function crumbs_get_trail($path = NULL) {
  static $trails = array();

  if (!isset($path)) {
    $path = $_GET['q'];
  }
  $path = drupal_get_normal_path($path);

  if (!isset($trails[$path])) {
    $finder = crumbs_get_trail_finder();
    $trails[$path] = $finder->buildTrail($path);
  }

  return $trails[$path];
}

/**
 * Builds the breadcrumb based on the provided trail.
 *
 * Each breadcrumb item is a router item taken from the trail, with
 * two additional/updated keys:
 * - title: The title of the breadcrumb item as received from a plugin.
 * - localized_options: An array of options passed to l() if needed.
 *
 * @param $trail
 *   The trail provided by crumbs_get_trail().
 *
 * @return
 *   An array of breadcrumb items.
 *
 * @see crumbs_BreadcrumbBuilder
 */
function crumbs_build_breadcrumb(array $trail) {
  $plugin_engine = crumbs_get_plugin_engine();
  $breadcrumb_builder = new crumbs_BreadcrumbBuilder($plugin_engine);
  return $breadcrumb_builder->buildBreadcrumb($trail);
}

/**
 * Loads and instantiates the trail finder.
 *
 * @return
 *   An instance of crumbs_TrailFinder.
 */
function crumbs_get_trail_finder() {
  static $trail_finder;

  if (!isset($trail_finder)) {
    $parent_finder = crumbs_get_parent_finder();
    $trail_finder = new crumbs_TrailFinder($parent_finder);
  }

  return $trail_finder;
}

/**
 * Loads and instantiates the parent finder.
 *
 * @return
 *   An instance of crumbs_ParentFinder.
 */
function crumbs_get_parent_finder() {
  static $parent_finder;

  if (!isset($parent_finder)) {
    $plugin_engine = crumbs_get_plugin_engine();
    $parent_finder = new crumbs_ParentFinder($plugin_engine);
  }

  return $parent_finder;
}

/**
 * Loads and instantiates the plugin engine.
 *
 * @return
 *   An instance of crumbs_PluginEngine.
 */
function crumbs_get_plugin_engine() {
  static $plugin_engine;

  if (!isset($plugin_engine)) {
    list($plugins, $disabled_keys) = crumbs_get_plugins();
    $weights = crumbs_get_weights();
    foreach ($disabled_keys as $key => $disabled) {
      if (!isset($weights[$key])) {
        $weights[$key] = FALSE;
      }
    }
    $plugin_engine = new crumbs_PluginEngine($plugins, $weights);
  }

  return $plugin_engine;
}

/**
 * Returns an array of loaded plugins and disabled plugin keys.
 *
 * @return
 *   An array containing two arrays:
 *   - An array of loaded plugins keyed by plugin key.
 *   - An array of disabled plugin keys.
 */
function crumbs_get_plugins() {
  static $plugins, $disabled_keys;

  if (!isset($plugins)) {
    $modules = array(
      'blog',
      'comment',
      'crumbs',
      'entityreference',
      'menu',
      'path',
      'taxonomy',
      'forum',
      'views',
    );

    // Include Crumbs-provided plugins.
    foreach ($modules as $module) {
      if (module_exists($module)) {
        module_load_include('inc', 'crumbs', 'plugins/crumbs.'. $module);
      }
    }

    // Organic groups is a special case,
    // because 7.x-2.x behaves different from 7.x-1.x.
    if (module_exists('og')) {
      if (function_exists('og_get_group')) {
        // We are using the og-7.x-1.x branch.
        module_load_include('inc', 'crumbs', 'plugins/crumbs.og');
      }
      else {
        // We are using the og-7.x-2.x branch.
        module_load_include('inc', 'crumbs', 'plugins/crumbs.og.2');
      }
    }

    $plugins = array();
    $disabled_keys = array();
    $api = new crumbs_InjectedAPI_hookCrumbsPlugins($plugins, $disabled_keys);
    foreach (module_implements('crumbs_plugins') as $module) {
      $function = $module .'_crumbs_plugins';
      $api->setModule($module);
      $function($api);
    }
  }

  return array($plugins, $disabled_keys);
}

/**
 * Returns an array of plugin keys and their weights.
 *
 * Make sure to distinguish the weight 0 from FALSE, which denotes a disabled
 * plugin.
 *
 * The "*" plugin key is a wildcard that triggers inheritance (runs all
 * implicitly enabled plugins that aren't in the provided list).
 * It always has the biggest weight.
 *
 * @return
 *   An array of plugin keys and their weights, sorted by weight.
 */
function crumbs_get_weights() {
  $weights = variable_get('crumbs_weights', array('crumbs.home_title' => 0));
  asort($weights);
  if (!isset($weights['*'])) {
    $weights['*'] = count($weights);
  }

  return $weights;
}

/**
 * Returns a router item.
 *
 * This is a wrapper around menu_get_item() that sets additional keys
 * (route, link_path, alias, fragments).
 *
 * @param $path
 *   The path for which the corresponding router item is returned.
 *   For example, node/5.
 *
 * @return
 *   The router item.
 */
function crumbs_get_router_item($path) {
  $normalpath = drupal_get_normal_path($path);
  try {
    $item = menu_get_item($normalpath);
  }
  catch (Exception $e) {
    // Some modules throw an exception, if a path has unloadable arguments.
    // We don't care, because we don't actually load this page.
    return NULL;
  }

  // Some additional keys.
  if (!empty($item) && is_array($item)) {
    // 'route' is a less ambiguous name for a router path than 'path'.
    $item['route'] = $item['path'];
    // 'href' sounds more like it had already run through url().
    $item['link_path'] = $normalpath;
    $item['alias'] = drupal_get_path_alias($normalpath);
    $item['fragments'] = explode('/', $normalpath);

    if (!isset($item['localized_options'])) {
      $item['localized_options'] = array();
    }

    return $item;
  }
}

/**
 * Chop off path fragments until we find a valid path.
 *
 * @param $path
 *   Starting path or alias
 * @param $depth
 *   Max number of fragments we try to chop off. -1 means no limit.
 */
function crumbs_reduce_path($path, $depth = -1) {
  $fragments = explode('/', $path);
  while (count($fragments) > 1 && $depth !== 0) {
    array_pop($fragments);
    $parent_path = implode('/', $fragments);
    $parent_item = crumbs_get_router_item($parent_path);
    if ($parent_item && $parent_item['href'] === $parent_item['link_path']) {
      return $parent_item['link_path'];
    }
    --$depth;
  }
}

/**
 * This function has exactly one possible input value for
 * each possible return value, except the return value FALSE.
 *
 * @param $router_path :string
 *   The router path can contain any character, but will typically
 *   have a format like "node/%/edit".
 * @return :string or FALSE
 *   A string that can be used as a method suffix,
 *   or FALSE, where that is not possible.
 *   The route "node/%/edit" will resolve as "node_x_edit".
 */
function crumbs_build_method_suffix($router_path) {
  $method_suffix = strtolower($router_path);
  $method_suffix = preg_replace('#[^a-z0-9\%]#', '_', $method_suffix);
  $method_suffix = strtr($method_suffix, array('%' => 'x'));
  $reverse = strtr($method_suffix, array('_' => '/'));
  $reverse = preg_replace(array('#/x/#', '#/x$#'), array('/%/', '/%'), $reverse);
  // we need to do this two time to catch things like "/x/x/x/x".
  $reverse = strtr($reverse, array('/x/' => '/%/'));
  if ($reverse === $router_path) {
    return $method_suffix;
  }
  return FALSE;
}

/**
 * Measures time between two runs and displays it in a human-readable format.
 *
 * @param $label
 *   String used to identify the measured time.
 *
 * @param
 *   A string with the measurement, or NULL if the function was run for the
 *   first time.
 */
function crumbs_benchmark($label = NULL) {
  static $previous_time;

  $current_time = microtime(TRUE);
  if (isset($previous_time) && isset($label)) {
    $str = 'Duration: ' . number_format(1000 * ($current_time - $previous_time), 3) . ' ms for '. $label;
  }
  $previous_time = $current_time;
  return isset($str) ? $str : NULL;
}

/**
 * Crumbs autoloader.
 *
 * Takes the class name, strips the "crumbs_" prefix, converts underscores
 * to directory separators.
 *
 * For example, crumbs_InjectedAPI_describeMonoPlugin will be loaded
 * from lib/InjectedAPI/describeMonoPlugin.php.
 *
 * @param $class
 *   The name of the class to load.
 */
function _crumbs_autoload($class) {
  if (preg_match('#^crumbs_(.*)$#', $class, $m)) {
    $path = strtr($m[1], '_', '/');
    module_load_include('php', 'crumbs', "lib/$path");
  }
}

/**
 * Clean tokens so they are URL friendly.
 * Taken directly from pathauto_clean_token_values()
 *
 * @param $replacements
 *   An array of token replacements that need to be "cleaned" for use in the URL.
 * @param $data
 *   An array of objects used to generate the replacements.
 * @param $options
 *   An array of options used to generate the replacements.
 */
function crumbs_clean_token_values(&$replacements, $data = array(), $options = array()) {
  foreach ($replacements as $token => &$value) {
    // Only clean non-path tokens.
    if (!preg_match('/(path|alias|url|url-brief)\]$/', $token)) {
      // We use a simple regex instead of pathauto_cleanstring().
      $value = preg_replace('#[^a-z0-9/]+#', '-', strtolower($value));
    }
  }
}

// ============================================ interfaces =====================

/**
 * Interface to be used internally by the plugin engine.
 */
interface crumbs_PluginOperationInterface_find {
  function invoke($plugin, $plugin_key, $weight_keeper);
}


/**
 * Interface to be used internally by the plugin engine.
 */
interface crumbs_PluginOperationInterface_alter {
  function invoke($plugin, $plugin_key);
}

// -----------------------------------------------------------------------------

/**
 * Interface for plugin objects registered with hook_crumbs_plugins().
 */
interface crumbs_MonoPlugin {

  /**
   * @param $api :crumbs_InjectedAPI_describeMonoPlugin
   *   Injected API object, with methods that allows the plugin to further
   *   describe itself.
   *
   * @return
   *   As an alternative to the API object's methods, the plugin can simply
   *   return a string label.
   */
  function describe($api);
}

// -----------------------------------------------------------------------------

/**
 * Interface for plugin objects registered with hook_crumbs_plugins().
 *
 */
interface crumbs_MultiPlugin {

  /**
   * @param $api :crumbs_InjectedAPI_describeMultiPlugin
   *   Injected API object, with methods that allow the plugin to further
   *   describe itself.
   *   The plugin is supposed to tell Crumbs about all possible rule keys, and
   *   can give a label and a description for each.
   *
   * @return
   *   As an alternative to the API object's methods, the plugin can simply
   *   return a key-value array, where the keys are the available rules, and the
   *   values are their respective labels.
   */
  function describe($api);
}
