<?php

/**
 * Implementation of hook_form_node_type_form_alter().
 */
function toc_node_form_node_type_form_alter(&$form, $form_state) {
  $type = $form['#node_type'];

  $form['toc_node'] = array(
    '#type'        => 'fieldset',
    '#title'       => t('Table of contents'),
    '#description' => t('Settings for adding TOC to nodes of this content type.'),
    '#collapsible' => TRUE,
    '#collapsed'   => FALSE,
    '#group'       => 'additional_settings',
    '#attached'    => array(
      'js' => array(drupal_get_path('module', 'menu') . '/menu.admin.js'),
    ),
  );

  $form['toc_node']['toc_node_enabled'] = array(
    '#type'          => 'checkbox',
    '#title'         => t('Enable table of contents'),
    '#default_value' => variable_get('toc_node_enabled_' . $type->type, 0),
  );

  $form['toc_node']['toc_node_back_to_top_links'] = array(
    '#type'          => 'select',
    '#title'         => t('Back to top links'),
    '#default_value' => variable_get('toc_node_back_to_top_links_' . $type->type, 0),
    '#empty_option'  => t('- Select -'),
    '#options'       => array(t('Disabled - always'), t('Enabled - always'), t('Enabled - per node option'), t('Disabled - per node option')),
  );

  $form['toc_node']['toc_node_level'] = array(
    '#type'          => 'select',
    '#title'         => t('Level'),
    '#description'   => t('Up to what heading level to include.'),
    '#default_value' => variable_get('toc_node_level_' . $type->type, 'h2'),
    '#empty_option'  => t('- Select -'),
    '#options'       => array(2 => 'h2', 3 => 'h3', 4 => 'h4', 5 => 'h5', 6 => 'h6', 7 => 'h7'),
  );

  $form['toc_node']['toc_node_styles'] = array(
    '#type'          => 'checkboxes',
    '#title'         => t('Styles available'),
    '#default_value' => variable_get('toc_node_styles_' . $type->type, array()),
    '#options'       => array('none' => t('No TOC'), 'bullets' => t('Bullets'), 'numbers' => t('Numbered')),
  );

  $form['toc_node']['toc_node_style_default'] = array(
    '#type'          => 'select',
    '#title'         => t('Display default'),
    '#default_value' => variable_get('toc_node_style_default_' . $type->type, 'bullets'),
    '#empty_option'  => t('- Select -'),
    '#options'       => array('none' => t('No TOC'), 'bullets' => t('Bullets'), 'numbers' => t('Numbered')),
  );

}

/**
 * Implementation of hook_form_node_form_alter().
 */
function toc_node_form_node_form_alter(&$form, &$form_state, $form_id) {

  $type        = $form['type']['#value'];
  $toc_enabled = variable_get('toc_node_enabled_' . $type, 0);

  // Only show options if TOC enabled for this content type.
  if (empty($toc_enabled)) {
    return;
  }

  $options_all                = array('none' => t('No TOC'), 'bullets' => t('Bullets'), 'numbers' => t('Numbered'));
  $options_enabled            = variable_get('toc_node_styles_' . $type, array());
  $options                    = array_intersect_key($options_all, array_flip($options_enabled));
  $style                      = variable_get('toc_node_style_default_' . $type, 'bullets');
  $back_to_top_links_defaults = variable_get('toc_node_back_to_top_links_' . $type, 0);
  $level                      = variable_get('toc_node_level_' . $type, 'h2');

  if (isset($form['nid']['#value'])) {
    $node_settings = db_query('SELECT * FROM {toc_node} WHERE nid = :nid', array(':nid' => $form['nid']['#value']))->fetchAssoc();

    if (!empty($node_settings) && isset($options[$node_settings['style']])) {
      $style             = $node_settings['style'];
      $back_to_top_links = $node_settings['back_links'];
      $level             = $node_settings['level'];
    }

  }

  $form['toc_node'] = array(
    '#type'        => 'fieldset',
    '#title'       => t('Table of contents'),
    '#collapsible' => TRUE,
    '#collapsed'   => FALSE,
    '#group'       => 'additional_settings',
    '#attached'    => array(
      'js' => array(drupal_get_path('module', 'menu') . '/menu.admin.js'),
    ),
  );

  $form['toc_node']['toc_node_style'] = array(
    '#type'          => 'select',
    '#title'         => t('TOC Display'),
    '#default_value' => $style,
    '#empty_option'  => t('- Select -'),
    '#options'       => $options,
  );

  // Check if back to top links should be on as default.
  if (!isset($back_to_top_links) && ($back_to_top_links_defaults == 1 || $back_to_top_links_defaults == 2)) {
    $back_to_top_links = 1;
  }
  elseif (!isset($back_to_top_links) && ($back_to_top_links_defaults == 0 || $back_to_top_links_defaults == 3)) {
    $back_to_top_links = 0;
  }

  $form['toc_node']['toc_node_back_to_top_links'] = array(
    '#type'          => 'checkbox',
    '#title'         => t('Back to top links'),
    '#default_value' => $back_to_top_links,
  );

  // Check if there is per node options.
  if ($back_to_top_links_defaults < 2) {
    $form['toc_node']['toc_node_back_to_top_links']['#access'] = FALSE;
  }

  $form['toc_node']['toc_node_level'] = array(
    '#type'          => 'select',
    '#title'         => t('Level'),
    '#description'   => t('Up to what heading level to include.'),
    '#default_value' => $level,
    '#empty_option'  => t('- Select -'),
    '#options'       => array(2 => 'h2', 3 => 'h3', 4 => 'h4', 5 => 'h5', 6 => 'h6', 7 => 'h7'),
  );

}

/**
 * Implementation of hook_node_insert().
 */
function toc_node_node_insert($node) {

  $toc_enabled = variable_get('toc_node_enabled_' . $node->type, 0);

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

  db_insert('toc_node')->fields(array(
    'nid'        => $node->nid,
    'style'      => $node->toc_node_style,
    'level'      => $node->toc_node_level,
    'back_links' => $node->toc_node_back_to_top_links,
  ))->execute();

}

/**
 * Implementation of hook_node_update().
 */
function toc_node_node_update($node) {

  $toc_enabled = variable_get('toc_node_enabled_' . $node->type, 0);

  if (empty($toc_enabled)) {
    return;
  }
  // check defaults
  if (!isset($node->toc_node_style)) {
    $node->toc_node_style = variable_get('toc_node_style_default_' . $node->type, 'bullets');
  }
  if (!isset($node->toc_node_level)) {
    $node->toc_node_level = variable_get('toc_node_level_' . $node->type, 'h2');
  }
  if (!isset($node->toc_node_back_to_top_links)) {
    $node->toc_node_back_to_top_links = variable_get('toc_node_back_to_top_links_' . $node->type, 0);
  }

  db_merge('toc_node')
    ->key(array('nid' => $node->nid))
    ->fields(array(
      'nid'        => $node->nid,
      'style'      => $node->toc_node_style,
      'level'      => $node->toc_node_level,
      'back_links' => $node->toc_node_back_to_top_links,
    ))
    ->execute();
}

/**
 * Implementation of hook_node_delete().
 */
function toc_node_node_delete($node) {
  db_delete('toc_node')->condition('nid', $node->nid)->execute();
}

/**
 * Implementation of hook_node_load().
 */
function toc_node_node_load($nodes, $types) {
  $skip = TRUE;

  foreach ($types as $type) {
    $toc_enabled = variable_get('toc_node_enabled_' . $type, 0);

    if (!empty($toc_enabled)) {
      $skip = FALSE;
    }
  }

  if ($skip) {
    return;
  }

  // Gather our extra data for each of these nodes.
  $result = db_query('SELECT * FROM {toc_node} WHERE nid IN(:nids)', array(':nids' => array_keys($nodes)));
  // Add our extra data to the node objects.
  foreach ($result as $record) {
    $nodes[$record->nid]->toc_node_style             = $record->style;
    $nodes[$record->nid]->toc_node_level             = $record->level;
    $nodes[$record->nid]->toc_node_back_to_top_links = $record->back_links;
  }
}

/**
 * Implementation of hook_theme().
 */
function toc_node_theme() {
  $theme = array(
    'toc_node_move_to_top_link' => array(
      'variables' => array('content' => NULL),
    ),
  );

  return $theme;
}

/**
 * Theming functions for 'Move to top' links.
 */
function theme_toc_node_move_to_top_link($variables) {
  $content    = $variables['content'];
  $path_alias = drupal_get_path_alias($_GET['q']);
  $link       = '<div class="toc-top-links">';
  $link .= l(t('Move to top'), $path_alias, array('fragment' => 'table-of-contents'));
  $link .= '</div>';

  $output = str_ireplace('</h2>', '</h2>' . $link, $content);

  return $output;
}

/**
 * Implements hook_block_info().
 */
function toc_node_block_info() {
  $return = array();

  $return['toc_node'] = array(
    'info'  => t('Table Of Contents'),
    'cache' => DRUPAL_CACHE_PER_PAGE,
  );

  return $return;
}

/**
 * Implements hook_block_view().
 */
function toc_node_block_view($delta = '') {
  $block = array();

  switch ($delta) {
    case 'toc_node':

      if (arg(0) != 'node' || !is_numeric(arg(1))) {
        return;
      }

      // Get the nid
      $nid  = arg(1);
      $node = node_load($nid);

      $toc_enabled = variable_get('toc_node_enabled_' . $node->type, 0);

      // Check if TOC is enabled for this content type.
      if (empty($toc_enabled)) {
        return;
      }

      // Check if TOC is set to not display.
      if (!isset($node->toc_node_style) || $node->toc_node_style == 'none') {
        return;
      }

      $node_rendered = node_view($node);
      $node_content  = drupal_render($node_rendered);
      $content       = toc_node_generate($node_content, $node->toc_node_style, $node->toc_node_level, $node->toc_node_back_to_top_links, 'toc_list');

      if ($content != $node_content) {
        $block['subject'] = t('Contents');
        $block['content'] = $content;
      }

      break;
  }

  return $block;
}

/**
 * Generate the TOC.
 *
 * @param $title
 * @param $content
 * @param $type
 * @return string
 */
function toc_node_generate($content, $style, $heading_levels, $back_to_top_links, $return = 'all') {

  static $toc_list_outputed = FALSE;
  $links        = array();
  $anchor_count = 0;

  $dom_document = new DOMDocument('1.0', 'utf-8');
  @$dom_document->loadHTML('<?xml encoding="UTF-8"><div id="toc-node">' . $content . '</div>');

  // Add anchors, and TOC tags which are used to work out the order of headings later.
  for ($level = 2; $level <= $heading_levels; $level++) {
    toc_node_add_anchors($dom_document, $anchor_count, $links, $level);
  }

  if (empty($links)) {
    return $content;
  }

  $title = NULL;
  // Create the TOC links.
  $toc = toc_node_links($dom_document, $links, $style);

  // Remove TOC tags.
  toc_node_remove_toc_tags($dom_document, $heading_levels);

  // Output DOM to a string.
  // Unfortunately below PHP 5.3.6 saveHTML() doesn't expect a parameter.
  $content_updated = $dom_document->saveHTML();
  // So we have to remove wrapping tags ourseleves.
  $content_fragments = explode('<div id="toc-node">', $content_updated);
  $output            = str_replace('</div></body></html>', '', $content_fragments[1]);

  // Check if table list is being output in a block.
  if ($return == 'toc_list') {
    $toc_list_outputed = TRUE;

    return $toc;
  }

  // If table list has already been output in a block don't output again.
  if (!$toc_list_outputed) {
    $output = $toc . $output;
  }

  // Back to top links.
  if (!empty($back_to_top_links)) {
    $output = theme('toc_node_move_to_top_link', array('content' => $output));
  }

  return $output;
}

/**
 * Add anchors, gather link data,
 * add TOC tags which are used to work out the order of headings later.
 *
 * @param $dom_document
 * @param $anchor_count
 * @param $links
 * @param $heading_level
 */
function toc_node_add_anchors(&$dom_document, &$anchor_count, &$links, $heading_level) {

  $headers = $dom_document->getElementsByTagName('h' . $heading_level);

  foreach ($headers as $header) {
    $anchor_count++;

    // TOC links.
    $label                                  = trim($header->nodeValue);
    $links['toc-' . $anchor_count]['label'] = $label;
    $links['toc-' . $anchor_count]['level'] = $heading_level;

    // Headings, add class.
    $classes = $header->getAttribute('class');
    $header->setAttribute('class', $classes . ' toc-headings');

    // Anchors above headings.
    $anchor_div = $dom_document->createElement('div');
    $anchor_div->setAttribute('class', 'toc-item-anchor');
    $target = $dom_document->createElement('a');
    $target->setAttribute('name', 'toc-' . $anchor_count);
    $anchor_div->appendChild($target);
    $header->parentNode->insertBefore($anchor_div, $header);

    $toc_tag = $dom_document->createElement('toc');
    $toc_tag->setAttribute('class', 'toc-' . $anchor_count);
    $header->appendChild($toc_tag);
  }
}

/**
 * Create the TOC links, including numbering of content items.
 *
 * @param $dom_document
 * @param $links_data
 * @return array
 */
function toc_node_links($dom_document, $links_data, $style) {
  $path       = drupal_get_path_alias($_GET['q']);
  $toc_number = array();

  $toc_tags = $dom_document->getElementsByTagName('toc');

  foreach ($toc_tags as $toc_tag) {
    $toc_id = $toc_tag->attributes->getNamedItem('class')->value;
    $link   = $links_data[$toc_id];
    // TOC always starts at H2, so we need to adjust level here.
    $toc_level       = $link['level'] - 1;
    $toc_level_count = count($toc_number);

    for ($i = $link['level']; $i <= $toc_level_count; $i++) {
      unset($toc_number[$i]);
    }

    if (empty($toc_number[$toc_level])) {
      $toc_number[$toc_level] = 1;
    }
    else {
      $toc_number[$toc_level] = $toc_number[$toc_level] + 1;
    }

    $link_number = implode('.', $toc_number);
    $data        = '';

    if ($style == 'numbers') {
      $data .= '<span class="toc-number">' . $link_number . '</span> ';
    }

    $data .= '<span class="toc-text">' . $link['label'] . '</span>';

    $links[] = array(
      'data'  => l($data, $path, array('fragment' => $toc_id, 'html' => TRUE)),
      'class' => array('toc-node-level-' . $toc_level),
    );

  }

  if (empty($links)) {
    return '';
  }

  $content = '<div id="table-of-contents-links">';
  $content .= '<a name="table-of-contents"></a>';
  $content .= theme('item_list', array('items' => $links, 'type' => 'ul', 'attributes' => array('class' => 'toc-node-' . $style)));
  $content .= '</div>';

  return $content;
}

/**
 * Remove TOC tags, which were used to work out order and numbering of content items.
 *
 * @param $dom_document
 * @param $heading_levels
 */
function toc_node_remove_toc_tags(&$dom_document, $heading_levels) {

  for ($level = 2; $level <= $heading_levels; $level++) {
    $headers = $dom_document->getElementsByTagName('h' . $level);

    foreach ($headers as $header) {
      $toc = $header->getElementsByTagName('toc');
      $header->removeChild($toc->item(0));
    }
  }
}

/**
 * Implementation of hook_form_node_view_alter().
 */
function toc_node_node_view_alter(&$build) {

  $toc_enabled = variable_get('toc_node_enabled_' . $build['#bundle'], 0);
  $node        = $build['#node'];

  if (empty($toc_enabled) || empty($node->toc_node_style) || $node->toc_node_style == 'none') {
    return;
  }

  // Only show TOC on node page.
  if (arg(0) != 'node' || !is_numeric(arg(1)) || arg(1) != $node->nid) {
    return;
  }

  // Add a #post_render callback to act on the rendered HTML of the node.
  $build['#post_render'][] = 'toc_node_node_post_render';
}

/**
 * Post render callback to act on the rendered HTML of the node.
 */
function toc_node_node_post_render($content, $element) {
  $node    = $element['#node'];
  $content = toc_node_generate($content, $node->toc_node_style, $node->toc_node_level, $node->toc_node_back_to_top_links);

  return $content;
}


