/**
 * Visualization helper class
 */
var Visualization = function (object, type, options, container, data, diffData, globalNames, material, editable, showEditButton) {
	// Constructor and vars
	this.object = object;
	this.type = type;
	this.options = options;
	this.container = container;
	this.data = data;
	this.diffData = diffData;
	this.rows = {};
	this.contextColumns = {};
	this.currentView = null;
	this.globalNames = globalNames;
	this.lastData = null;
	this.lastOptions = null;
	this.isMaterial = material;
	this.editable = editable;
	
	if ( editable ) {
		// ChartEditor Notes: https://goo.gl/VPTaCi
		this.editor = new google.visualization.ChartEditor() ;
		
		// Bind the 'Edit' button in ops_Page
		// $.proxy is used here to maintain the context of this Visualization instance.
		
		if (showEditButton) {
			this.setEditButton(
				$(this.container).parent().find('.ops_visualization_editButton .ops_form_element_button'),
				function(editButton){ $(editButton).lockSubmitReset(); }
			);
		}
		
		// Event when the OK button is clicked (save edit changes).
		// We use $.proxy to preserve the context / value of 'this' in the function,
		google.visualization.events.addListener( this.editor, 'ok', $.proxy(function(){			
			// Update and redraw
			var cw = this.editor.getChartWrapper();
			this.setChartWrapper(cw);
			this.draw(null, cw.getOptions());
			
			// Run any callbacks on finish:
			if (this.editFinishCallback) this.editFinishCallback();		
		}, this ) ); 
		
		// Event when the user clicks the exit button (cross icon) or the Cancel button:
		google.visualization.events.addListener( this.editor, 'cancel', $.proxy(function(){
			// Run any callbacks when the user has finished editting.
			if (this.editFinishCallback()) this.editFinishCallback();
		}, this) );
	}
};

Visualization.prototype.setEditButton = function(editButton, editFinishCallback) {
	if (typeof editFinishCallback !== 'undefined') {
		this.editFinishCallback = $.proxy(editFinishCallback, this, editButton);
	} else {
		this.editFinishCallback = null; 
	}
	$(editButton).click($.proxy(this.loadEditor, this));
};

// Load the ChartEditor window/dialog.
Visualization.prototype.loadEditor = function() {
	// Param 1 is the wrapper to load the editor into.
	// Param 2 is for any options related to the ChartEditor itself.
	this.editor.openDialog(this.getChartWrapper(), {});
};

Visualization.prototype.cloneOptions = function () {
	var options = Object.create(this.options);
	return options;
};

Visualization.prototype.hideColumn = function (colName) {
	var view = new google.visualization.DataView(this.data);
	if (this.currentView != null) {
		view = this.currentView;
	}
	var colId = this.getColumnIndexFromName(colName);
	var columnsShown = view.getViewColumns();
	if (columnsShown.indexOf(colId) > -1) {
		var cols = [colId].concat(this.contextColumns[colId]);
		view.hideColumns(cols);
		// Update view
		this.currentView = view;
		// Redraw chart
		this.draw(view, this.options);
	}
};

// Redraws chart with column removed in filtered view
Visualization.prototype.toggleColumn = function (colName) {
	var view = new google.visualization.DataView(this.data);
	// Update view reference
	if (this.currentView != null) {
		view = this.currentView;
	}
	var colId = this.getColumnIndexFromName(colName);
	var columnsShown = view.getViewColumns();
	if (columnsShown.indexOf(colId) > -1) {
		// Hide
		var cols = [colId].concat(this.contextColumns[colId]);
		view.hideColumns(cols);
	} else {
		// Show
		var cols = [colId].concat(this.contextColumns[colId]);
		columnsShown.push(cols);
		view.setColumns(columnsShown);
	}
	// Update view
	this.currentView = view;
	// Redraw chart
	this.draw(view, this.options);
};

Visualization.prototype.rowIsEmpty = function (rowId, colId) {
	var cols = this.data.getNumberOfColumns();
	var empty = true;
	for (var i = 1; i < cols - 1; i++) {
		if (i == colId) {
			// Known col is ignored
			continue;
		}
		// is col non-null?
		var val = this.data.getValue(rowId, i);
		// is col a role col?
		var role = this.data.getColumnRole(i);
		if (role == "" && val != null) {
			empty = false;
		}
	}
	return empty;
};

// Null values in column
Visualization.prototype.clearChartRows = function (colName) {
	var colId = this.getColumnIndexFromName(colName);
	var rowIds = this.rows[colId];
	var rowsRemoved = 0;
	for (var r in rowIds) {
		var rowId = rowIds[r] - rowsRemoved;
		// Delete if only cols 0 and n are filled [ignore roles]
		if (this.rowIsEmpty(rowId, colId)) {
			this.data.removeRow(rowId, colId);
			rowsRemoved++;
		} else {
			this.setCell(rowId, colId, null);
		}
	}
	this.rows[colId] = [];
};

Visualization.prototype.setOptions = function (options) {
	this.options = options;
};

Visualization.prototype.getOptions = function () {
	return this.options;
};

// Alias for draw
Visualization.prototype.redraw = function (data, options) {
	if( data === undefined ) {
		data = null ;
	}
	if( options === undefined ) {
		options = null ;
	}
	this.draw(data, options);
};

Visualization.prototype.draw = function (tempData, tempOptions) {
	if (!this.object) {
		return;
	}
	if (tempData == null) {
		if (this.lastData != null) {
			tempData = this.lastData;
		} else {
			tempData = this.data;
		}
	}

	if (tempOptions == null) {
		if (this.lastOptions != null) {
			tempOptions = this.lastOptions;
		} else {
			tempOptions = this.options;
		}
	}
	this.lastData = tempData;
	this.lastOptions = tempOptions;
	// Hack interpolation (linear) for material charts until interpolateNulls works
	// TODO At which point you can delete this
	if (tempOptions.interpolateNulls && this.isMaterial) {
		for (var c = 1; c < tempData.getNumberOfColumns(); c++) {
			tempData = this.interpolateNulls(c, tempData); 
		}
	}

	if ( this.editable ) {

		// ChartEditor will only take a ChartWrapper, not a Visualization (in Google's namespace)
		// so lazy-load the ChartWrapper to initialize so it can be rendered and later 
		// invoked by the ChartEditor to be edited and such.
		var cw = this.getChartWrapper();
		cw.setDataTable(tempData);
		cw.setOptions(tempOptions);
		cw.draw(this.container);
	} else {
		this.object.draw(tempData, tempOptions) ;
	}
};

Visualization.prototype.setCell = function (row, col, val) {
	this.data.setCell(row, col, val);
};

Visualization.prototype.getCell = function (row, col) {
	return this.data.getValue(row, col);
};

Visualization.prototype.addNewChartColumn = function (type, columnName, columnId, options, contextColumns) {
	// Check if already exists
	var colId = this.getColumnIndexFromName(columnId);
	if ($.isNumeric(colId) && colId >= 0) {
		return colId ;
	}
	// Add new column and return the ID if not
	var colId = this.data.addColumn(type, columnName, columnId, options);
	this.contextColumns[colId] = [] ;

	// Add context columns
	if (contextColumns) {
		contextIds = [];
		for (var contextColName in contextColumns) {
			var contextColumnOptions = contextColumns[contextColName];
			contextColumnOptions.id = contextColName;
			var contextId = this.data.addColumn(contextColumnOptions);
			this.contextColumns[colId].push(contextId);
		}
	}
	return colId;
};

Visualization.prototype.getColumnName = function (colId) {
	return this.data.getColumnLabel(colId);
};

Visualization.prototype.getColumnIndexFromName = function (colName) {
	var data = this.data;

	for (var i = 0; i < data.getNumberOfColumns(); i++) {
		// Return ID if it does
		var checkName = data.getColumnId(i);
		if (checkName.toLowerCase() === colName.toLowerCase()) {
			return i;
		}
	}
	return false;
};

// Record columnName/RowIds for faster looping
Visualization.prototype.addChartRowRecord = function (rowId, columnId) {
	var columnList = this.rows[columnId];
	if (columnList === undefined) {
		columnList = this.rows[columnId] = [];
	}
	columnList.push(rowId);
};

// Not sure how well this will work with discrete axes
// TODO allow passing in of data to add
Visualization.prototype.addNewDatapoint = function ( colName, referenceValue, data ) {
	if ( data === undefined ) {
		data = null ;
	}
	// Check column for a null we can reuse
	var data = this.data;
	var colId = this.getColumnIndexFromName(colName);
	var rowId = null;
	if (referenceValue) {
		rowId = this.getColumnIndexFromRefValue(referenceValue);
	}

	if ($.isNumeric(colId) && colId >= 0) {
		// Found column
		if (rowId) {
			return rowId;
		} else {
			var rowIds = this.rows[colName];
			for (var n in rowIds) {
				var rowId = rowIds[n];
				if (data.getValue(rowId, colId) == null && data.getValue(rowId, 0) == null) {
					this.addChartRowRecord(rowId, colId);
					return rowId;
				}
			}
		}
	}
	var rowId = data.addRow();
	this.addChartRowRecord(rowId, colId);
	return rowId; 
};

Visualization.prototype.listen = function (actionName, callback) {
	google.visualization.events.addListener(this.object, actionName, callback);
};

Visualization.prototype.getSelection = function () {
	return this.object.getSelection();
};

Visualization.prototype.sort = function (colIds) {
	this.data.sort(colIds);
};

/**
 * Used while Material charts don't have interpolateNulls functionality
 */
Visualization.prototype.interpolateNulls = function (colId, data) {
	var colType = data.getColumnType(colId).toLowerCase();
	var colRole = data.getColumnRole(colId).toLowerCase();
	
	var nulls = [];
	var prevVal = null;
	var nullRows = [];
	
	for (var r = 0; r < data.getNumberOfRows(); r++) {
		var val = data.getValue(r, colId); 
		if (val !== null) {
			if (nullRows.length == 0) {
				// Found number and null count is zero - overwrite firstVal
				prevVal = val;
			} else {
				// Found number and null count > 0 - add new nulls object 
				nulls.push({
					startVal:parseFloat(prevVal),
					endVal:parseFloat(val), 
					startIndex: r - nullRows.length,
					count: nullRows.length,
					nullRows: nullRows
				});
				// Reset counter
				nullRows = [];
				prevVal = val;
			}
		} else {
			nullRows.push(r);
		}
	}

	for (var n = 0; n < nulls.length; n++) {
		var nuller = nulls[n];
		for (var rn = 0; rn < nuller.count; rn++) {
			var rowToSet = nuller.nullRows[rn];
			var interpolatedVal = 0;
			var position = (rn+1) / (nuller.count + 1);
			var difference = nuller.endVal - nuller.startVal;
			if (difference == 0) {
				interpolatedVal = nuller.startVal; 
			} else {
				if (colRole == 'data' && colType == 'number') {
					// Linear interpolation
					interpolatedVal = (1 - position) * nuller.startVal + position * nuller.endVal;
					break;
				} else {
					// Everything else mjust gets duplication of starting value
				 interpolatedVal = nuller.startVal; 
				}
			}
			//console.log("interp set row", rowToSet, 'col', colId, 'val', interpolatedVal, nuller, 'pos', position, data.getValue(rowToSet, 0));
			this.setCell(rowToSet, colId, interpolatedVal);
		}
	}

	for (var z = 0; z < data.getNumberOfRows(); z++) {
		var val = data.getValue(z, colId); 
		//console.log('row', z, 'col', data.getColumnId(colId), 'val', val);
	}

	return data;
};

Visualization.prototype.getColumnIndexFromRefValue = function (refValue) {
	var type = this.data.getColumnType(0);
	for (var i = 0; i < this.data.getNumberOfRows(); i++) {
		var val = this.data.getValue(i, 0);
		switch (type) {
			case 'string':
				if (val && val.toLowerCase() == refValue.toLowerCase()) {
					return i;
				}
				break;
			case 'date':
			case 'datetime':
				if (val && new Date(val).toGMTString() == new Date(refValue).toGMTString()) {
					return i;
				}
				break;
			case 'number':
				if (val && parseFloat(val) == parseFloat(refValue)) {
					return i;
				}
				break;
		}
	}
	return null;
};

/** 
 * Retrieve the ChartWrapper variant of this visualization, something that simplifies the 
 * DataTable-to-Chart mapping, and is required for running a ChartEditor. 
 * ChartWrapper notes: https://goo.gl/K50wY6
 * @param data DataTable [optional] If we want to update the data in the ChartWrapper.
 */
Visualization.prototype.getChartWrapper = function( data ) {

	// The chartWrapper is lazy loaded as it is not required in a normal visualization usage.
	if (this.hasOwnProperty('wrapper')) {
		if (typeof data !== 'undefined') this.wrapper.setDataTable(data); // Update the DataTable
		return this.wrapper;
	}
	var newWrapperData = data ? data : this.data;
	
	// The format for the ChartWrapper constructor is the same as the visualization drawChart().
	// See https://developers.google.com/chart/interactive/docs/reference#google.visualization.drawchart
	this.wrapper = new google.visualization.ChartWrapper({
		'chartType':this.type,
		'dataTable':newWrapperData,
		'options':this.options,
		'containerId':$(this.container).attr('id')
	});
	
	return this.wrapper;
};

Visualization.prototype.setChartWrapper = function( cw ) {	
	this.wrapper = cw;
	return this;
};

Visualization.prototype.debugData = function () {
	data = this.data;
	var rows = data.getNumberOfRows();
	var cols = data.getNumberOfColumns();
	var plainData = {};
	for (var c = 0; c < cols; c++) {
		var row = {};
		for (var r = 0; r < rows; r++) {
			row[r] = data.getValue(r, c);
		}
		plainData[this.getColumnName(c)] = row;
	}
	console.table(plainData);
};
