<?php

require('context.core.inc');

define('CONTEXT_GET', 0);
define('CONTEXT_SET', 1);
define('CONTEXT_ISSET', 2);
define('CONTEXT_CLEAR', 3);

define('CONTEXT_CONDITION_MODE_OR', 0);
define('CONTEXT_CONDITION_MODE_AND', 1);

/**
 * Master context function. Avoid calling this directly -- use one of the helper functions below.
 *
 * @param $op
 *   The operation to perform - handled by the context helper functions. Use them.
 * @param $namespace
 *   A string to be used as the namespace for the context information.
 * @param $attribute
 *   Usually a string to be used as a key to set/retrieve context information. An array can
 *   also be used when setting context to establish an entire context namespace at once.
 *   (At some point objects may also be accepted, but currently functionaliy isn't complete.)
 * @param $value
 *   A value to set for the provided key. If omitted the value will be set to true.
 *
 * @return
 *   Either the requested value, or false if the operation fails.
 */
function context_context($op = CONTEXT_GET, $namespace = NULL, $attribute = NULL, $value = NULL) {
  static $context;
  $context = !$context ? array() : $context;
  switch ($op) {
    case CONTEXT_GET:
      // return entire context
      if (!$namespace) {
        return $context;
      }
      // return entire space if set
      elseif (isset($context[(string) $namespace])) {
        // return val of key from space
        if (is_array($context[(string) $namespace]) && isset($context[(string) $namespace][(string) $attribute])) {
          return $context[(string) $namespace][(string) $attribute];
        }
        elseif (!$attribute) {
          return $context[(string) $namespace];
        }
      }
      break;
    case CONTEXT_SET:
      // bail if invalid space is specified or context is already set
      if (is_string($namespace) || is_int($namespace)) {
        // initialize namespace if no key is specified
        if (!$attribute) {
          $context[(string) $namespace] = array();
          return TRUE;
        }
        // set to true if key is a usable identifier. otherwise, allow a key or object to be inserted
        if ($value === NULL) {
          if (is_string($attribute) || is_int($attribute)) {
            $context[(string) $namespace][(string) $attribute] = TRUE;
            return TRUE;
          }
          elseif (is_array($attribute) || is_object($attribute)) {
            $context[(string) $namespace] = $attribute;
            return TRUE;
          }
        }
        // set value if key is valid
        if ((is_string($attribute) || is_int($attribute)) && $value !== NULL) {
          $context[$namespace][$attribute] = $value;
          return TRUE;
        }
      }
      break;
    case CONTEXT_ISSET:
      // return entire context
      if (!$namespace) return FALSE;
      if (!$attribute) {
        // return entire space if set
        return isset($context[$namespace]);
      }
      // return val of key from space
      return isset($context[$namespace][$attribute]);
    case CONTEXT_CLEAR:
      $context = array();
      return TRUE;
  }
  return FALSE;
}

/**
 * Sets a context by namespace + attribute.
 */
function context_set($namespace, $attribute = NULL, $value = NULL) {
  return context_context(CONTEXT_SET, $namespace, $attribute, $value);
}

/**
 * Retrieves a context by namespace + (optional) attribute.
 */
function context_get($namespace = NULL, $attribute = NULL) {
  return context_context(CONTEXT_GET, $namespace, $attribute, NULL);
}

/**
 * Returns a boolean for whether a context namespace + attribute have been set.
 */
function context_isset($namespace = NULL, $attribute = NULL) {
  return context_context(CONTEXT_ISSET, $namespace, $attribute, NULL);
}

/**
 * Deprecated context_exists() function. Retained for backwards
 * compatibility -- please use context_isset() instead.
 */
function context_exists($namespace = NULL, $attribute = NULL) {
  return context_context(CONTEXT_ISSET, $namespace, $attribute, NULL);
}

/**
 * Clears static context array() -- meant only for testing
 */
function context_clear() {
  return context_context(CONTEXT_CLEAR);
}

/**
 * Implemented hooks ==================================================
 */

/**
 * Implementation of hook_ctools_plugin_type().
 */
function context_ctools_plugin_type() {
  return array(
    'plugins' => array(
      'cache' => TRUE,
      'use hooks' => TRUE,
      'classes' => array('handler'),
    ),
  );
}

/**
 * Implementation of hook_context_plugins().
 *
 * This is a ctools plugins hook.
 */
function context_context_plugins() {
  module_load_include('inc', 'context', 'context.plugins');
  return _context_context_plugins();
}


/**
 * Implementation of hook_context_registry().
 */
function context_context_registry() {
  module_load_include('inc', 'context', 'context.plugins');
  return _context_context_registry();
}

/**
 * Implementation of hook_init().
 */
function context_init() {
  if ($plugin = context_get_plugin('condition', 'sitewide')) {
    $plugin->execute(1);
  }
  if ($plugin = context_get_plugin('condition', 'path')) {
    $plugin->execute();
  }
  if ($plugin = context_get_plugin('condition', 'query_string')) {
    $plugin->execute();
  }
  if ($plugin = context_get_plugin('condition', 'language')) {
    global $language;
    $plugin->execute($language->language);
  }
  if ($plugin = context_get_plugin('condition', 'user')) {
    global $user;
    $plugin->execute($user);
  }
}

/**
 * Implementation of hook_preprocess_menu_link().
 *
 * This allows menus that are not primary/secondary menus to get
 * the "active" class assigned to them. This assumes they are using
 * theme('menu_link') for the menu rendering to html.
 */
function context_preprocess_menu_link(&$variables) {
  if ($contexts = context_active_contexts()) {
    foreach ($contexts as $context) {
      if (isset($context->reactions['menu'])) {
        // In context module < v3.2 the url was a string. In version 3.3+ this is
        // an array of urls. Implement interims BC layer.
        //
        // Examples:
        // - OLD < v3.2 context reaction structure:
        // array('menu' => 'taxonomy/term/1')
        //
        // - NEW 3.3+ context reaction structure:
        // array(
        //   'menu' => array(
        //     0 => 'navigation:taxonomy/term/1'
        //     1 => 'foo-menu:taxonomy/term/1'
        //   )
        // )
        $reactions_menu = is_array($context->reactions['menu']) ? array_values($context->reactions['menu']) : array($context->reactions['menu']);

        // Get everything after the first ':' character (if found) as the url to
        // match against element '#href'.
        $urls = array();
        foreach ($reactions_menu as $url) {
          if (strpos($url, ':') !== FALSE) {
            // Get unique menu name 'navigation' from 'navigation:taxonomy/term/1'
            $reaction_menu = explode(':', $url);
            $path = $reaction_menu[1];
            $urls[$path] = $reaction_menu[0];
          }
          else {
            // BC layer for menu contexts that have not re-saved. This is for
            // urls like 'taxonomy/term/1'. We need to add a fake menu key
            // 'bc-context-menu-layer' or the BC link get's removed by
            // array_intersect below.
            //
            // @TODO: Remove BC layer in 4.x
            $urls[$url] = 'context-reaction-menu-bc-layer';
          }
        }

        // Filter urls by the menu name of the current link. The link reaction
        // can be configured per menu link in specific menus and the contect
        // reaction should not applied to other menus with the same menu link.
        $menu_name = $variables['element']['#original_link']['menu_name'];
        $menu_paths = array_intersect($urls, array($menu_name, 'context-reaction-menu-bc-layer'));
        $reaction_menu_paths = array_keys($menu_paths);

        // - If menu href and context reaction menu url match, add the 'active'
        //   css class to the link of this menu.
        // - Do not add class twice on current page.
        if (in_array($variables['element']['#href'], $reaction_menu_paths) && $variables['element']['#href'] != $_GET['q']) {
          // Initialize classes array if not set.
          if (!isset($variables['element']['#localized_options']['attributes']['class'])) {
            $variables['element']['#localized_options']['attributes']['class'] = array();
          }

          // Do not add the 'active' class twice in views tabs.
          if (!in_array('active', $variables['element']['#localized_options']['attributes']['class'])) {
            $variables['element']['#localized_options']['attributes']['class'][] = 'active';
          }
        }
      }
    }
  }
}

/**
 * Load & crud functions ==============================================
 */

/**
 * Context loader.
 *
 * @param $name
 *   The name for this context object.
 *
 * @return
 *   Returns a fully-loaded context definition.
 */
function context_load($name = NULL, $reset = FALSE) {
  ctools_include('export');
  static $contexts;
  static $altered;
  if (!isset($contexts) || $reset) {
    $contexts = $altered = array();
    if (!$reset && $contexts = context_cache_get('context')) {
      // Nothing here.
    }
    else {
      if ($reset) {
        ctools_export_load_object_reset('context');
      }
      $contexts = ctools_export_load_object('context', 'all');
      context_cache_set('context', $contexts);
    }
  }
  if (isset($name)) {
    // Allow other modules to alter the value just before it's returned.
    if (isset($contexts[$name]) && !isset($altered[$name])) {
      $altered[$name] = TRUE;
      drupal_alter('context_load', $contexts[$name]);
    }
    return isset($contexts[$name]) ? $contexts[$name] : FALSE;
  }
  return $contexts;
}

/**
 * Inserts or updates a context object into the database.
 * @TODO: should probably return the new cid on success -- make sure
 * this doesn't break any checks elsewhere.
 *
 * @param $context
 *   The context object to be inserted.
 *
 * @return
 *   Returns true on success, false on failure.
 */
function context_save($context) {
  $existing = context_load($context->name, TRUE);
  if ($existing && ($existing->export_type & EXPORT_IN_DATABASE)) {
    drupal_write_record('context', $context, 'name');
  }
  else {
    drupal_write_record('context', $context);
  }
  context_load(NULL, TRUE);
  context_invalidate_cache();
  return TRUE;
}

/**
 * Deletes an existing context.
 *
 * @param $context
 *   The context object to be deleted.
 *
 * @return
 *   Returns true on success, false on failure.
 */
function context_delete($context) {
  if (isset($context->name) && ($context->export_type & EXPORT_IN_DATABASE)) {
    db_query("DELETE FROM {context} WHERE name = :name", array(':name' => $context->name));
    context_invalidate_cache();
    return TRUE;
  }
  return FALSE;
}

/**
 * Exports the specified context.
 */
function context_export($context, $indent = '') {
  $output = ctools_export_object('context', $context, $indent);
  $translatables = array();
  foreach (array('description', 'tag') as $key) {
    if (!empty($context->{$key})) {
      $translatables[] = $context->{$key};
    }
  }
  $translatables = array_filter(array_unique($translatables));
  if (!empty($translatables)) {
    $output .= "\n";
    $output .= "{$indent}// Translatables\n";
    $output .= "{$indent}// Included for use with string extractors like potx.\n";
    sort($translatables);
    foreach ($translatables as $string) {
      $output .= "{$indent}t(" . ctools_var_export($string) . ");\n";
    }
  }
  return $output;
}

/**
 * API FUNCTIONS ======================================================
 */

/**
 * CTools list callback for bulk export.
 */
function context_context_list() {
  $contexts = context_load(NULL, TRUE);
  $list = array();
  foreach ($contexts as $context) {
    $list[$context->name] = $context->name;
  }
  return $list;
}

/**
 * Wrapper around cache_get() to make it easier for context to pull different
 * datastores from a single cache row.
 */
function context_cache_get($key, $reset = FALSE) {
  static $cache;
  if (!isset($cache) || $reset) {
    $cache = cache_get('context', 'cache');
    $cache = $cache ? $cache->data : array();
  }
  return !empty($cache[$key]) ? $cache[$key] : FALSE;
}

/**
 * Wrapper around cache_set() to make it easier for context to write different
 * datastores to a single cache row.
 */
function context_cache_set($key, $value) {
  $cache = cache_get('context', 'cache');
  $cache = $cache ? $cache->data : array();
  $cache[$key] = $value;
  cache_set('context', $cache);
}

/**
 * Wrapper around context_load() that only returns enabled contexts.
 */
function context_enabled_contexts($reset = FALSE) {
  $enabled = array();
  foreach (context_load(NULL, $reset) as $context) {
    if (empty($context->disabled)) {
      $enabled[$context->name] = $context;
    }
  }
  return $enabled;
}

/**
 * Queue or activate contexts that have met the specified condition.
 *
 * @param $context
 *   The context object to queue or activate.
 * @param $condition
 *   String. Name for the condition that has been met.
 * @param $reset
 *   Reset flag for the queue static cache.
 */
function context_condition_met($context, $condition, $reset = FALSE) {
  static $queue;
  if (!isset($queue) || $reset) {
    $queue = array();
  }
  if (!context_isset('context', $context->name)) {
    // Context is using AND mode. Queue it.
    if (isset($context->condition_mode) && $context->condition_mode == CONTEXT_CONDITION_MODE_AND) {
      $queue[$context->name][$condition] = $condition;

      // If all conditions have been met. set the context.
      if (!array_diff(array_keys($context->conditions), $queue[$context->name])) {
        context_set('context', $context->name, $context);
      }
    }
    // Context is using OR mode. Set it.
    else {
      context_set('context', $context->name, $context);
    }
  }
}

/**
 * Loads any active contexts with associated reactions. This should be run
 * at a late stage of the page load to ensure that relevant contexts have been set.
 */
function context_active_contexts() {
  $contexts = context_get('context');
  return !empty($contexts) && is_array($contexts) ? $contexts : array();
}

/**
 * Loads an associative array of conditions => context identifiers to allow
 * contexts to be set by different conditions.
 */
function context_condition_map($reset = FALSE) {
  static $condition_map;
  if (!isset($condition_map) || $reset) {
    if (!$reset && $cache = context_cache_get('condition_map')) {
      $condition_map = $cache;
    }
    else {
      $condition_map = array();
      foreach (array_keys(context_conditions()) as $condition) {
        if ($plugin = context_get_plugin('condition', $condition)) {
          foreach (context_enabled_contexts() as $context) {
            $values = $plugin->fetch_from_context($context, 'values');
            foreach ($values as $value) {
              if (!isset($condition_map[$condition][$value])) {
                $condition_map[$condition][$value] = array();
              }
              $condition_map[$condition][$value][] = $context->name;
            }
          }
        }
      }
      context_cache_set('condition_map', $condition_map);
    }
  }
  return $condition_map;
}

/**
 * Invalidates all context caches().
 * @TODO: Update to use a CTools API function for clearing plugin caches
 * when/if it becomes available.
 */
function context_invalidate_cache() {
  cache_clear_all('context', 'cache', TRUE);
  cache_clear_all('plugins:context', 'cache', TRUE);
}

/**
 * Implementation of hook_flush_caches().
 */
function context_flush_caches() {
  context_invalidate_cache();
}

/**
 * Recursive helper function to determine whether an array and its
 * children are entirely empty.
 */
function context_empty($element) {
  $empty = TRUE;
  if (is_array($element)) {
    foreach ($element as $child) {
      $empty = $empty && context_empty($child);
    }
  }
  else {
    $empty = $empty && !isset($element);
  }
  return $empty;
}

/**
 * Get a plugin handler.
 */
function context_get_plugin($type = 'condition', $key, $reset = FALSE) {
  static $cache = array();
  if (!isset($cache[$type][$key]) || $reset) {
    switch ($type) {
      case 'condition':
        $registry = context_conditions();
        break;
      case 'reaction':
        $registry = context_reactions();
        break;
    }
    if (isset($registry[$key], $registry[$key]['plugin'])) {
      ctools_include('plugins');
      $info = $registry[$key];
      $plugins = ctools_get_plugins('context', 'plugins');
      if (isset($plugins[$info['plugin']]) && $class = ctools_plugin_get_class($plugins[$info['plugin']], 'handler')) {
        // Check that class exists until CTools & registry issues are resolved.
        if (class_exists($class)) {
          $cache[$type][$key] = new $class($key, $info);
        }
      }
    }
  }
  return isset($cache[$type][$key]) ? $cache[$type][$key] : FALSE;
}

/**
 * Get all context conditions.
 */
function context_conditions($reset = FALSE) {
  return _context_registry('conditions', $reset);
}

/**
 * Get all context reactions.
 */
function context_reactions($reset = FALSE) {
  return _context_registry('reactions', $reset);
}

/**
 * Retrieves & caches the context registry.
 */
function _context_registry($key = NULL, $reset = FALSE) {
  static $registry;
  if (!isset($registry) || $reset) {
    if (!$reset && $cache = context_cache_get('registry')) {
      $registry = $cache;
    }
    else {
      $registry = module_invoke_all('context_registry');
      drupal_alter('context_registry', $registry);
      context_cache_set('registry', $registry);
    }
  }
  if (isset($key)) {
    return isset($registry[$key]) ? $registry[$key] : array();
  }
  return $registry;
}

/**
 * hook_block_view_alter - if the context editor block is on this page,
 * ensure that all blocks have some content so that empty blocks are
 * not dropped
 */
function context_block_view_alter(&$data, $block) {
  if (context_isset('context_ui', 'context_ui_editor_present') && empty($data['content'])) {
    $data['content']['#markup'] = "<div class='context-block-empty-content'>" . t('This block appears empty when displayed on this page.') . "</div>";
    $data['context_block_hidden'] = TRUE;
  }
}

/**
 * implement hook_page_alter()
 *
 * used for region context
 */
function context_page_alter(&$page) {
  if ($plugin = context_get_plugin('reaction', 'region')) {
    $plugin->execute($page);
  }
}

/**
 * hook_block_view_alter - if the context editor block is on this page,
 * ensure that all blocks have some content so that empty blocks are
 * not dropped
 */
function context_preprocess_block(&$vars) {
  if (isset($vars['block']->context_block_hidden)) {
    $vars['classes_array'][] = 'context-block-hidden';
    $vars['classes_array'][] = 'context-block-empty';
  }
}
