/**
 * @class table model
 */
var DataTable = new Class({
	Implements: Events,

/**
 * data object for model
 * @var data			array			of objects
 * @private
 */
	data: [],
	
/**
 * data object for model
 * @var view			DOMNode		view, associated with this model
 * @private
 */
	view: null,

/**
 * initializes object
 * @constructor
 */
	initialize: function(){
	},
	
/**
 * updates one cell
 * @param row		int				row index
 * @param col			string			column name
 * @param value		mixed			new value
 */	
	setValue: function(row, col, value) {
		if (!this.data[row]) {
			this.data[row] = {};
		}
		var not_eqauls = false;
		if (typeof value == "object") {
			not_eqauls = (value.toString() != this.data[row][col].toString());
		} else {
			not_eqauls = (this.data[row][col] != value);
		}
		if (not_eqauls) {
			this.fireEvent('change', {
				idx: row,
				name: col,
				value: value
			});
			this.data[row][col] = value;
		}
	},
/**
 * updates one row
 * @param obj			object			with values
 * @param idx			int				index of a row
 */
	updateRecord: function(obj, idx) {
		for (field in obj) {
			this.setValue(idx, field, obj[field]);
		}
	},

/**
 * updates one column
 * @param values		array			of values
 * @param field		string			name of column
 */	
	updateCol: function(values, field) {
		for (var i = 0, l = values.length; i < l; i++) {
			this.setValue(i, field, values[i]);
		}
	},
/**
 * updates several rows
 * @param a			array			of new values - assoc.arrays
 */
	updateRecords: function(a) {
		for (var i = 0, l = a.length; i < l; i++) {
			this.updateRecord(a[i], i);
		}
	}
});



var DataTableRangeSum = new Class({
	Extends: DataTable,
/**
 * @var assoc.array of summator objects
 */
	summators: {},

/**
 * initializes object
 * @constructor
 */
	initialize: function(summators) {
		this.parent();
		this.summators = summators;
	},
	
/**
 * gets value from one cell
 * @param row		int				row index
 * @param col			string			column name
 * @param value		mixed			new value
 */	
	getValue: function(row, col) {
		if (!this.data[row]) {
			return undefined;
		}
		return this.data[row][col];
	},

/**
 * updates sum row on updating 
 * @param a			array			of new values - assoc.arrays
 */
	updateSum: function() {
		var obj = {};
		for(col in this.summators) {
			var sm = this.summators[col];
			obj[col] = sm.func(this, col);
		}
		this.updateRecord(obj, "sumRow");
	},
	
/**
 * updates range of summing
 * @param low		int				lower row index
 * @param high		int				upper row index (inclusive)
 */	
	updateRange: function(low, high) {
		this.rangeLow = low;
		this.rangeHigh = high;
		this.updateSum();
	},

/**
 * updates several rows - overriding parent one
 * @param a			array			of new values - assoc.arrays
 */
	updateRecords: function(a) {
		this.parent(a);
		this.updateSum();
	},

/**
 * returns range of summing
 * @return 			object			assoc.array of 'low' & 'high'
 */	
	getRange: function() {
		return {
			low: this.rangeLow,
			high: this.rangeHigh
		}
	}
});


/* summators */
var SummatorHelper = {
	trivial: {
		defaultValue: 0,
		func: function(model, field) {
			var ranges = model.getRange();
			var val = this.defaultValue;
			for (var r = ranges.low; r <= ranges.high; r++) {
				val += model.getValue(r, field);
			}
			return val;
		}
	},
	delta: {
		defaultValue: 0,
		func: function(model, field) {
			var ranges = model.getRange();
			var subLowerValue = model.getValue(ranges.low-1, field);
			if (typeof(subLowerValue) == "undefined") {
				subLowerValue = this.defaultValue;
			}
			return model.getValue(ranges.high, field) - subLowerValue;
		}		
	}
}


/**
 * @class controller for dom table with "smart" update functionality
 */
var DOMTable = new Class({
	
	Implements: Events,
	
/**
 * DOM object, represnting view
 * @var DOMNode
 * @private
 */
	table: null,

/**
 * how much rows contained in header
 * @var int
 * @private
 */
	offsetTop: 0,

/**
 * how much rows contained in header
 * @var int
 * @private
 */
	offsetLeft: 0,

/**
 * array of internal column names
 * @var array
 * @private
 */
	fields: [],

/**
 * for some columns data shouldn't be pasted directly 
 * for that comulns use converting functions
 * @var assoc.array of functions
 * @private
 */
	convert: {},

/**
 * creates wrapper for DOM events, used to process own "software" events
 * @param _type		string			type of custom events @see this.events
 */
	eventWrapperFactory: function(_type) {
		var _self = this;
		return (function(evt) {
			var elt = evt.target;
			var tagName = elt.tagName.toLowerCase();
			if (tagName == "td") {
				_self.fireEvent(_type, {
					x: elt.cellIndex - _self.offsetLeft,
					y: elt.parentNode.rowIndex - _self.offsetTop,
					cell: elt
				});
			}
/*			if (_type.match(/^table/) && (tagName == "table")) {
				_self.fireEvent(_type, {});
			}*/
		});
	},
/**
 * initialize object
 * @param table		DOMNode
 * @param fields		array			of columns
 * @param convert	assoc.array		of functions
 * @param options		assoc.array		of other options
 * @constructor
 */
	initialize: function(table, options) {
		this.table = table;
		this.fields = options.fields;
		this.convert = options.convert;
		if (options) {
			if (options.offsetLeft) this.offsetLeft = options.offsetLeft;
			if (options.offsetTop) this.offsetTop = options.offsetTop;			
		}
		table.addEvent('mousemove',	this.eventWrapperFactory('cellenter'));
//		table.addEvent('mouseleave',	this.eventWrapperFactory('cellleave'));
//		table.addEvent('mouseleave',	this.eventWrapperFactory('tableleave'));
		table.addEvent('click',			this.eventWrapperFactory('cellclick'));
	},

/**
 * gets row of a table 
 * @param row_idx 	int				index of row
 * @private
 */
	getRow: function(row_idx) {
		row_idx += this.offsetTop;
		if (row_idx < 0) {
			throw TypeError();
		}
		if (row_idx >= this.table.rows.length) {
			throw TypeError();
		}
		return this.table.rows[row_idx];
	},	

/**
 * actually updates cell in a table
 * @param row_idx 	int				index of row
 * @param cell_idx		int				index of cell
 * @param value		mixed			value, would be pasted into cell
 */
	updateCell: function(row_idx, name, value) {
		if (typeof value == "undefined") return;
		var cell_idx = this.fields.indexOf(name);
		if (cell_idx == -1) return;
		var cell = $(this.getRow(row_idx).cells[cell_idx + this.offsetLeft]);
		if (this.convert[name]) {
			this.convert[name](cell, value);
		} else {
			cell.set("text", value);
		}
	},

/**
 * hides rows range (inclusive), determined by 2 numbers
 * @param from		int				lower index of rows
 * @param to			int				upper index of rows
 */	
	hideRows: function(from, to) {
		for (var i = from; i <= to; i++) {
			this.getRow(i).style.display = "none";
		}
	},
/**
 * shows rows range (inclusive), determined by 2 numbers
 * @param from		int				lower index of rows
 * @param to			int				upper index of rows
 */
	showRows: function(from, to) {
		for (var i = from; i <= to; i++) {
			this.getRow(i).style.display = "";
		}
	},

/**
 * shows or hide columns determined by assoc.array
 * @param arr			a.array	of		column name => show / hide (true/false)
 */
	showCols: function(arr) {
		for (var r = 0, l = this.table.rows.length; r < l; r++) {
			var row = this.table.rows[r];
			for (name in arr) {
				var cell_idx = this.fields.indexOf(name);
				if (cell_idx != -1) {
					var cell = row.cells[cell_idx + this.offsetLeft];
					if (!cell) continue;
					cell.style.display = arr[name] ? "" : "none";
				}
			}
		}
	}/*,
/**
 * adds event handler
 * @param type		string 			type of event
 * @param String / DOM-Element elementName element for the event
 * @param Hash params parameter for event execution
 *
	addEvent: function(type, callback) {
		this.events[type].push(callback);
	}*/
});


/**
 * @class DOMTable controller with range summator
 */
var DOMTableRangeSelect = new Class({
	Extends: DOMTable,
	Binds: ['_enter', '_click'],

	classHover: "hover",
	classActive: "active",	
/**
 * @const class constant to indicate index of unselected bound
 */
	UNSELECTED: -1,

/**
 * @const class constants set to indicate state of current selection
 */
	STATES: {
		SELECT_FROM: 0,
		SELECT_TO: 1,
		SELECT_NONE: 2
	},

/**
 * initialize object
 * @constructor
 */
	initialize: function(table, options) {
		this.parent(table, options);
		this.hoveredRows = [];
		for (var r = 0; r <= 20; r++) {
			this.hoveredRows[r] = "";
		}
		this.fromRow = this.UNSELECTED;
		this.toRow = this.UNSELECTED;
		this.state = this.STATES.SELECT_FROM;
		this.addEvent('cellenter', this._enter);
		this.addEvent('cellclick', this._click);
	},

/**
 * gets row of a table 
 * @param row_idx 	int				index of row
 * @return 		 	DOMNode		<TR>
 * @private
 */
	getRow: function(row_idx) {
		if (row_idx == "sumRow") {
			var rr = this.table.rows;
			return rr[rr.length-1];
		}
		return this.parent(row_idx);
	},
	
/**
 * gets row of a table 
 * @param row_idx 	int				index of a row
 * @param className 	string			new class name
 * @private
 */
	updateRowClass: function(row_idx, className) {
		var row = $(this.getRow(row_idx));
		var cl = this.hoveredRows[row_idx];
		if (className) {
			if (cl != className) {
				if (cl) row.removeClass(cl);
				row.addClass(className);
			}
		} else {
			if (cl) row.removeClass(cl);
		}
		this.hoveredRows[row_idx] = className;
	},
	
/**
 * event handler for cellenter
 * @private
 */
	_enter: function(evt) {
		if (evt.y < 0) return;
		if (evt.y > 20) return;
		switch(this.state) {
			case this.STATES.SELECT_FROM: {
				for(var r = 0, l = 20; r <= l; r++) {
					if (r == evt.y) {
						this.updateRowClass(r, this.classHover);
					} else {
						this.updateRowClass(r, "");
					}
				}
			} break;
			case this.STATES.SELECT_TO: {
				var low, high;
				if (this.fromRow < evt.y) {
					low = this.fromRow;
					high = evt.y;
				} else {
					low = evt.y;
					high = this.fromRow;
				}
				for(var r = 0; r < low; r++) {
					this.updateRowClass(r, "");
				}
				this.updateRowClass(low, this.classActive);
				for(var r = low+1; r < high; r++) {
					this.updateRowClass(r, this.classHover);
				}
				this.updateRowClass(high, this.classActive);
				for(var r = high+1; r <= 20; r++) {
					this.updateRowClass(r, "");
				}
			} break;
			case this.STATES.SELECT_NONE: break;
		}
	},

/**
 * event handler for click
 * @private
 */
	_click: function(evt) {
		switch(this.state) {
			case this.STATES.SELECT_FROM: {
				this.state = this.STATES.SELECT_TO
				this.fromRow = evt.y;
			} break;
			case this.STATES.SELECT_TO: {
				this.state = this.STATES.SELECT_NONE
				if (evt.y < this.fromRow) {
					this.toRow = this.fromRow;
					this.fromRow = evt.y;
				} else {
					this.toRow = evt.y;
				}
				this.fireEvent("rangeChange", {
					low: this.fromRow,
					high: this.toRow
				});
			} break;
			case this.STATES.SELECT_NONE: {
				this.state = this.STATES.SELECT_FROM;
				this.toRow = this.UNSELECTED;
				this.fromRow = this.UNSELECTED;
				for(var r = 0, l = 20; r <= l; r++) {
					this.updateRowClass(r, "");
				}
				this.fireEvent("rangeChange", {
					low: undefined,
					high: undefined
				});
			} break;
		}
	}
});


/* view helper */
var TableHelper = {
	cellNiceFixed: function(cell, value) {
		if (value) {
			cell.innerHTML = value.toFixed(1).replace(/(\.)?0+$/, "");
		} else {
			cell.innerHTML = "&mdash;";
		}
	},
	cellTime: function(cell, value) { 
		if (value > 0) {
			cell.innerHTML = timeI2S(value);
			cell.title = Math.round(86400 / value) + " / " + I18N.day_name;
		} else {
			cell.innerHTML = "&mdash;";
			cell.title = "";
		}
	},
	cellImg: function(cell, value) {
		cell.getElement("img").className = value;
	}
}