/*
 * Copyright (C) 2013-2021 Combodo SARL
 *
 * This file is part of iTop.
 *
 * iTop 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, either version 3 of the License, or
 * (at your option) any later version.
 *
 * iTop 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
 */

;
$(function()
{
	$.widget( 'itop.navigation_menu',
		{
			// default options
			options:
				{
					active_menu_group: null,
					display_counts: false,
					filter_keyup_throttle: 200,             // In milliseconds
					org_id: ''
				},
			css_classes:
				{
					menu_expanded: 'ibo-is-expanded',
					menu_active: 'ibo-is-active',
					menu_filtered: 'ibo-is-filtered',
					menu_group_active: 'ibo-is-active',
					menu_nodes_active: 'ibo-is-active'
				},
			js_selectors:
				{
					menu_toggler: '[data-role="ibo-navigation-menu--toggler"]',
					menu_group: '[data-role="ibo-navigation-menu--menu-group"]',
					menu_drawer: '[data-role="ibo-navigation-menu--drawer"]',
					menu_filter_placeholder: '[data-role="ibo-navigation-menu--menu--placeholder"]',
					menu_filter_input: '[data-role="ibo-navigation-menu--menu-filter-input"]',
					menu_filter_clear: '[data-role="ibo-navigation-menu--menu-filter-clear"]',
					menu_filter_hint: '[data-role="ibo-navigation-menu--menu-filter-hint"]',
					menu_filter_hint_close: '[data-role="ibo-navigation-menu--menu-filter-hint-close"]',
					user_menu_toggler: '[data-role="ibo-navigation-menu--user-menu--toggler"]',
					user_menu_container: '[data-role="ibo-navigation-menu--user-menu-container"]',
					user_menu: '[data-role="ibo-navigation-menu--user-menu-container"] > [data-role="ibo-popover-menu"]'
				},
			filter_throttle_timeout: null,

			// the constructor
			_create: function () {
				this.element.addClass('ibo-navigation-menu');
				$(this.js_selectors.user_menu).popover_menu({'toggler': this.js_selectors.user_menu_toggler});
				this._bindEvents();
			},
			// events bound via _bind are removed automatically
			// revert other modifications here
			_destroy: function () {
				this.element.removeClass('ibo-navigation-menu');
			},
			_bindEvents: function () {
				const me = this;
				const oBodyElem = $('body');

				// Click on collapse/expand toggler
				this.element.find(this.js_selectors.menu_toggler).on('click', function (oEvent) {
					me._onTogglerClick(oEvent);
				});
				// Click on menu group
				this.element.find(this.js_selectors.menu_group).on('click', function (oEvent) {
					me._onMenuGroupClick(oEvent, $(this))
				});
				// Mostly for outside clicks that should close elements
				oBodyElem.on('click', function (oEvent) {
					me._onBodyClick(oEvent);
				});
				// Mostly for hotkeys
				oBodyElem.on('keyup', function (oEvent) {
					me._onBodyKeyUp(oEvent);
				});

				// Menus filter
				// - Input itself
				this.element.find(this.js_selectors.menu_filter_input).on('keyup', function (oEvent) {
					me._onFilterKeyUp(oEvent);
				});
				// - Clear icon
				this.element.find(this.js_selectors.menu_filter_clear).on('click', function (oEvent) {
					me._onFilterClearClick(oEvent);
				});
				// - Hint close
				this.element.find(this.js_selectors.menu_filter_hint_close).on('click', function (oEvent) {
					me._onFilterHintCloseClick(oEvent);
				});

				// User info
				this.element.find(this.js_selectors.user_menu_toggler).on('click', function (oEvent) {
					me._onUserMenuTogglerClick(oEvent);
				});
			},

			// Events callbacks
			_onTogglerClick: function(oEvent)
			{
				// Avoid anchor glitch
				oEvent.preventDefault();

				// Toggle menu
				this.element.toggleClass(this.css_classes.menu_expanded);

				// Save state in user preferences
				const sPrefValue = this.element.hasClass(this.css_classes.menu_expanded) ? 'expanded' : 'collapsed';
				SetUserPreference('navigation_menu.expanded', sPrefValue, true);
			},
			_onMenuGroupClick: function(oEvent, oMenuGroupElem)
			{
				// Avoid anchor glitch
				oEvent.preventDefault();

				var sMenuGroupId = oMenuGroupElem.attr('data-menu-group-id');
				if(this._getActiveMenuGroupId() === sMenuGroupId) {
					this._closeDrawer();
				}
				else {
					this._openDrawer(sMenuGroupId);
				}
			},
			_onBodyClick: function(oEvent)
			{
				if(this._checkIfClickShouldCloseDrawer(oEvent))
				{
					this._closeDrawer();
				}
			},
			_onBodyKeyUp: function(oEvent)
			{
				// Note: We thought about extracting the oEvent.key in a variable to lower case it, but this would be done
				// on every single key up in the application, which might not be what we want... (time consuming)
				if((oEvent.altKey === true) && (oEvent.key === 'm' || oEvent.key === 'M'))
				{
					if(this._getActiveMenuGroupId() === null)
					{
						const sFirstMenuGroupId = this.element.find(this.js_selectors.menu_group+':first').attr('data-menu-group-id');
						this._openDrawer(sFirstMenuGroupId);
					}

					this._focusFilter();
				}
			},

			_onFilterKeyUp: function(oEvent)
			{
				const me = this;
				const oInputElem = this.element.find(this.js_selectors.menu_filter_input);
				const sValue = oInputElem.val();

				if((sValue === '') && (oEvent.key === 'Escape'))
				{
					this._closeDrawer();
				}
				else if((sValue === '') || (oEvent.key === 'Escape'))
				{
					this._clearFiltering();
				}
				else {
					// Reset throttle timeout on key stroke
					clearTimeout(this.filter_throttle_timeout);
					this.filter_throttle_timeout = setTimeout(function () {
						me._doFiltering(sValue);
						me.refreshCounts();
					}, this.options.filter_keyup_throttle);
				}
			},
			_onFilterClearClick: function(oEvent)
			{
				// Avoid anchor glitch
				oEvent.preventDefault();

				// Remove current filter value
				this._clearFiltering();
				// Position focus in the input for better UX
				this._focusFilter();
			},
			_onFilterHintCloseClick: function(oEvent)
			{
				this.element.find(this.js_selectors.menu_filter_hint).hide();

				// Save state in user preferences
				SetUserPreference('navigation_menu.show_filter_hint', false, true);
			},

			_onUserMenuTogglerClick: function(oEvent)
			{
				// Avoid anchor glitch
				oEvent.preventDefault();
				var oEventTarget = $(oEvent.target);
				var aEventTargetPos = oEventTarget.position();

				$(this.js_selectors.user_menu_container).css({
					'top': (aEventTargetPos.top + parseInt(oEventTarget.css('marginTop'), 10) -  $(this.js_selectors.user_menu).height()) + 'px',
					'left': (aEventTargetPos.left + parseInt(oEventTarget.css('marginLeft'), 10) + oEventTarget.width()) + 'px'
				});
				$(this.js_selectors.user_menu).popover_menu('togglePopup');
			},

			// Methods
			_checkIfClickShouldCloseDrawer: function(oEvent)
			{
				if(
					$(oEvent.target.closest(this.js_selectors.menu_drawer)).length === 0
					&& $(oEvent.target.closest('[data-role="ibo-navigation-menu--menu-group"]')).length === 0
					&& $(oEvent.target.closest(this.js_selectors.menu_toggler)).length === 0
				)
				{
					this._closeDrawer();
				}
			},
			/**
			 * Return the ID of the active menu group, or null if none (typically when the drawer is closed)
			 * @returns {null|*}
			 * @private
			 */
			_getActiveMenuGroupId: function()
			{
				const oActiveMenuGroup = this.element.find('.'+this.css_classes.menu_group_active);
				if(oActiveMenuGroup.length > 0)
				{
					return oActiveMenuGroup.attr('data-menu-group-id');
				}
				else
				{
					return null;
				}
			},
			/**
			 * Clear the current active menu group but does NOT close the drawer
			 * @private
			 */
			_clearActiveMenuGroup: function()
			{
				this.element.find('[data-role="ibo-navigation-menu--menu-group"]').removeClass(this.css_classes.menu_group_active);
				this.element.find('[data-role="ibo-navigation-menu--menu-nodes"]').removeClass(this.css_classes.menu_nodes_active);
			},
			/**
			 * Open the drawer and set sMenuGroupId as the current active menu group
			 * @param sMenuGroupId string
			 * @private
			 */
			_openDrawer: function (sMenuGroupId) {
				this.refreshCounts();
				this._clearActiveMenuGroup();
				// Note: This causes the filter to be cleared event when using the hotkey to reopen a previously filled filter
				this._clearFiltering();

				// Set new active group
				this.element.find('[data-role="ibo-navigation-menu--menu-group"][data-menu-group-id="' + sMenuGroupId + '"]').addClass(this.css_classes.menu_group_active);
				this.element.find('[data-role="ibo-navigation-menu--menu-nodes"][data-menu-group-id="' + sMenuGroupId + '"]').addClass(this.css_classes.menu_nodes_active);

				// Set menu as active
				this.element.addClass(this.css_classes.menu_active);
			},
			/**
			 * Close the drawer after clearing the active menu group
			 * @private
			 */
			_closeDrawer: function () {
				this._clearActiveMenuGroup();

				// Set menu as non active
				this.element.removeClass(this.css_classes.menu_active);
			},

			// Menus filter methods
			_focusFilter: function () {
				this.element.find(this.js_selectors.menu_filter_input)
					.trigger('click')
					.trigger('focus');
			},
			/**
			 * Remove the current filter value and reset the menu nodes display
			 * @private
			 */
			_clearFiltering: function () {
				this.element.find(this.js_selectors.menu_filter_input).val('');

				// Reset display of everything
				// Note: We work on the 'display' property directly as there is a CSS rule managing the visibility of the active menu group
				this.element.find('[data-role="ibo-navigation-menu--menu-nodes"]').css('display', '');
				this.element.find('[data-role="ibo-navigation-menu--menu-node"]').css('display', '');
				
				this.element.find(this.js_selectors.menu_filter_placeholder).css('display', 'none');

				// Mark menu as unfiltered
				this.element.removeClass(this.css_classes.menu_filtered);
			},
			/**
			 * Filter the displayed menu nodes regarding the current filter value
			 * @param sRawFilterValue string
			 * @private
			 */
			_doFiltering: function(sRawFilterValue)
			{
				const me = this;
				const aFilterValueParts = this._formatValueForFilterComparison(sRawFilterValue).split(' ');
				let bHasAnyMatch = false;

				// Mark menu as filtered
				this.element.addClass(this.css_classes.menu_filtered);

				// Hide everything
				this.element.find('[data-role="ibo-navigation-menu--menu-nodes"]').hide();
				this.element.find('[data-role="ibo-navigation-menu--menu-node"]').hide();

				// Show matching menu node
				this.element.find('[data-role="ibo-navigation-menu--menu-node"]').each(function () {
					// Note: We don't filter on data-role="ibo-navigation-menu--menu-node-label" on purpose so we can also filter the counters
					const sNodeValue = me._formatValueForFilterComparison($(this).children('[data-role="ibo-navigation-menu--menu-node-title"]:first').text());
					let bMatches = true;

					// On first non matching part, we consider that the menu node is not a match
					for (let iIdx in aFilterValueParts) {
						if (sNodeValue.indexOf(aFilterValueParts[iIdx]) === -1) {
							bMatches = false;
							break;
						}
					}

					if (bMatches) {
						bHasAnyMatch = true;
						// Note: Selector must be recursive
						$(this).parents('[data-role="ibo-navigation-menu--menu-nodes"], [data-role="ibo-navigation-menu--menu-node"]').show();
						$(this).show();
					}
				});
				if(!bHasAnyMatch)
				{
					this.element.find(this.js_selectors.menu_filter_placeholder).css('display', '');
				}
			},
			/**
			 * Format sOriginalValue for an easier comparison (change accents, capitalized letters, ...)
			 *
			 * @param sOriginalValue string
			 * @returns string
			 * @private
			 */
			_formatValueForFilterComparison: function (sOriginalValue) {
				return sOriginalValue.toLowerCase().latinise();
			},
			/**
			 * Refresh count badges for OQL menus
			 */
			refreshCounts: function () {
				const me = this;
				if (this.options.display_counts) {
					$.ajax({
						method: "POST",
						url: GetAbsoluteUrlAppRoot() + 'pages/ajax.render.php',
						data: {
							operation: "get_menus_count",
							c: { org_id: me.options.org_id }
						},
						dataType: "json"
					})
						.done(function (data) {
							if (data.code === "done") {
								for (const [key, value] of Object.entries(data.counts)) {
									let menuEntry = me.element.find('[data-menu-id="' + key + '"]');
									menuEntry.html(value);
									menuEntry.show();
								}
							}
						});
				}
			}
		});
});
