/*
** 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/>.
**/


/**
 * Represents DOM node for notification list. Stores the collection of ZBX_Notification objects.
 */
function ZBX_NotificationCollection() {
	this._dangling_nodes = [];

	this._list_sequence = [];
	this._list_obj = {};

	this.makeNodes();

	this.node.style.right = '10px';
	this.node.style.top = '0px';
}

/**
 * @return {array}
 */
ZBX_NotificationCollection.prototype.getIds = function() {
	return this._list_sequence;
};

/**
 * @param {callable} callback
 */
ZBX_NotificationCollection.prototype.map = function(callback) {
	var len = this._list_sequence.length;

	while (--len > -1) {
		callback(this.getById(this._list_sequence[len]), len);
	}
};

/**
 * @param {callable} callback
 *
 * @return {array}
 */
ZBX_NotificationCollection.prototype.filterList = function(callback) {
	var list = [],
		len = this._list_sequence.length;

	while (--len > -1) {
		var ret = callback(this.getById(this._list_sequence[len]));
		(ret !== false) && list.push(ret);
	}

	return list;
};

/**
 * @return {array} List of raw notification objects.
 */
ZBX_NotificationCollection.prototype.getRawList = function() {
	return this.filterList(function(notif) {
		return notif.getRaw();
	});
};

/**
 * Merges current with new list. Updates changes for ZBX_Notification if possible, or creates new ZBX_Notification.
 * Recoverable notification is just partial of raw with one mandatory field - `eventid`. New iterator sequence reflects
 * the order of list given to this method. During merge, nodes that are not present in list will be removed.
 *
 * @param {array} list List of raw/recoverable notification objects.
 */
ZBX_NotificationCollection.prototype.consumeList = function(list) {
	var new_list_sequence = [];

	while (raw = list.pop()) {
		/*
		 * Case if server returns with "recovered" notification type, that cannot be recovered by client.
		 * This should never happen.
		 */
		if (!raw.body && !this._list_obj[raw.eventid]) {
			continue;
		}

		if (this._list_obj[raw.eventid]) {
			this._list_obj[raw.eventid].updateRaw(raw);
		}
		else {
			this._list_obj[raw.eventid] = new ZBX_Notification(raw);
		}

		new_list_sequence.push(raw.eventid);
	}

	for (var id in this._list_obj) {
		if (new_list_sequence.indexOf(id) == -1) {
			this._dangling_nodes.push(this._list_obj[id].node);
			delete this._list_obj[id];
		}
	}

	this._list_sequence = new_list_sequence;
};

/**
 * Creates detached DOM nodes.
 */
ZBX_NotificationCollection.prototype.makeNodes = function() {
	const header = document.createElement('div');
	const controls = document.createElement('ul');

	this.node = document.createElement('div');
	this.node.style.display = 'none';
	this.node.hidden = true;
	this.node.className = 'overlay-dialogue notif';

	this.btn_close = document.createElement('button');
	this.btn_close.setAttribute('title', locale['S_CLOSE']);
	this.btn_close.setAttribute('type', 'button');
	this.btn_close.className = 'btn-overlay-close';

	header.className = 'overlay-dialogue-header cursor-move';
	this.node.appendChild(header);

	header.appendChild(controls);
	header.appendChild(this.btn_close);

	this.btn_snooze = this.makeToggleBtn(
		{class: [ZBX_STYLE_BTN_ICON + ' ' + ZBX_ICON_BELL]},
		{class: [ZBX_STYLE_BTN_ICON + ' ' + ZBX_ICON_BELL_OFF]}
	);

	const li_btn_snooze = document.createElement('li');
	li_btn_snooze.appendChild(this.btn_snooze);

	this.btn_mute = this.makeToggleBtn(
		{class: ZBX_STYLE_BTN_ICON + ' ' + ZBX_ICON_SPEAKER},
		{class: ZBX_STYLE_BTN_ICON + ' ' + ZBX_ICON_SPEAKER_OFF}
	);

	const li_btn_mute = document.createElement('li');
	li_btn_mute.appendChild(this.btn_mute);

	controls.appendChild(li_btn_snooze);
	controls.appendChild(li_btn_mute);

	this.list_node = document.createElement('ul');
	this.list_node.className = 'notif-body';

	this.node.appendChild(this.list_node);
};

/**
 * Creates a button node with a method `renderState(bool)`.
 *
 * @param {object} attrs_inactive  Attribute key-value object to be mapped on renderState(true).
 * @param {object} attrs_active    Attribute key-value object to be mapped on renderState(false).
 *
 * @return {HTMLElement} DOM button element.
 */
ZBX_NotificationCollection.prototype.makeToggleBtn = function(attrs_inactive, attrs_active) {
	var button = document.createElement('button');
	button.renderState = function(is_active) {
		var attrs = is_active ? attrs_active : attrs_inactive,
			attr_name;

		for (attr_name in attrs) {
			this.setAttribute(attr_name, attrs[attr_name]);
		}
	};

	return button;
};

/**
 * Iterator property will be updated and reference to DOM node kept to be gracefully removed at render. No reference
 * to notification object would exist after this call - notification is removed and will not cycle back into LS.
 *
 * @param {string} id
 */
ZBX_NotificationCollection.prototype.removeById = function(id) {
	var index = this._list_sequence.indexOf(id);

	if (index === -1) {
		return;
	}

	this._list_sequence.splice(index, 1);

	if (this._list_obj[id]) {
		this._list_obj[id].display_timeoutid && clearTimeout(this._list_obj[id].display_timeoutid);
		this._dangling_nodes.push(this._list_obj[id].node);
		delete this._list_obj[id];
	}
};

/**
 * @param {string} id
 *
 * @return {ZBX_Notification}
 */
ZBX_NotificationCollection.prototype.getById = function(id) {
	return this._list_obj[id];
};

/**
 * Shows list of notifications.
 *
 * @return {Promise}
 */
ZBX_NotificationCollection.prototype.show = function() {
	return ZBX_Notifications.util.fadeIn(this.node);
};

/**
 * Hides list of notifications.
 *
 * @return {Promise}
 */
ZBX_NotificationCollection.prototype.hide = function() {
	return ZBX_Notifications.util.fadeOut(this.node);
};

/**
 * @return {boolean}
 */
ZBX_NotificationCollection.prototype.isEmpty = function() {
	return !this._list_sequence.length;
};

/**
 * Animates slide-up-remove on dangling nodes one by one.
 */
ZBX_NotificationCollection.prototype.removeDanglingNodes = function() {
	var duration = this._dangling_nodes.length > 4 ? 200 : 500;
	var first = true;

	while (node = this._dangling_nodes.pop()) {
		ZBX_Notifications.util.slideUp(node, first && 500 || duration, this._dangling_nodes.length * duration)
			.then(function(node) {
				node.parentNode && node.remove();
			});
		first = false;
	}
};

/**
 * Notification sequence is maintained in DOM, in server response notifications must be ordered.
 * Shows or hides list node, updates and appends notification nodes, then delegates to remove dangling nodes.
 * Updates button tooltip text based on actual data.
 *
 * @param {object}                 severity_styles  Severity styles.
 * @param {ZBX_NotificationsAlarm} alarm_state      Alarm state.
 * @param {string}                 username         Username of logged-in user.
 * @param {boolean}                muted            Indicator whether notifications are muted.
 */
ZBX_NotificationCollection.prototype.render = function(severity_styles, alarm_state, username, muted) {
	this.btn_snooze.setAttribute('title', t('Snooze for %1$s').replace('%1$s', username));
	this.btn_mute.setAttribute('title', muted
		? t('Unmute for %1$s').replace('%1$s', username)
		: t('Mute for %1$s').replace('%1$s', username)
	);

	this.btn_snooze.renderState(alarm_state.isSnoozed(this.getRawList()));

	if (alarm_state.supported) {
		this.btn_mute.renderState(alarm_state.muted);
	}
	else {
		this.btn_mute.renderState(true);
		this.btn_mute.disabled = true;
		this.btn_mute.title = locale['S_CANNOT_SUPPORT_NOTIFICATION_AUDIO'];
	}

	const list_node = this.list_node;
	let prev_notif_node = null;

	if (this.isEmpty()) {
		return this.hide().then(function() {
			list_node.innerHTML = '';
			this._dangling_nodes = [];
		}.bind(this));
	}

	const slide_down = list_node.children.length != 0;

	this.map(function(notif) {
		notif.render(severity_styles);

		if (notif.isNodeConnected()) {
			prev_notif_node = notif.node;
			return;
		}

		if (prev_notif_node) {
			prev_notif_node.insertAdjacentElement('afterend', notif.node);
		}
		else {
			list_node.insertAdjacentElement('afterbegin', notif.node);
		}

		slide_down && ZBX_Notifications.util.slideDown(notif.node, 200);
		prev_notif_node = notif.node;
	});

	this.removeDanglingNodes();

	(this.node.style.display === 'none') && this.show();
};
