/*
 * @copyright   Copyright (C) 2010-2023 Combodo SARL
 * @license     http://opensource.org/licenses/AGPL-3.0
 */

$(function()
{
    $.widget( 'itop.tab_container',
        {
            // default options
            options:
            {
	            remote_tab_load_dict: 'Click to load',
            },
            css_classes:
            {
            	is_hidden: 'ibo-is-hidden',
            	is_disabled: 'ibo-is-disabled',
	            is_transparent: 'ibo-is-transparent',
	            is_opaque: 'ibo-is-opaque',
	            is_scrollable: 'ibo-is-scrollable',
	            tab_container: 'ibo-tab-container',
            },
            js_selectors:
            {
                tabs_list: '[data-role="ibo-tab-container--tabs-list"]',
                tab_header: '[data-role="ibo-tab-container--tab-header"]',
                tab_ajax_type: '[data-tab-type="ajax"]',
                tab_html_type: '[data-tab-type="html"]',
                tab_toggler: '[data-role="ibo-tab-container--tab-toggler"]',
                extra_tabs_container: '[data-role="ibo-tab-container--extra-tabs-container"]',
                extra_tabs_list_toggler: '[data-role="ibo-tab-container--extra-tabs-list-toggler"]',
                extra_tabs_list: '[data-role="ibo-tab-container--extra-tabs-list"]',
                extra_tab_toggler: '[data-role="ibo-tab-container--extra-tab-toggler"]',
            },

            // the constructor
            _create: function()
            {
	            var me = this;
	            this.element.addClass(this.css_classes.tab_container);

	            // Ugly patch for a change in the behavior of jQuery UI:
	            // Before jQuery UI 1.9, tabs were always considered as "local" (opposed to Ajax)
	            // when their href was beginning by #. Starting with 1.9, a <base> tag in the page
	            // is taken into account and causes "local" tabs to be considered as Ajax
	            // unless their URL is equal to the URL of the page...
	            if ($('base').length > 0) {
		            this.element.find(this.js_selectors.tab_toggler).each(function () {
			            const sHash = location.hash;
			            const sCleanLocation = location.href.toString().replace(sHash, '').replace(/#$/, '');
			            $(this).attr('href', sCleanLocation+$(this).attr('href'));
		            });
	            }

	            let oTabsParams = {
		            classes: {
			            'ui-tabs-panel': 'ibo-tab-container--tab-container',    // For ajax tabs, so their containers have the right CSS classes
		            },
		            // There we want a number as it is for the jQuery Tabs API
		            active: this._getTabIndexFromTabId($.bbq.getState(this.element.attr('id'), true)) || 0,
		            remote_tab_load_dict: this.options.remote_tab_load_dict
	            };
	            if ($.bbq) {
		            // Enable tabs on all tab widgets. The `event` property must be overridden so
		            // that the tabs aren't changed on click, and any custom event name can be
		            // specified. Note that if you define a callback for the 'select' event, it
		            // will be executed for the selected tab whenever the hash changes.
		            oTabsParams['event'] = 'change';
	            }
	            
	            // While our tab widget is loading, protect tab toggler from being triggered
	            this.element.find(this.js_selectors.tab_toggler).on('click', function(e){
		            if(me.element.attr('data-status') === 'loading') {
			            e.preventDefault();
			            e.stopImmediatePropagation();
		            }
	            });
	            // Now that we are protected from toggler being triggered without tab container being loaded, we can put back
	            // data-target attribute value back into href attribute
	            $.each(this.element.find(this.js_selectors.tab_header + this.js_selectors.tab_ajax_type), function (a){
	            	let oLink = $(this).find(me.js_selectors.tab_toggler);
		            oLink.attr('href', oLink.attr('data-target'));
	            })
	            
	            this._addTabsWidget(oTabsParams);

	            this._bindEvents()
	            
	            // We're done, set our status as loaded
	            this.element.attr('data-status', 'loaded');
            },
	        /**
	         * @param oParams {Object} Structured object representing the options for the jQuery UI Tabs widget
	         * @private
	         */
	        _addTabsWidget: function (oParams) {
		        if (this.element.hasClass(this.css_classes.is_scrollable)) {
			        this.element.scrollabletabs(oParams);
		        } else {
			        this.element.regulartabs(oParams);
		        }
	        },
	        /**
	         * Return tabs widget instance
	         * @public
	         */
	        GetTabsWidget: function () {
		        if (this.element.hasClass(this.css_classes.is_scrollable)) {
			        return this.element.scrollabletabs('instance');
		        } else {
			        return this.element.regulartabs('instance');
		        }
	        },
            // events bound via _bind are removed automatically
            // revert other modifications here
            _destroy: function()
            {
                this.element.removeClass(this.css_classes.tab_container);
            },
            _bindEvents: function()
            {
                const me = this;

                // Bind an event on tab activation
                this.element.on('tabsactivate', function(oEvent, oUI){
                    me._onTabActivated(oUI);
                });
	            this.element.on('tabscrolled', function(oEvent, oUI){
		            me._onTabActivated(oUI);
	            });
                // Bind an event to window.onhashchange that, when the history state changes,
                // iterates over all tab widgets, changing the current tab as necessary.
                $(window).on('hashchange', function(){
                    me._onHashChange();
                });
                // Click on tab togglers
                this.element.find(this.js_selectors.tab_toggler).on('click', function(){
                    me._onTabTogglerClick($(this));
                });
                // Resize of the tab container
                if(window.IntersectionObserver) {
                	const oTabsListIntersectObs = new IntersectionObserver(function(aEntries, oTabsListIntersectObs){
                		aEntries.forEach(oEntry => {
                			let oTabHeaderElem = $(oEntry.target);
                			let bIsVisible = oEntry.isIntersecting;
			                if(bIsVisible) {
				                oTabHeaderElem.removeClass(me.css_classes.is_transparent);
				                oTabHeaderElem.css('visibility', '');
			                }
			                else {
				                oTabHeaderElem.removeClass(me.css_classes.is_transparent);
				                // This is necessary, otherwise link will still be clickable
				                oTabHeaderElem.css('visibility', 'hidden');
			                }
			                me._updateTabHeaderDisplay(oTabHeaderElem, bIsVisible);
		                });
                		me._updateExtraTabsList();
	                }, {
                		root: this.element.find(this.js_selectors.tabs_list)[0],
		                threshold: [0.9] // N°4783 Should be completely visible, but lowering the threshold prevents a bug in the JS Observer API when the window is zoomed in/out, in which case all items respond as being hidden even when they are not.
	                });
                	this.element.find(this.js_selectors.tab_header).each(function(){
		                oTabsListIntersectObs.observe(this);
	                });
                }
                // Click on extra tabs list toggler
                this.element.find(this.js_selectors.extra_tabs_list_toggler).on('click', function(oEvent){
                    me._onExtraTabsListTogglerClick($(this), oEvent);
                });
                // Click on "extra tab togglers"
                this.element.find(this.js_selectors.extra_tab_toggler).on('click', function(oEvent){
	                me._onExtraTabTogglerClick($(this), oEvent);
                });
                // Mostly for outside clicks that should close elements
                $('body').on('click', function(oEvent){
                    me._onBodyClick(oEvent);
                });
            },

            // Events callbacks
            // - Update tab headers display on container resize
            _onTabContainerResize: function()
            {
                const me = this;
                this.element.find(this.js_selectors.tab_header).each(function(){
                	me._updateTabHeaderDisplay($(this));
                });
                this._updateExtraTabsList();
            },
            // - Update URL hash when tab is activated
            _onTabActivated: function(oUI)
            {
	            let oState = {};

	            // Get the id of this tab widget.
	            const sId = this.element.attr('id');

	            //Datatable are not displayed correctly when hidden
	            $(oUI.newPanel).find('.dataTables_scrollBody > .ibo-datatable').each(function () {
		            $('#'+this.id).DataTable().columns.adjust().draw();
	            });

	            // Get the ID of this tab.
	            const sTabId = $(oUI.newTab).attr('data-tab-id');

	            // Set the state!
	            oState[sId] = sTabId;
	            $.bbq.pushState(oState);
            },
	        // - Change current tab as necessary when URL hash changes
	        _onHashChange: function () {
		        // Get the index for this tab widget from the hash, based on the
		        // appropriate id property. In jQuery 1.4, you should use e.getState()
		        // instead of $.bbq.getState(). The second, 'true' argument coerces the
		        // string value to a number.
		        const sTabId = $.bbq.getState(this.element.attr('id'), true) || this._getTabIdFromTabIndex(0);

		        // Select the appropriate tab for this tab widget by triggering the custom
		        // event specified in the .tabs() init above (you could keep track of what
		        // tab each widget is on using .data, and only select a tab if it has
		        // changed).
		        this.element.children(this.js_selectors.tabs_list).children(this.js_selectors.tab_header+'[data-tab-id="'+sTabId+'"]').find(this.js_selectors.tab_toggler).triggerHandler('change');

		        // Iterate over all truncated lists to find whether they are expanded or not
		        $('a.truncated').each(function () {
			        const sState = $.bbq.getState(this.id, true) || 'close';
			        if (sState === 'open') {
				        $(this).trigger('open');
			        } else {
				        $(this).trigger('close');
			        }
		        });
	        },
	        // - Define our own click handler for the tabs, overriding the default.
	        _onTabTogglerClick: function (oTabHeaderElem) {
		        if ($.bbq) {
			        let oState = {};

			        // Get the id of this tab widget.
			        const sId = this.element.attr('id');

			        // Get the index of this tab.
			        const sTabId = oTabHeaderElem.closest(this.js_selectors.tab_header).attr('data-tab-id');

			        // Set the state!
			        oState[sId] = sTabId;
			        $.bbq.pushState(oState);
		        }
	        },
	        // - Forward click event to real tab toggler
	        _onExtraTabTogglerClick: function (oExtraTabTogglerElem, oEvent) {
		        // Prevent anchor default behaviour
		        oEvent.preventDefault();

		        if (oExtraTabTogglerElem.attr('aria-disabled') === 'true') {
		            // Corresponding tab is disabled, do nothing			
		            oEvent.stopPropagation();
		            return;
		        }			
		        // Trigger click event on real tab toggler (the hidden one)
		        const sTargetTabId = oExtraTabTogglerElem.attr('href').replace(/#/, '');
		        this.element.find(this.js_selectors.tab_header+'[data-tab-id="'+sTargetTabId+'"] '+this.js_selectors.tab_toggler).trigger('click');
	        },
            // - Toggle extra tabs list
            _onExtraTabsListTogglerClick: function(oElem, oEvent)
            {
                // Prevent anchor default behaviour
                oEvent.preventDefault();

				// Compute list position
	            // Note: Arbitrary +6px for the position as we don't want it to be exactly against the toggler
	            let fTopOffset = this.element.find(this.js_selectors.extra_tabs_list_toggler).offset().top + this.element.find(this.js_selectors.extra_tabs_list_toggler).outerHeight() + 6;
				// We need to compute position from the right side of the screen because at this time the list isn't visible and we can't know its width, so we can't position it regarding the left side of the screen
	            // Note: We use window.innerWidth instead of outerWidth as we need the width of the actual viewport, not the OS browser window
				let fRightOffset = window.innerWidth - this.element.find(this.js_selectors.extra_tabs_list_toggler).offset().left - this.element.find(this.js_selectors.extra_tabs_list_toggler).outerWidth();
	            this.element.find(this.js_selectors.extra_tabs_list)
		            .css('top', fTopOffset + 'px')
		            .css('right', fRightOffset + 'px');

                // TODO 3.0.0: Should/could we use a popover menu instead here?
                this.element.find(this.js_selectors.extra_tabs_list).toggleClass(this.css_classes.is_hidden);
            },
            _onBodyClick: function(oEvent)
            {
                // Close extra tabs list if opened
                if($(oEvent.target.closest(this.js_selectors.extra_tabs_container)).length === 0)
                {
                    this.element.find(this.js_selectors.extra_tabs_list).addClass(this.css_classes.is_hidden);
                }
            },

            // Helpers
            /**
             * Update tab header display based on its visibility to the user
             *
             * @param oTabHeaderElem {Object} jQuery element
             * @param bIsVisible {boolean|null} If null, visibility will be computed automatically. Not that performance might not be great so it's preferable to pass the value when known
             * @private
             */
            _updateTabHeaderDisplay(oTabHeaderElem, bIsVisible = null)
            {
            	const sTabId = oTabHeaderElem.attr('data-tab-id');
            	const oMatchingExtraTabElem = this.element.find(this.js_selectors.extra_tab_toggler+'[href="#'+sTabId+'"]');

                // Disabled tabs should be disabled in the ExtraTabs list as well
                let bIsDisabled = false;
                if (oTabHeaderElem.attr('aria-disabled') === 'true') {
                    bIsDisabled = true;
                }
            	// Manually check if the tab header is visible if the info isn't passed
            	if (bIsVisible === null) {
                    bIsVisible = CombodoGlobalToolbox.IsElementVisibleToTheUser(oTabHeaderElem[0], true, 2);
            	}

            	// Hide/show the corresponding extra tab element
            	if (bIsVisible) {
                    oMatchingExtraTabElem.addClass(this.css_classes.is_hidden);
            	} else {
                    oMatchingExtraTabElem.removeClass(this.css_classes.is_hidden);
            	}
            	// Enable/disable the corresponding extra tab element
            	if (bIsDisabled) {
                    oMatchingExtraTabElem.attr('aria-disabled', 'true');
                    oMatchingExtraTabElem.addClass(this.css_classes.is_disabled);
            	} else {
                    oMatchingExtraTabElem.attr('aria-disabled', 'false');
                     oMatchingExtraTabElem.removeClass(this.css_classes.is_disabled);
            	}
            },
	        // - Update extra tabs list
	        _updateExtraTabsList: function () {
		        const iVisibleExtraTabsCount = this.element.find(this.js_selectors.extra_tab_toggler+':not(.'+this.css_classes.is_hidden+')').length;
		        const oExtraTabsContainerElem = this.element.find(this.js_selectors.extra_tabs_container);

		        if (iVisibleExtraTabsCount > 0) {
			        oExtraTabsContainerElem.removeClass(this.css_classes.is_hidden);
		        } else {
			        oExtraTabsContainerElem.addClass(this.css_classes.is_hidden);
		        }
	        },
	        // - Get tab's "data-tab-id" from tab's index
	        /**
	         * @param iIdx {number} Index (starting from 0) of the tab in the container
	         * @return {string} The [data-tab-id] of the iIdx-th tab (zero based). Can return undefined if it has not [data-tab-id] attribute
	         * @private
	         */
	        _getTabIdFromTabIndex: function(iIdx) {
		        return this.element.children(this.js_selectors.tabs_list).children(this.js_selectors.tab_header).eq(iIdx).attr('data-tab-id');
	        },
	        /**
	         * @param sId {string} The [data-tab-id] of the tab
	         * @return {number} The index (zero based) of the tab. If no matching tab, 0 will be returned.
	         * @private
	         */
	        _getTabIndexFromTabId: function(sId) {
		        const oTabElem = this.element.children(this.js_selectors.tabs_list).children(this.js_selectors.tab_header+'[data-tab-id="'+sId+'"]');

		        return oTabElem.length === 0 ? 0 : oTabElem.prevAll().length;
	        },
            /**
             * @param sId {string} The [data-tab-id] of the tab
             * @return {Object} The jQuery object representing the tab element
             *
             * @private
             */
            _getTabElementFromTabId: function(sId) {
                return this.element.children(this.js_selectors.tabs_list).children(this.js_selectors.tab_header+'[data-tab-id="'+sId+'"]');
            },
            /**
             * @param sId {string} The [data-tab-id] of the tab
             * @return {Object} The jQuery object representing the tab element
             */
            disableTab: function(sId){
               const tabsWidget = this.GetTabsWidget();
               const iIdx = this._getTabIndexFromTabId(sId);
               tabsWidget.disable(iIdx);
               const tabElement = this._getTabElementFromTabId(sId);
               this._updateTabHeaderDisplay(tabElement); 
            },
            /**
             * @param sId {string} The [data-tab-id] of the tab
             * @return {Object} The jQuery object representing the tab element
             */
            enableTab: function(sId){
               const tabsWidget = this.GetTabsWidget();
               const iIdx = this._getTabIndexFromTabId(sId);
               tabsWidget.enable(iIdx);
               const tabElement = this._getTabElementFromTabId(sId);
               this._updateTabHeaderDisplay(tabElement);                
            }
        });
});
