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

/**
 * Icinga utility functions
 */
(function(Icinga, $) {

    'use strict';

    Icinga.Utils = function (icinga) {

        /**
         * Utility functions may need access to their Icinga instance
         */
        this.icinga = icinga;

        /**
         * We will use this to create an URL helper only once
         */
        this.urlHelper = null;
    };

    Icinga.Utils.prototype = {

        timeWithMs: function (now) {

            if (typeof now === 'undefined') {
                now = new Date();
            }

            var ms = now.getMilliseconds() + '';
            while (ms.length < 3) {
                ms = '0' + ms;
            }

            return now.toLocaleTimeString() + '.' + ms;
        },

        timeShort: function (now) {

            if (typeof now === 'undefined') {
                now = new Date();
            }

            return now.toLocaleTimeString().replace(/:\d{2}$/, '');
        },

        formatHHiiss: function (date) {
            var hours = date.getHours();
            var minutes = date.getMinutes();
            var seconds = date.getSeconds();
            if (hours < 10) hours = '0' + hours;
            if (minutes < 10) minutes = '0' + minutes;
            if (seconds < 10) seconds = '0' + seconds;
            return hours + ':' + minutes + ':' + seconds;
        },

        /**
         * Format the given byte-value into a human-readable string
         *
         * @param   {number}    The amount of bytes to format
         * @returns {string}    The formatted string
         */
        formatBytes: function (bytes) {
            var log2  = Math.log(bytes) / Math.LN2;
            var pot   = Math.floor(log2 / 10);
            var unit  = (['b', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB'])[pot];
            return ((bytes / Math.pow(1024, pot)).toFixed(2)) + ' ' + unit;
        },

        /**
        * Return whether the given element is visible in the users view
        *
        * Borrowed from: http://stackoverflow.com/q/487073
        *
        * @param   {selector}   element     The element to check
        * @returns {Boolean}
        */
        isVisible: function(element) {
            var $element = $(element);
            if (!$element.length) {
                return false;
            }

            var docViewTop = $(window).scrollTop();
            var docViewBottom = docViewTop + $(window).height();
            var elemTop = $element.offset().top;
            var elemBottom = elemTop + $element.height();

            return ((elemBottom >= docViewTop) && (elemTop <= docViewBottom) &&
                 (elemBottom <= docViewBottom) && (elemTop >= docViewTop));
        },

        getUrlHelper: function () {
            if (this.urlHelper === null) {
                this.urlHelper = document.createElement('a');
            }

            return this.urlHelper;
        },

        /**
         * Parse a given Url and return an object
         */
        parseUrl: function (url) {

            var a = this.getUrlHelper();
            a.href = url;

            var result = {
                source  : url,
                protocol: a.protocol.replace(':', ''),
                host    : a.hostname,
                port    : a.port,
                query   : a.search,
                file    : (a.pathname.match(/\/([^\/?#]+)$/i) || [,''])[1],
                hash    : a.hash.replace('#',''),
                path    : a.pathname.replace(/^([^\/])/,'/$1'),
                relative: (a.href.match(/tps?:\/\/[^\/]+(.+)/) || [,''])[1],
                segments: a.pathname.replace(/^\//,'').split('/'),
                params  : this.parseParams(a)
            };
            a = null;

            return result;
        },

        // Local URLs only
        addUrlParams: function (url, params) {
            var parts = this.parseUrl(url),
                result = parts.path,
                newparams = parts.params;

            // We overwrite existing params
            $.each(params, function (key, value) {
                key = encodeURIComponent(key);
                value = typeof value !== 'string' || !! value ? encodeURIComponent(value) : null;

                var found = false;
                for (var i = 0; i < newparams.length; i++) {
                    if (newparams[i].key === key) {
                        newparams[i].value = value;
                        found = true;
                        break;
                    }
                }

                if (! found) {
                    newparams.push({ key: key, value: value });
                }
            });

            if (newparams.length) {
                result += '?' + this.buildQuery(newparams);
            }

            if (parts.hash.length) {
                result += '#' + parts.hash;
            }

            return result;
        },

        // Local URLs only
        removeUrlParams: function (url, params) {
            var parts = this.parseUrl(url),
                result = parts.path,
                newparams = parts.params;

            $.each(params, function (_, key) {
                key = encodeURIComponent(key);

                for (var i = 0; i < newparams.length; i++) {
                    if (newparams[i].key === key) {
                        newparams.splice(i, 1);
                        return;
                    }
                }
            });

            if (newparams.length) {
                result += '?' + this.buildQuery(newparams);
            }

            if (parts.hash.length) {
                result += '#' + parts.hash;
            }

            return result;
        },

        /**
         * Return a query string for the given params
         *
         * @param {Array} params
         * @return {string}
         */
        buildQuery: function (params) {
            var query = '';

            for (var i = 0; i < params.length; i++) {
                if (!! query) {
                    query += '&';
                }

                query += params[i].key;
                switch (params[i].value) {
                    case true:
                        break;
                    case false:
                        query += '=0';
                        break;
                    case null:
                        query += '=';
                        break;
                    default:
                        query += '=' + params[i].value;
                }
            }

            return query;
        },

        /**
         * Parse url params
         */
        parseParams: function (a) {
            var params = [],
                segment = a.search.replace(/^\?/,'').split('&'),
                len = segment.length,
                i = 0,
                key,
                value,
                equalPos;

            for (; i < len; i++) {
                if (! segment[i]) {
                    continue;
                }

                equalPos = segment[i].indexOf('=');
                if (equalPos !== -1) {
                    key = segment[i].slice(0, equalPos);
                    value = segment[i].slice(equalPos + 1);
                } else {
                    key = segment[i];
                    value = true;
                }

                params.push({ key: key, value: value });
            }

            return params;
        },

        /**
         * Add the specified flag to the given URL
         *
         * @param {string} url
         * @param {string} flag
         *
         * @returns {string}
         */
        addUrlFlag: function (url, flag) {
            var pos = url.search(/#(?!!)/);

            if (url.indexOf('?') !== -1) {
                flag = '&' + flag;
            } else {
                flag = '?' + flag;
            }

            if (pos === -1) {
                return url + flag;
            }

            return url.slice(0, pos) + flag + url.slice(pos);
        },

        /**
         * Check whether two HTMLElements overlap
         *
         * @param a {HTMLElement}
         * @param b {HTMLElement}
         *
         * @returns {Boolean}      whether elements overlap, will return false when one
         *                         element is not in the DOM
         */
        elementsOverlap: function(a, b)
        {
            // a bounds
            var aoff = $(a).offset();
            if (!aoff) {
                return false;
            }
            var at = aoff.top;
            var ah = a.offsetHeight || (a.getBBox && a.getBBox().height);
            var al = aoff.left;
            var aw = a.offsetWidth || (a.getBBox && a.getBBox().width);

            // b bounds
            var boff = $(b).offset();
            if (!boff) {
                return false;
            }
            var bt = boff.top;
            var bh = b.offsetHeight || (b.getBBox && b.getBBox().height);
            var bl = boff.left;
            var bw = b.offsetWidth || (b.getBBox && b.getBBox().width);

            return !(at > (bt + bh) || bt > (at + ah)) && !(bl  > (al + aw) || al > (bl + bw));
        },

        /**
         * Create a selector that can be used to fetch the element the same position in the DOM-Tree
         *
         * Create the path to the given element in the DOM-Tree, comparable to an X-Path. Climb the
         * DOM tree upwards until an element with an unique ID is found, this id is used as the anchor,
         * all other elements will be addressed by their position in the parent.
         *
         * @param   {HTMLElement} el    The element to extract the path for.
         *
         * @returns {Array}             The path of the element, that can be passed to getElementByPath
         */
        getDomPath: function (el) {
            if (! el) {
                return [];
            }
            if (el.id !== '') {
                return ['#' + el.id];
            }
            if (el === document.body) {
                return ['body'];
            }

            var siblings = el.parentNode.childNodes;
            var index = 0;
            for (var i = 0; i < siblings.length; i ++) {
                if (siblings[i].nodeType === 1) {
                    index ++;
                }

                if (siblings[i] === el) {
                    var p = this.getDomPath(el.parentNode);
                    p.push(':nth-child(' + (index) + ')');
                    return p;
                }
            }
        },

        /**
         * Get the CSS selector to the given node
         *
         * @param {HTMLElement} element
         *
         * @returns {string}
         */
        getCSSPath: function(element) {
            if (typeof element === 'undefined') {
                throw 'Requires a element';
            }

            if (typeof element.jquery !== 'undefined') {
                if (! element.length) {
                    throw 'Requires a element';
                }

                element = element[0];
            }

            var path = [];

            while (true) {
                let id = element.id;
                if (typeof id !== 'undefined' && typeof id !== 'string') {
                    // Sometimes there may be a form element with the name "id"
                    id = element.getAttribute("id");
                }

                if (!! id) {
                    // Only use ids if they're truly unique
                    let results = document.querySelectorAll('* #' + this.escapeCSSSelector(id));
                    if (results.length === 1) {
                        path.push('#' + id);
                        break;
                    }
                }

                var tagName = element.tagName;
                var parent = element.parentElement;

                if (! parent) {
                    path.push(tagName.toLowerCase());
                    break;
                }

                if (parent.children.length) {
                    var index = 0;
                    do {
                        if (element.tagName === tagName) {
                            index++;
                        }
                    } while ((element = element.previousElementSibling));

                    path.push(tagName.toLowerCase() + ':nth-of-type(' + index + ')');
                } else {
                    path.push(tagName.toLowerCase());
                }

                element = parent;
            }

            return path.reverse().join(' > ');
        },

        /**
         * Escape the given string to be used in a CSS selector
         *
         * @param {string} selector
         * @returns {string}
         */
        escapeCSSSelector: function (selector) {
            if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {
                return CSS.escape(selector);
            }

            return selector.replaceAll(/^(\d)/, '\\\\3$1 ');
        },

        /**
         * Climbs up the given dom path and returns the element
         *
         * This is the counterpart
         *
         * @param   path    {Array}         The selector
         * @returns         {HTMLElement}   The corresponding element
         */
        getElementByDomPath: function (path) {
            var $element;
            $.each(path, function (i, selector) {
                if (! $element) {
                    $element = $(selector);
                } else {
                    $element = $element.children(selector).first();
                    if (! $element[0]) {
                        return false;
                    }
                }
            });
            return $element[0];
        },

        objectKeys: Object.keys || function (obj) {
            var keys = [];
            $.each(obj, function (key) {
                keys.push(key);
            });
            return keys;
        },

        objectsEqual: function equals(obj1, obj2) {
            var obj1Keys = Object.keys(obj1);
            var obj2Keys = Object.keys(obj2);
            if (obj1Keys.length !== obj2Keys.length) {
                return false;
            }

            return obj1Keys.concat(obj2Keys)
                .every(function (key) {
                    return obj1[key] === obj2[key];
                });
        },

        arraysEqual: function (array1, array2) {
            if (array1.length !== array2.length) {
                return false;
            }

            var value1, value2;
            for (var i = 0; i < array1.length; i++) {
                value1 = array1[i];
                value2 = array2[i];

                if (typeof value1 === 'object') {
                    if (typeof value2 !== 'object' || ! this.objectsEqual(value1, value2)) {
                        return false;
                    }
                } else if (value1 !== value2) {
                    return false;
                }
            }

            return true;
        },

        /**
         * Cleanup
         */
        destroy: function () {
            this.urlHelper = null;
            this.icinga = null;
        },

        /**
         * Encode the parenthesis too
         *
         * @param str {String} A component of a URI
         *
         * @returns {String} Encoded component
         */
        fixedEncodeURIComponent: function (str) {
            return encodeURIComponent(str).replace(/[()]/g, function(c) {
                return '%' + c.charCodeAt(0).toString(16);
            });
        },

        escape: function (str) {
            return String(str).replace(
                /[&<>"']/gm,
                function (c) {
                    return {
                        '&': '&amp;',
                        '<': '&lt;',
                        '>': '&gt;',
                        '"': '&quot;',
                        "'": '&#039;'
                    }[c];
                }
            );
        },

        /**
         * Pad a string with another one
         *
         * @param   {String}    str         the string to pad
         * @param   {String}    padding     the string to use for padding
         * @param   {Number}    minLength   the minimum length of the result
         *
         * @returns {String}    the padded string
         */
        padString: function(str, padding, minLength) {
            str = String(str);
            padding = String(padding);
            while (str.length < minLength) {
                str = padding + str;
            }
            return str;
        },

        /**
         * Shuffle a string
         *
         * @param   {String}    str     The string to shuffle
         *
         * @returns {String}    The shuffled string
         */
        shuffleString: function(str) {
            var a = str.split(""),
                n = a.length;

            for(var i = n - 1; i > 0; i--) {
                var j = Math.floor(Math.random() * (i + 1));
                var tmp = a[i];
                a[i] = a[j];
                a[j] = tmp;
            }
            return a.join("");
        },

        /**
         * Generate an id
         *
         * @param   {Number}    len     The desired length of the id
         *
         * @returns {String}    The id
         */
        generateId: function(len) {
            return this.shuffleString('abcefghijklmnopqrstuvwxyz').substr(0, len);
        }
    };

}(Icinga, jQuery));
