
 


(function($){

$.sfs = $.extend($.sfs, 
{
	appSearch:
	{
		init: function(_opt)
		{
			$(function() {
				// Element.
				var $element = $(_opt.selector ? _opt.selector : _opt);
				
				// Break.
				if(!$element.get(0)) return;
				
				// Apply.
				$element
				.data('sfs', $.extend($element.data('sfs'), {appSearch: _opt}))
				.appSearch(_opt);
			});
		}
	}
});

$.widget("ui.appSearch", {
	
	options: {
		controller: undefined,
		payload: {},
		
		// Search.
		searchEnabled: true,
		searchDelay: 300,
		
		// Loading State.
		loadingClass: 'ui-state-loading',
		loadingIndicator: undefined,
		
		// Location.
		locationEnabled: false,
		locatingClass: 'ui-state-locating',
		locatingIndicator: undefined,
		
		// Suggest.
		suggestDelay: 300,
		
		// Settings Form.
		settingsForm: undefined,
		
		// Template.
		template: undefined,
		
		// Typeahead.
		typeaheadEnabled: true,
		typeaheadHint: true,
		typeaheadHighlight: true,
		typeaheadMinLength: 1,
		typeaheadLimit: Infinity
	},
	
	_create: function(){
		
		// Requests, Timeouts.
		this.searchReqId = 0;
		this.searchTimeout = null;
		this.suggestReqId = 0;
		this.suggestTimeout = null;
		this.pendingRequestCnt = 0;
		
		// Current Input Value.
		this.currentValue = null;
		this._setCurrentValue(this.element.val());
		
		// Loading Indicator.
		this.loadingIndicator = null;
		if (this.options.loadingIndicator) {
			this.loadingIndicator = $(this.options.loadingIndicator);
			this.loadingIndicator.hide();
		}
		
		// Location.
		this.locatingIndicator = null;
		this.currentPosition = null;
		if (this.options.locatingIndicator) {
			this.locatingIndicator = $(this.options.locatingIndicator);
			this.locatingIndicator.hide();
		}
		
		// Parent Form.
		this.parentForm = null;
		if (this.element.parents('form').eq(0)) {
			this.parentForm = this.element.parents('form').eq(0);
			
			this.options.searchEnabled && this._on(this.parentForm, {
				submit: this._handleParentFormSubmit
			});
		}
		
		// Settings Form.
		this.settingsForm = null;
		if (this.options.settingsForm) {
			this.settingsForm = $(this.options.settingsForm);
			
			this._syncSettingsForm();
			
			this._on(this.settingsForm, {
				change: this._handleSettingsFormChange
			});
		}
		
		// Template.
		this.template = null;
		this.templateStr = null;
		this.templateContainer = null;
		if (this.options.template) {
			// Mustache Pre-parse.
			this.template = $(this.options.template);
			this.templateStr = this.template.html();
			Mustache.parse(this.templateStr);
			// Insert Template Container.
			this.templateContainer = $('<div class="ui-appsearch-results" />');
			this.templateContainer.insertAfter(this.template);
		}
	},
	
	_init: function(){
		// Type Ahead.
		this.options.typeaheadEnabled && this._initTypeAhead();
		
		// Location.
		if (this.options.locationEnabled) {
			// Init Location.
			 this._initGeoLocation($.proxy(function(err, position){
				// Initial Search.
				this.search();
			}, this));
		
		} else {
			// Initial Search.
			this.search();
		}
	},
	
	// Settings Form.
	
	_syncSettingsForm: function(){
		var payload = {};
		
		var data = this.settingsForm.serializeArray().map(function(ent){
			return {[ent.name]: ent.value};
		});
		
		$.each(data, function(i, val){
			$.extend(payload, val);
		});
		
		this.options.payload = payload;
	},
	
	_handleSettingsFormChange: function(e){
		this._syncSettingsForm();
		
		// Search.
		this.search();
	},
	
	// Type Ahead.
	
	_initTypeAhead: function(){
		this.element
		.typeahead({
			hint: this.options.typeaheadHint,
			highlight: this.options.typeaheadHiglight,
			minLength: this.options.typeaheadMinLength
		},{
			name: 'results',
			async: true,
			source: $.proxy(this._handleTypeAheadSearch, this),
			limit: this.options.typeaheadLimit
		})
		.on('typeahead:change', $.proxy(this._handleTypeAheadChange, this))
		.on('typeahead:select', $.proxy(this._handleTypeAheadSelect, this));
	},
	
	_handleTypeAheadChange: function (e, suggestion) {
		// Set Current Value.
		this._setCurrentValue(suggestion);
		
		// Search.
		this.search();
	},
	
	_handleTypeAheadSelect: function (e, suggestion) {
		
		if (!this.options.searchEnabled && this.parentForm) {
			this.parentForm.submit();
			return true;
		}
		
		// Set Current Value.
		this._setCurrentValue(suggestion);
		
		// Search.
		this.search();
	},

	_handleTypeAheadSearch: function (query, syncCb, asyncCb) {
		this._setCurrentValue(query);

		// Break.
		if (this.currentValue == '') return;
		
		// Search for Suggestions.
		this.suggest(null, $.proxy(function(res, params){
			
			// No Suggestions.
			if (res.data.length == 0) {
				// Search.
				this.search();
				
				// Break.
				return;
			}
			
			// Map Suggestions.			
			var a = res.data.slice();
			a = $.map(a, function(val, i){return val.text});

			// Show Suggestions.
			asyncCb(a);
			
		}, this));
	},
	
	// Geo Location.
	
	_initGeoLocation: function(cb){
		if (!navigator.geolocation) {
			cb && cb(false);
			return;
		}
		
		this._setLocating(true);
		
		try {
			navigator.geolocation.getCurrentPosition(
				$.proxy(this._handleCurrentPositionSuccess, this, cb),
				$.proxy(this._handleCurrentPositionError, this, cb)
			);
		} catch (e) {
			this._setLocating(false);
			cb && cb(false);
		}
	},
	
	_handleCurrentPositionSuccess: function(cb, position){
		this.currentPosition = position;
		this._setLocating(false);
		
		cb && cb(true, position);
	},
	
	_handleCurrentPositionError: function(cb){
		this.currentPosition = null;
		this._setLocating(false);
		
		cb && cb(false);
	},
	
	_injectGeoLocation: function(data){
		if (!this.currentPosition) return;
		
		var position = this.currentPosition;
		
		var location = {
			latitude: position.coords.latitude,
			longitude: position.coords.longitude
		};
		
		$.extend(data, {
			location: JSON.stringify(location)
		});
	},
	
	// Parent Form.
	
	_handleParentFormSubmit: function (e) {
		e.preventDefault();
		
		// Close Type Ahead Menu.
		this.element.typeahead('close');
		
		this._setCurrentValue(this.element.val());
		
		// Search.
		this.search();
	},

	// Search.

	_sendRequest: function(type, params, cb){
		var self = this;
		this[type + 'ReqId']++;
		
		var data = $.extend({
			action: type,
			_reqId: this[type + 'ReqId']
		}, this.options.payload, params);
		
		// Inject Geo Location.
		if (type == 'search') {
			this._injectGeoLocation(data);
		}

		this._setLoading(true);
		
		$.get( this.options.controller, data, $.proxy(this._handleResponse, this, type, cb, data), 'json' )
		.always(function(){
			self._setLoading(false);
		});
	},
	
	_handleResponse: function(type, cb, reqData, response){
		// Break, if newer request exists.
		if (this[type + 'ReqId'] > reqData._reqId) return;
		
		// Callback, if present.
		cb && cb(response, reqData);
		
		// Render Template, if present.
		if (type == 'search' && this.options.template) {
			// Render.
			this.templateContainer.html(Mustache.render(this.templateStr, response));
			// Cache.
			this.cachedSearchResponse = response;
		}
		
		// Trigger Event.
		this._trigger( ":response", null, {response:response, request:{data:reqData}});
	},
	
	// Operations.
	
	search: function(params, cb){
		// Break.
		if (!this.options.searchEnabled) return;
		
		// Clear Search Timeout.
		this._clearSearchTimeout();
		
		// Data.
		var data = $.extend({
			query: this.currentValue
		}, params);
		
		// Set Search Timeout.
		this.searchTimeout = window.setTimeout($.proxy(this._sendRequest, this, 'search', data, cb), this.options.searchDelay);
	},
	
	suggest: function(params, cb){
		// Clear Suggest Timeout.
		this._clearSuggestTimeout();
		
		// Data.
		var data = $.extend({
			query: this.currentValue
		}, params);
		
		// Set Suggest Timeout.
		this.suggestTimeout = window.setTimeout($.proxy(this._sendRequest, this, 'suggest', data, cb), this.options.suggestDelay);
	},
	
	_setOption: function( key, value ) {
		
		this._super( key, value );
		
		// Set Template.
		if ( key === 'template' ) {
			this.template = $(value);
			this.templateStr = this.template.html();
			Mustache.parse(this.templateStr);
			// Render.
			this.templateContainer.html(Mustache.render(this.templateStr, this.cachedSearchResponse));
			// Trigger.
			this._trigger( ":templatechange", null, {value: value});
		}
	},
	
	_clearSearchTimeout: function(){
		this.searchTimeout && window.clearTimeout(this.searchTimeout);
	},
	_clearSuggestTimeout: function(){
		this.suggestTimeout && window.clearTimeout(this.suggestTimeout);
	},
	
	_setLoading: function(isLoading){
		if (isLoading) {
			if (this.pendingRequestCnt == 0) {
				this.element.addClass(this.options.loadingClass);
				this.parentForm && this.parentForm.addClass(this.options.loadingClass);
				this.loadingIndicator && this.loadingIndicator.show();
			}
			
			this.pendingRequestCnt++;
		} else {
			this.pendingRequestCnt--;
			
			if (this.pendingRequestCnt == 0) {
				this.element.removeClass(this.options.loadingClass);
				this.parentForm && this.parentForm.removeClass(this.options.loadingClass);
				this.loadingIndicator && this.loadingIndicator.hide();
			}
		}
	},
	
	_setLocating: function(isLocating){
		if (isLocating) {
			this.element.addClass(this.options.locatingClass);
			this.parentForm && this.parentForm.addClass(this.options.locatingClass);
			this.locatingIndicator && this.locatingIndicator.show();
		} else {
			this.element.removeClass(this.options.locatingClass);
			this.parentForm && this.parentForm.removeClass(this.options.locatingClass);
			this.locatingIndicator && this.locatingIndicator.hide();
		}
	},
	
	_setCurrentValue: function(value){
		this.currentValue = $.trim(value);
	}
	
});

})(jQuery);


