/*
** Copyright (C) 2001-2025 Zabbix SIA
**
** This program is free software: you can redistribute it and/or modify it under the terms of
** the GNU Affero General Public License as published by the Free Software Foundation, version 3.
**
** This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
** without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
** See the GNU Affero General Public License for more details.
**
** You should have received a copy of the GNU Affero General Public License along with this program.
** If not, see <https://www.gnu.org/licenses/>.
**/


var LCL_SUGGESTS = [];

function createSuggest(oid) {
	var sid = LCL_SUGGESTS.length;
	LCL_SUGGESTS[sid] = new CSuggest(sid, oid);

	return sid;
}

var CSuggest = function(id, objid) {
	this.id = id;
	this.cleanCache();
	this.dom.input = document.getElementById(objid);

	addListener(this.dom.input, 'keyup', this.keyPressed.bindAsEventListener(this));
	addListener(this.dom.input, 'blur', this.suggestBlur.bindAsEventListener(this));
	addListener(window, 'resize', this.positionSuggests.bindAsEventListener(this));

	this.timeoutNeedle = null;
	this.userNeedle = this.dom.input.value;
};

CSuggest.prototype = {
	// public
	'useLocal':			true,	// use cache to find suggests
	'useServer':		true,	// use server to find suggests
	'saveToCache':		true,	// save results to cache
	'cacheTimeOut':		60,		// cache timeout (seconds)
	'suggestLimit':		15,		// suggestion show limit
	'searchDelay':		200,	// milliseconds

	// private
	'id':				null,	// sugg obj identity
	'rpcid':			0,		// rpc request id
	'needles':			{},		// searched strings
	'userNeedle':		'',		// userNeedle
	'timeoutNeedle':	null,	// Timeout reference
	'cache': {
		'time':			0,		// cache creation time
		'list':			{},		// cache by word
		'needle':		{}		// cache by needle
	},
	'dom': {
		'input':		null,	// DOM node input
		'suggest':		null,	// DOM node suggest div
		'sugtab':		null	// DOM node suggests table
	},
	'hlIndex':			0,		// indicates what row should be highlighted
	'suggestCount':		0,		// suggests shown
	'mouseOverSuggest':	false,	// indicates if mouse is over suggests

	needleChange: function(e) {
		this.hlIndex = 0;
		this.suggestCount = 0;

		clearTimeout(this.timeoutNeedle);

		var target = e.target,
			needle = target.value.toLowerCase();

		if (empty(needle)) {
			this.hideSuggests();

			return true;
		}

		this.userNeedle = target.value;
		this.needles[needle] = {'needle': needle, 'list': {}};

		var found = false;

		if (this.useLocal) {
			found = this.searchClient(needle);
		}

		if (!found && this.useServer) {
			this.timeoutNeedle = setTimeout(this.searchServer.bind(this, needle), this.searchDelay);
		}
	},

	// search
	searchServer: function(needle) {
		if (needle !== this.userNeedle.toLowerCase()) {
			return;
		}

		var rpcRequest = {
			'method': 'search',
			'params': {
				'search': needle
			},
			'onSuccess': this.serverRespond.bind(this, needle),
			'onFailure': function() {
				throw('Suggest Widget: search request failed.');
			}
		};

		new RPC.Call(rpcRequest);
	},

	serverRespond: function(needle, respond) {
		var params = {
			'list': {},
			'needle': needle
		};

		for (var i = 0; i < respond.length; i++) {
			if (!isset(i, respond) || empty(respond[i])) {
				continue;
			}

			params.list[i] = respond[i].name;
		}

		this.needles[params.needle].list = params.list;

		if (needle == this.userNeedle.toLowerCase()) {
			this.showSuggests();
			this.newSugTab(params.needle);
		}

		if (this.saveToCache) {
			this.saveCache(params.needle, params.list);
		}
	},

	searchClient: function(needle) {
		var found = false;

		if (this.inCache(needle)) {
			this.needles[needle].list = this.cache.needle[needle];
			found = true;
		}
		else if (!this.useServer) {
			found = this.searchCache(needle);
		}

		if (found) {
			this.showSuggests();
			this.newSugTab(needle);
		}

		return found;
	},

	// cache
	searchCache: function(needle) {
		var fkey = needle[0];

		if (!isset(fkey, this.cache.list)) {
			return false;
		}

		var found = false,
			list = {};

		for (var key in this.cache.list[fkey]) {
			var value = this.cache.list[fkey][key];

			if (empty(value)) {
				continue;
			}

			if (key.indexOf(needle) === 0) {
				list[value] = value;
				found = true;
			}
		}

		this.needles[needle].list = list;

		if (this.saveToCache) {
			this.saveCache(needle, list);
		}

		return found;
	},

	inCache: function(needle) {
		if (this.useServer) {
			var dd = new Date();

			if ((this.cache.time + (this.cacheTimeOut * 1000)) < dd.getTime()) {
				this.cleanCache();
			}
		}

		return isset(needle, this.cache.needle);
	},

	saveCache: function(needle, list) {
		if (this.useServer) {
			var dd = new Date();

			if ((this.cache.time + (this.cacheTimeOut * 1000)) < dd.getTime()) {
				this.cleanCache();
			}
		}

		// needles
		if (!is_null(needle)) {
			this.cache.needle[needle] = list;
		}

		// list
		for (var key in list) {
			if (empty(list[key])) {
				continue;
			}

			var word = list[key],
				lWord = word.toLowerCase(),
				fkey = lWord[0];

			// Indexing by first letter.
			if (!isset(fkey, this.cache.list)) {
				this.cache.list[fkey] = {};
			}

			this.cache.list[fkey][lWord] = word;
		}
	},

	cleanCache: function() {
		var time = new Date();

		this.cache = {
			'time':		time.getTime(),
			'list':		{},
			'needle':	{}
		}
	},

	// events
	onSelect: function(selection) {
		return true;
	},

	keyPressed: function(e) {
		var key = e.keyCode;

		switch (true) {
			// escape
			case (key == 27):
				this.hlIndex = 0;
				this.suggestCount = 0;
				this.removeHighLight(e);
				this.setNeedleByHighLight(e);
				this.hideSuggests(e);
				break;

			// enter
			case (key == 13):
				if (this.dom.input.value.trim() !== '') {
					this.needleChange(e);
					this.selectSuggest(e);
				}
				break;

			// left, right, tab
			case (key == 37 || key == 39 || key == 9):
				break;

			// up
			case (key == 38):
				this.keyUp(e);
				break;

			// down
			case (key == 40):
				this.keyDown(e);
				break;

			default:
				this.needleChange(e);
		}

		e.preventDefault();
		e.stopPropagation();
	},

	keyUp: function(e) {
		if (this.hlIndex == 0) {
			this.hlIndex = this.suggestCount;
		}
		else {
			this.hlIndex--;
		}

		this.removeHighLight(e);
		this.highLightSuggest(e);
		this.setNeedleByHighLight(e);
	},

	keyDown: function(e) {
		if (is_null(this.dom.suggest) || (this.dom.suggest.style.display === 'none')) {
			this.needleChange(e);

			return true;
		}

		if (this.hlIndex == this.suggestCount) {
			this.hlIndex = 0;
		}
		else {
			this.hlIndex++;
		}

		this.removeHighLight(e);
		this.highLightSuggest(e);
		this.setNeedleByHighLight(e);
	},

	mouseOver: function(e) {
		this.mouseOverSuggest = true;

		var row = e.target;

		if (is_null(row) || (row.tagName.toLowerCase() !== 'li') || !isset('id',row)) {
			return true;
		}

		var tmp = row.id.split('_');

		if (tmp.length != 2) {
			return true;
		}

		this.hlIndex = parseInt(tmp[1], 10);
		this.removeHighLight(e);
		this.highLightSuggest(e);
	},

	mouseOut: function(e) {
		this.mouseOverSuggest = false;
	},

	suggestBlur: function(e) {
		if (this.mouseOverSuggest) {
			e.preventDefault();
			e.stopPropagation();
		}
		else {
			this.hideSuggests(e);
		}
	},

	// highLight
	removeHighLight: function() {
		jQuery('li.suggest-hover').each(function() {
			this.className = '';
		});
	},

	highLightSuggest: function() {
		var row = document.getElementById('line_' + this.hlIndex);

		if (!is_null(row)) {
			row.className = 'suggest-hover';
		}
	},

	setNeedleByHighLight: function() {
		if (this.hlIndex == 0) {
			this.dom.input.value = this.userNeedle;
		}
		else {
			this.dom.input.value = document.getElementById('line_' + this.hlIndex).getAttribute('needle');
		}
	},

	selectSuggest: function(e) {
		this.setNeedleByHighLight(e);
		this.hideSuggests();

		if (this.onSelect(this.dom.input.value)) {
			this.dom.input.form.submit();
		}
	},

	// DOM creation
	showSuggests: function() {
		if (is_null(this.dom.suggest)) {
			this.dom.suggest = document.createElement('ul');

			var doc_body = document.getElementsByTagName('body')[0];

			if (empty(doc_body)) {
				return false;
			}

			doc_body.appendChild(this.dom.suggest);
			this.dom.suggest.className = 'search-suggest';
			this.positionSuggests();

			// Insert just after input.
			this.dom.input.parentNode.insertBefore(this.dom.suggest, this.dom.input.nextSibling);
		}

		this.dom.suggest.style.display = 'block';
	},

	hideSuggests: function() {
		if (!is_null(this.dom.suggest)) {
			this.dom.suggest.style.display = 'none';
		}
	},

	positionSuggests: function() {
		if (is_null(this.dom.suggest)) {
			return true;
		}

		var dims = getDimensions(this.dom.input);

		this.dom.suggest.style.top = dims.height + 'px';
	},

	newSugTab: function(needle) {
		var list = this.needles[needle].list,
			sugTab = document.createElement('div'),
			count = 0;

		needle = needle.toLowerCase();

		for (var key in list) {
			if (empty(list[key])) {
				continue;
			}

			count++;

			var li = document.createElement('li'),
				text = list[key].toLowerCase(),
				start = 0,
				end = 0;

			li.setAttribute('id', 'line_' + count);
			li.setAttribute('needle', list[key]);

			while (text.indexOf(needle, end) > -1) {
				end = text.indexOf(needle, end);

				if (end > start) {
					li.appendChild(document.createTextNode(list[key].substring(start, end)));
				}

				var bold = document.createElement('span');
				bold.appendChild(document.createTextNode(list[key].substring(end, end + needle.length)));
				bold.setAttribute('class', 'suggest-found');
				li.appendChild(bold);

				end += needle.length;
				start = end;
			}

			if (end < list[key].length) {
				li.appendChild(document.createTextNode(list[key].substring(end, list[key].length)));
			}

			addListener(li, 'mouseover', this.mouseOver.bindAsEventListener(this), true);
			addListener(li, 'mouseup', this.selectSuggest.bindAsEventListener(this), true);
			addListener(li, 'mouseout', this.mouseOut.bindAsEventListener(this), true);
			sugTab.appendChild(li);

			if (count >= this.suggestLimit) {
				break;
			}
		}

		this.dom.suggest.appendChild(sugTab);

		if (!is_null(this.dom.sugtab)) {
			this.dom.sugtab.remove();
		}

		this.dom.sugtab = sugTab;
		this.dom.suggest.appendChild(this.dom.sugtab);

		if (count == 0) {
			this.hideSuggests();
		}

		this.suggestCount = count;
	}
};
