<?php

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

define('CRUMBS_SHOW_CURRENT_PAGE', 1);
define('CRUMBS_TRAILING_SEPARATOR', 2);
define('CRUMBS_SHOW_CURRENT_PAGE_SPAN', 5);
define('CRUMBS_SHOW_CURRENT_PAGE_LINK', 9);

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

/**
 * 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");
  }
}


// Info hook implementations
// -----------------------------------------------------------------------------

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

/**
 * Implements hook_help()
 */
function crumbs_help($path, $arg) {
  switch ($path) {
    case 'admin/structure/crumbs':
    case 'admin/structure/crumbs/weights':
      // TODO: Isn't there a better way to define long translated strings?
      $help = <<<EOT
<p>To build a breadcrumb trail, Crumbs takes the system path of the current
page, and determines a "parent path". This process is repeated with the parent,
until it arrives at the front page path, or until a loop is detected.</p>
<p>There are plenty of criteria available, that Crumbs can use to find a parent
path. This settings form allows to enable, disable and prioritize these
criteria.</p>
<p>Criteria further up in the "Enabled" section take precedence over those
further down. Criteria in the "Disabled" section have their effect nullified.
Criteria in the "Inherit / automatic" section inherit the status from
the parent wildcard criterion, unless they are "disabled by default".</p>
EOT;
      return t($help);
  }
}

/**
 * Implements hook_menu().
 */
function crumbs_menu() {
  $base = 'admin/structure/crumbs';
  $items = array();
  $items["$base"] = array(
    'title' => 'Crumbs',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('crumbs_admin_weights_form'),
    'access arguments' => array('administer crumbs'),
    'file' => 'admin/crumbs.admin.inc',
  );
  $items["$base/weights"] = array(
    'title' => 'Plugin weights',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );
  $items["$base/display"] = array(
    'title' => 'Display',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('crumbs_admin_display_form'),
    'access arguments' => array('administer crumbs'),
    'file' => 'admin/crumbs.admin.inc',
    'type' => MENU_LOCAL_TASK,
    'weight' => 0,
  );
  $items["$base/entity-parent"] = array(
    'title' => 'Entity parent',
    'weight' => 10,
    'type' => MENU_LOCAL_TASK,
  );
  $items["$base/debug"] = array(
    'title' => 'Debug',
    'page callback' => 'crumbs_debug_page',
    'access arguments' => array('administer crumbs'),
    'file' => 'admin/crumbs.debug.inc',
    'weight' => 20,
    'type' => MENU_LOCAL_TASK,
  );

  foreach (array(
    'node' => 'Node',
    'taxonomy_term' => module_exists('taxonomy') ? 'Taxonomy' : FALSE,
    'user' => 'User',
  ) as $entity_type => $title) {
    if (FALSE !== $title) {
      $entity_slug = str_replace('_', '-', $entity_type);
      $items["$base/entity-parent/$entity_slug"] = array(
        'title' => $title,
        'page callback' => 'drupal_get_form',
        'page arguments' => array('crumbs_admin_entity_parent_form', $entity_type),
        'access arguments' => array('administer crumbs'),
        'file' => 'admin/crumbs.entity_parent.inc',
        'type' => MENU_LOCAL_TASK,
      );
    }
  }

  $items["$base/entity-parent"] += $items["$base/entity-parent/node"];

  $items["$base/entity-parent/node"] = array(
    'title' => 'Node',
    'weight' => -10,
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );

  if (module_exists('menu')) {
    $items['crumbs/special-menu-item/%menu_link'] = array(
      'access callback' => TRUE,
      'page callback' => 'crumbs_special_menu_link_page',
      'page arguments' => array(2),
      'title callback' => 'crumbs_special_menu_link_title',
      'title arguments' => array(2),
      'type' => MENU_CALLBACK,
    );
  }

  return $items;
}

/**
 * @param array $menu_link
 */
function crumbs_special_menu_link_page(array $menu_link) {
  drupal_goto('<front>');
}

/**
 * @param array $menu_link
 */
function crumbs_special_menu_link_title(array $menu_link) {
  return $menu_link['title'];
}

/**
 * Implements hook_theme().
 */
function crumbs_theme($existing, $type, $theme, $path) {
  return array(
    'crumbs_breadcrumb_link' => array(
      'item' => NULL,
    ),
    'crumbs_breadcrumb_current_page' => array(
      'variables' => array('item' => NULL, 'show_current_page' => TRUE),
    ),
    'crumbs_weights_tabledrag' => array(
      'file' => 'admin/crumbs.tabledrag.inc',
      'render element' => 'element',
    ),
    'crumbs_weights_textual' => array(
      'file' => 'admin/crumbs.textual.inc',
      'render element' => 'element',
    ),
    'crumbs_weights_expansible' => array(
      'file' => 'admin/crumbs.expansible.inc',
      'render element' => 'element',
    ),
  );
}

/**
 * Implements hook_theme_registry_alter()
 */
function crumbs_theme_registry_alter(array &$registry) {
  if (!isset($registry['breadcrumb']['function'])) {
    return;
  }

  // Figure out which theme this applies to.
  if (version_compare(PHP_VERSION, '5.4.0', '>=')) {
    $trace = debug_backtrace(0, 3);
  }
  else {
    // Second parameter not supported in PHP < 5.4.0. It would cause a
    // "Warning: debug_backtrace() expects at most 1 parameter, 2 given".
    $trace = debug_backtrace(0);
  }
  if (1
    && isset($trace[2]['function'])
    && '_theme_build_registry' === $trace[2]['function']
    && isset($trace[2]['args'][0])
  ) {
    // Get the first argument of _theme_build_registry().
    $theme = $trace[2]['args'][0];
  }
  else {
    // The hook is called from a weird place.
    return;
  }

  // Remember the original theme function for the settings page.
  $f = $registry['breadcrumb']['function'];
  $originals = variable_get('crumbs_original_theme_breadcrumb', array());
  $originals[$theme->name] = $f;
  variable_set('crumbs_original_theme_breadcrumb', $originals);

  $settings = variable_get('crumbs_override_theme_breadcrumb', array('theme_breadcrumb'));
  if (in_array($f, $settings, TRUE)) {
    $registry['breadcrumb']['function'] = 'crumbs_theme_breadcrumb';
  }
}

/**
 * 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);
}

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

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


// Page hook implementations
// -----------------------------------------------------------------------------

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

  $block = array();

  switch ($delta) {
    case 'breadcrumb':
      $html = crumbs()->page->breadcrumbHtml;
      if (!empty($html)) {
        $block['content'] = $html;
      }
      break;
  }

  return $block;
}

/**
 * Implements hook_preprocess_page().
 */
function crumbs_preprocess_page(&$vars) {

  $vars['crumbs_trail'] = crumbs()->page->trail;
  $vars['breadcrumb'] = crumbs()->page->breadcrumbHtml;
}


// Theme implementations
// -----------------------------------------------------------------------------

/**
 * Replacement theme callback for theme('breadcrumb').
 *
 * @param $variables
 * @return string
 *   Rendered breadcrumb HTML
 */
function crumbs_theme_breadcrumb($variables) {
  $breadcrumb = $variables['breadcrumb'];

  if (empty($breadcrumb)) {
    return;
  }

  // These settings may be missing, if theme('breadcrumb') is called from
  // somewhere outside of Crumbs, or if another module is messing with the theme
  // registry.
  $variables += array(
    'crumbs_trailing_separator' => FALSE,
    'crumbs_separator' => ' &raquo; ',
    'crumbs_separator_span' => FALSE,
  );

  $separator = $variables['crumbs_separator'];
  if ($variables['crumbs_separator_span']) {
    $separator = '<span class="crumbs-separator">' . $separator . '</span>';
  }

  $output = implode($separator, $breadcrumb);
  if ($variables['crumbs_trailing_separator']) {
    $output .= $separator;
  }

  $output = '<div class="breadcrumb">' . $output . '</div>';

  // Provide a navigational heading to give context for breadcrumb links to
  // screen-reader users. Make the heading invisible with .element-invisible.
  return '<h2 class="element-invisible">' . t('You are here') . '</h2>' . $output;
}

/**
 * Default theme implementation for theme('crumbs_breadcrumb_link').
 */
function theme_crumbs_breadcrumb_link(array $item) {
  if ('<nolink>' === $item['href']) {
    return check_plain($item['title']);
  }
  else {
    $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 $variables) {
  $item = $variables['item'];
  switch ($variables['show_current_page']) {
    case CRUMBS_SHOW_CURRENT_PAGE_SPAN:
      return '<span class="crumbs-current-page">' . check_plain($item['title']) . '</span>';
    case CRUMBS_SHOW_CURRENT_PAGE_LINK:
      $options = isset($item['localized_options']) ? $item['localized_options'] : array();
      return l($item['title'], $item['href'], $options);
    default:
      return check_plain($item['title']);
  }
}

/**
 * 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);
}


// Public API functions
// -----------------------------------------------------------------------------

/**
 * 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 array
 *   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() {
  return crumbs()->page->breadcrumbData;
}

/**
 * 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 mixed
 *   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) {
  if (!isset($path)) {
    $path = $_GET['q'];
  }
  return crumbs()->trails->getForPath($path);
}

/**
 * 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 array
 *   The router item.
 */
function crumbs_get_router_item($path) {
  return crumbs()->router->getRouterItem($path);
}

/**
 * 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) {
  return crumbs()->router->reducePath($path, $depth);
}

/**
 * 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()) {
  crumbs_Util::cleanTokenValues($replacements, $data, $options);
}


// Service cache
// -----------------------------------------------------------------------------

/**
 * Get the service with the given key.
 * If it does not exist, it will be lazy-created with all its dependencies.
 *
 * @param string $key
 *
 * @return crumbs_Container_LazyServices
 */
function crumbs($key = NULL) {
  static $cache = NULL;
  if (!isset($cache)) {
    $factory = new crumbs_ServiceFactory();
    $cache = new crumbs_Container_LazyServices($factory);
  }
  if (isset($key)) {
    return $cache->__get($key);
  }
  else {
    return $cache;
  }
}


// Admin tabledrag element
// -----------------------------------------------------------------------------

/**
 * Implements hook_element_info()
 */
function crumbs_element_info() {
  $types = array();
  // Nice documentation to be found in
  // http://api.drupal.org/api/drupal/includes%21form.inc/function/form_builder/7
  $common = array(
    '#input' => TRUE,
    '#value_callback' => '_crumbs_element_value_callback',
    '#process' => array('_crumbs_element_process'),
    '#after_build' => array('_crumbs_element_after_build'),
    '#element_validate' => array('_crumbs_element_validate'),
    '#pre_render' => array('_crumbs_element_pre_render'),
    '#tree' => TRUE,
  );
  $types['crumbs_weights_tabledrag'] = array(
    '#theme' => 'crumbs_weights_tabledrag',
  ) + $common;
  $types['crumbs_weights_textual'] = array(
    '#theme' => 'crumbs_weights_textual',
  ) + $common;
  $types['crumbs_weights_expansible'] = array(
    '#theme' => 'crumbs_weights_expansible',
  ) + $common;
  return $types;
}

/**
 * Value callback for tabledrag element ($element['#value_callback']).
 */
function _crumbs_element_value_callback(&$element, $input = FALSE, $form_state = array()) {
  return _crumbs_element_object($element)->value_callback($element, $input, $form_state);
}

/**
 * Processor callback for tabledrag element ($element['#process']).
 * See form_process_checkboxes() for an example how these processors work.
 */
function _crumbs_element_process($element, $form_state) {
  return _crumbs_element_object($element)->process($element, $form_state);
}

/**
 * After build callback for tabledrag element ($element['#after_build']).
 */
function _crumbs_element_after_build($element, $form_state) {
  return _crumbs_element_object($element)->after_build($element, $form_state);
}

/**
 * Pre-render callback for tabledrag element ($element['#pre_render']).
 */
function _crumbs_element_pre_render($element) {
  return _crumbs_element_object($element)->pre_render($element);
}

/**
 * Validation callback for tabledrag element ($element['#element_validate']).
 */
function _crumbs_element_validate(&$element, &$form_state) {
  return _crumbs_element_object($element)->validate($element, $form_state);
}

/**
 * Lazy-create an object representing the form element.
 * This allows to use methods instead of procedural callbacks.
 *
 * There will be one instance per element, so the object can actually hold some
 * state information.
 *
 * This mechanic would even make sense as a reusable module, but for now we just
 * have it crumbs-specific.
 */
function _crumbs_element_object(&$element) {
  if (!isset($element['#crumbs_element_object'])) {
    switch ($element['#type']) {
      case 'crumbs_weights_tabledrag':
        $obj = new crumbs_Admin_ElementObject_WeightsTabledrag($element);
        break;
      case 'crumbs_weights_textual':
        $obj = new crumbs_Admin_ElementObject_WeightsTextual($element);
        break;
      case 'crumbs_weights_expansible':
        $obj = new crumbs_Admin_ElementObject_WeightsExpansible($element);
        break;
      default:
        throw new Exception('Unknown element type "' . $element['#type'] . '".');
    }
    $element['#crumbs_element_object'] = $obj;
  }
  return $element['#crumbs_element_object'];
}

/**
 * Submit callback registered on crumbs admin forms in addition to the usual
 * system_settings_form stuff.
 */
function _crumbs_admin_flush_cache() {

  // This will only hit the 'cache_page' and 'cache_block' cache bins.
  cache_clear_all();

  // Clear plugin info in 'cache' cache bin.
  crumbs()->pluginInfo->flushCaches();
}

/**
 * Callback to be registered with ini_set('unserialize_callback_func', *)
 *
 * @param string $class
 * @throws crumbs_UnserializeException
 */
function _crumbs_unserialize_failure($class) {
  throw new crumbs_UnserializeException();
}