<?php

/**
 * @file
 * highcharttable.module
 *
 * Uses the Highcharts javascript library and HighchartTable jQuery add-on to
 * decorate any HTML table on your site with a graphical visualisation as a
 * colourful chart.
 * Does NOT require Views.
 */

define('HIGHCHARTTABLE_DEFAULT_TABLE_SELECTOR', 'table');

define('HIGHCHARTTABLE_CONTEXTUAL_LINKS_EXCLUDE_PAGES', '
admin/*
~admin/reports/access-denied
~admin/reports/page*
~admin/reports/referrers
~admin/reports/search
~admin/reports/visitors');

/**
 * Implements hook_preprocess_page().
 *
 * Many hooks qualify for what we do here...
 */
function highcharttable_preprocess_page($variables) {

  $module_path = drupal_get_path('module', 'highcharttable');

  // Add some Javascript, with a group and weight such that it will run before
  // modules/contextual/contextual.js
  // We need this on all pages because we do not know a-priori whether the
  // page to be loaded will have HTML tables on it or not -- that is exactly
  // what this Javascript figures out!
  $js = $module_path . '/js/highcharttable.js';
  drupal_add_js($js, array('group' => JS_LIBRARY, 'weight' => -1));

  $global_settings = variable_get('highcharttable_global_settings', array());

  $path = current_path();
  if (user_access('configure chart decorations')) {
    $exclude_pages = isset($global_settings['contextual-links-exclude-pages'])
      ? $global_settings['contextual-links-exclude-pages']
      : HIGHCHARTTABLE_CONTEXTUAL_LINKS_EXCLUDE_PAGES;
    if (highcharttable_match_path($path, $exclude_pages)) {
      $global_settings['contextual-links'] = 'off';
    }
  }
  else {
    $global_settings['contextual-links'] = 'off';
  }
  if ($global_settings['contextual-links'] != 'off') {
    drupal_add_css($module_path . '/css/highcharttable.css');

    if ($global_settings['contextual-links'] == '') {
      // Default core-style contexutual links behaviour.
      // These lines will do nothing, if already included by core Contextual.
      $contextual_path = drupal_get_path('module', 'contextual');
      drupal_add_css($contextual_path . '/contextual.css');
      drupal_add_js( $contextual_path . '/contextual.js');
    }
  }

  // First, pass the global settings to the js.
  drupal_add_js(array('highcharttable' => array('global' => $global_settings)), array('type' => 'setting'));

  // ... now pass the decorations too.
  $path_alias = drupal_get_path_alias($path);
  $decoration_settings = variable_get('highcharttable_decorations', array());

  foreach ($decoration_settings as $decoration) {
    if (!empty($decoration['pages-and-selector'])) {
      $include_pages = $decoration['pages-and-selector']['include-pages'];
      if (highcharttable_match_path($path, $include_pages) || highcharttable_match_path($path_alias, $include_pages)) {
        $settings = highcharttable_get_js_settings($global_settings, $decoration);
        drupal_add_js(array('highcharttable' => $settings), array('type' => 'setting'));
        if (empty($is_loaded)) {
          $variant = empty($global_settings['use-patched-library']) ? NULL : 'patched';
          highcharttable_load_libs($variant);
          $is_loaded = TRUE;
        }
      }
    }
  }
}

/**
 * Set up an array of configurations for the DataTables JS call.
 *
 * @param array $decoration
 *   An array of HighchartTable jQuery settings, indexed by the table selector
 */
function highcharttable_get_js_settings($global_settings, $decoration) {

  $table_selector = empty($decoration['pages-and-selector']['table-selector']) ? HIGHCHARTTABLE_DEFAULT_TABLE_SELECTOR : $decoration['pages-and-selector']['table-selector'];
  $decoration_params = empty($decoration['decoration-params']) ? array() : $decoration['decoration-params'];

  $chart_type = isset($decoration_params['chart-type']) ? $decoration_params['chart-type'] : NULL;

  $settings = array($table_selector => array(
    'animation' => empty($global_settings['animation']) ? NULL : $global_settings['animation'],
    'color-1' => empty($decoration_params['color-1']) ? NULL : $decoration_params['color-1'],
    'color-2' => empty($decoration_params['color-2']) ? NULL : $decoration_params['color-2'],
    'color-3' => empty($decoration_params['color-3']) ? NULL : $decoration_params['color-3'],
    'container-before' => 1,
    'datalabels-enabled' => !empty($decoration_params['labels']),
    'height' => empty($decoration_params['height']) ? NULL : (int) $decoration_params['height'],
    'hide-table' => !empty($decoration['pages-and-selector']['hide-table']),
    'inverted' => !empty($decoration_params['swap-axes']),
    'legend-disabled' => empty($decoration_params['legend']),
    'legend-layout' => empty($decoration_params['legend']) ? NULL : $decoration_params['legend'],
    'pie-show-in-legend' => ($chart_type == 'pie') && !empty($decoration_params['legend']),
    'subtitle-text' => empty($decoration_params['subtitle']) ? NULL : filter_xss_admin($decoration_params['subtitle']),
    'suppress-invalid-series' => isset($decoration_params['suppress-invalid-series']) ? $decoration_params['suppress-invalid-series'] : 2,
    'type' => $chart_type,
    'xaxis' => empty($decoration_params['xaxis']) ? 0 : max(0, (int)$decoration_params['xaxis'] - 1),
    // Only doing formatter for primary yaxis, ignoring opposite yaxes.
    'yaxis-1-formatter-callback' => empty($decoration_params['formatter']) ? NULL : $decoration_params['formatter'],
  ));
  return $settings;
}

/**
 * Include all required external javascript code.
 *
 * Highcharts JS and HighchartTable jQuery libraries.
 *
 * @param $variant, string
 *   Either NULL for the original version or 'patched' for the local version.
 */
function highcharttable_load_libs($variant) {

  // Equivalent to core's drupal_add_library('highcharttable', 'highcharts'),
  // this loads what is set up in the .libraries.info file, rather than core's
  // hook_library(). The format of the array returned is similar.
  $highcharts_lib = libraries_load('highcharts');
  if (!empty($highcharts_lib['error'])) {
    drupal_set_message($highcharts_lib['error message'], 'warning');
  }

  $highcharttable_lib = libraries_load('highcharttable', $variant);
  if (!empty($highcharttable_lib['error'])) {
    drupal_set_message($highcharttable_lib['error message'], 'warning');
  }
}

/**
 * Implements hook_menu().
 */
function highcharttable_menu() {
  // Put the administrative settings, on the Configuration page, under Content
  $items['admin/config/content/highcharttable'] = array(
    'title' => 'HighchartTable',
    'description' => 'Configure chart decorations and global settings.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('highcharttable_admin_configure_form'),
    'access arguments' => array('configure chart decorations'),
    'file' => 'highcharttable.admin.inc',
  );
  $items['highcharttable/contextual-link'] = array(
    'title' => 'HighchartTable contextual links',
    'description' => 'HighchartTable contextual link',
    'page callback' => 'highcharttable_contextual_link',
    'access arguments' => array('configure chart decorations'),
  );
  return $items;
}

/**
 * Menu callback for highcharttable/contextual-link
 *
 * @param string $operation ($arg0)
 *   The operation to perform, one of 'insert', 'delete'.
 *   'configure' is obsolete as this operation can be done via a direct link.
 * @param string $chart_type ($arg1)
 *   The chart type, one of 'column', 'line', 'spline', 'area', 'pie'.
 *   Defaults to 'column'.
 * @param string $arg2
 *   Optional argument, not used
 * @param string $arg3
 *   Optional argument, not used
 */
function highcharttable_contextual_link($operation = NULL, $chart_type = NULL, $arg2 = NULL, $arg3 = NULL) {
  $decorations = variable_get('highcharttable_decorations', array());
  $page = filter_input(INPUT_GET, 'destination');
  $url_options = array('query' => array('destination' => $page));

  switch ($operation) {

    case 'insert':
      highcharttable_add_decoration($page, $chart_type, $decorations);
      break;

    case 'delete':
      highcharttable_remove_page_from_decorations($page, $decorations);
      break;

    default:
      // Not used. As there are no actions required for 'configure', link to
      // this page directly, rather than using this function.
      drupal_goto('admin/config/content/highcharttable', $url_options);
  }
  // Make sure to save with keys that are numeric, consecutive and start at 0.
  variable_set('highcharttable_decorations', array_values($decorations));

  // Having created/deleted the chart decoration we return where we came from.
  drupal_goto($page, $url_options);
}

/**
 * Add a new char decoration, using the supplied page path and chart type.
 *
 * @param string $page
 * @param string $chart_type, defaults to 'column'
 * @param array $decorations
 */
function highcharttable_add_decoration($page, $chart_type = NULL, &$decorations) {
  $decoration = array(
    'decoration-params' => array(
      'chart-type' => empty($chart_type) ? 'column' : $chart_type,
    ),
    'pages-and-selector' => array(
      'include-pages' => $page,
      'table-selector' => '' // wish we could be more specific here
    ),
  );
  $decorations[] = $decoration;
}

/**
 * Removes the supplied page path from all decorations that use it.
 *
 * If as a result of this the decoration no longer has any 'include-pages', then
 * the decoration is removed altogether.
 *
 * @param string $page
 * @param array $decorations
 */
function highcharttable_remove_page_from_decorations($page, &$decorations) {
  $url_options = array('query' => array('destination' => $page));
  foreach ($decorations as $key => $decoration) {
    $include_pages = trim($decoration['pages-and-selector']['include-pages']);
    // Remove page path from 'include-pages', if found.
    $reduced = str_replace($page, '', $include_pages);
    if (strlen($reduced) < strlen($include_pages)) {
      $is_deleted = TRUE;
      if (empty($reduced)) {
        unset($decorations[$key]);
        drupal_set_message(t('Chart and chart decoration removed.'));
      }
      else {
        $decorations[$key]['pages-and-selector']['include-pages'] = $reduced;
        drupal_set_message(t('Chart removed. However, the underlying chart decoration was retained, as it appears to be used on other pages. Visit the <a href="@url_config">configuration page</a> to learn more.', array(
          '@url_config' => url('admin/config/content/highcharttable', $url_options)
        )));
      }
    }
  }
  if (empty($is_deleted)) {
    drupal_set_message(t('The chart was not removed. Most likely this was because this page is part of a wild-card specification shared with other charts. Visit the <a href="@url_config">configuration page</a> to learn more.', array(
      '@url_config' => url('admin/config/content/highcharttable', $url_options)
    )));
  }
}

/**
 * Match a path against one or more regex patterns.
 *
 * A drop-in replacement for drupal_match_path(), this also handles pattern
 * negation through the use of the twiddle/squiggle (~) character.
 *
 * Adapted from function context_condition_path::match() in the Context module.
 *
 * @param string $path
 *   The path string to be matched.
 * @param string $patterns
 *   String containing a sequence of patterns separated by \n, \r or \r\n.
 *   Each pattern will be trimmed of leading spaces.
 *   Any pattern that begins with ~ is interpreted as an exclusion and overrides
 *   any match amongst patterns.
 *   Example:
 *   With $patterns = "admin/*\n~admin/reports/visitors", this function will
 *   return TRUE for all "admin/*" paths, except "admin/reports/visitors".
 */
function highcharttable_match_path($path, $patterns) {
  $regexps = &drupal_static(__FUNCTION__, array());
  $patterns = preg_split('/[\r\n]+/', $patterns);
  $match = FALSE;
  $positives = $negatives = 0;
  foreach ($patterns as $pat) {
    $pattern = trim($pat);
    if (!empty($pattern)) {
      if ($negate = (drupal_substr($pattern, 0, 1) === '~')) {
        // Remove the tilde before executing a standard preg_match().
        $pattern = drupal_substr($pattern, 1);
        $negatives++;
      }
      else {
        $positives++;
      }
      if (!isset($regexps[$pattern])) {
        $regexps[$pattern] = '/^(' . preg_replace(array('/(\r\n?|\n)/', '/\\\\\*/', '/(^|\|)\\\\<front\\\\>($|\|)/'), array('|', '.*', '\1' . preg_quote(variable_get('site_frontpage', 'node'), '/') . '\2'), preg_quote($pattern, '/')) . ')$/';
      }
      if (preg_match($regexps[$pattern], $path)) {
        if ($negate) {
          return FALSE;
        }
        $match = TRUE;
      }
    }
  }
  // If there are *only* negative conditions and we got this far, we actually
  // have a match.
  if ($positives === 0 && $negatives > 0) {
    return TRUE;
  }
  return $match;
}

/**
 * Implements hook_permission().
 */
function highcharttable_permission() {
  return array(
    'configure chart decorations' => array(
      'title' => t('Add and configure chart decorations'),
    ),
  );
}

/**
 * Implements hook_help().
 */
function highcharttable_help($path, $arg) {
  switch ($path) {
    case 'admin/help#highcharttable':
      $t = t('Configuration instructions and tips are in this <a target="readme" href="@README">README</a> file.<br/>Known issues and solutions may be found on the <a taget="project" href="@highcharttable">HighchartTable</a> project page.', array(
        '@README' => url(drupal_get_path('module', 'highcharttable') . '/README.txt'),
        '@highcharttable' => url('http://drupal.org/project/highcharttable')));
      break;

    case 'admin/config/content/highcharttable':
      $t = t('A <strong>chart decoration</strong> consists of a set of chart options, selected below. Apart from the features you wish to include in each chart decoration, you specify the pages the chart(s) should appear on.');
      break;
  }
  return empty($t) ? '' : '<p>' . $t . '</p>';
}

/**
 * Implements hook_libraries_info_file_paths().
 *
 * Using the .libraries.info files instead of hook_libraries_info().
 */
function highcharttable_libraries_info_file_paths() {
  return array(drupal_get_path('module', 'highcharttable') . '/libraries');
}

/**
 * Implements hook_libraries_info_alter().
 *
 * This is a dynamic appendix to the .libraries.info files.
 * Through the configuration page, the user may opt for a variant, used here.
 */
function highcharttable_libraries_info_alter(&$libraries) {
  if (!isset($libraries['highcharttable'])) {
    return;
  }
  // The d.o. packaging script unfortunately adds to the .libraries.info file
  // the version number of the module in the same way as it does for
  // highcharttables.info. This is bad news for us, as once set, the Libraries
  // module will not attempt to read it from the specified files. So unset.
  unset($libraries['highcharts']['version']);
  unset($libraries['highcharttable']['version']);

  $global_settings = variable_get('highcharttable_global_settings', array());
  $variant = empty($global_settings['use-patched-library']) ? NULL : 'patched';

  // Based on the variant, the version number is found in a different file.
  // Not only is the file where we obtain the version different, so is its path.
  // The default path is sites/all/libraries/highcharttable, the variant path
  // is the module path.
  if ($variant == 'patched') {
    $libraries['highcharttable']['library path'] = drupal_get_path('module', 'highcharttable');
  }
  $libraries['highcharttable']['version arguments']['file'] = ($variant == 'patched')
    ? 'libraries/variants/highchartTable-patched.jquery.json'
    : 'highchartTable.jquery.json';
}

/**
 * Returns whether path matches any of the wildcards in decoration.
 *
 * @param string $path
 * @param array $decoration
 *
 * @return bool
 */
function _highcharttable_path_matches_decoration($path, $decoration) {
  return !empty($decoration['pages-and-selector']) &&
    highcharttable_match_path($path, $decoration['pages-and-selector']['include-pages']);
}