// SPDX-FileCopyrightText: 2018 Icinga GmbH <https://icinga.com>
// SPDX-License-Identifier: GPL-3.0-or-later

/**
 * Icinga.UI
 *
 * Our user interface
 */
(function(Icinga, $) {

    'use strict';

    Icinga.UI = function (icinga) {

        this.icinga = icinga;

        this.currentLayout = 'default';

        this.debug = false;

        this.debugTimer = null;

        this.timeCounterTimer = null;

        // detect currentLayout
        var classList = $('#layout').attr('class').split(/\s+/);
        var _this = this;
        var matched;
        $.each(classList, function(index, item) {
            if (null !== (matched = item.match(/^([a-z]+)-layout$/))) {
                var layout = matched[1];
                if (layout !== 'fullscreen') {
                    _this.currentLayout = layout;
                    // Break loop
                    return false;
                }
            }
        });
    };

    Icinga.UI.prototype = {

        initialize: function () {
            $('html').removeClass('no-js').addClass('js');
            this.enableTimeCounters();
            this.triggerWindowResize();
            this.fadeNotificationsAway();

            $(document).on('click', '#mobile-menu-toggle', this.toggleMobileMenu);
            $(document).on('keypress', '#search',{ self: this, type: 'key' }, this.closeMobileMenu);
            $(document).on('mouseleave', '#sidebar', { self: this, type: 'leave' }, this.closeMobileMenu);
            $(document).on('click', '#sidebar a', { self: this, type: 'navigate' }, this.closeMobileMenu);
        },

        fadeNotificationsAway: function() {
            var icinga = this.icinga;
            $('#notifications li')
                .not('.fading-out')
                .not('.persist')
                .addClass('fading-out')
                .delay(7000)
                .fadeOut('slow',
            function() {
                $(this).remove();
            });
        },

        toggleDebug: function() {
            if (this.debug) {
                return this.disableDebug();
            } else {
                return this.enableDebug();
            }
        },

        enableDebug: function () {
            if (this.debug === true) { return this; }
            this.debug = true;
            this.debugTimer = this.icinga.timer.register(
                this.refreshDebug,
                this,
                1000
            );
            this.fixDebugVisibility();

            return this;
        },

        fixDebugVisibility: function () {
            if (this.debug) {
                $('#responsive-debug').css({display: 'block'});
            } else {
                $('#responsive-debug').css({display: 'none'});
            }
            return this;
        },

        disableDebug: function () {
            if (this.debug === false) { return; }

            this.debug = false;
            this.icinga.timer.unregister(this.debugTimer);
            this.debugTimer = null;
            this.fixDebugVisibility();
            return this;
        },

        reloadCss: function () {
            var icinga = this.icinga;
            icinga.logger.info('Reloading CSS');
            $('link').each(function() {
                var $oldLink = $(this);
                if ($oldLink.hasAttr('type') && $oldLink.attr('type').indexOf('css') > -1) {
                    var $newLink = $oldLink.clone().attr(
                        'href',
                        icinga.utils.addUrlParams(
                            $oldLink.attr('href'),
                            { id: new Date().getTime() } // Only required for Firefox to reload CSS automatically
                        )
                    ).on('load', function() {
                        $oldLink.remove();
                        $('head').trigger('css-reloaded');
                    });

                    $newLink.appendTo($('head'));
                }
            });
        },

        enableTimeCounters: function () {
            this.timeCounterTimer = this.icinga.timer.register(
                this.refreshTimeSince,
                this,
                1000
            );
            return this;
        },

        disableTimeCounters: function () {
            this.icinga.timer.unregister(this.timeCounterTimer);
            this.timeCounterTimer = null;
            return this;
        },

        /**
         * Focus the given element and scroll to its position
         *
         * @param   {string}    element         The name or id of the element to focus
         * @param   {object}    [$container]    The container containing the element
         * @param   {boolean}   [scroll]        Whether the viewport should be scrolled to the focused element
         */
        focusElement: function(element, $container, scroll) {
            var $element = element;

            if (typeof scroll === 'undefined') {
                scroll = true;
            }

            if (typeof element === 'string') {
                if ($container && $container.length) {
                    $element = $container.find('#' + element);
                } else {
                    $element = $('#' + element);
                }

                if (! $element.length) {
                    // The name attribute is actually deprecated, on anchor tags,
                    // but we'll possibly handle links from another source
                    // (module etc) so that's used as a fallback
                    if ($container && $container.length) {
                        $element = $container.find('[name="' + element.replace(/'/, '\\\'') + '"]');
                    } else {
                        $element = $('[name="' + element.replace(/'/, '\\\'') + '"]');
                    }
                }
            }

            if ($element.length) {
                if (! this.isFocusable($element)) {
                    $element.attr('tabindex', -1);
                }

                $element[0].focus();

                if (scroll && $container && $container.length) {
                    if (! $container.is('.container')) {
                        $container = $container.closest('.container');
                    }

                    if ($container.css('display') === 'flex' && $container.is('.container')) {
                        var $controls = $container.find('.controls');
                        var $content = $container.find('.content');
                        $content.scrollTop($element.offsetTopRelativeTo($content) - $controls.outerHeight() - (
                            $element.outerHeight(true) - $element.innerHeight()
                        ));
                    } else {
                        $container.scrollTop($element.first().position().top);
                    }
                }
            }
        },

        isFocusable: function ($element) {
            return $element.is('*[tabindex], a[href], input:not([disabled]), button:not([disabled])' +
                ', select:not([disabled]), textarea:not([disabled]), iframe, area[href], object' +
                ', embed, *[contenteditable]');
        },

        moveToLeft: function () {
            var col2 = this.cutContainer($('#col2'));
            var kill = this.cutContainer($('#col1'));
            this.pasteContainer($('#col1'), col2);
            this.icinga.behaviors.navigation.trySetActiveAndSelectedByUrl($('#col1').data('icingaUrl'));
            $('#col1').trigger('column-moved', 'col2');
        },

        moveToRight: function () {
            let col1 = document.getElementById('col1'),
                col2 = document.getElementById('col2'),
                col1Backup = this.cutContainer($(col1));

            this.cutContainer($(col2)); // Clear col2 states
            this.pasteContainer($(col2), col1Backup);
            this.layout2col();
            $(col2).trigger('column-moved', 'col1');
        },

        cutContainer: function ($col) {
            var props = {
              'elements': $('#' + $col.attr('id') + ' > *').detach(),
              'data': {
                'data-icinga-url': $col.data('icingaUrl'),
                'data-icinga-title': $col.data('icingaTitle'),
                'data-icinga-refresh': $col.data('icingaRefresh'),
                'data-last-update': $col.data('lastUpdate'),
                'data-icinga-module': $col.data('icingaModule'),
                'data-icinga-container-id': $col[0].dataset.icingaContainerId
              },
              'class': $col.attr('class')
            };
            this.icinga.loader.stopPendingRequestsFor($col);
            $col.removeData('icingaUrl');
            $col.removeData('icingaTitle');
            $col.removeData('icingaRefresh');
            $col.removeData('lastUpdate');
            $col.removeData('icingaModule');
            delete $col[0].dataset.icingaContainerId;
            $col.removeAttr('class').attr('class', 'container');
            return props;
        },

        pasteContainer: function ($col, backup) {
            backup['elements'].appendTo($col);
            $col.attr('class', backup['class']); // TODO: ie memleak? remove first?
            $col.data('icingaUrl', backup['data']['data-icinga-url']);
            $col.data('icingaTitle', backup['data']['data-icinga-title']);
            $col.data('icingaRefresh', backup['data']['data-icinga-refresh']);
            $col.data('lastUpdate', backup['data']['data-last-update']);
            $col.data('icingaModule', backup['data']['data-icinga-module']);
            $col[0].dataset.icingaContainerId = backup['data']['data-icinga-container-id'];
        },

        triggerWindowResize: function () {
            this.onWindowResize({data: {self: this}});
        },

        /**
         * Our window got resized, let's fix our UI
         */
        onWindowResize: function (event) {
            var _this = event.data.self;

            if (_this.layoutHasBeenChanged()) {
                _this.icinga.logger.info(
                    'Layout change detected, switching to',
                    _this.currentLayout
                );
            }

            _this.refreshDebug();
        },

        /**
         * Returns whether the layout is too small for more than one column
         *
         * @returns {boolean}   True when more than one column is available
         */
        hasOnlyOneColumn: function () {
            return this.currentLayout === 'poor' || this.currentLayout === 'minimal';
        },

        layoutHasBeenChanged: function () {

            var layout = $('html').css('fontFamily').replace(/['",]/g, '');
            var matched;

            if (null !== (matched = layout.match(/^([a-z]+)-layout$/))) {
                if (matched[1] === this.currentLayout &&
                    $('#layout').hasClass(layout)
                ) {
                    return false;
                } else {
                    $('#layout').removeClass(this.currentLayout + '-layout').addClass(layout);
                    this.currentLayout = matched[1];
                    if (this.currentLayout === 'poor' || this.currentLayout === 'minimal') {
                        this.layout1col();
                        this.icinga.history.replaceCurrentState();
                    } else if (this.icinga.initialized) {
                        // layout1col() also triggers this, that's why an else is required
                        $('#layout').trigger('layout-change');
                    }
                    return true;
                }
            }
            this.icinga.logger.error(
                'Someone messed up our responsiveness hacks, html font-family is',
                layout
            );
            return false;
        },

        /**
         * Returns whether only one column is displayed
         *
         * @returns {boolean}   True when only one column is displayed
         */
        isOneColLayout: function () {
            return ! $('#layout').hasClass('twocols');
        },

        layout1col: function () {
            if (this.isOneColLayout()) { return; }
            this.icinga.logger.debug('Switching to single col');
            $('#layout').removeClass('twocols');
            this.closeContainer($('#col2'));

            if (this.icinga.initialized) {
                $('#layout').trigger('layout-change');
            }

            // one-column layouts never have any selection active
            $('#col1').removeData('icinga-actiontable-former-href');
            this.icinga.behaviors.actiontable.clearAll();
        },

        closeContainer: function($c) {
            this.icinga.loader.stopPendingRequestsFor($c);
            $c.removeData('icingaUrl');
            $c.removeData('icingaTitle');
            $c.removeData('icingaRefresh');
            $c.removeData('lastUpdate');
            $c.removeData('icingaModule');
            delete $c[0].dataset.icingaContainerId;
            $c.removeAttr('class').attr('class', 'container');
            $c.trigger('close-column');
            $c.html('');
        },

        layout2col: function () {
            if (! this.isOneColLayout()) { return; }
            this.icinga.logger.debug('Switching to double col');
            $('#layout').addClass('twocols');

            if (this.icinga.initialized) {
                $('#layout').trigger('layout-change');
            }
        },

        prepareColumnFor: function ($el, $target) {
            var explicitTarget;

            if ($target.attr('id') === 'col2') {
                if ($el.closest('#col2').length) {
                    explicitTarget = $el.closest('[data-base-target]').data('baseTarget');
                    if (typeof explicitTarget !== 'undefined' && explicitTarget === '_next') {
                        this.moveToLeft();
                    }
                } else {
                    this.layout2col();
                }
            } else { // if ($target.attr('id') === 'col1')
                explicitTarget = $el.closest('[data-base-target]').data('baseTarget');
                if (typeof explicitTarget !== 'undefined' && explicitTarget === '_main') {
                    this.layout1col();
                }
            }
        },

        getAvailableColumnSpace: function () {
            return $('#main').width() / this.getDefaultFontSize();
        },

        setColumnCount: function (count) {
            if (count === 3) {
                $('#main > .container').css({
                    width: '33.33333%'
                });
            } else if (count === 2) {
                $('#main > .container').css({
                    width: '50%'
                });
            } else {
                $('#main > .container').css({
                    width: '100%'
                });
            }
        },

        setTitle: function (title) {
            document.title = title;
            return this;
        },

        getColumnCount: function () {
            return $('#main > .container').length;
        },

        /**
         * Assign a unique ID to each .container without such
         *
         * This usually applies to dashlets
         */
        assignUniqueContainerIds: function() {
            var currentMax = 0;
            $('.container').each(function() {
                var $el = $(this);
                var m;
                if (!$el.attr('id')) {
                    return;
                }
                if (m = $el.attr('id').match(/^ciu_(\d+)$/)) {
                    if (parseInt(m[1]) > currentMax) {
                         currentMax = parseInt(m[1]);
                    }
                }
            });
            $('.container').each(function() {
                var $el = $(this);
                if (!!$el.attr('id')) {
                    return;
                }
                currentMax++;
                $el.attr('id', 'ciu_' + currentMax);
            });
        },

        refreshDebug: function () {
            if (! this.debug) {
                return;
            }

            var size = this.getDefaultFontSize().toString();
            var winWidth = $( window ).width();
            var winHeight = $( window ).height();
            var loading = '';

            $.each(this.icinga.loader.requests, function (el, req) {
                if (loading === '') {
                    loading = '<br />Loading:<br />';
                }
                loading += el + ' => ' + encodeURI(req.url);
            });

            $('#responsive-debug').html(
                '   Time: ' +
                this.icinga.utils.formatHHiiss(new Date()) +
                '<br />    1em: ' +
                size +
                'px<br />    Win: ' +
                winWidth +
                'x'+
                winHeight +
                'px<br />' +
                ' Layout: ' +
                this.currentLayout +
                loading
            );
        },

        /**
         * Refresh partial time counters
         *
         * This function runs every second.
         */
        refreshTimeSince: function () {
            $('.time-ago, .time-since').each(function (idx, el) {
                var partialTime = /(\d{1,2})m (\d{1,2})s/.exec(el.innerHTML);
                if (partialTime !== null) {
                    var minute = parseInt(partialTime[1], 10),
                        second = parseInt(partialTime[2], 10);
                    if (second < 59) {
                        ++second;
                    } else {
                        ++minute;
                        second = 0;
                    }
                    el.innerHTML = el.innerHTML.substring(0, partialTime.index) + minute.toString() + 'm '
                        + second.toString() + 's' + el.innerHTML.substring(partialTime.index + partialTime[0].length);
                }
            });

            $('.time-until').each(function (idx, el) {
                var partialTime = /(-?)(\d{1,2})m (\d{1,2})s/.exec(el.innerHTML);
                if (partialTime !== null) {
                    var minute = parseInt(partialTime[2], 10),
                        second = parseInt(partialTime[3], 10),
                        invert = partialTime[1];
                    if (invert.length) {
                        // Count up because partial time is negative
                        if (second < 59) {
                            ++second;
                        } else {
                            ++minute;
                            second = 0;
                        }
                    } else {
                        // Count down because partial time is positive
                        if (second === 0) {
                            if (minute === 0) {
                                // Invert counter
                                minute = 0;
                                second = 1;
                                invert = '-';
                            } else {
                                --minute;
                                second = 59;
                            }
                        } else {
                            --second;
                        }

                        if (minute === 0 && second === 0 && el.dataset.agoLabel) {
                            el.innerText = el.dataset.agoLabel;
                            el.classList.remove('time-until');
                            el.classList.add('time-ago');

                            return;
                        }
                    }
                    el.innerHTML = el.innerHTML.substring(0, partialTime.index) + invert + minute.toString() + 'm '
                        + second.toString() + 's' + el.innerHTML.substring(partialTime.index + partialTime[0].length);
                }
            });
        },

        createFontSizeCalculator: function () {
            var $el = $('<div id="fontsize-calc">&nbsp;</div>');
            $('#layout').append($el);
            return $el;
        },

        getDefaultFontSize: function () {
            var $calc = $('#fontsize-calc');
            if (! $calc.length) {
                $calc = this.createFontSizeCalculator();
            }
            return $calc.width() / 1000;
        },

        /**
         * Toggle mobile menu
         *
         * @param {object} e Event
         */
        toggleMobileMenu: function(e) {
            $('#sidebar').toggleClass('expanded');
        },

        /**
         * Close mobile menu when the enter key is pressed during search or the user leaves the sidebar
         *
         * @param {object} e Event
         */
        closeMobileMenu: function(e) {
            if (e.data.self.currentLayout !== 'minimal') {
                return;
            }

            if (e.data.type === 'key') {
                if (e.which === 13) {
                    $('#sidebar').removeClass('expanded');
                    $(e.target)[0].blur();
                }
            } else {
                $('#sidebar').removeClass('expanded');
            }
        },

        toggleFullscreen: function () {
            $('#layout').toggleClass('fullscreen-layout');
        },

        getUniqueContainerId: function (container) {
            if (typeof container.jquery !== 'undefined') {
                if (! container.length) {
                    return null;
                }

                container = container[0];
            } else if (typeof container === 'undefined') {
                return null;
            }

            var containerId = container.dataset.icingaContainerId || null;
            if (containerId === null) {
                /**
                 * Only generate an id if it's not for col1 or the menu (which are using the non-suffixed window id).
                 * This is based on the assumption that the server only knows about the menu and first column
                 * and therefore does not need to protect its ids. (As the menu is most likely part of the sidebar)
                 */
                var col1 = document.getElementById('col1');
                if (container.id !== 'menu' && col1 !== null && ! col1.contains(container)) {
                    containerId = this.icinga.utils.generateId(6); // Random because the content may move
                    container.dataset.icingaContainerId = containerId;
                }
            }

            return containerId;
        },

        getWindowId: function () {
            if (! this.hasWindowId()) {
                return undefined;
            }
            return window.name.match(/^Icinga-([a-zA-Z0-9]+)$/)[1];
        },

        hasWindowId: function () {
            var res = window.name.match(/^Icinga-([a-zA-Z0-9]+)$/);
            return typeof res === 'object' && null !== res;
        },

        setWindowId: function (id) {
            this.icinga.logger.debug('Setting new window id', id);
            window.name = 'Icinga-' + id;
        },

        destroy: function () {
            // This is gonna be hard, clean up the mess
            this.icinga = null;
            this.debugTimer = null;
            this.timeCounterTimer = null;
        }
    };

}(Icinga, jQuery));
