/* A quick module for generating flot charts from an HTML table. Options can
 * be passed directly to flot using the data-module-* attributes. The tables
 * are currently expected to be marked up as follows:
 *
 *   <table data-module="plot">
 *     <thead>
 *       <tr>
 *         <th>X Axis</th>
 *         <th>Series A Legend</th>
 *         <th>Series B Legend</th>
 *       </tr>
 *     </thead>
 *     <tbody>
 *       <tr>
 *         <th>X Value</th>
 *         <td>Series A Y Value</td>
 *         <td>Series B Y Value</td>
 *       </tr>
 *       ...
 *     </tbody>
 *   </table>
 *
 * Points are pulled out of the th/td elements using innerHTML or by looking
 * for a data-value attribute. This is useful when a more readable value
 * needs to be used in the elements contents (eg. dates). A data-type attribute
 * can also be applied to parse the value. Only data-type="date" is currently
 * supported and expects data-value to be a unix timestamp.
 */
this.ckan.module('plot', function (jQuery, _) {
  return {
    /* Holds the jQuery.plot() object when created */
    graph: null,

    /* Holds the canvas container when created */
    canvas: null,

    /* Default options */
    options: {
      xaxis: {},
      yaxis: {},
      legend: {position: 'nw'},
      colors: ['#ffcc33', '#ff8844']
    },

    /* Sets up the canvas element and parses the table.
     *
     * Returns nothing.
     */
    initialize: function () {
      jQuery.proxyAll(this, /_on/);

      if (!this.el.is('table')) {
        throw new Error('CKAN module plot can only be called on table elements');
      }

      this.setupCanvas();

      // Because the canvas doesn't render correctly unless visible we must
      // listen for events that reveal the canvas and then try and re-render.
      // Currently the most common of these is the "shown" event triggered by
      // the tabs plugin.
      this.sandbox.body.on("shown", this._onShown);
      this.data = this.parseTable(this.el);

      this.draw();
    },

    /* Removes event handlers when the module is removed from the DOM.
     *
     * Returns nothing.
     */
    teardown: function () {
      this.sandbox.body.off("shown", this._onShown);
    },

    /* Creates the canvas wrapper and removes the table from the document.
     *
     * Returns nothing.
     */
    setupCanvas: function () {
      this.canvas = jQuery('<div class="module-plot-canvas">');
      this.el.replaceWith(this.canvas);
    },

    /* Attempts to draw the chart if the canvas is visible. If not visible the
     * graph does not render correctly. So we keep trying until it is.
     *
     * Examples
     *
     *   module.draw();
     *
     * Returns nothing.
     */
    draw: function () {
      if (!this.drawn && this.canvas.is(':visible')) {
        this.graph = jQuery.plot(this.canvas, this.data, this.options);
      }
    },

    /* Parses an HTML table element to build the data array for the chart.
     * The thead element provides the axis and labels for the series. The
     * first column in the tbody is used for the x-axis and subsequent
     * columns are the series.
     *
     * table - A table element to parse.
     *
     * Examples
     *
     *   module.parseTable(module.el);
     *
     * Returns data object suitable for use in jQuery.plot().
     */
    parseTable: function (table) {
      var data = [];
      var _this = this;

      var headings = table.find('thead tr:first th').map(function () {
        return this.innerHTML;
      });

      table.find('tbody tr').each(function (row) {
        var element = jQuery(this);
        var x = [];

        x[row] = _this.getValue(element.find('th'));

        element.find('td').each(function (series) {
          var value   = _this.getValue(this);
          var label = headings[series + 1];

          data[series] = data[series] || {data: [], label: label};
          data[series].data[row] = [x[row], value];
        });
      });

      return data;
    },

    /* Retrieves the value from a td/th element. This first looks for a
     * data-value attribute on the element otherwise uses the element
     * text contents.
     *
     * A data-type attribute can also be provided to tell the module how
     * to deal with the element. By default we let jQuery.data() handle
     * the parsing but this can provide additional data. See .parseValue()
     * for more info.
     *
     * cell - An element to extract a value from.
     *
     * Examples
     *
     *   var element = jQuery('<td data-value="10">Ten</td>');
     *   module.getValue(element); //=> 10
     *
     *   var element = jQuery('<td>20</td>');
     *   module.getValue(element); //=> 20
     *
     *   var element = jQuery('<td data-type="date">1343747094</td>');
     *   module.getValue(element); //=> <Date Tue Jul 31 2012 16:04:54 GMT+0100 (BST)>
     *
     * Returns the parsed value.
     */
    getValue: function (cell) {
      var item  = cell instanceof jQuery ? cell : jQuery(cell);
      var type  = item.data('type')  || 'string';
      var value = item.data('value') || item.text();
      return this.parseValue(value, type);
    },

    /* Provides the ability to further format a value.
     *
     * If date is provided as a type then it expects value to be a unix
     * timestamp in seconds.
     *
     * value - The value extracted from the element.
     * type  - A type string, currently only supports "date".
     *
     * Examples
     *
     *   module.parseValue(10); // => 10
     *   module.parseValue("cat"); // => "cat"
     *   module.parseValue(1343747094, 'date'); // => <Date Tue Jul 31 2012 16:04:54 GMT+0100 (BST)>
     *
     * Returns the parsed value.
     */
    parseValue: function (value, type) {
      if (type === 'date') {
        value = new Date(parseInt(value, 10) * 1000);
        if (!value) {
          value = 0;
        }
      }
      return value;
    },

    /* Event handler for when tabs are toggled. Determines if the canvas
     * resides in the shown element and attempts to re-render.
     *
     * event - The shown event object.
     *
     * Returns nothing.
     */
    _onShown: function (event) {
      if (!this.drawn && jQuery.contains(jQuery(event.target.hash)[0], this.canvas[0])) {
        this.draw();
      }
    }
  };
});
