<?php

/**
 * @file
 * Controls the visual building blocks a page is constructed with.
 */

/**
 * Implements hook_help().
 */
function block_manager_help($path, $arg) {
  $output = '';
  switch ($path) {
    case 'admin/help#block_manager':
      $output .= '<h3>' . t('About') . '</h3>';
      $output .= '<p>' . t('The Block Manager module provides a simple interface for managing blocks on a page, and allows blocks to be ordered on a per-page basis.') . '</p>';
      $output .= '<h3>' . t('Uses') . '</h3>';
      return $output;
  }
  if ($arg[0] == 'admin' && $arg[1] == 'structure' && $arg['2'] == 'block_manager') {
    if ($arg[3] == 'manage') {
      $output = '<p>' . t('This page provides a drag-and-drop interface for assigning a block to a region, and for controlling the order of blocks within regions. Since not all themes implement the same regions, or display regions in the same way, blocks are positioned on a per-theme basis. Remember that your changes will not be saved until you click the <em>Save blocks</em> button at the bottom of the page.') . '</p>';
    }
    return $output;
  }
}

/**
 * Implements hook_theme().
 */
function block_manager_theme() {
  return array(
    'block_manager_theme_fieldset' => array(
      'render element' => 'form',
      'file' => 'block_manager.admin.inc',
    ),
  );
}

/**
 * Implements hook_menu().
 */
function block_manager_menu() {
  $items['admin/config/user-interface/block-manager'] = array(
    'title' => 'Block Manager Configuration',
    'description' => 'Configure regions to be controlled by the Block Manager Module',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('block_manager_admin_settings'),
    'file' => 'block_manager.admin.inc',
    'access arguments' => array('administer site configuration'),
    'weight' => 0,
  );
  $items['admin/structure/block_manager/remove/%'] = array(
    'title' => 'Remove all blocks from a region',
    'description' => 'This page removes all blocks from a given region on a page',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('block_manager_remove_blocks_form', 4),
    'file' => 'block_manager.admin.inc',
    'access arguments' => array('use block manager'),
    'weight' => 1,
  );
  $items['admin/structure/block_manager/manage/%'] = array(
    'title' => 'Manage Blocks',
    'description' => 'Manage block display',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('block_manager_manage_blocks', 4),
    'file' => 'block_manager.admin.inc',
    'access arguments' => array('use block manager'),
    'weight' => 2,
  );

  return $items;
}

/**
 * Implements hook_permission().
 */
function block_manager_permission() {
  return array(
    'use block manager' => array(
      'title' => 'Use Block Manager',
      'description' => 'Use block manager to manage blocks in a region',
    ),
  );
}

/**
 * Implements hook_page_alter().
 *
 * Takes over display of blocks in regions.
 * Adds a link to managed regions to go to the management admin page.
 */
function block_manager_page_alter(&$page) {
  global $theme;

  $managed_regions = _block_manager_get_managed_regions();

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

  // Get configuration options for managed regions.
  $config = _block_manager_get_config($managed_regions, $theme);

  // Set up the path array.  It can be:
  // 1) the drupal path, i.e. /node/12.
  // 2) the path alias, i.e. /about.
  // 3) the front page, <front>.
  $paths = array();
  if (drupal_is_front_page()) {
    $paths[] = '<front>';
  }
  $paths[] = current_path();
  $paths[] = drupal_get_path_alias(current_path());
  $paths = array_unique($paths);

  // Get all the managed blocks for these regions.
  $blocks = _block_manager_get_region_blocks($managed_regions, $paths, $theme);

  // Blocks array will hold the render arrays for all the blocks we need.
  // This is done to avoid re-rendering blocks that have already been processed.
  $blocks_array = array();

  if (module_exists('context')) {
    $context_reaction = context_get_plugin('reaction', 'block');
  }

  foreach ($managed_regions as $region) {
    // In existing regions, remove the blocks (collect them in $blocks_array for
    // later use, if need be).
    if (array_key_exists($region, $page)) {
      foreach ($page[$region] as $key => $block) {
        if ($key{0} !== '#') {
          // Make sure that the #block key exists before we try to access it.
          if (!array_key_exists('#block', $block)) {
            continue;
          }
          // System blocks need to be put into our array so that they are placed
          // back into the region at the end.
          if (strpos($key, 'system_') === 0) {
            $blocks[$region][$block['#block']->bid] = array('weight' => -100);
            $blocks_array[$block['#block']->bid] = array('key' => $key, 'block' => $block);
          }

          if (array_key_exists($region, $blocks)) {
            if (array_key_exists($block['#block']->bid, $blocks[$region])) {
              $blocks_array[$block['#block']->bid] = array('key' => $key, 'block' => $block);
            }
            unset($page[$region][$key]);
          }
        }
      }
    }

    if (module_exists('context')) {
      $context_blocks = $context_reaction->block_get_blocks_by_region($region);

      foreach( $context_blocks as $key => $block ) {
        if (is_array($block) && array_key_exists('#block', $block)) {
          $blocks[$region][$block['#block']->bid] = array('weight' => -100);
          $blocks_array[$block['#block']->bid] = array( 'key' => $key, 'block' => $block);
        }
      }
    }

    // Remove the region from the page, so that if empty, it doesn't display.
    unset($page[$region]);
  }

  // Gather up blocks that need to be rendered separately by the block module.
  $render_blocks = array();
  $bids = array();
  foreach ($blocks as $region => $block_info) {
    foreach (array_keys($block_info) as $bid) {
      if (!array_key_exists($bid, $blocks_array)) {
        $bids[] = $bid;
      }
    }
  }

  if (!empty($bids)) {
    $block_query = db_query(
      'SELECT * FROM {block} b WHERE b.bid IN (:bids)',
      array(':bids' => $bids)
    );
    $results = $block_query->fetchAll();
    foreach ($results as $block) {
      $render_blocks["{$block->module}_{$block->delta}"] = $block;
    }
  }

  $rendered_blocks = _block_get_renderable_array(_block_render_blocks($render_blocks));
  unset($rendered_blocks['#sorted']);

  // Add the rendered blocks to the blocks array.
  foreach ($rendered_blocks as $key => $block) {
    $blocks_array[$block['#block']->bid] = array('key' => $key, 'block' => $block);
  }

  // Now add the blocks back to each region, in order.
  foreach ($managed_regions as $region) {
    // If the current user has permission to administer blocks, add the block
    // admin toolbar to the region.
    if (user_access('use block manager')) {
      if (!array_key_exists($region, $page)) {
        $page[$region] = array('#sorted' => FALSE);
      }

      drupal_add_css(drupal_get_path('module', 'block_manager') . '/css/block_manager.css');
      $page[$region]['block_manager_toolbar'] = _block_manager_render_toolbar($region);
    }

    if (array_key_exists($region, $blocks)) {
      if (!array_key_exists($region, $page)) {
        $page[$region] = array('#sorted' => FALSE);
      }

      foreach ($blocks[$region] as $bid => $info) {
        if (array_key_exists($bid, $blocks_array)) {
          $page[$region][$blocks_array[$bid]['key']] = $blocks_array[$bid]['block'];
          // Adjust the weight from whatever it was in the block module.
          $page[$region][$blocks_array[$bid]['key']]['#weight'] = $info['weight'];
        }
      }
    }
  }
}

/**
 * Creates the render array for the Block Manager region toolbar.
 *
 * @return array
 *   Render array for the Block Manager toolbar
 */
function _block_manager_render_toolbar($region) {
  return array(
    '#type' => 'container',
    '#weight' => -1000,
    '#attributes' => array(
      'class' => array('block_manager_toolbar'),
    ),
    0 => array(
      '#type' => 'link',
      '#title' => t('Remove all blocks'),
      '#href' => "admin/structure/block_manager/remove/{$region}",
      '#attributes' => array(
        'class' => array('bm_button'),
      ),
      '#options' => array(
        'query' => array_merge(
          array('page' => drupal_get_path_alias(current_path())),
          drupal_get_destination()
        ),
      ),
    ),
    1 => array(
      '#type' => 'link',
      '#title' => t('Manage blocks'),
      '#href' => "admin/structure/block_manager/manage/{$region}",
      '#attributes' => array(
        'class' => array('bm_button'),
      ),
      '#options' => array(
        'query' => array_merge(
          array('page' => drupal_get_path_alias(current_path())),
          drupal_get_destination()
        ),
      ),
    ),
  );
}

/**
 * Implements hook_menu_local_tasks_alter().
 *
 * @see hook_menu_local_tasks_alter()
 */
function block_manager_menu_local_tasks_alter(&$data, $router, $path) {
  // Add links to create new blocks, if this is the mangagement page.
  if ($path == 'admin/structure/block_manager/manage/%') {
    $region = $router['map'][4];

    $default_theme = variable_get('theme_default', 'bartik');

    // Get configuration options for this region.
    $config = _block_manager_get_config($region, $default_theme);

    // There should only be one result.
    $config = $config[$region];

    if (!$config['limit_types']) {
      $data['actions']['output'][] = array(
        '#theme' => 'menu_local_task',
        '#link' => array(
          'title' => t('Create new block'),
          'href' => 'admin/structure/block/add',
          'localized_options' => array(
            'attributes' => array(
              'title' => t('Create new block'),
            ),
          ),
        ),
      );

      // Check to see if the menu block module exists.
      if (drupal_valid_path('admin/structure/block/add-menu-block')) {
        $data['actions']['output'][] = array(
          '#theme' => 'menu_local_task',
          '#link' => array(
            'title' => t('Create new menu block'),
            'href' => 'admin/structure/block/add-menu-block',
            'localized_options' => array(
              'attributes' => array(
                'title' => t('Create new menu block'),
              ),
            ),
          ),
        );
      }
    }

    // Check to see if the nodeblock module is installed, and if it is,
    // add links to create each node type managed by nodeblock.
    if (module_exists('nodeblock')) {
      // Get a list of nodes managed by nodeblock.
      // Start by getting a list of all node types.
      $node_types = node_type_get_types();
      foreach ($node_types as $type => $info) {
        if (variable_get('nodeblock_' . $type, 0) != 0
          && (!$config['limit_types']
          || in_array($type, $config['allowed_types']))) {
          $link_type = str_replace('_', '-', $type);
          $data['actions']['output'][] = array(
            '#theme' => 'menu_local_task',
            '#link' => array(
              'title' => t('Create new @type', array('@type' => $info->name)),
              'href' => "node/add/$link_type",
              'localized_options' => array(
                'attributes' => array(
                  'title' => t('Create new @type', array('@type' => $info->name)),
                ),
              ),
            ),
          );
        }
      }
    }
  }
}

/**
 * Implements hook_view_alter().
 *
 * Remove the 'configure block' links added by the block module.
 */
function block_manager_contextual_links_view_alter(&$element, $items) {
  if (array_key_exists('#block', $element['#element'])) {
    $managed_regions = _block_manager_get_managed_regions();
    if (in_array($element['#element']['#block']->region, $managed_regions)) {
      unset($element['#links']['block-configure']);
    }
  }
}

/**
 * Implements hook_node_delete().
 *
 * Remove nodeblocks from the block manager table when the nodes are deleted.
 */
function block_manager_node_delete($node) {
  // Find the block id for this node's nodeblock.
  $bid = db_query(
    'SELECT b.bid FROM {block} b WHERE b.module = :mod AND b.delta = :delta',
    array(':mod' => 'nodeblock', ':delta' => $node->nid)
  )->fetchField();

  db_delete('block_manager')
    ->condition('bid', $bid)
    ->execute();
}

/**
 * Get an array of the regions managed in this theme.
 */
function _block_manager_get_managed_regions() {
  global $theme;

  $managed_regions = array_keys(db_query(
    'SELECT c.region FROM {block_manager_config} c WHERE ' .
      'c.theme = :theme AND c.manage = :manage',
    array(':theme' => $theme, ':manage' => 1)
  )->fetchAllKeyed(0, 0));

  return $managed_regions;
}

/**
 * Get all the managed blocks for a set of regions.
 *
 * @param string|array $region
 *   Managed region(s) to get blocks for
 * @param string|array $page
 *   Page(s) to get blocks for
 * @param string $theme
 *   Theme to get blocks for
 */
function _block_manager_get_region_blocks($region, $page, $theme) {
  $blocks = array();

  if (!is_array($region)) {
    $region = array($region);
  }
  if (!is_array($page)) {
    $page = array($page);
  }

  $blocks_query = db_query(
    'SELECT bm.bid, bm.region, bm.weight, b.module, ' .
      'b.delta FROM {block_manager} bm JOIN {block} b ON bm.bid = b.bid ' .
      'WHERE bm.theme = :theme AND bm.region IN (:regs) AND bm.url IN (:page) ' .
      'ORDER BY bm.weight',
    array(':theme' => $theme, ':regs' => $region, ':page' => $page)
    );

  $managed_blocks = $blocks_query->fetchAll();

  // Get configuration options for managed regions.
  $config = _block_manager_get_config($region, $theme);

  // If not using the nodeblock module, limiting block types is disabled.
  if (!module_exists('nodeblock')) {
    foreach ($managed_blocks as $b) {
      $blocks[$b->region][$b->bid] = array('weight' => $b->weight);
    }
    return $blocks;
  }

  $node_objects_query = db_select('node', 'n')
    ->fields('n', array('nid', 'type'))
    ->addTag('node_access');

  // Get all nodes of each type for each region.
  $all_allowed_types = array();
  foreach ($config as $region => $info) {
    if ($info['limit_types'] && is_array($info['allowed_types'])) {
      $all_allowed_types = array_merge($all_allowed_types, $info['allowed_types']);
    }
  }

  if (!empty($all_allowed_types)) {
    $node_objects_query->condition('type', $all_allowed_types, 'IN');
    $node_objects = $node_objects_query->execute()
      ->fetchAll();
  }
  else {
    $node_objects = array();
  }

  // Build an array of node types by node ID.
  $type_for_id = array();
  foreach ($node_objects as $object) {
    $type_for_id[$object->nid] = $object->type;
  }

  // Limit blocks by the allowed types for each region.
  foreach ($managed_blocks as $b) {
    // Block is valid if:
    // 1) region is not type-limited.
    // 2) block module is nodeblock, and block type is in the allowed types.
    if (!$config[$b->region]['limit_types']
      || (($b->module == 'nodeblock')
      && array_key_exists($b->delta, $type_for_id)
      && in_array($type_for_id[$b->delta], $config[$b->region]['allowed_types']))) {
      $blocks[$b->region][$b->bid] = array('weight' => $b->weight);
    }
  }

  return $blocks;
}

/**
 * Get configuration options for a given region and theme as an array.
 *
 * @param String|Array $region
 *   Name of region to get configuration options for, or array of regions
 * @param String $theme
 *   Name of the theme to get configuration options for
 */
function _block_manager_get_config($region, $theme) {
  $config_query = db_query(
    'SELECT c.region, c.layout, c.limit_types, c.allowed_types FROM ' .
      '{block_manager_config} c WHERE c.theme = :theme AND c.region IN (:regs)',
    array(':regs' => $region, ':theme' => $theme)
  );
  $config_results = $config_query->fetchAll();

  $config = array();
  foreach ($config_results as $c) {
    $config[$c->region] = array(
      'layout' => $c->layout,
      'limit_types' => (bool) $c->limit_types,
      'allowed_types' => unserialize($c->allowed_types),
    );
  }

  return $config;
}
