<?php

/**
 * @file
 * This module provides blocks to inform users of relevant content. This is done on a per-content-type basis
 */


/**
 * Preset storage constant for user-defined presets in the DB.
 */
define('RELEVANT_CONTENT_STORAGE_NORMAL', 0);

/**
 * Preset storage constant for module-defined presets in code.
 */
define('RELEVANT_CONTENT_STORAGE_DEFAULT', 1);

/**
 * Preset storage constant for user-defined presets that override
 * module-defined presets.
 */
define('RELEVANT_CONTENT_STORAGE_OVERRIDDEN', 2);


/**
 * Preset status constants for blocks.
 */
define('RELEVANT_CONTENT_STATUS_DISABLED', 0);
define('RELEVANT_CONTENT_STATUS_ENABLED', 1);

define('RELEVANT_CONTENT_ADMIN_BASE_PATH', 'admin/config/search/relevant_content');

/**
 * Implement hook_help().
 */
function relevant_content_help($path, $arg) {
  switch ($path) {
    case 'admin/help' :
      return t('Provides a block using the Views module to display relevant nodes for the current page.');
    case 'admin/blocks/relevant_content' :
      $output  = '<p>' . t('On this page you can configure which blocks should be provided on a per-content-type basis. If you enabled a content type, please make sure to provided a block title.') . '</p>';
      $output .= '<p>' . t('The <em>Limit</em> field allows you to provide a maximum number of nodes to be displayed for that block.') . '</p>';
      $output .= '<p>' . t('The <em>Block Header Text</em> field allows you to provide some text which can appear at the top of the block - good for explaining to the user what the block is.') . '</p>';
      return $output;
  }
}


/**
 * Implement hook_perm().
 */
function relevant_content_perm() {
  return array('administer relevant content');
}


/**
 * Implement hook_menu().
 */
function relevant_content_menu() {
  // Rebuild the settings when the menu gets rebuilt - this is the only way to
  // clear the settings cache on a module submit
  relevant_content_get_settings(NULL, TRUE);

  $items = array();

  $defaults = array(
    'access arguments' => array('administer relevant content'),
    'file' => 'relevant_content.admin.inc',
    'file path' => drupal_get_path('module', 'relevant_content'),
  );

  $items[RELEVANT_CONTENT_ADMIN_BASE_PATH] = array(
    'title' => 'Relevant Content',
    'description' => 'Configure the sites <em>relevant content</em> blocks.',
    'page callback' => 'relevant_content_admin',
  ) + $defaults;

  $items[RELEVANT_CONTENT_ADMIN_BASE_PATH . '/overview'] = array(
    'title' => 'Overview',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  ) + $defaults;

  $items[RELEVANT_CONTENT_ADMIN_BASE_PATH . '/add'] = array(
    'title' => 'Add',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('relevant_content_admin_block_form'),
    'type' => MENU_LOCAL_TASK,
  ) + $defaults;

  $items[RELEVANT_CONTENT_ADMIN_BASE_PATH . '/%relevant_content_block/%relevant_content_admin_op'] = array(
    'title callback' => 'relevant_content_admin_block_operator_title',
    'title arguments' => array(4, 5),
    'page callback' => 'relevant_content_admin_block_operator_callback',
    'page arguments' => array(4, 5),
    'type' => MENU_CALLBACK,
  ) + $defaults;

  return $items;
}


/**
 * Implement hook_load().
 *
 * This is simply here (currently) to act as a nice URL delta parser. Returns false if block not available.
 * TODO: Modify system to load the settings. This means modifying form inputs too.
 */
function relevant_content_block_load($block_id) {
  return relevant_content_get_settings($block_id);
}


/**
 * Implement hook_load().
 *
 * Relevant Content admin op loader callback for the Menu API
 * This allows a single Admin URL to serve several operations on a block
 */
function relevant_content_admin_op_load($op) {
  $ret = array('op' => $op);

  switch ($op) {
    case 'edit'    : $ret['title'] = t('Edit');    $ret['callback'] = 'relevant_content_admin_block_form'; break;
    case 'delete'  : $ret['title'] = t('Delete');  $ret['callback'] = 'relevant_content_admin_delete_confirm'; break;
    case 'revert'  : $ret['title'] = t('Revert');  $ret['callback'] = 'relevant_content_admin_revert_confirm'; break;
    case 'enable'  : $ret['title'] = t('Enable');  $ret['callback'] = 'relevant_content_admin_enable_confirm'; break;
    case 'disable' : $ret['title'] = t('Disable'); $ret['callback'] = 'relevant_content_admin_disable_confirm'; break;

    default : return FALSE;
  }
  return $ret;
}


/**
 * Admin block operation title callback
 */
function relevant_content_admin_block_operator_title($block, $op) {
  return t('!op Relevant Content Block - @id', array('!op' => $op['title'], '@id' => $block['id']));
}


/**
 * Implement hook_form_alter().
 */
function relevant_content_form_alter(&$form, &$form_state, $form_id) {
  // If we're on a field edit form and the fiel type is relevant content, tweak the form to remove unecessary elements
  if ($form_id == 'field_ui_field_edit_form' && $form['#field']['type'] == 'relevant_content') {
    unset($form['instance']['default_value_widget']['#type']);
    $form['field']['cardinality']['#type'] = $form['instance']['required']['#type'] = 'value';
  }
}


/**
 * Implement hook_block_info().
 */
function relevant_content_block_info() {
  $blocks = array();
  $settings = relevant_content_get_settings();

  if (!empty($settings)) {
    foreach ($settings as $block) {
      if (!$block['status']) continue;

      $blocks[$block['id']] = array(
        'info' => t('Relevant Content: @title', array('@title' => $block['id'])),
        'cache' => DRUPAL_CACHE_PER_ROLE | DRUPAL_CACHE_PER_PAGE,
        'visibility' => 1,
        'pages' => 'node/*',
      );
    }
  }

  return $blocks;
}


/**
 * Implement hook_block_view().
 */
function relevant_content_block_view($block_id) {
  // Get the block by its ID
  $block = relevant_content_get_settings($block_id);

  // If no block, return
  if (!$block) {
    return;
  }

  //Get the terms for the current page using a little reusable wrapper function
  $terms = relevant_content_get_page_terms();

  //If there are no terms, not a lot of point in continuing
  if (empty($terms)) {
    return;
  }

  //Filter out the terms which are not in a selected vocabulary
  foreach ($terms as $key => $term) {
    if (isset($block['vocabs'][$term->vid])) {
      $terms[$key] = $term->tid;
    }
    else {
      unset($terms[$key]);
    }
  }

  //Again - if there are no terms, no need to continue!
  if (empty($terms)) {
    return;
  }

  //Create a node exclusion list - this will exclude the currently viewed node - if applicable.
  //This stops the currently viewed node appearing top of a list - afterall, it IS the most relevant!
  $exclude = array();
  if ($node = menu_get_object()) {
    $exclude[] = $node->nid;
  }

  if ($nodes = relevant_content_get_nodes($block['types'], $terms, $exclude, $block['max_items'])) {
    // Return a block
    // See @template_preprocess_relevant_content_block
    return array(
      'subject' => t('Relevant Content'),
      'content' => theme('relevant_content_block', array('nodes' => $nodes, 'block' => $block)),
    );
  }
}


/**
 * Handy wrapper function to find the terms for the current page
 */
function relevant_content_get_page_terms($node = NULL) {
  /**
   * If we have passed in a node, or if there is one available from the menu object, use the node ID to load all the terms for the node...
   * This seems to be necessary due to the new Field API not really giving us "one" taxonomy array on the node.
   */
  if (isset($node) || ($node = menu_get_object())) {
    // Select from the taxonomy_index and the taxonomy_term_data
    $query = db_select('taxonomy_index', 't');
    $query->join('taxonomy_term_data', 'td', 'td.tid = t.tid');

    // We need the Term ID, Name and Vocabulary ID
    $query->addField('t', 'tid');
    $query->fields('td', array('vid', 'name'));

    // And we need to filter for the node in question
    $query->condition('t.nid', $node->nid);

    // Get the terms as a tid-indexed array of objects
    $terms = $query->execute()->fetchAllAssoc('tid');
  }
  // Fall back to the term_cache if the above worked
  else {
    $terms = relevant_content_term_cache();
  }

  // Provide a hook_relevant_content_terms where other modules can change or add to the relevant terms...
  drupal_alter('relevant_content_terms', $terms);

  return $terms;
}


/**
 * Implement hook_theme().
 */
function relevant_content_theme($existing, $type, $theme, $path) {
  $base = array('file' => 'relevant_content.theme.inc', 'theme path' => $path);

  return array(
    'relevant_content_block'            => $base + array('variables' => array('nodes' => NULL, 'block' => NULL), 'template' => 'relevant_content_block'),
    'relevant_content_admin_overview'   => $base + array('render_element' => 'element', 'template' => 'relevant_content_admin_overview'),
    'relevant_content_individual_field' => $base + array('variables' => array('attributes' => NULL, 'result' => NULL)),
  );
}


/**
 * Function to get a set of nodes.
 *
 * This returns a set of nodes based on the provided type and array of term
 * ID's.
 *
 * @param $type
 *   Array representing the node types
 * @param $terms
 *   Array of Term ID's
 * @param $exclude
 *   Array - Optional: An array of Node ID's to exclude. Useful for excluding the node you might be comparing to currently. Default: No exclusions.
 * @param $limit
 *   Integer - Optional: Integer controlling the maximum number of nodes returned. Default: 5
 * @param $languages
 *   Array - Optional: An array of languages to restrict nodes to.
 *                     An empty string in the array corresponds to Language Neutral nodes.
 *                     An empty array will include all nodes regardless of language.
 *
 * @return mixed
 *   FALSE if no result or error or an associative array with node ID's as keys and the value's as arrays of nid's, vid's, title's, type's & term match counts.
 */
function relevant_content_get_nodes($types, $terms, $exclude = array(), $limit = 5, $languages = array()) {
  // If terms or types are empty, there isn't anything to match to so not a lot of point continuing.
  if (empty($terms) || empty($types)) return FALSE;

  // Define the query
  $query = db_select('node', 'n');
  $query->leftJoin('taxonomy_index', 'ti', 'n.nid = ti.nid');
  $query->fields('n', array('nid', 'vid', 'title', 'type'));
  $query->addExpression('COUNT(*)', 'cnt');

  // Filter for all nodes which are published, in the "types" array and has at least one of the selected terms
  $query->condition('n.status', 1)->condition('n.type', $types, 'IN')->condition('ti.tid', $terms, 'IN');

  // Exclude any specific node id's
  if (!empty($exclude)) {
    $query->condition('n.nid', $exclude, 'NOT IN');
  }

  // IF language is specified, make sure we filter for those too
  if (!empty($languages)) {
    $query->condition('n.language', $languages, 'IN');
  }

  // Group, order and limit the query
  $query->groupBy('n.nid')->orderBy('cnt', 'DESC')->orderBy('n.created', 'DESC')->orderBy('n.nid', 'DESC')->range(0, $limit);

  // Execute and loop to store the results against the node ID key
  $nodes = $query->execute()->fetchAllAssoc('nid', PDO::FETCH_ASSOC);

  // Return the node array or FALSE if there is no result/an error.
  return empty($nodes) ? FALSE : $nodes;
}


/**
 * Function to locally cache terms
 * TODO: Use the Drupal Static Cache function
 *
 * This allows either this module or any other module to add terms to the cache.
 * This cache is used to determine which nodes appear in the relevant content
 * blocks.
 *
 * @param $terms
 *   Array of term id's
 * @param $clear
 *   Boolean flag - can be set to force a clearing of the local cache.
 * @return
 *   Array of the term id's currently in the cache
 */
function relevant_content_term_cache($terms = array(), $clear = FALSE) {
  static $term_cache = array();

  if ($clear) {
    $term_cache = array();
  }

  if (!empty($terms)) {
    $term_cache = array_merge($term_cache, $terms);
  }

  return $term_cache;
}


/**
 * Implement hook_field_info().
 *
 * Field settings: ...
 */
function relevant_content_field_info() {
  $default_format = filter_default_format();

  return array(
    'relevant_content' => array(
      'label' => t('Relevant Content'),
      'description' => t('This is an output-only field which looks up related content for the node the field is attached to.'),
      'default_widget' => 'relevant_content_list',
      //'default_formatter' => 'relevant_content_default',
      'default_formatter' => 'default',
      'settings' => array(
        'relevant_nodetypes' => array(),
        'relevant_vocabs' => array(),
        'relevant_result_limit' => 5,
        'relevant_header' => array('value' => '', 'format' => $default_format),
      ),
      'instance_settings' => array(
        'token_settings' => array(
          'token_teaser' => array('value' => '', 'format' => $default_format),
          'token_full' => array('value' => '', 'format' => $default_format),
        ),
      ),
    ),
  );
}


/**
 *  Implement hook_field_settings_form().
 */
function relevant_content_field_settings_form($field, $instance, $has_data) {
  $form = array();

  // Node Type Filter
  $form['relevant_nodetypes'] = array(
    '#type' => 'checkboxes',
    '#title' => t('Relevant Node Types'),
    '#default_value' => isset($field['settings']['relevant_nodetypes']) ? $field['settings']['relevant_nodetypes'] : array(),
    '#options' => node_type_get_names(),
    '#required' => TRUE,
    '#description' => t('Select the node types you would like to include in this <em>Relevant Content Field</em>'),
  );

  // Load the system vocabs
  $vocabs = array();
  foreach (taxonomy_get_vocabularies() as $vid => $voc) {
    $vocabs[$vid] = $voc->name;
  }

  // If there are no vocabularies configured, show an error
  if (empty($vocabs)) {
    // TODO: Show error
  }
  // Otherwise, show a vocabulary selector
  else {
    $form['relevant_vocabs'] = array(
      '#type' => 'checkboxes',
      '#title' => t('Relevant Vocabularies'),
      '#default_value' => isset($field['settings']['relevant_vocabs']) ? $field['settings']['relevant_vocabs'] : array(),
      '#options' => $vocabs,
      '#required' => TRUE,
      '#description' => t('Select the vocabularies you would like to include in this <em>Relevant Content Field</em>. Only terms in the selected vocabularies will be used to find relevant content.'),
    );
  }

  // Define a result limitor field
  $form['relevant_result_limit'] = array(
    '#type' => 'textfield',
    '#title' => t('Results Per Page'),
    '#description' => t('Relevant content to display per page. Must be more than 0'),
    '#default_value' => isset($field['settings']['relevant_result_limit']) ? $field['settings']['relevant_result_limit'] : 5,
    '#required' => TRUE,
  );

  // Define a result header field with formatter options
  $form['relevant_header'] = array(
    '#type' => 'text_format',
    '#title' => t('Header Text'),
    '#description' => t('Optionally include some text to appear above the list and below the label (if the label is enabled).'),
    '#default_value' => isset($field['settings']['relevant_header']['value']) ? $field['settings']['relevant_header']['value'] : '',
    '#format' => isset($field['settings']['relevant_header']['format']) ? $field['settings']['relevant_header']['format'] : NULL,
  );

  return $form;
}


/**
 * Implement hook_field_widget_settings_form().
 */
function relevant_content_field_instance_settings_form($field, $instance) {
  $form = array();

  $form['token_settings'] = array(
    '#type' => 'fieldset',
    '#title' => t('Token Output Settings'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  );

  $form['token_settings']['token_teaser'] = array(
    '#title' => t('Tokens for teaser view formatter'),
    '#type' => 'text_format',
    '#description' => t('Tokens entered in here will be used for the optional token teaser formatter. This allows customized output.'),
    '#default_value' => isset($instance['settings']['token_settings']['token_teaser']['value']) ? $instance['settings']['token_settings']['token_teaser']['value'] : '',
    '#format' => isset($instance['settings']['token_settings']['token_teaser']['format']) ? $instance['settings']['token_settings']['token_teaser']['format'] : NULL,
    '#rows' => 4,
  );

  $form['token_settings']['token_full'] = array(
    '#type' => 'text_format',
    '#title' => t('Tokens for full view formatter'),
    '#description' => t('Tokens entered in here will be used for the optional token full formatter. This allows customized output.'),
    '#default_value' => isset($instance['settings']['token_settings']['token_full']['value']) ? $instance['settings']['token_settings']['token_full']['value'] : '',
    '#format' => isset($instance['settings']['token_settings']['token_full']['format']) ? $instance['settings']['token_settings']['token_full']['format'] : '',
    '#rows' => 4,
  );

  $form['token_settings']['token_help'] = array(
    '#theme' => 'token_tree',
    '#token_types' => array('node'),
  );

  return $form;
}


/**
 * Implement hook_field_is_empty().
 */
// TODO
function relevant_content_field_is_empty($item, $field) {
  return FALSE;
}


/**
 * Implement hook_field_formatter_info().
 */
function relevant_content_field_formatter_info() {
  return array(
    'default' => array(
      'label' => t('Default'),
      'field types' => array('relevant_content'),
      'settings' => array('item_style' => 'default'),
    ),
  );

/*
  $defaults = array(
    'field types' => array('relevant_content'),
    'behaviors' => array(
      //'multiple values' => FIELD_BEHAVIOR_CUSTOM,
    ),
  );

  return array(
    'relevant_content_default'      => $defaults + array('label' => t('Node Title - Link')),
    'relevant_content_plain'        => $defaults + array('label' => t('Node Title - Plain')),
    'relevant_content_teaser'       => $defaults + array('label' => t('Node Teaser')),
    'relevant_content_full'         => $defaults + array('label' => t('Node Full')),
    'relevant_content_token_teaser' => $defaults + array('label' => t('Node Token Teaser')),
    'relevant_content_token_full'   => $defaults + array('label' => t('Node Token Full')),
  );
  */
}


/**
 * Local function to make the options array re-usable.
 * TODO: Would a static cache be more efficient here if t() is used several times?
 */
function _relevant_content_field_formatter_settings_options() {
  return array(
    'default' => t('Node Title - Link'),
    'plain' => t('Node Title - Plain'),
    'teaser' => t('Node Teaser'),
    'full' => t('Node Full'),
    'token_teaser' => t('Token Teaser Template'),
    'token_full' => t('Token Full Template'),
  );
}


/**
 * Implement hook_field_formatter_settings_form().
 */
function relevant_content_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
  $display = $instance['display'][$view_mode];
  $settings = $display['settings'];
  $element = array();

  $element['item_style'] = array(
    '#title' => t('Item Style'),
    '#type' => 'select',
    '#default_value' => $settings['item_style'],
    '#options' => _relevant_content_field_formatter_settings_options(),
  );

  // TODO Add Row Style (div, ol/ul li, dt/dd, etc)
  return $element;
}


/**
 * Implement hook_field_formatter_settings_summary().
 */
function relevant_content_field_formatter_settings_summary($field, $instance, $view_mode) {
  $display = $instance['display'][$view_mode];
  $settings = $display['settings'];

  $options = _relevant_content_field_formatter_settings_options();
  $summary = array();

  $summary[] = $options[$settings['item_style']];

  return implode('<br />', $summary);
}


/**
 * Implement hook_field_formatter_view().
 */
function relevant_content_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  // If there are no items, return nothing. Anything else risks empty fields
  if (empty($items)) return;

  // Get the settings
  $display_settings = $display['settings'];
  $item_style = $display_settings['item_style'];

  // Get the formatter type from the variables (only default available ATM).
  $type = $display['type'];

  // We only need this bit for token_* formatters
  if ($item_style == 'token_full' || $item_style == 'token_teaser') {
    // Get the settings from the instance - this is a convenience variable for later
    $token_settings = $instance['settings']['token_settings'][$item_style];

    // Unfortunately, we cannot do a single check_markup() on the token pattern as filter_xss appears to effect some tokens
    // D7 BUG ? filter_xss('[node:url]') == 'url]'
  }

  // Init the count and num item variables
  $count = 0;
  $num_items = count($items);
  $element = array();

  // Loop over all the items and create the field output
  foreach ($items as $item) {
    // Depending on the type, render a field
    switch ($item_style) {
      default:
      case 'default' :
        $result = l($item['title'], 'node/' . $item['nid']);
        break;

      case 'plain' :
        $result = check_plain($item['title']);
        break;

      case 'teaser' :
      case 'full' :
        // TODO - make much better user of node_view_multiple()...
        // TODO - this isn't right...
        $node = node_load($item['nid']);
        $result = render(node_view_multiple(array($node->nid => $node), $item_style));
        break;

      case 'token_full' :
      case 'token_teaser' :
        // Run a token replace on the content
        $types = array('global' => NULL, 'node' => node_load($item['nid']));

        // Do a token replace on the pattern
        $result = token_replace($token_settings['value'], $types);

        // Do a check_markup. Needs to be done here due to the filter_xss bug on [node:url:relative]...
        $result = check_markup($result, $token_settings['format']);
        break;
    }

    // Add the result as a row to the table
    $element[$count] = array('#markup' => $result);

    $count++;
  }

  return $element;
}


/**
 * Implement hook_field_load().
 */
function relevant_content_field_load($obj_type, $objects, $field, $instances, $langcode, &$items, $age) {
  foreach ($objects as $id => $object) {
    //Get the terms using the handy term wrapper in the parent module
    $terms = relevant_content_get_page_terms($object);

    // If there are no terms, return an empty item set - there will be nothing in common with this.
    if (empty($terms)) return;

    // Grab the types & vocabs
    $types  = array_values(array_filter($field['settings']['relevant_nodetypes']));
    $vocabs = array_values(array_filter($field['settings']['relevant_vocabs']));

    // Filter the terms from the vocabs associated with this field.
    foreach ($terms as $tid => $term) {
      if (in_array($term->vid, $vocabs)) {
        $terms[$tid] = $tid;
      }
      else {
        unset($terms[$tid]);
      }
    }

    // If there are no terms *after* filtering, return an empty item set - there will be nothing in common with this.
    if (empty($terms)) return;

    // Search for items and return the item set.
    $relevant_items = relevant_content_get_nodes($types, $terms, array($object->nid), $field['settings']['relevant_result_limit']);
    if (empty($relevant_items)) return;

    // Set the items which will get passed to the theme layer
    // Use Array Values as relevant_content_get_nodes(...) keys by Node ID. Field API expects keys to be Delta's
    $items[$id] = array_values($relevant_items);
  }
}


/**
 * Implement hook_field_widget_info().
 */
function relevant_content_field_widget_info() {
  return array(
    'relevant_content_list' => array(
      'label' => t('Relevant Nodes (Read Only)'),
      'field types' => array('relevant_content'),
      'behaviors' => array(
        'multiple values' => FIELD_BEHAVIOR_DEFAULT,
      ),
    ),
  );
}


/**
 * Implement hook_element_info().
 */
function relevant_content_element_info() {
  return array(
    'relevant_content_list' => array(
      '#input' => FALSE,
    ),
  );
}


function relevant_content_preprocess_field(&$variables) {
  // If the field is a relevant content field, we need to add more suggestion types (so we can easily theme all RC fields).
  // This is necessary so that we can override the default content-field template (which doesn't work correctly for us)
  if (isset($variables['field_type']) && ($variables['field_type'] == 'relevant_content')) {
    $suggestions = array(
      'field-relevant-content',
      'field-relevant-content-' . $variables['field_name'],
      'field-relevant-content-' . $variables['instance']['bundle'],
      'field-relevant-content-' . $variables['field_name'] . '-' . $variables['instance']['bundle'],
    );

    $variables['template_files'] = array_merge($variables['template_files'], $suggestions);
    $header = $variables['element']['items']['header']['#item'];
    $variables['header'] = check_markup($header['value'], $header['format']);
  }
  drupal_add_css(drupal_get_path('module', 'relevant_content') . '/relevant_content.css');
}


function relevant_content_theme_registry_alter(&$theme_registry) {
  // This seems necessary to allow the theme registry to also search the RC folder for template files.
  $theme_registry['field']['theme paths'][] = drupal_get_path('module', 'relevant_content');
}


/**
 * Function to get the relevant_content settings
 * Settings are cached both statically and in the cache table for performance.
 */
function relevant_content_get_settings($block_delta = NULL, $reset = FALSE) {
  // Static settings cache variable
  static $settings = NULL;

  // If we're resetting, clear static and cache table
  if ($reset) {
    $settings = NULL;
    cache_clear_all('relevant_content:settings', 'cache');
  }

  // If settings are not set, lets define them
  if (!isset($settings)) {
    // First, try to get from the cache table
    if (($cache = cache_get('relevant_content:settings', 'cache')) && is_array($cache->data)) {
      $settings = $cache->data;
    }
    // Nope, ok - lets make them from scratch
    else {
      // Initialize the settings
      $settings = array();

      // Get anything we have defined from the DB
      $blocks = db_select('relevant_content_blocks', 'r')
                  ->fields('r')
                  ->execute()
                  ->fetchAllAssoc('id', PDO::FETCH_ASSOC);

      foreach ($blocks as $block) {
        // This ensures all blocks have a complete set of default values if not already provided.
        // This includes a "normal" storage
        $block = _relevant_content_santize_block($block);

        // TODO - tidy this line a bit...
        $block['status'] = ($block['status'] == RELEVANT_CONTENT_STATUS_ENABLED) ? RELEVANT_CONTENT_STATUS_ENABLED : RELEVANT_CONTENT_STATUS_DISABLED;

        // Load types
        $block['types'] = db_select('relevant_content_blocks_types', 'r')
                            ->fields('r', array('type'))
                            ->condition('id', $block['id'])
                            ->execute()
                            ->fetchAllKeyed(0, 0);
        // Load vocabs
        // TODO - cast to int?
        $block['vocabs'] = db_select('relevant_content_blocks_vocabs', 'r')
                            ->fields('r', array('vid'))
                            ->condition('id', $block['id'])
                            ->execute()
                            ->fetchAllKeyed(0, 0);


        // Store the block
        $settings[$block['id']] = $block;
      }

      // Now loop over the hook to get any module defined relevant content
      // blocks, using hook_relevant_content_default_blocks().
      $default_blocks = module_invoke_all('relevant_content_default_blocks');

      // Allow other modules to alter the module-defined blocks using:
      // hook_relevant_content_default_blocks_alter(&$default_blocks)
      drupal_alter('relevant_content_default_blocks', $default_blocks);

      // Add each defined block to our list...
      foreach ($default_blocks as $block) {
        // ... but only if an id is set!
        if (!empty($block['id'])) {
          $block = _relevant_content_santize_block($block);
          // If the id is already in our settings, then we have overridden a module-level item
          if (isset($settings[$block['id']])) {
            // Set the block as 'overridden', but do not store the module defined block
            $settings[$block['id']]['storage'] = RELEVANT_CONTENT_STORAGE_OVERRIDDEN;
          }
          // Otherwise, it's a custom block
          else {
            // Store the block as a default store
            $block['storage'] = RELEVANT_CONTENT_STORAGE_DEFAULT;
            $settings[$block['id']] = $block;
          }
        }
      }

      // Cache it for later use
      cache_set('relevant_content:settings', $settings);
    }
  }

  // Did we ask for a SPECIFIC block by delta? If so, return it (or FALSE if
  // it is not available)
  if ($block_delta) {
    return isset($settings[$block_delta]) ? $settings[$block_delta] : FALSE;
  }

  // Otherwise, return ALL settings
  return $settings;
}


/**
 * Internal function for santizing a block. Also useful for defining "defaults" for a form.
 */
function _relevant_content_santize_block($block = array()) {
  // Define the defaults
  $defaults = array(
    'id' => '',
    'types' => array(),
    'vocabs' => array(),
    'max_items' => 5,
    'header_text' => '',
    'header_format' => filter_default_format(),
    'token_settings' => '',
    'absolute_links' => 0,
    'status' => RELEVANT_CONTENT_STATUS_DISABLED,
    'storage' => RELEVANT_CONTENT_STORAGE_NORMAL,
  );

  // Overlay the defaults onto $block
  $block = $block + $defaults;

  // Cast some items to "int" - this helps keep feature exporting clean
  $block['max_items'] = (int)$block['max_items'];

  return $block;
}


/**
 * Delete Block Handler
 */
function relevant_content_delete_block($block_id, $flush_cache = TRUE) {
  // Delete the settings
  db_delete('relevant_content_blocks')->condition('id', $block_id)->execute();
  db_delete('relevant_content_blocks_types')->condition('id', $block_id)->execute();
  db_delete('relevant_content_blocks_vocabs')->condition('id', $block_id)->execute();

  // Tidy up the blocks table...
  foreach (array('block', 'block_node_type', 'block_role') as $table) {
    db_delete($table)
      ->condition('module', 'relevant_content')
      ->condition('delta', $block_id)
      ->execute();
  }

  // We may want to delete without flushing - for example when saving.
  if ($flush_cache) {
    // Reset the settings cache
    relevant_content_get_settings(NULL, TRUE);
    block_flush_caches();
  }
}


/**
 * Disable Block Handler
 */
function relevant_content_set_block_status($block_id, $status = 1, $flush_cache = TRUE) {
  // Load the block from cached settings
  $block = relevant_content_get_settings($block_id);

  // Set the status
  $block['status'] = $status;

  // Save the block - this also flushes all caches and rebuilds the block hash
  relevant_content_save_block($block);
}



/**
 * Save block handler - it essentially deletes and inserts for 'updates'.
 */
function relevant_content_save_block($block) {
  // TODO - this feels like a hack...
  if (isset($block['header']) && is_array($block['header'])) {
    $block['header_text'] = $block['header']['value'];
    $block['header_format'] = $block['header']['format'];
  }

  // Define the fields to insert
  $fields = array(
    'id'             => $block['id'],
    'max_items'      => $block['max_items'],
    'header_text'    => $block['header_text'],
    'header_format'  => $block['header_format'],
    'token_settings' => $block['token_settings'],
    'absolute_links' => $block['absolute_links'],
    'status'         => $block['status'],
  );

  // Run a MERGE query
  db_merge('relevant_content_blocks')
    ->key(array('id' => $block['id']))
    ->fields($fields)
    ->execute();


  // Add one row for each assigned type
  db_delete('relevant_content_blocks_types')
    ->condition('id', $block['id'])
    ->execute();
  if (is_array($block['types'])) {
    $query = db_insert('relevant_content_blocks_types')->fields(array('id', 'type'));
    foreach ($block['types'] as $type) {
      $query->values(array($block['id'], $type));
    }
    $query->execute();
  }

  // Add one row for each assigned vocab
  db_delete('relevant_content_blocks_vocabs')
    ->condition('id', $block['id'])
    ->execute();
  if (is_array($block['vocabs'])) {
    $query = db_insert('relevant_content_blocks_vocabs')->fields(array('id', 'vid'));
    foreach ($block['vocabs'] as $vid) {
      $query->values(array($block['id'], $vid));
    }
    $query->execute();
  }

  // Reset the settings cache
  relevant_content_get_settings(NULL, TRUE);

  // Rehash all the blocks for all themes
  module_invoke('block', 'flush_caches');
  _block_rehash();
}


/**
 * Implementation of hoko_features_api().
 */
function relevant_content_features_api() {
  return array(
    'relevant_content_block' => array(
      'name' => t('Relevant Content Block'),
      'default_hook' => 'relevant_content_default_blocks',
      'default_file' => FEATURES_DEFAULTS_INCLUDED_COMMON,
      'features_source' => TRUE,
      'file' => drupal_get_path('module', 'relevant_content') . '/relevant_content.features.inc',
    ),
  );
}


function relevant_content_relevant_content_default_blocks() {
  return array(
    'test' => array(
      'id' => 'test',
      'max_items' => 10,
      'vocabs' => array(1 => 1),
      'types' => array('page' => 'page'),
      'header_text' => t('TESTING TEXT!!'),
      'header_format' => filter_default_format(),
      'status' => RELEVANT_CONTENT_STATUS_DISABLED,
    ),
  );
}


/**
 * Module specific XSS Filtering.
 */
function relevant_content_filter_xss($string) {
  return filter_xss($string, _relevant_content_filter_xss_allowed_tags());
}

/**
 * List of allowed tags for the relevant content token field.
 * This is more permissive than normal filter_xss() but less permissive than
 * filter_xss_admin(). Also gives us more control.
 */
function _relevant_content_filter_xss_allowed_tags() {
  return array('a', 'b', 'big',  'code', 'del', 'em', 'i', 'ins',  'pre', 'q', 'small', 'span', 'strong', 'sub', 'sup', 'tt', 'ol', 'ul', 'li', 'p', 'br', 'img');
}


/**
 * Human readable list of allowed tags, handy for help text.
 */
function _relevant_content_filter_xss_display_allowed_tags() {
  return '<' . implode('> <', _relevant_content_filter_xss_allowed_tags()) . '>';
}
