Ext.ns('Nexts.grid');

/**
 * @class Nexts.grid.GridSearch
 * @extends Ext.util.Observable
 * @param {Object} config configuration object
 * @constructor
 */
Nexts.grid.GridSearch = function(config) {
	Ext.apply(this, config);
	Nexts.grid.GridSearch.superclass.constructor.call(this);
};

Ext.extend(Nexts.grid.GridSearch, Ext.util.Observable, {
	/**
	 * @cfg {String} updateBuffer milliseconds to fire update event (default: 1500)
	 */

	/**
	 * @cfg {String} searchText Text to display on menu button
	 */
	searchText: 'Search',

	/**
	 * @cfg {String} selectAllText Text to display on menu item that selects all fields
	 */
	selectAllText: 'Select All',

	/**
	 * @cfg {String} position Where to display the search controls. Valid values are top and bottom (defaults to bottom)
	 * Corresponding toolbar has to exist at least with mimimum configuration tbar:[] for position:top or bbar:[]
	 * for position bottom. Plugin does NOT create any toolbar.
	 */
	position: 'bottom',

	/**
	 * @cfg {String} iconCls Icon class for menu button (defaults to icon-magnifier)
	 */
	iconCls: 'ne-gridsearch-icon',

	/**
	 * @cfg {String/Array} checkIndexes Which indexes to check by default. Can be either 'all' for all indexes
	 * or array of dataIndex names, e.g. ['persFirstName', 'persLastName']
	 */
	checkIndexes: 'all',

	/**
	 * @cfg {Array} disableIndexes Array of index names to disable (not show in the menu), e.g. ['persTitle', 'persTitle2']
	 */
	disableIndexes: [],

	/**
	 * @cfg {String} dateFormat how to format date values. If undefined (the default) 
	 * date is formatted as configured in colummn model
	 */
	dateFormat: undefined,

	/**
	 * @cfg {Boolean} showSelectAll Select All item is shown in menu if true (defaults to true)
	 */
	showSelectAll: true,

	/**
	 * @cfg {String} menuStyle Valid values are 'checkbox' and 'radio'. If menuStyle is radio
	 * then only one field can be searched at a time and selectAll is automatically switched off.
	 */
	menuStyle: 'checkbox',

	/**
	 * @cfg {String} remote Set true for remote stores or false for local stores. If mode is local
	 * no data requests are sent to server the grid's store is filtered instead (defaults to true)
	 */
	remote: true,

	/**
	 * @cfg {Array} readonlyIndexes Array of index names to disable (show in menu disabled), e.g. ['persTitle', 'persTitle2']
	 */

	/**
	 * @cfg {Number} width Width of input field in pixels (defaults to 150)
	 */
	width: 150,

	/**
	 * @cfg {String} xtype xtype is usually not used to instantiate this plugin but you have a chance to identify it
	 */
	xtype: 'gridsearch',

	/**
	 * @cfg {Object} paramNames Params name map (defaults to {fields:'fields', query:'query'}
	 */
	paramNames: {
		 fields: 'fields'
		,query: 'query'
	},

	/**
	 * @cfg {String} shortcutKey Key to fucus the input field (defaults to s = Search). Empty string disables shortcut
	 */
	shortcutKey: 's',

	/**
	 * @cfg {String} shortcutModifier Modifier for shortcutKey. Valid values: alt, ctrl, shift (defaults to alt)
	 */
	shortcutModifier: 'alt',

	/**
	 * @cfg {String} align 'left' or 'right' (defaults to 'left')
	 */

	/**
	 * @cfg {Number} minLength force user to type this many character before he can make a search
	 */

	/**
	 * @cfg {Ext.Panel/String} toolbarContainer Panel (or id of the panel) which contains toolbar we want to render
	 * search controls to (defaults to this.grid, the grid this plugin is plugged-in into)
	 */
	

	/**
	 * private
	 * @param {Ext.grid.GridPanel/Ext.grid.EditorGrid} grid reference to grid this plugin is used for
	 */
	init: function(grid) {
		this.grid = grid;

		// setup toolbar container if id was given
		if ('string' === typeof this.toolbarContainer) {
			this.toolbarContainer = Ext.getCmp(this.toolbarContainer);
		}

		this.store = this.grid.getStore();
		if (this.remote) {
			this.store.on('beforeload', this.onBeforeLoad, this);
		} 
		else {
			this.store.on('load', function(store) {
				store.filterBy(this.localFilter());
			}, this);
		}

		// do our processing after grid render and reconfigure
		grid.onRender = grid.onRender.createSequence(this.onRender, this);
		grid.reconfigure = grid.reconfigure.createSequence(this.reconfigure, this);
		grid.on("beforestaterestore", this.applyState, this);
		grid.on("beforestatesave", this.saveState, this);
	},

	/**
	 * private add plugin controls to <b>existing</b> toolbar and calls reconfigure
	 */
	onRender: function() {
		var panel = this.toolbarContainer || this.grid;
		var tb = 'bottom' === this.position ? panel.bottomToolbar : panel.topToolbar;

		// add menu
		this.menu = new Ext.menu.Menu();

		// handle position
		if ('right' === this.align) {
			tb.addFill();
		}
		else {
			if(0 < tb.items.getCount()) {
				tb.addSeparator();
			}
		}

		// add menu button
		tb.add({
			 text:this.searchText
			,menu:this.menu
			,iconCls:this.iconCls
		});

		// add input field (TwinTriggerField in fact)
		this.field = new Ext.form.TwinTriggerField({
			width:this.width
			,selectOnFocus:undefined === this.selectOnFocus ? true : this.selectOnFocus
			,trigger1Class:'x-form-clear-trigger'
			,trigger2Class:'x-form-search-trigger'
			,onTrigger1Click:this.onTriggerClear.createDelegate(this)
			,onTrigger2Click:this.onTriggerSearch.createDelegate(this)
			,minLength:this.minLength
			,plugins: new Nexts.plugins.EditorChange({
				changeDelay: this.updateBuffer,
				listeners: {
					change: {
						fn: this.onTriggerSearch,
						scope: this
					}
				}
			})
		});

		tb.add(this.field);

		// reconfigure
		this.reconfigure();

		// keyMap
		if (this.shortcutKey && this.shortcutModifier) {
			var shortcutEl = this.grid.getEl();
			var shortcutCfg = [{
				 key:this.shortcutKey
				,scope:this
				,stopEvent:true
				,fn:function() {
					this.field.focus();
				}
			}];
			shortcutCfg[0][this.shortcutModifier] = true;
			this.keymap = new Ext.KeyMap(shortcutEl, shortcutCfg);
		}
		
		this.rendered = true;
		
		if (this.searchState) {
			this._applyState();
		}
	},

	_applyState: function() {
		this.field.setValue(this.searchState.query);
		
		this.menu.items.each(function(item) {
			if (!Ext.isEmpty(item.dataIndex)) {
				if (this.searchState.fields.indexOf(item.dataIndex) >= 0) {
					item.setChecked(true);
				}
				else {
					item.setChecked(false);
				}
			}
		}, this);

		delete this.searchState;
	},
	
	/** private **/
	applyState: function(grid, state) {
		if (state.search) {
			this.searchState = state.search;
			if (this.rendered) {
				this._applyState();
			}
		}

		if (!this.remote) {
			this.reload();
		}
	},
	
	/** private **/
	saveState: function(grid, state) {
		if (this.menu) {
			var fields = [];
			this.menu.items.each(function(item) {
				if (item.checked && !Ext.isEmpty(item.dataIndex)) {
					fields.push(item.dataIndex);
				}
			});

			state.search = {
				fields: fields,
				query: this.field.getValue()
			};
		}
	},
	
	/** private **/
	onBeforeLoad: function(store, options) {
		options.params = options.params || {};
		
		this.cleanParams(options.params);
		var params = this.buildQuery();
		Ext.apply(options.params, params);
	},
	
	/**
	 * Function to take structured filter data and 'flatten' it into query parameteres. The default function
	 * will produce a query string of the form:
	 *		fields=field&query=text
	 *
	 * @return {Object} Query keys and values
	 */
	buildQuery: function() {
		var p = {};

		if (this.rendered) {
			// get fields to search array
			var fields = [];
			this.menu.items.each(function(item) {
				if (item.checked && !Ext.isEmpty(item.dataIndex)) {
					fields.push(item.dataIndex);
				}
			});
			
			if (!Ext.isEmpty(this.field.getValue()) && fields.length > 0) {
				p[this.paramNames.fields] = fields;
				p[this.paramNames.query] = this.field.getValue();
			}
		}
		else if (this.searchState) {
			if (!Ext.isEmpty(this.searchState.query) && this.searchState.fields.length > 0) {
				p[this.paramNames.fields] = this.searchState.fields;
				p[this.paramNames.query] = this.searchState.query;
			}
		}
		return p;
	},
	
	/**
	 * Removes search related query parameters from the provided object.
	 * 
	 * @param {Object} p Query parameters that may contain filter related fields.
	 */
	cleanParams: function(p) {
		delete p[this.paramNames.fields];
		delete p[this.paramNames.query];
	},
	
	/** private **/
	reload: function() {
		if (this.remote) {
			var store = this.grid.store;
			if (store.lastOptions && store.lastOptions.params) {
				store.lastOptions.params[store.paramNames.start] = 0;
			}
			store.reload();
		} 
		else {
			this.grid.store.clearFilter(true);
			if (this.field.getValue()) {
				this.grid.store.filterBy(this.localFilter());
			}
		}
	},

	/**
	 * private Clear Trigger click handler
	 */
	onTriggerClear: function() {
		this.field.setValue('');
		this.field.focus();
		this.onTriggerSearch();
	},


	/**
	 * private Search Trigger click handler (executes the search, local or remote)
	 */
	onTriggerSearch: function() {
		if (!this.field.isValid()) {
			return;
		}

		this.reload();
		this.grid.saveState();
	},

	/**
	 * Method factory that generates a record validator for the filters active at the time
	 * of invokation.
	 * 
	 * @private
	 */
	localFilter: function(r) {
		var re = new RegExp(this.field.getValue(), 'gi');
		var retval = false;

		this.menu.items.each(function(item) {
			if (!item.checked || retval) {
				return;
			}
			var rv = r.get(item.dataIndex);
			rv = rv instanceof Date ? rv.format(this.dateFormat || r.fields.get(item.dataIndex).dateFormat) : rv;
			retval = re.test(rv);
		}, this);

		return retval;
	},
	
	/**
	 * @param {Boolean} true to disable search (TwinTriggerField), false to enable
	 */
	setDisabled:function() {
		this.field.setDisabled.apply(this.field, arguments);
	},


	/**
	 * Enable search (TwinTriggerField)
	 */
	enable:function() {
		this.setDisabled(false);
	},


	/**
	 * Enable search (TwinTriggerField)
	 */
	disable:function() {
		this.setDisabled(true);
	},


	/**
	 * private (re)configures the plugin, creates menu items from column model
	 */
	reconfigure:function() {
		// remove old items
		var menu = this.menu;

		menu.removeAll();

		// add Select All item plus separator
		if (this.showSelectAll && 'radio' !== this.menuStyle) {
			menu.add(new Ext.menu.CheckItem({
				 text:this.selectAllText
				,checked:!(this.checkIndexes instanceof Array)
				,hideOnClick:false
				,handler:function(item) {
					var checked = ! item.checked;
					item.parentMenu.items.each(function(i) {
						if(item !== i && i.setChecked && !i.disabled) {
							i.setChecked(checked);
						}
					});
				}
			}),'-');
		}

		// add new items
		var cm = this.grid.colModel;
		var group = undefined;
		if('radio' === this.menuStyle) {
			group = 'g' + (new Date).getTime();	
		}
		Ext.each(cm.config, function(config) {
			var disable = false;
			if(config.header && config.dataIndex) {
				Ext.each(this.disableIndexes, function(item) {
					disable = disable ? disable : item === config.dataIndex;
				});
				if(!disable) {
					menu.add(new Ext.menu.CheckItem({
						 text:config.header
						,hideOnClick:false
						,group:group
						,checked:'all' === this.checkIndexes
						,dataIndex:config.dataIndex
					}));
				}
			}
		}, this);
	

		// check items
		if(this.checkIndexes instanceof Array) {
			Ext.each(this.checkIndexes, function(di) {
				var item = menu.items.find(function(itm) {
					return itm.dataIndex === di;
				});
				if(item) {
					item.setChecked(true, true);
				}
			}, this);
		}
	

		// disable items
		if(this.readonlyIndexes instanceof Array) {
			Ext.each(this.readonlyIndexes, function(di) {
				var item = menu.items.find(function(itm) {
					return itm.dataIndex === di;
				});
				if(item) {
					item.disable();
				}
			}, this);
		}
	}
});

