/**
 * @class VoyantTable
 * A VoyantTable can facilitate working with tabular data structures, as well as
 * displaying results (especially with {@link #embed} and {@link show}). 
 * Here's a simple example showing the Zipf-Law distribution of the top 20 frequency terms.
 * 
 * 	new Corpus("austen").loadCorpusTerms(20).then(function(corpusTerms) {
 * 		var table = new VoyantTable({rowKey: 0}); // use first column as row key
 * 		corpusTerms.each(function(corpusTerm) {
 *			table.addRow([corpusTerm.getTerm(), corpusTerm.getRawFreq()]);
 * 		});
 * 		table.embed("voyantchart"); // graph table as line chart
 * 	});
 */
Ext.define('Voyant.data.table.Table', {
	alternateClassName: ["VoyantTable"],
	mixins: ['Voyant.notebook.util.Embed','Voyant.notebook.util.Show'],
	embeddable: ['Voyant.widget.VoyantTableTransform','Voyant.widget.VoyantChart','Voyant.widget.CodeEditor'],
	config: {
		
		/**
		 * @private
		 */
		rows: [],

		/**
		 * @private
		 */
		headers: [],

		/**
		 * @private
		 */
		rowsMap: {},
		
		/**
		 * @private
		 */
		headersMap: {},
		
		/**
		 * Specifies that a specific header should serve as row key.
		 * 
		 */
		rowKey: undefined,
		
		/**
		 * @private
		 */
		model: undefined
	},
	
	clone: function() {
		var table = new VoyantTable();
		table.setRows(Ext.clone(this.getRows()));
		table.setHeaders(Ext.clone(this.getHeaders()))
		table.setRowsMap(Ext.clone(this.getRowsMap()))
		table.setHeadersMap(Ext.clone(this.getHeadersMap()))
		table.setRowKey(Ext.clone(this.getRowKey()))
		return table;
	},

	constructor: function(config, opts) {

		config = config || {};
		if (config.fromBlock) {
			var data = Voyant.notebook.Notebook.getDataFromBlock(config.fromBlock);
			if (data) {
				data = data.trim();
				config.rows = [];
				data.split(/\n+/).forEach(function(line,i) {
					var cells = line.split("\t");
					if (i==0 && !config.noHeaders) {
						config.headers = cells
					} else {
						config.rows.push(cells)
					}
				})
				
			}
		} else if (config.count && Ext.isArray(config.count)) {
			// create counts
			var freqs = {};
			config.count.forEach(function(item) {freqs[item] = freqs[item] ? freqs[item]+1 : 1;});
			// sort counts
			var counts = [];
			for (var key in freqs) {counts.push([key, freqs[key]])}
			counts.sort(function(a,b) {return b[1] - a[1]});
			if (config.limit && counts.length>config.limit) {
				counts.splice(config.limit);
			}
			if (config.orientation && config.orientation=="horizontal") {
				config.headers = counts.map(function(item) {return item[0]});
				config.rows = [counts.map(function(item) {return item[1]})];
			} else {
				config.headers = config.headers ? config.headers : ["Item","Count"];
				config.rows = counts;
			}
		} else if (config.isStore || config.store) {
			var store = config.store ? config.store : config;
			if (opts && opts.headers) {
				config.headers = opts.headers;
			} else {
				// store.getModel() doesn't seem to work (for CorpusTerms at least)
				// so instead we'll try looking at the first record to get headers
				var record = store.getAt(0);
				if (record) { // don't know what to do if this fails?
					config.headers = record.getFields().map(function(field) {return field.getName()});
				}
			}
			
			// now we get rows
			config.rows = [];
			store.each(function(record) {
				var data = record.getData();
				var cells = config.headers.map(function(header) {return data[header]}); // only from headers
				config.rows.push(cells);
			}, this);
		}

		// not sure why config isn't working
		if (!config.rows && Ext.isArray(config)) {
			config.rows = config;
		}
		if (!this.getHeaders()) {
			if (!config.headers && !config.noHeaders && config.rows) {
				this.setHeaders(config.rows.shift())
			} else {
				this.setHeaders(Ext.Array.from(config.headers));
			}
		}
		this.setRows(Ext.Array.from(config.rows));
		this.setRowKey(config.rowKey ? config.rowKey : this.getHeaders()[0]);

		// if we have no headers, use the index as header
		if (this.getHeaders().length==0) {
			var firstRow = this.getRow(0, false);
			if (firstRow) {
				this.setHeaders(firstRow.map(function(cell, i) {return i}));
			}
		}
		
		var headersMap = {};
		this.getHeaders().forEach(function(header, i) {
			headersMap[header] = i;
		});
		this.setHeadersMap(headersMap);
		
		this.reMapRows();
		
		this.callParent();
	},
	addRow: function(row) {
		if (Ext.isArray(row))
		if (Ext.isObject(row)) {
			var len = this.getRows().length;
			for (var key in row) {
				this.updateCell(len, key, row[key])
			}
			
		} else if (Ext.isArray(row)) {
			this.getRows().push(row);
			var header = this.getColumnIndex(this.getRowKey());
			if (header!==undefined && row[header]!==undefined) {
				this.getRowsMap()[row[header]] = this.getRows().length-1;
			}
		}
	},
	eachRecord: function(fn, scope) {
		var item, i=0, len=this.getRows().length;
		for (; i<len; i++) {
            item = this.getRecord(i);
			if (fn.call(scope || item, item, i, len) === false) {
                break;
            }			
		}
	},
	eachRow: function(fn, asMap, scope) {
		var item, i=0, len=this.getRows().length;
		for (; i<len; i++) {
            item = this.getRow(i, asMap);
			if (fn.call(scope || item, item, i, len) === false) {
                break;
            }			
		}
	},
	getRow: function(index, asMap) {
		var r = this.getRowIndex(index);
		if (asMap) {
			var row = {};
			var headers = this.getHeaders();
			Ext.Array.from(this.getRows()[r]).forEach(function(item, i) {
				row[headers[i] || i] = item;
			}, this);
			return row;
		} else {
			return this.getRows()[r];
		}
	},
	getRecord: function(index) {
		if (this.model) {return new this.model(this.getRow(index, true))}
	},
	mapRows: function(fn, asMap, scope) {
		var rows = [];
		this.eachRow(function(row, i) {
//			if (Object.keys(row).length>0) {
				rows.push(fn.call(scope || this, row, i))
//			}
		}, asMap, this)
		return rows;
	},
	
	/**
	 * Update the cell value at the specified row and column.
	 * 
	 * This will create the row and column as needed. If there's an existing value in the cell,
	 * it will be added to the new value, unless the `replace` argument is set to true.
	 * 
	 * @param {Number/String} row The cell's row.
	 * @param {Number/String} column The cell's column.
	 * @param {Mixed} value The cell's value.
	 * @param {boolean} [replace] Replace the current value (if it exists), otherwise
	 * the value is added to any current value (which is the default behaviour).
	 */
	updateCell: function(row, column, value, replace) {
		var rows = this.getRows();
		var r = Ext.isNumber(row) ? row : this.getRowIndex(row);
		var c = this.getColumnIndex(column);
		if (rows[r]===undefined) {rows[r]=[]}
		if (rows[r][c]===undefined || replace) {rows[r][c]=value}
		else {rows[r][c]+=value}
		// add to rowsMap if this is the header
		if (this.getHeaders()[c]===this.getRowKey()) {
			this.getRowsMap()[column] = r;
		}
	},
	
	getRowIndex: function(key) {
		if (Ext.isNumber(key)) {return key;}
		if (Ext.isString(key)) {
			var rowsMap = this.getRowsMap();
			if (!(key in rowsMap)) {
				rowsMap[key] = this.getRows().length;
				this.getRows().push(new Array(this.getHeaders().length))
			}
			return rowsMap[key];
		}
	},
	
	getColumnIndex: function(column) {
		var headers = this.getHeaders();
		if (Ext.isNumber(column)) {
			if (headers[column]===undefined) {
				headers[column]=column;
				this.getRows().forEach(function(row) {
					row.splice(column, 0, undefined);
				});
			}
			return column;
		} else if (Ext.isString(column)) {
			if (!(column in this.getHeadersMap())) {
				// we don't have this column yet, so create it and expand rows
				this.getHeaders().push(column);
				this.getHeadersMap()[column] = this.getHeaders().length-1
				this.getRows().forEach(function(row) {
					row.push(undefined)
				});
			}
			return this.getHeadersMap()[column]
		}
	},
	
	getColumnHeader: function(column) {
		var c = this.getColumnIndex(column);
		return this.getHeaders()[c];
	},
	
	/**
	 * Compute the sum of the values in the column.
	 * 
	 * @param {Number/String} column The column index (as a number) or key (as a string).
	 */
	getColumnSum: function(column) {
		return Ext.Array.sum(this.getColumnValues(column, true));
	},
	
	/**
	 * Compute the sum of the values in the column.
	 * 
	 * @param {Number/String} column The column index (as a number) or key (as a string).
	 */
	getColumnMean: function(column) {
		return Ext.Array.mean(this.getColumnValues(column, true));
	},
	
	/**
	 * Get the largest value in the array.
	 * 
	 * @param {Number/String} column The column index (as a number) or key (as a string).
	 */
	getColumnMax: function(column) {
		return Ext.Array.max(this.getColumnValues(column, true));
	},
	
	/**
	 * Get the smallest value in the array.
	 * 
	 * @param {Number/String} column The column index (as a number) or key (as a string).
	 */
	getColumnMin: function(column) {
		return Ext.Array.min(this.getColumnValues(column, true));
	},
	
	getColumnValues: function(column, clean) {
		var c = this.getColumnIndex(column), vals = [];
		this.eachRow(function(row) {
			vals.push(row[c]);
		});
		if (clean) {return Ext.Array.clean(vals)}
		else {return vals;}
	},
	
	/**
	 * @private
	 */
	reMapRows: function() {
		var rowKey = this.getRowKey();
		var rowsMap = {}
		this.eachRow(function(row, i) {
			if (rowKey in row) {
				rowsMap[row[rowKey]] = i;
			}
		}, true);
		this.setRowsMap(rowsMap)
	},
	
	sortByColumn: function(columns) {
		var rows = this.getRows(),
			sortColumnsIndices = Ext.Array.from(columns).map(function(column) {
				if (Ext.isObject(column)) {
					for (key in column) {
						return {
							index: this.getColumnIndex(key),
							direction: column[key].indexOf("asc")>-1 ? 'asc' : 'desc'
						}
					}
				} else {
					return {
						index: this.getColumnIndex(column),
						direction: "desc"
					}
				}
			}, this);
		rows.sort(function(a, b) {
			for (var i=0, len=sortColumnsIndices.length; i<len; i++) {
				var header = sortColumnsIndices[i].index
				if (a[header]!=b[header]) {
					if (sortColumnsIndices[i].direction=='asc') {return a[header] > b[header] ? 1 : -1}
					else {return a[header] > b[header] ? -1 : 1}
				}
			}
		});
		this.reMapRows();
		return this;
	},
	
	loadCorrespondenceAnalysis: function(config) {
		return this._doAnalysisLoad('table.CA', 'Voyant.data.store.CAAnalysis', config);
	},
	
	loadPrincipalComponentAnalysis: function(config) {
		return this._doAnalysisLoad('table.PCA', 'Voyant.data.store.PCAAnalysis', config);
	},
	
	loadTSNEAnalysis: function(config) {
		return this._doAnalysisLoad('table.TSNE', 'Voyant.data.store.TSNEAnalysis', config);		
	},
	
	_doAnalysisLoad: function(tool, storeType, config) {
		if (this.then) {
			return Voyant.application.getDeferredNestedPromise(this, arguments);
		} else {
	    	config = config || {};
			var dfd = Voyant.application.getDeferred(this);
			Ext.apply(config, {
		        columnHeaders: true,
		        rowHeaders: true,
		        tool: tool,
		        analysisInput: this.toTsv(),
		        inputFormat: 'tsv'
			});
			var store = Ext.create(storeType, {noCorpus: true});
			store.load({
				params: config,
				callback: function(records, operation, success) {
					if (success) {
						dfd.resolve(store, records)
					} else {
						dfd.reject(operation.error.response);
					}
				}
			})
			return dfd.promise
		}
	},
	
	embed: function(cmp, config) {
		if (!config && Ext.isObject(cmp)) {
			config = cmp;
			cmp = this.embeddable[0];
		}
		config = config || {};
		
		var columnHeaders = Ext.Array.from(config.headers || this.getHeaders()).map(function(header) {return this.getColumnHeader(header);}, this);
		
		var json = {
				rowkey: this.getRowKey(),
				config: config,
				headers: columnHeaders
		};
		if ("headers" in config) {
			var columnIndices = Ext.Array.from(config.headers).map(function(header) {return this.getColumnIndex(header);}, this);
			var rows = [];
			this.getRows().forEach(function(row) {
				rows.push(columnIndices.map(function(i) {
					return row[i]
				}))
			})
			Ext.apply(json, {
				rows: rows
			})
		} else {
			Ext.apply(json, {
				rows: this.getRows()
			})
		}
		Ext.apply(config, {
			tableJson: JSON.stringify(json)
		});
		delete config.axes;
		delete config.series;
		
		embed.call(this, cmp, config);
		
	},
	
	toTsv: function(config) {
		var tsv = this.getHeaders().join("\t");
		this.getRows().forEach(function(row, i) {
			if (config && Ext.isNumber(config) && i>config) {return;}
			tsv += "\n"+row.map(function(cell) {
				return Ext.isString(cell) ? cell.replace(/(\n|\t)/g, "") : cell;
			}).join("\t");
		})
		return tsv;
	},
	
	getString: function(config) {
		config = config || {};
		var table = "<table class='voyant-table' style='"+(config.width ? ' width: '+config.width : '')+"' id='"+(config.id ? config.id : Ext.id())+"'>";
		var headers = this.getHeaders();
		if (headers.length) {
			table+="<thead><tr>";
			for (var i=0, len = headers.length; i<len; i++) {
				table+="<th>"+headers[i]+"</th>";
			}
			table+="</tr></thead>";
		}
		table+="<tbody>";
		for (var i=0, len = Ext.isNumber(config) ? config : this.getRows().length; i<len; i++) {
			var row = this.getRow(i);
			if (row && Ext.isArray(row)) {
				table+="<tr>";
				row.forEach(function(cell) {
					table+="<td>"+cell+"</td>";
				})
				table+="</tr>";
			}
		}
		table+="</tbody></table>";
		return table;
	}
});