/**
 * Auto completer
 * @author Andrey Lugovtsov
 * 
 * Ответ от сервера должен приходить с заголовком application/json
 * Структура ответа - [[text1, addinfo1], [text2, addinfo2]]
 */


/**
 * @param mixed targetInput Элемент, в котором происходит набирание буков
 * @param mixed resultsHolder Элемент, в котором отображаются результаты поиска
 * @param mixed busyIndicator Элемент, который появляется на время запроса, а затем исчезает по окончании
 * @param string requestUrl Url для запроса к серверу
 * @param int resultsLimit Лимит отображаемых результатов (не входит в параметры запроса)
 * @param function onResultClick Функция, срабатывающая по клику на результате поиска. Это не событие, а внетрення функция класса
 * @param function beforeResultClick Функция, срабатывающая вначале ф-ции onResultClick. Получает две переменные - text, addinfo
 * @param function afterResultClick Функция, срабатывающая в конце ф-ции onResultClick. Получает две переменные - text, addinfo
 * @param int minChars Минимально число символов, необходимое для отправки запроса
 * @param boolean singleRequest Если передано true, будет выполнен лишь один запрос с singleRequestQueryString, после чего поиск будет осуществляться только в кеше
 * @param string singleRequestQueryString параметр для отправки на сервер в режиме singleRequest
 * @param object additionalParams Объект с дополнительными параметрами для отправки на сервер
 * @param string paramName Имя основного параметра для отправки на сервер
 * @param string ulClassName Имя класс, вещающегося на <UL> в результатах
 * @param string liClassName Имя класс, вещающегося на <LI> в результатах
 * 
 */
var autoCompleter = Class.create({
	initialize: function(config) {
		this.config = {
			'targetInput': '',
			'resultsHolder': '',
			'busyIndicator': '',
			'requestUrl': '/friend/myFriendsJson/',
			'resultsLimit': 5,
			'onResultClick': this.onResultClick,
			'beforeResultClick': this.beforeResultClick,
			'afterResultClick': Prototype.emptyFunction,
			'manualQuery': true,
			'listParent': 'ul',
			'listNode': 'li',
			'minChars': 3,
			'singleRequest': false,
			'singleRequestQueryString': '',
			'additionalParams': {},
			'paramName': 'login',
			'hideOnBodyClick': false,
			'ulClassName': '',
			'liClassName': ''
		};
		Object.extend(this.config, config);
		this.init();
	},
	
	init: function() {
		/*
		 	this.cache = new Hash();
			this.queryString = null;
			this.busy = false;
			this.lastQueryString = null;
			this.lastAddUrlParams = null;
			this.config.resultsHolder = $(this.config.resultsHolder);
			this.config.busyIndicator = $(this.config.busyIndicator);
			this.config.targetInput = $(this.config.targetInput);
			this.activateKeysNavigation();
			if (this.config.hideOnBodyClick) {
				this.hideOnBodyClick();
			}
		*/
		this.cache = new Hash();
		this.queryString = null;
		this.actualQuery = null;
		this.busy = false;
		this.lastQueryString = null;
		this.lastAddUrlParams = null;
		with (this.config) {
			this.config.resultsHolder = $(resultsHolder);
			this.config.busyIndicator = $(busyIndicator);
			this.config.targetInput = $(targetInput);
			this.activateKeysNavigation();
			if (hideOnBodyClick) {
				this.hideOnBodyClick();
			}
			if (! manualQuery) {
				targetInput.observe('keyup', function(e) {
					this.query(this.config.targetInput.getValue());
				}.bind(this));
			}
		}
	},
	
	/**
	 * Точка входа для всего комплетера.
	 * Вызывать ее
	 * 
	 * @param string query
	 */
	query: function(q, forceQuery, addUrlParams) {
		q = q.strip();
		forceQuery = forceQuery || false;
		with (this) {
			if (! config.singleRequest && cache) {
				if (q.length < config.minChars) {
					hideResults();
					return false;
				}
			}
			
			this.queryString = q.toLowerCase();
			
			// если запрос совпадает с предыдущим, просто покажем результаты
			if (
				! forceQuery && typeof lastQueryString == 'string' &&
					(queryString.strip() == lastQueryString.strip()) &&
					! addUrlParams
				) {
				//return showResults();
				return false;
			}
	
			this.actualQuery = queryString.toLowerCase();
			if (! busy) {
				_internalRequest(queryString, addUrlParams);
			}
		}
//		this.log('query');
	},
	
	beforeResultClick: function(val, addInfo) {
		return val;
	},
	
	hideOnBodyClick: function() {
		// клик по body спрячет результаты комплитера
		Event.observe(document.body, 'click', function() {
			this.hideResults();
		}.bind(this))
	},
	
	onResultClick: function(val, addInfo) {
		val = this.config.beforeResultClick.call(this, val, addInfo);
		with (this.config.targetInput) {
			var v = getValue().toLowerCase(), nv = '', p = '';
			function ch(letter, i) {
				p += letter;
				if (v.endsWith(p)) {
					// рекурсивно проверим и следующую букву (если она есть)
					if (val.length >= i) {
						if (! ch(val[i + 1], i + 1)) {
							p = p.substring(0, p.length - 1);
							nv = v.reverse().sub(p.reverse(), val.reverse(), 1).reverse();
							throw $break;
						}
						else {
							return true;
						}
					}
					else {
						nv = v + val;
					}
				}
				return false;
			}
			// идем с начала подставляемого значения
			val.toLowerCase().toArray().each(function(letter, i) {
				ch(letter, i);
			}, this);
			if (nv.length < 1) {
				nv = v + val;
			}
			setValue(nv).focus();
		}
		this.hideResults();
		this.config.afterResultClick.call(this, val, addInfo);
	},
	
	log: function(v) {
		window.console.log('autoCompleter: ', v);
	},
	
	showIndicator: function() {
		//window.console.log(this.config.busyIndicator)
		with (this.config) {
			if (busyIndicator) {
				busyIndicator.show();
			}
		}
	},
	
	hideIndicator: function() {
		with (this.config) {
			if (busyIndicator) {
				busyIndicator.hide();
			}
		}
	},
	
	/**
	 * навигация по вариантам при помощи кнопочек вверх/вниз, энтер
	 */
	activateKeysNavigation: function() {
		this.config.targetInput.observe('keydown', function(e) {
//			this.log(e)
			with (this.config.resultsHolder) {
				if (visible()) { // если виден контейнер с подсказками
					var isKeyUp = e.keyCode == 38,
						isKeyDown = e.keyCode == 40,
						isEnterKey = e.keyCode == 13,
						isEscKey = e.keyCode == 27,
						pE = this.config.listParent,
						ln = this.config.listNode,
						s = down(pE + ' .selected'); // выбранный элемент комплитера
					// UP DOWN 
					if (isKeyUp || isKeyDown) { // стрелка вверх || стрелка вниз
						e.stop();
						// если результаты отображены, перейти к предыдущему
						var p = null;
						if (s) { // если есть выбранный
							s.removeClassName('selected');
							// сделаем выбранным предыдущий либо следующий элемент
							var p = isKeyUp ? s.previous(ln) : s.next(ln);
							if (! p) {
								p = isKeyUp ? select(pE + ' ' + ln).last() : select(pE + ' ' + ln).first();
							}
						}
						else if (isKeyUp) { // если нажата стрелка вверх
							// подсветим последний элемент списка
							p = select(pE + ' ' + ln).last();
						}
						else if (isKeyDown) { // если нажата стрелка вниз 
							// подсветим первый элемент списка
							p = down(pE + ' ' + ln);
						}
						if (p) {
							p.addClassName('selected');
						}
					}
					// ENTER
					if (isEnterKey && s) {
						e.stop();
						s.fire('variant:click');
					}
					// ESC					
					if (isEscKey) {
						e.stop();
						hide();
					}
				}
			}
		}.bind(this));
	},
	
	showResults: function() {
//		new Effect.BlindDown(this.config.resultsHolder, {duration: 0.1});
		this.config.resultsHolder.show();
	},
	
	hideResults: function() {
		this.config.resultsHolder.hide();
//		this.log('hideResults');
//		new Effect.BlindUp(this.confg.resultsHolder, {duration: 0.1});
	},
	
	/**
	 * @private
	 */
	_internalRequest: function(query, addUrlParams) {
		if (this.config.singleRequest) {
			query = this.config.singleRequestQueryString;
		}
		var ql = query.toLowerCase();
		if (this.cache.get(ql) && !addUrlParams) {
			this.lastQueryString = query;
			this._populateResults(this.cache.get(ql));
		}
		else {
			var p = {'parameters': {}};
			p['parameters'][this.config.paramName] = query;
			Object.extend(p['parameters'], addUrlParams);
			p['onSuccess'] = function(r) {
				this.lastQueryString = query;
				this.busy = false;
				// уточним актуальность данных
				if (this.actualQuery != this.lastQueryString.toLowerCase()) {
					this.query(this.actualQuery);
				}
				else {
					this._populateResults(this.cache.set(ql, r.responseJSON));
					this.hideIndicator();
				}
			}.bind(this);
			this._ajaxRequest(query, p);
		}
	},
	
	/**
	 * @private
	 */
	_ajaxRequest: function(query, options) {
		var p = {};
		p[this.config.paramName] = query;
		new Ajax.Request(this.config.requestUrl, Object.extend({
			method: 'get',
			parameters: p,
			onCreate: function() {
				this.busy = true;
				this.showIndicator();
				this.hideResults();
			}.bind(this)
		}, options));
	},
	
	/**
	 * @private
	 */
	_populateResults: function(r) {
		var h = this.config.resultsHolder;

		if (this.config.singleRequest) {
			var matches = r.findAll(function(a) {
				var v = a[0].toLowerCase();
				if (v.strip().startsWith(this.queryString) && v.strip() != this.queryString) {
					return true;
				}
			}, this);
		}
		else {
			matches = r;
		}
		
		if (matches.size() < 1) {
			this.hideResults();
			return;
		}
		
		this.clearResultBox(h);
		
		var ul = this.config.resultsHolder.down(this.config.listParent);

		matches.each(function(a, i) {
			if (i >= this.config.resultsLimit) {
				throw $break;
			}
			this.appendVariant(a, ul);
		}, this);
		
		this.showResults();
	},
	
	appendVariant: function(a, ul) {
		var li = new Element(this.config.listNode, {'class': this.config.liClassName});
		function click(e, a) {
			Event.stop(e);
			this.config.onResultClick.call(this, a[0], a[1]);
		}
		var onClick = click.bindAsEventListener(this, a);
		// listen custom event also
		li.update(a[0]).observe('click', onClick).observe('variant:click', onClick);
		ul.appendChild(li);
	},
	
	clearResultBox: function(c) {
		c.update(null);
		var ul = new Element(this.config.listParent, {'class': this.config.ulClassName});
		c.appendChild(ul);
	}
});

var withImgAndGroupsAutoCompleter = Class.create(autoCompleter, {
	initialize: function($super, config) {
		config.listParent = 'dl';
		config.listNode = 'dd';
		$super(config);
	},
	appendVariant: function(a, ul) {
		// итак, создаем новый элемент списка
		var li = new Element(this.config.listNode, {'class': this.config.liClassName});
		
		// функция для того чтобы ее повесить на клик
		function click(e, a) {
			Event.stop(e);
			this.config.onResultClick.call(this, a[0], a[1]);
		}
		
		var onClick = click.bindAsEventListener(this, a);
		
		// слушаем кастомное событие (где-то используется)
		li.update(a[0]).observe('click', onClick).observe('variant:click', onClick);
		
		// крепим картиночку рядом
		var img = new Element('img', {'src': a[1][0], 'width': 16, 'height': 16});
		with (li.update(null)) {
			appendChild(img);
			update(innerHTML + a[0]);
		}
		
		// определяем тип записи
		// нужно будет создать <span> с соотв id для данной категории, если его нет
		var type = a[1][1];
		var span = ul.down('dt#type-' + type);
		if (!span) {
			span = (new Element('dt', {id: 'type-' + type})).update(a[1][2]);
			ul.appendChild(span);
			if (!a[1][2]) {
				span.hide();
			}
		}
		//присоединяем наконец на элемент к его корню
		Element.insert(span, {after: li});
	}
});
