/**

 * @license Data plugin for Highcharts

 *

 * (c) 2012-2014 Torstein Honsi

 *

 * License: www.highcharts.com/license

 */



/*

 * The Highcharts Data plugin is a utility to ease parsing of input sources like

 * CSV, HTML tables or grid views into basic configuration options for use 

 * directly in the Highcharts constructor.

 *

 * Demo: http://jsfiddle.net/highcharts/SnLFj/

 *

 * --- OPTIONS ---

 *

 * - columns : Array<Array<Mixed>>

 * A two-dimensional array representing the input data on tabular form. This input can

 * be used when the data is already parsed, for example from a grid view component.

 * Each cell can be a string or number. If not switchRowsAndColumns is set, the columns

 * are interpreted as series. See also the rows option.

 *

 * - complete : Function(chartOptions)

 * The callback that is evaluated when the data is finished loading, optionally from an 

 * external source, and parsed. The first argument passed is a finished chart options

 * object, containing series and an xAxis with categories if applicable. Thise options

 * can be extended with additional options and passed directly to the chart constructor.

 *

 * - csv : String

 * A comma delimited string to be parsed. Related options are startRow, endRow, startColumn

 * and endColumn to delimit what part of the table is used. The lineDelimiter and 

 * itemDelimiter options define the CSV delimiter formats.

 * 

 * - endColumn : Integer

 * In tabular input data, the first row (indexed by 0) to use. Defaults to the last 

 * column containing data.

 *

 * - endRow : Integer

 * In tabular input data, the last row (indexed by 0) to use. Defaults to the last row

 * containing data.

 *

 * - googleSpreadsheetKey : String 

 * A Google Spreadsheet key. See https://developers.google.com/gdata/samples/spreadsheet_sample

 * for general information on GS.

 *

 * - googleSpreadsheetWorksheet : String 

 * The Google Spreadsheet worksheet. The available id's can be read from 

 * https://spreadsheets.google.com/feeds/worksheets/{key}/public/basic

 *

 * - itemDelimiter : String

 * Item or cell delimiter for parsing CSV. Defaults to the tab character "\t" if a tab character

 * is found in the CSV string, if not it defaults to ",".

 *

 * - lineDelimiter : String

 * Line delimiter for parsing CSV. Defaults to "\n".

 *

 * - parsed : Function

 * A callback function to access the parsed columns, the two-dimentional input data

 * array directly, before they are interpreted into series data and categories.

 *

 * - parseDate : Function

 * A callback function to parse string representations of dates into JavaScript timestamps.

 * Return an integer on success.

 *

 * - rows : Array<Array<Mixed>>

 * The same as the columns input option, but defining rows intead of columns.

 *

 * - startColumn : Integer

 * In tabular input data, the first column (indexed by 0) to use. 

 *

 * - startRow : Integer

 * In tabular input data, the first row (indexed by 0) to use.

 *

 * - switchRowsAndColumns : Boolean

 * Switch rows and columns of the input data, so that this.columns effectively becomes the

 * rows of the data set, and the rows are interpreted as series.

 *

 * - table : String|HTMLElement

 * A HTML table or the id of such to be parsed as input data. Related options ara startRow,

 * endRow, startColumn and endColumn to delimit what part of the table is used.

 */



// JSLint options:

/*global jQuery */



(function (Highcharts) {	

	

	// Utilities

	var each = Highcharts.each;

	

	

	// The Data constructor

	var Data = function (dataOptions, chartOptions) {

		this.init(dataOptions, chartOptions);

	};

	

	// Set the prototype properties

	Highcharts.extend(Data.prototype, {

		

	/**

	 * Initialize the Data object with the given options

	 */

	init: function (options, chartOptions) {

		this.options = options;

		this.chartOptions = chartOptions;

		this.columns = options.columns || this.rowsToColumns(options.rows) || [];



		// No need to parse or interpret anything

		if (this.columns.length) {

			this.dataFound();



		// Parse and interpret

		} else {



			// Parse a CSV string if options.csv is given

			this.parseCSV();

			

			// Parse a HTML table if options.table is given

			this.parseTable();



			// Parse a Google Spreadsheet 

			this.parseGoogleSpreadsheet();	

		}



	},



	/**

	 * Get the column distribution. For example, a line series takes a single column for 

	 * Y values. A range series takes two columns for low and high values respectively,

	 * and an OHLC series takes four columns.

	 */

	getColumnDistribution: function () {

		var chartOptions = this.chartOptions,

			getValueCount = function (type) {

				return (Highcharts.seriesTypes[type || 'line'].prototype.pointArrayMap || [0]).length;

			},

			globalType = chartOptions && chartOptions.chart && chartOptions.chart.type,

			individualCounts = [];



		each((chartOptions && chartOptions.series) || [], function (series) {

			individualCounts.push(getValueCount(series.type || globalType));

		});



		this.valueCount = {

			global: getValueCount(globalType),

			individual: individualCounts

		};

	},



	/**

	 * When the data is parsed into columns, either by CSV, table, GS or direct input,

	 * continue with other operations.

	 */

	dataFound: function () {

		

		if (this.options.switchRowsAndColumns) {

			this.columns = this.rowsToColumns(this.columns);

		}



		// Interpret the values into right types

		this.parseTypes();

		

		// Use first row for series names?

		this.findHeaderRow();

		

		// Handle columns if a handleColumns callback is given

		this.parsed();

		

		// Complete if a complete callback is given

		this.complete();

		

	},

	

	/**

	 * Parse a CSV input string

	 */

	parseCSV: function () {

		var self = this,

			options = this.options,

			csv = options.csv,

			columns = this.columns,

			startRow = options.startRow || 0,

			endRow = options.endRow || Number.MAX_VALUE,

			startColumn = options.startColumn || 0,

			endColumn = options.endColumn || Number.MAX_VALUE,

			itemDelimiter,

			lines,

			activeRowNo = 0;

			

		if (csv) {

			

			lines = csv

				.replace(/\r\n/g, "\n") // Unix

				.replace(/\r/g, "\n") // Mac

				.split(options.lineDelimiter || "\n");



			itemDelimiter = options.itemDelimiter || (csv.indexOf('\t') !== -1 ? '\t' : ',');

			

			each(lines, function (line, rowNo) {

				var trimmed = self.trim(line),

					isComment = trimmed.indexOf('#') === 0,

					isBlank = trimmed === '',

					items;

				

				if (rowNo >= startRow && rowNo <= endRow && !isComment && !isBlank) {

					items = line.split(itemDelimiter);

					each(items, function (item, colNo) {

						if (colNo >= startColumn && colNo <= endColumn) {

							if (!columns[colNo - startColumn]) {

								columns[colNo - startColumn] = [];					

							}

							

							columns[colNo - startColumn][activeRowNo] = item;

						}

					});

					activeRowNo += 1;

				}

			});



			this.dataFound();

		}

	},

	

	/**

	 * Parse a HTML table

	 */

	parseTable: function () {

		var options = this.options,

			table = options.table,

			columns = this.columns,

			startRow = options.startRow || 0,

			endRow = options.endRow || Number.MAX_VALUE,

			startColumn = options.startColumn || 0,

			endColumn = options.endColumn || Number.MAX_VALUE;



		if (table) {

			

			if (typeof table === 'string') {

				table = document.getElementById(table);

			}

			

			each(table.getElementsByTagName('tr'), function (tr, rowNo) {

				if (rowNo >= startRow && rowNo <= endRow) {

					each(tr.children, function (item, colNo) {

						if ((item.tagName === 'TD' || item.tagName === 'TH') && colNo >= startColumn && colNo <= endColumn) {

							if (!columns[colNo - startColumn]) {

								columns[colNo - startColumn] = [];					

							}

							

							columns[colNo - startColumn][rowNo - startRow] = item.innerHTML;

						}

					});

				}

			});



			this.dataFound(); // continue

		}

	},



	/**

	 */

	parseGoogleSpreadsheet: function () {

		var self = this,

			options = this.options,

			googleSpreadsheetKey = options.googleSpreadsheetKey,

			columns = this.columns,

			startRow = options.startRow || 0,

			endRow = options.endRow || Number.MAX_VALUE,

			startColumn = options.startColumn || 0,

			endColumn = options.endColumn || Number.MAX_VALUE,

			gr, // google row

			gc; // google column



		if (googleSpreadsheetKey) {

			jQuery.ajax({

				dataType: 'json', 

				url: 'https://spreadsheets.google.com/feeds/cells/' + 

				  googleSpreadsheetKey + '/' + (options.googleSpreadsheetWorksheet || 'od6') +

					  '/public/values?alt=json-in-script&callback=?',

				error: options.error,

				success: function (json) {

					// Prepare the data from the spreadsheat

					var cells = json.feed.entry,

						cell,

						cellCount = cells.length,

						colCount = 0,

						rowCount = 0,

						i;

				

					// First, find the total number of columns and rows that 

					// are actually filled with data

					for (i = 0; i < cellCount; i++) {

						cell = cells[i];

						colCount = Math.max(colCount, cell.gs$cell.col);

						rowCount = Math.max(rowCount, cell.gs$cell.row);			

					}

				

					// Set up arrays containing the column data

					for (i = 0; i < colCount; i++) {

						if (i >= startColumn && i <= endColumn) {

							// Create new columns with the length of either end-start or rowCount

							columns[i - startColumn] = [];



							// Setting the length to avoid jslint warning

							columns[i - startColumn].length = Math.min(rowCount, endRow - startRow);

						}

					}

					

					// Loop over the cells and assign the value to the right

					// place in the column arrays

					for (i = 0; i < cellCount; i++) {

						cell = cells[i];

						gr = cell.gs$cell.row - 1; // rows start at 1

						gc = cell.gs$cell.col - 1; // columns start at 1



						// If both row and col falls inside start and end

						// set the transposed cell value in the newly created columns

						if (gc >= startColumn && gc <= endColumn &&

							gr >= startRow && gr <= endRow) {

							columns[gc - startColumn][gr - startRow] = cell.content.$t;

						}

					}

					self.dataFound();

				}

			});

		}

	},

	

	/**

	 * Find the header row. For now, we just check whether the first row contains

	 * numbers or strings. Later we could loop down and find the first row with 

	 * numbers.

	 */

	findHeaderRow: function () {

		var headerRow = 0;

		each(this.columns, function (column) {

			if (typeof column[0] !== 'string') {

				headerRow = null;

			}

		});

		this.headerRow = 0;			

	},

	

	/**

	 * Trim a string from whitespace

	 */

	trim: function (str) {

		return typeof str === 'string' ? str.replace(/^\s+|\s+$/g, '') : str;

	},

	

	/**

	 * Parse numeric cells in to number types and date types in to true dates.

	 */

	parseTypes: function () {

		var columns = this.columns,

			col = columns.length, 

			row,

			val,

			floatVal,

			trimVal,

			dateVal;

			

		while (col--) {

			row = columns[col].length;

			while (row--) {

				val = columns[col][row];

				floatVal = parseFloat(val);

				trimVal = this.trim(val);



				/*jslint eqeq: true*/

				if (trimVal == floatVal) { // is numeric

				/*jslint eqeq: false*/

					columns[col][row] = floatVal;

					

					// If the number is greater than milliseconds in a year, assume datetime

					if (floatVal > 365 * 24 * 3600 * 1000) {

						columns[col].isDatetime = true;

					} else {

						columns[col].isNumeric = true;

					}					

				

				} else { // string, continue to determine if it is a date string or really a string

					dateVal = this.parseDate(val);

					

					if (col === 0 && typeof dateVal === 'number' && !isNaN(dateVal)) { // is date

						columns[col][row] = dateVal;

						columns[col].isDatetime = true;

					

					} else { // string

						columns[col][row] = trimVal === '' ? null : trimVal;

					}

				}

				

			}

		}

	},

	

	/**

	 * A collection of available date formats, extendable from the outside to support

	 * custom date formats.

	 */

	dateFormats: {

		'YYYY-mm-dd': {

			regex: '^([0-9]{4})-([0-9]{2})-([0-9]{2})$',

			parser: function (match) {

				return Date.UTC(+match[1], match[2] - 1, +match[3]);

			}

		}

	},

	

	/**

	 * Parse a date and return it as a number. Overridable through options.parseDate.

	 */

	parseDate: function (val) {

		var parseDate = this.options.parseDate,

			ret,

			key,

			format,

			match;



		if (parseDate) {

			ret = parseDate(val);

		}

			

		if (typeof val === 'string') {

			for (key in this.dateFormats) {

				format = this.dateFormats[key];

				match = val.match(format.regex);

				if (match) {

					ret = format.parser(match);

				}

			}

		}

		return ret;

	},

	

	/**

	 * Reorganize rows into columns

	 */

	rowsToColumns: function (rows) {

		var row,

			rowsLength,

			col,

			colsLength,

			columns;



		if (rows) {

			columns = [];

			rowsLength = rows.length;

			for (row = 0; row < rowsLength; row++) {

				colsLength = rows[row].length;

				for (col = 0; col < colsLength; col++) {

					if (!columns[col]) {

						columns[col] = [];

					}

					columns[col][row] = rows[row][col];

				}

			}

		}

		return columns;

	},

	

	/**

	 * A hook for working directly on the parsed columns

	 */

	parsed: function () {

		if (this.options.parsed) {

			this.options.parsed.call(this, this.columns);

		}

	},

	

	/**

	 * If a complete callback function is provided in the options, interpret the 

	 * columns into a Highcharts options object.

	 */

	complete: function () {

		

		var columns = this.columns,

			firstCol,

			type,

			options = this.options,

			valueCount,

			series,

			data,

			i,

			j,

			seriesIndex;

			

		

		if (options.complete) {



			this.getColumnDistribution();

			

			// Use first column for X data or categories?

			if (columns.length > 1) {

				firstCol = columns.shift();

				if (this.headerRow === 0) {

					firstCol.shift(); // remove the first cell

				}

				

				

				if (firstCol.isDatetime) {

					type = 'datetime';

				} else if (!firstCol.isNumeric) {

					type = 'category';

				}

			}



			// Get the names and shift the top row

			for (i = 0; i < columns.length; i++) {

				if (this.headerRow === 0) {

					columns[i].name = columns[i].shift();

				}

			}

			

			// Use the next columns for series

			series = [];

			for (i = 0, seriesIndex = 0; i < columns.length; seriesIndex++) {



				// This series' value count

				valueCount = Highcharts.pick(this.valueCount.individual[seriesIndex], this.valueCount.global);

				

				// Iterate down the cells of each column and add data to the series

				data = [];



				// Only loop and fill the data series if there are columns available.

				// We need this check to avoid reading outside the array bounds.

				if (i + valueCount <= columns.length) {

					for (j = 0; j < columns[i].length; j++) {

						data[j] = [

							firstCol[j],

							columns[i][j] !== undefined ? columns[i][j] : null

						];

						if (valueCount > 1) {

							data[j].push(columns[i + 1][j] !== undefined ? columns[i + 1][j] : null);

						}

						if (valueCount > 2) {

							data[j].push(columns[i + 2][j] !== undefined ? columns[i + 2][j] : null);

						}

						if (valueCount > 3) {

							data[j].push(columns[i + 3][j] !== undefined ? columns[i + 3][j] : null);

						}

						if (valueCount > 4) {

							data[j].push(columns[i + 4][j] !== undefined ? columns[i + 4][j] : null);

						}

					}

				}



				// Add the series

				series[seriesIndex] = {

					name: columns[i].name,

					data: data

				};



				i += valueCount;

			}

			

			// Do the callback

			options.complete({

				xAxis: {

					type: type

				},

				series: series

			});

		}

	}

	});

	

	// Register the Data prototype and data function on Highcharts

	Highcharts.Data = Data;

	Highcharts.data = function (options, chartOptions) {

		return new Data(options, chartOptions);

	};



	// Extend Chart.init so that the Chart constructor accepts a new configuration

	// option group, data.

	Highcharts.wrap(Highcharts.Chart.prototype, 'init', function (proceed, userOptions, callback) {

		var chart = this;



		if (userOptions && userOptions.data) {

			Highcharts.data(Highcharts.extend(userOptions.data, {

				complete: function (dataOptions) {

					

					// Merge series configs

					if (userOptions.hasOwnProperty('series')) {

						if (typeof userOptions.series === 'object') {

							each(userOptions.series, function (series, i) {

								userOptions.series[i] = Highcharts.merge(series, dataOptions.series[i]);

							});

						} else { // Allow merging in dataOptions.series (#2856)

							delete userOptions.series;

						}

					}



					// Do the merge

					userOptions = Highcharts.merge(dataOptions, userOptions);



					proceed.call(chart, userOptions, callback);

				}

			}), userOptions);

		} else {

			proceed.call(chart, userOptions, callback);

		}

	});



}(Highcharts));

