if (typeof(PhpDebugBar) == 'undefined') {
    // namespace
    var PhpDebugBar = {};
    PhpDebugBar.$ = jQuery;
}

(function($) {

    if (typeof(localStorage) == 'undefined') {
        // provide mock localStorage object for dumb browsers
        localStorage = {
            setItem: function(key, value) {},
            getItem: function(key) { return null; }
        };
    }

    if (typeof(PhpDebugBar.utils) == 'undefined') {
        PhpDebugBar.utils = {};
    }

    /**
     * Returns the value from an object property.
     * Using dots in the key, it is possible to retrieve nested property values
     *
     * @param {Object} dict
     * @param {String} key
     * @param {Object} default_value
     * @return {Object}
     */
    var getDictValue = PhpDebugBar.utils.getDictValue = function(dict, key, default_value) {
        var d = dict, parts = key.split('.');
        for (var i = 0; i < parts.length; i++) {
            if (!d[parts[i]]) {
                return default_value;
            }
            d = d[parts[i]];
        }
        return d;
    }

    /**
     * Counts the number of properties in an object
     *
     * @param {Object} obj
     * @return {Integer}
     */
    var getObjectSize = PhpDebugBar.utils.getObjectSize = function(obj) {
        if (Object.keys) {
            return Object.keys(obj).length;
        }
        var count = 0;
        for (var k in obj) {
            if (obj.hasOwnProperty(k)) {
                count++;
            }
        }
        return count;
    }

    /**
     * Returns a prefixed css class name
     *
     * @param {String} cls
     * @return {String}
     */
    PhpDebugBar.utils.csscls = function(cls, prefix) {
        if (cls.indexOf(' ') > -1) {
            var clss = cls.split(' '), out = [];
            for (var i = 0, c = clss.length; i < c; i++) {
                out.push(PhpDebugBar.utils.csscls(clss[i], prefix));
            }
            return out.join(' ');
        }
        if (cls.indexOf('.') === 0) {
            return '.' + prefix + cls.substr(1);
        }
        return prefix + cls;
    };

    /**
     * Creates a partial function of csscls where the second
     * argument is already defined
     *
     * @param  {string} prefix
     * @return {Function}
     */
    PhpDebugBar.utils.makecsscls = function(prefix) {
        var f = function(cls) {
            return PhpDebugBar.utils.csscls(cls, prefix);
        };
        return f;
    }

    var csscls = PhpDebugBar.utils.makecsscls('phpdebugbar-');


    // ------------------------------------------------------------------

    /**
     * Base class for all elements with a visual component
     *
     * @param {Object} options
     * @constructor
     */
    var Widget = PhpDebugBar.Widget = function(options) {
        this._attributes = $.extend({}, this.defaults);
        this._boundAttributes = {};
        this.$el = $('<' + this.tagName + ' />');
        if (this.className) {
            this.$el.addClass(this.className);
        }
        this.initialize.apply(this, [options || {}]);
        this.render.apply(this);
    };

    $.extend(Widget.prototype, {

        tagName: 'div',

        className: null,

        defaults: {},

        /**
         * Called after the constructor
         *
         * @param {Object} options
         */
        initialize: function(options) {
            this.set(options);
        },

        /**
         * Called after the constructor to render the element
         */
        render: function() {},

        /**
         * Sets the value of an attribute
         *
         * @param {String} attr Can also be an object to set multiple attributes at once
         * @param {Object} value
         */
        set: function(attr, value) {
            if (typeof(attr) != 'string') {
                for (var k in attr) {
                    this.set(k, attr[k]);
                }
                return;
            }

            this._attributes[attr] = value;
            if (typeof(this._boundAttributes[attr]) !== 'undefined') {
                for (var i = 0, c = this._boundAttributes[attr].length; i < c; i++) {
                    this._boundAttributes[attr][i].apply(this, [value]);
                }
            }
        },

        /**
         * Checks if an attribute exists and is not null
         *
         * @param {String} attr
         * @return {[type]} [description]
         */
        has: function(attr) {
            return typeof(this._attributes[attr]) !== 'undefined' && this._attributes[attr] !== null;
        },

        /**
         * Returns the value of an attribute
         *
         * @param {String} attr
         * @return {Object}
         */
        get: function(attr) {
            return this._attributes[attr];
        },

        /**
         * Registers a callback function that will be called whenever the value of the attribute changes
         *
         * If cb is a jQuery element, text() will be used to fill the element
         *
         * @param {String} attr
         * @param {Function} cb
         */
        bindAttr: function(attr, cb) {
            if (Array.isArray(attr)) {
                for (var i = 0, c = attr.length; i < c; i++) {
                    this.bindAttr(attr[i], cb);
                }
                return;
            }

            if (typeof(this._boundAttributes[attr]) == 'undefined') {
                this._boundAttributes[attr] = [];
            }
            if (typeof(cb) == 'object') {
                var el = cb;
                cb = function(value) { el.text(value || ''); };
            }
            this._boundAttributes[attr].push(cb);
            if (this.has(attr)) {
                cb.apply(this, [this._attributes[attr]]);
            }
        }

    });


    /**
     * Creates a subclass
     *
     * Code from Backbone.js
     *
     * @param {Array} props Prototype properties
     * @return {Function}
     */
    Widget.extend = function(props) {
        var parent = this;

        var child = function() { return parent.apply(this, arguments); };
        $.extend(child, parent);

        var Surrogate = function() { this.constructor = child; };
        Surrogate.prototype = parent.prototype;
        child.prototype = new Surrogate;
        $.extend(child.prototype, props);

        child.__super__ = parent.prototype;

        return child;
    };

    // ------------------------------------------------------------------

    /**
     * Tab
     *
     * A tab is composed of a tab label which is always visible and
     * a tab panel which is visible only when the tab is active.
     *
     * The panel must contain a widget. A widget is an object which has
     * an element property containing something appendable to a jQuery object.
     *
     * Options:
     *  - title
     *  - badge
     *  - widget
     *  - data: forward data to widget data
     */
    var Tab = Widget.extend({

        className: csscls('panel'),

        render: function() {
            this.$tab = $('<a />').addClass(csscls('tab'));
            this.$icon = $('<i />').appendTo(this.$tab);
            this.bindAttr('icon', function(icon) {
                if (icon) {
                    this.$icon.attr('class', 'phpdebugbar-fa phpdebugbar-fa-' + icon);
                } else {
                    this.$icon.attr('class', '');
                }
            });

            this.bindAttr('title', $('<span />').addClass(csscls('text')).appendTo(this.$tab));

            this.$badge = $('<span />').addClass(csscls('badge')).appendTo(this.$tab);
            this.bindAttr('badge', function(value) {
                if (value !== null) {
                    this.$badge.text(value);
                    this.$badge.addClass(csscls('visible'));
                } else {
                    this.$badge.removeClass(csscls('visible'));
                }
            });

            this.bindAttr('widget', function(widget) {
                this.$el.empty().append(widget.$el);
            });

            this.bindAttr('data', function(data) {
                if (this.has('widget')) {
                    this.get('widget').set('data', data);
                    this.$tab.attr('data-empty', $.isEmptyObject(data) || data.count === 0);
                }
            })
        }

    });

    // ------------------------------------------------------------------

    /**
     * Indicator
     *
     * An indicator is a text and an icon to display single value information
     * right inside the always visible part of the debug bar
     *
     * Options:
     *  - icon
     *  - title
     *  - tooltip
     *  - data: alias of title
     */
    var Indicator = Widget.extend({

        tagName: 'span',

        className: csscls('indicator'),

        render: function() {
            this.$icon = $('<i />').appendTo(this.$el);
            this.bindAttr('icon', function(icon) {
                if (icon) {
                    this.$icon.attr('class', 'phpdebugbar-fa phpdebugbar-fa-' + icon);
                } else {
                    this.$icon.attr('class', '');
                }
            });

            this.bindAttr('link', function(link) {
                if (link) {
                    this.$el.on('click', () => {
                        this.get('debugbar').showTab(link);
                    }).css('cursor', 'pointer')
                } else {
                    this.$el.off('click', false).css('cursor', '')
                }
            });

            this.bindAttr(['title', 'data'], $('<span />').addClass(csscls('text')).appendTo(this.$el));

            this.$tooltip = $('<span />').addClass(csscls('tooltip disabled')).appendTo(this.$el);
            this.bindAttr('tooltip', function(tooltip) {
                if (tooltip) {
                    var dl = $('<dl />');
                    if (Array.isArray(tooltip) || typeof tooltip === 'object') {
                        $.each(tooltip, function(key, value) {
                            $('<dt />').text(key).appendTo(dl);
                            $('<dd />').text(value).appendTo(dl);
                        });
                        this.$tooltip.html(dl).removeClass(csscls('disabled'));
                    } else {
                        this.$tooltip.text(tooltip).removeClass(csscls('disabled'));
                    }
                } else {
                    this.$tooltip.addClass(csscls('disabled'));
                }
            });
        }

    });


    /**
     * Displays datasets in a table
     *
     */
    var Settings = Widget.extend({

        tagName: 'form',

        className: csscls('settings'),

        settings: {},

        initialize: function(options) {
            this.set(options);

            var debugbar = this.get('debugbar');
            this.settings = JSON.parse(localStorage.getItem('phpdebugbar-settings')) || {};

            $.each(debugbar.options, (key, value)=>  {
                if (key in this.settings) {
                    debugbar.options[key] = this.settings[key];
                }

                // Theme requires dark/light mode detection
                if (key === 'theme') {
                    debugbar.setTheme(debugbar.options[key]);
                } else {
                    debugbar.$el.attr('data-' + key, debugbar.options[key]);
                }
            })
        },

        clearSettings: function() {
            var debugbar = this.get('debugbar');

            // Remove item from storage
            localStorage.removeItem('phpdebugbar-settings');
            localStorage.removeItem('phpdebugbar-ajaxhandler-autoshow');
            this.settings = {};

            // Reset options
            debugbar.options = { ...debugbar.defaultOptions };

            // Reset ajax handler
            if (debugbar.ajaxHandler) {
                var autoshow = debugbar.ajaxHandler.defaultAutoShow;
                debugbar.ajaxHandler.setAutoShow(autoshow);
                this.set('autoshow', autoshow);
                if(debugbar.controls['__datasets']) {
                    debugbar.controls['__datasets'].get('widget').set('autoshow', $(this).is(':checked'));
                }
            }

            this.initialize(debugbar.options);
        },

        storeSetting: function(key, value) {
            this.settings[key] = value;

            var debugbar = this.get('debugbar');
            debugbar.options[key] = value;
            if (key !== 'theme') {
                debugbar.$el.attr('data-' + key, value);
            }

            localStorage.setItem('phpdebugbar-settings', JSON.stringify(this.settings));
        },

        render: function() {
            this.$el.empty();

            var debugbar = this.get('debugbar');
            var self = this;

            var fields = {};

            // Set Theme
            fields["Theme"] = $('<select>' +
                '<option value="auto">Auto (System preference)</option>' +
                '<option value="light">Light</option>' +
                '<option value="dark">Dark</option>' +
                '</select>')
                .val(debugbar.options.theme)
                .on('change', function() {
                    self.storeSetting('theme', $(this).val())
                    debugbar.setTheme($(this).val());
                });

            fields["Open Button Position"] = $('<select>' +
                '<option value="bottomLeft">Bottom Left</option>' +
                '<option value="bottomRight">Bottom Right</option>' +
                '<option value="topLeft">Top Left</option>' +
                '<option value="topRight">Top Right</option>' +
                '</select>')
                .val(debugbar.options.openBtnPosition)
                .on('change', function() {
                    self.storeSetting('openBtnPosition', $(this).val())
                });

            this.$hideEmptyTabs = $('<input type=checkbox>')
                .prop('checked', debugbar.options.hideEmptyTabs)
                .on('click', function() {
                    self.storeSetting('hideEmptyTabs', $(this).is(':checked'));
                });

            fields["Hide Empty Tabs"] = $('<label/>').append(this.$hideEmptyTabs, 'Hide empty tabs until they have data');

            this.$autoshow = $('<input type=checkbox>')
                .prop('checked', debugbar.ajaxHandler && debugbar.ajaxHandler.autoShow)
                .on('click', function() {
                    if (debugbar.ajaxHandler) {
                        debugbar.ajaxHandler.setAutoShow($(this).is(':checked'));
                    }
                    if (debugbar.controls['__datasets']) {
                        debugbar.controls['__datasets'].get('widget').set('autoshow', $(this).is(':checked'));
                    }
                });

            this.bindAttr('autoshow', function() {
                this.$autoshow.prop('checked', this.get('autoshow')).closest('.form-row').show();
            })
            fields["Autoshow"] = $('<label/>').append(this.$autoshow, 'Automatically show new incoming Ajax requests');

            fields["Reset to defaults"] = $('<button>Reset settings</button>').on('click', function(e) {
                e.preventDefault();
                self.clearSettings();
                self.render();
            })

            $.each(fields, function(key, value) {
                $('<div />').addClass(csscls('form-row')).append(
                    $('<div />').addClass(csscls('form-label')).text(key),
                    $('<div />').addClass(csscls('form-input')).html(value)
                ).appendTo(self.$el);

            })

            if (!this.ajaxHandler) {
                this.$autoshow.closest('.form-row').hide();
            }
        },

    });

    // ------------------------------------------------------------------

    /**
     * Dataset title formater
     *
     * Formats the title of a dataset for the select box
     */
    var DatasetTitleFormater = PhpDebugBar.DatasetTitleFormater = function(debugbar) {
        this.debugbar = debugbar;
    };

    $.extend(DatasetTitleFormater.prototype, {

        /**
         * Formats the title of a dataset
         *
         * @this {DatasetTitleFormater}
         * @param {String} id
         * @param {Object} data
         * @param {String} suffix
         * @return {String}
         */
        format: function(id, data, suffix, nb) {
            if (suffix) {
                suffix = ' ' + suffix;
            } else {
                suffix = '';
            }

            var nb = nb || getObjectSize(this.debugbar.datasets) ;

            if (typeof(data['__meta']) === 'undefined') {
                return "#" + nb + suffix;
            }

            var uri = data['__meta']['uri'].split('/'), filename = uri.pop();

            // URI ends in a trailing /, avoid returning an empty string
            if (!filename) {
                filename = (uri.pop() || '') + '/'; // add the trailing '/' back
            }

            // filename is a number, path could be like /action/{id}
            if (uri.length && !isNaN(filename)) {
                filename = uri.pop() + '/' + filename;
            }

            // truncate the filename in the label, if it's too long
            var maxLength = 150;
            if (filename.length > maxLength) {
                filename = filename.substr(0, maxLength) + '...';
            }

            var label = "#" + nb + " " + filename + suffix + ' (' + data['__meta']['datetime'].split(' ')[1] + ')';
            return label;
        }

    });

    // ------------------------------------------------------------------


    /**
     * DebugBar
     *
     * Creates a bar that appends itself to the body of your page
     * and sticks to the bottom.
     *
     * The bar can be customized by adding tabs and indicators.
     * A data map is used to fill those controls with data provided
     * from datasets.
     */
    var DebugBar = PhpDebugBar.DebugBar = Widget.extend({

        className: "phpdebugbar " + csscls('minimized'),

        options: {
            bodyMarginBottom: true,
            theme: 'auto',
            openBtnPosition: 'bottomLeft',
            hideEmptyTabs: false,
        },

        initialize: function(options = {}) {
            this.options = $.extend(this.options, options);
            this.defaultOptions = { ...this.options };
            this.controls = {};
            this.dataMap = {};
            this.datasets = {};
            this.firstTabName = null;
            this.activePanelName = null;
            this.activeDatasetId = null;
            this.datesetTitleFormater = new DatasetTitleFormater(this);
            this.bodyMarginBottomHeight = parseInt($('body').css('margin-bottom'));
            try {
                this.isIframe = window.self !== window.top && window.top.phpdebugbar;
            } catch (error) {
                this.isIframe = false;
            }
            this.registerResizeHandler();
            this.registerMediaListener();

            // Attach settings
            this.settings = new PhpDebugBar.DebugBar.Tab({"icon":"sliders", "title":"Settings", "widget": new Settings({
                    'debugbar': this,
                })});
        },

        /**
         * Register resize event, for resize debugbar with reponsive css.
         *
         * @this {DebugBar}
         */
        registerResizeHandler: function() {
            if (typeof this.resize.bind == 'undefined' || this.isIframe) return;

            var f = this.resize.bind(this);
            this.respCSSSize = 0;
            $(window).resize(f);
            setTimeout(f, 20);
        },

        registerMediaListener: function() {
            const mediaQueryList = window.matchMedia("(prefers-color-scheme: dark)");
            mediaQueryList.addEventListener('change', event => {
                if (this.options.theme === 'auto') {
                    this.setTheme('auto');
                }
            })
        },

        setTheme: function(theme) {
            this.options.theme = theme;

            if (theme === 'auto') {
                const mediaQueryList = window.matchMedia("(prefers-color-scheme: dark)");
                theme = mediaQueryList.matches ? 'dark' : 'light';
            }

            this.$el.attr('data-theme', theme)
            if (this.openHandler) {
                this.openHandler.$el.attr('data-theme', theme)
            }
        },

        /**
         * Resizes the debugbar to fit the current browser window
         */
        resize: function() {
            if (this.isIframe) return;

            var contentSize = this.respCSSSize;
            if (this.respCSSSize == 0) {
                this.$header.find("> *:visible").each(function () {
                    contentSize += $(this).outerWidth(true);
                });
            }

            var currentSize = this.$header.width();
            var cssClass = csscls("mini-design");
            var bool = this.$header.hasClass(cssClass);

            if (currentSize <= contentSize && !bool) {
                this.respCSSSize = contentSize;
                this.$header.addClass(cssClass);
            } else if (contentSize < currentSize && bool) {
                this.respCSSSize = 0;
                this.$header.removeClass(cssClass);
            }

            // Reset height to ensure bar is still visible
            this.setHeight(this.$body.height());
        },

        /**
         * Initialiazes the UI
         *
         * @this {DebugBar}
         */
        render: function() {
            if (this.isIframe) {
                this.$el.hide();
            }

            var self = this;
            this.$el.appendTo('body');
            this.$dragCapture = $('<div />').addClass(csscls('drag-capture')).appendTo(this.$el);
            this.$resizehdle = $('<div />').addClass(csscls('resize-handle')).appendTo(this.$el);
            this.$header = $('<div />').addClass(csscls('header')).appendTo(this.$el);
            this.$headerBtn = $('<a />').addClass(csscls('restore-btn')).appendTo(this.$header);
            this.$headerBtn.click(function() {
                self.close();
            });
            this.$headerLeft = $('<div />').addClass(csscls('header-left')).appendTo(this.$header);
            this.$headerRight = $('<div />').addClass(csscls('header-right')).appendTo(this.$header);
            var $body = this.$body = $('<div />').addClass(csscls('body')).appendTo(this.$el);
            this.recomputeBottomOffset();

            // dragging of resize handle
            var pos_y, orig_h;
            this.$resizehdle.on('mousedown', function(e) {
                orig_h = $body.height(), pos_y = e.pageY;
                $body.parents().on('mousemove', mousemove).on('mouseup', mouseup);
                self.$dragCapture.show();
                e.preventDefault();
            });
            var mousemove = function(e) {
                var h = orig_h + (pos_y - e.pageY);
                self.setHeight(h);
            };
            var mouseup = function() {
                $body.parents().off('mousemove', mousemove).off('mouseup', mouseup);
                self.$dragCapture.hide();
            };

            // close button
            this.$closebtn = $('<a />').addClass(csscls('close-btn')).appendTo(this.$headerRight);
            this.$closebtn.click(function() {
                self.close();
            });

            // minimize button
            this.$minimizebtn = $('<a />').addClass(csscls('minimize-btn') ).appendTo(this.$headerRight);
            this.$minimizebtn.click(function() {
                self.minimize();
            });

            // maximize button
            this.$maximizebtn = $('<a />').addClass(csscls('maximize-btn') ).appendTo(this.$headerRight);
            this.$maximizebtn.click(function() {
                self.restore();
            });

            // restore button
            this.$restorebtn = $('<a />').addClass(csscls('restore-btn')).hide().appendTo(this.$el);
            this.$restorebtn.click(function() {
                self.restore();
            });

            // open button
            this.$openbtn = $('<a />').addClass(csscls('open-btn')).appendTo(this.$headerRight).hide();
            this.$openbtn.click(function() {
                self.openHandler.show(function(id, dataset) {
                    self.addDataSet(dataset, id, "(opened)");
                    self.showTab();
                });
            });

            // select box for data sets
            this.$datasets = $('<select />').addClass(csscls('datasets-switcher')).attr('name', 'datasets-switcher')
                .appendTo(this.$headerRight);
            this.$datasets.change(function() {
                self.showDataSet(this.value);
            });

            this.controls['__settings'] = this.settings;
            this.settings.$tab.addClass(csscls('tab-settings'));
            this.settings.$tab.attr('data-collector', '__settings');
            this.settings.$el.attr('data-collector', '__settings');
            this.settings.$tab.insertAfter(this.$minimizebtn).show();
            this.settings.$tab.click(() => {
                if (!this.isMinimized() && this.activePanelName == '__settings') {
                    this.minimize();
                } else {
                    this.showTab('__settings');
                    this.settings.get('widget').render();
                }
            });
            this.settings.$el.appendTo(this.$body);
        },

        /**
         * Sets the height of the debugbar body section
         * Forces the height to lie within a reasonable range
         * Stores the height in local storage so it can be restored
         * Resets the document body bottom offset
         *
         * @this {DebugBar}
         */
        setHeight: function(height) {
            var min_h = 40;
            var max_h = $(window).innerHeight() - this.$header.height() - 10;
            height = Math.min(height, max_h);
            height = Math.max(height, min_h);
            this.$body.css('height', height);
            localStorage.setItem('phpdebugbar-height', height);
            this.recomputeBottomOffset();
        },

        /**
         * Restores the state of the DebugBar using localStorage
         * This is not called by default in the constructor and
         * needs to be called by subclasses in their init() method
         *
         * @this {DebugBar}
         */
        restoreState: function() {
            if (this.isIframe) return;
            // bar height
            var height = localStorage.getItem('phpdebugbar-height');
            this.setHeight(height || this.$body.height());

            // bar visibility
            var open = localStorage.getItem('phpdebugbar-open');
            if (open && open == '0') {
                this.close();
            } else {
                var visible = localStorage.getItem('phpdebugbar-visible');
                if (visible && visible == '1') {
                    var tab = localStorage.getItem('phpdebugbar-tab');
                    if (this.isTab(tab)) {
                        this.showTab(tab);
                    } else {
                        this.showTab();
                    }
                }
            }
        },

        /**
         * Creates and adds a new tab
         *
         * @this {DebugBar}
         * @param {String} name Internal name
         * @param {Object} widget A widget object with an element property
         * @param {String} title The text in the tab, if not specified, name will be used
         * @return {Tab}
         */
        createTab: function(name, widget, title) {
            var tab = new Tab({
                title: title || (name.replace(/[_\-]/g, ' ').charAt(0).toUpperCase() + name.slice(1)),
                widget: widget
            });
            return this.addTab(name, tab);
        },

        /**
         * Adds a new tab
         *
         * @this {DebugBar}
         * @param {String} name Internal name
         * @param {Tab} tab Tab object
         * @return {Tab}
         */
        addTab: function(name, tab) {
            if (this.isControl(name)) {
                throw new Error(name + ' already exists');
            }

            var self = this;
            tab.$tab.appendTo(this.$headerLeft).click(function() {
                if (!self.isMinimized() && self.activePanelName == name) {
                    self.minimize();
                } else {
                    self.showTab(name);
                }
            })
            tab.$tab.attr('data-empty', true);
            tab.$tab.attr('data-collector', name);
            tab.$el.attr('data-collector', name);
            tab.$el.appendTo(this.$body);

            this.controls[name] = tab;
            if (this.firstTabName == null) {
                this.firstTabName = name;
            }
            return tab;
        },

        /**
         * Creates and adds an indicator
         *
         * @this {DebugBar}
         * @param {String} name Internal name
         * @param {String} icon
         * @param {String|Object} tooltip
         * @param {String} position "right" or "left", default is "right"
         * @return {Indicator}
         */
        createIndicator: function(name, icon, tooltip, position) {
            var indicator = new Indicator({
                icon: icon,
                tooltip: tooltip
            });
            return this.addIndicator(name, indicator, position);
        },

        /**
         * Adds an indicator
         *
         * @this {DebugBar}
         * @param {String} name Internal name
         * @param {Indicator} indicator Indicator object
         * @return {Indicator}
         */
        addIndicator: function(name, indicator, position) {
            if (this.isControl(name)) {
                throw new Error(name + ' already exists');
            }

            indicator.set('debugbar', this);

            if (position == 'left') {
                indicator.$el.insertBefore(this.$headerLeft.children().first());
            } else {
                indicator.$el.appendTo(this.$headerRight);
            }

            this.controls[name] = indicator;
            return indicator;
        },

        /**
         * Returns a control
         *
         * @param {String} name
         * @return {Object}
         */
        getControl: function(name) {
            if (this.isControl(name)) {
                return this.controls[name];
            }
        },

        /**
         * Checks if there's a control under the specified name
         *
         * @this {DebugBar}
         * @param {String} name
         * @return {Boolean}
         */
        isControl: function(name) {
            return typeof(this.controls[name]) != 'undefined';
        },

        /**
         * Checks if a tab with the specified name exists
         *
         * @this {DebugBar}
         * @param {String} name
         * @return {Boolean}
         */
        isTab: function(name) {
            return this.isControl(name) && this.controls[name] instanceof Tab;
        },

        /**
         * Checks if an indicator with the specified name exists
         *
         * @this {DebugBar}
         * @param {String} name
         * @return {Boolean}
         */
        isIndicator: function(name) {
            return this.isControl(name) && this.controls[name] instanceof Indicator;
        },

        /**
         * Removes all tabs and indicators from the debug bar and hides it
         *
         * @this {DebugBar}
         */
        reset: function() {
            this.minimize();
            var self = this;
            $.each(this.controls, function(name, control) {
                if (self.isTab(name)) {
                    control.$tab.remove();
                }
                control.$el.remove();
            });
            this.controls = {};
        },

        /**
         * Open the debug bar and display the specified tab
         *
         * @this {DebugBar}
         * @param {String} name If not specified, display the first tab
         */
        showTab: function(name) {
            if (!name) {
                if (this.activePanelName) {
                    name = this.activePanelName;
                } else {
                    name = this.firstTabName;
                }
            }

            if (!this.isTab(name)) {
                throw new Error("Unknown tab '" + name + "'");
            }

            this.$resizehdle.show();
            this.$body.show();
            this.recomputeBottomOffset();

            $(this.$header).find('> div > .' + csscls('active')).removeClass(csscls('active'));
            $(this.$body).find('> .' + csscls('active')).removeClass(csscls('active'));

            this.controls[name].$tab.addClass(csscls('active'));
            this.controls[name].$el.addClass(csscls('active'));
            this.activePanelName = name;

            this.$el.removeClass(csscls('minimized'));
            localStorage.setItem('phpdebugbar-visible', '1');
            localStorage.setItem('phpdebugbar-tab', name);

            this.resize();
        },

        /**
         * Hide panels and minimize the debug bar
         *
         * @this {DebugBar}
         */
        minimize: function() {
            this.$header.find('> div > .' + csscls('active')).removeClass(csscls('active'));
            this.$body.hide();
            this.$resizehdle.hide();
            this.recomputeBottomOffset();
            localStorage.setItem('phpdebugbar-visible', '0');
            this.$el.addClass(csscls('minimized'));
            this.resize();
        },

        /**
         * Checks if the panel is minimized
         *
         * @return {Boolean}
         */
        isMinimized: function() {
            return this.$el.hasClass(csscls('minimized'));
        },

        /**
         * Close the debug bar
         *
         * @this {DebugBar}
         */
        close: function() {
            this.$resizehdle.hide();
            this.$header.hide();
            this.$body.hide();
            this.$restorebtn.show();
            localStorage.setItem('phpdebugbar-open', '0');
            this.$el.addClass(csscls('closed'));
            this.recomputeBottomOffset();
        },

        /**
         * Checks if the panel is closed
         *
         * @return {Boolean}
         */
        isClosed: function() {
            return this.$el.hasClass(csscls('closed'));
        },

        /**
         * Restore the debug bar
         *
         * @this {DebugBar}
         */
        restore: function() {
            this.$resizehdle.show();
            this.$header.show();
            this.$restorebtn.hide();
            localStorage.setItem('phpdebugbar-open', '1');
            var tab = localStorage.getItem('phpdebugbar-tab');
            if (this.isTab(tab)) {
                this.showTab(tab);
            } else {
                this.showTab();
            }
            this.$el.removeClass(csscls('closed'));
            this.resize();
        },

        /**
         * Recomputes the margin-bottom css property of the body so
         * that the debug bar never hides any content
         */
        recomputeBottomOffset: function() {
            if (this.options.bodyMarginBottom) {
                if (this.isClosed()) {
                    return $('body').css('margin-bottom', this.bodyMarginBottomHeight || '');
                }

                var offset = parseInt(this.$el.height()) + (this.bodyMarginBottomHeight || 0);
                $('body').css('margin-bottom', offset);
            }
        },

        /**
         * Sets the data map used by dataChangeHandler to populate
         * indicators and widgets
         *
         * A data map is an object where properties are control names.
         * The value of each property should be an array where the first
         * item is the name of a property from the data object (nested properties
         * can be specified) and the second item the default value.
         *
         * Example:
         *     {"memory": ["memory.peak_usage_str", "0B"]}
         *
         * @this {DebugBar}
         * @param {Object} map
         */
        setDataMap: function(map) {
            this.dataMap = map;
        },

        /**
         * Same as setDataMap() but appends to the existing map
         * rather than replacing it
         *
         * @this {DebugBar}
         * @param {Object} map
         */
        addDataMap: function(map) {
            $.extend(this.dataMap, map);
        },

        /**
         * Resets datasets and add one set of data
         *
         * For this method to be usefull, you need to specify
         * a dataMap using setDataMap()
         *
         * @this {DebugBar}
         * @param {Object} data
         * @return {String} Dataset's id
         */
        setData: function(data) {
            this.datasets = {};
            return this.addDataSet(data);
        },

        /**
         * Adds a dataset
         *
         * If more than one dataset are added, the dataset selector
         * will be displayed.
         *
         * For this method to be usefull, you need to specify
         * a dataMap using setDataMap()
         *
         * @this {DebugBar}
         * @param {Object} data
         * @param {String} id The name of this set, optional
         * @param {String} suffix
         * @param {Bool} show Whether to show the new dataset, optional (default: true)
         * @return {String} Dataset's id
         */
        addDataSet: function(data, id, suffix, show) {
            if (!data || !data.__meta) return;
            if (this.isIframe) {
                window.top.phpdebugbar.addDataSet(data, id, '(iframe)' + (suffix || ''), show);
                return;
            }

            var nb = getObjectSize(this.datasets) + 1;
            id = id || nb;
            data.__meta['nb'] = nb;
            data.__meta['suffix'] = suffix;
            this.datasets[id] = data;

            var label = this.datesetTitleFormater.format(id, this.datasets[id], suffix, nb);

            if (this.datasetTab) {
                this.datasetTab.set('data', this.datasets);
                var datasetSize = getObjectSize(this.datasets);
                this.datasetTab.set('badge', datasetSize > 1 ? datasetSize : null);
                this.datasetTab.$tab.show();
            }

            this.$datasets.append($('<option value="' + id + '">' + label + '</option>'));
            if (this.$datasets.children().length > 1) {
                this.$datasets.show();
            }

            if (typeof(show) == 'undefined' || show) {
                this.showDataSet(id);
            }

            this.resize();

            return id;
        },

        /**
         * Loads a dataset using the open handler
         *
         * @param {String} id
         * @param {Bool} show Whether to show the new dataset, optional (default: true)
         */
        loadDataSet: function(id, suffix, callback, show) {
            if (!this.openHandler) {
                throw new Error('loadDataSet() needs an open handler');
            }
            var self = this;
            this.openHandler.load(id, function(data) {
                self.addDataSet(data, id, suffix, show);
                self.resize();
                callback && callback(data);
            });
        },

        /**
         * Returns the data from a dataset
         *
         * @this {DebugBar}
         * @param {String} id
         * @return {Object}
         */
        getDataSet: function(id) {
            return this.datasets[id];
        },

        /**
         * Switch the currently displayed dataset
         *
         * @this {DebugBar}
         * @param {String} id
         */
        showDataSet: function(id) {
            this.activeDatasetId = id;
            this.dataChangeHandler(this.datasets[id]);

            if (this.$datasets.val() !== id) {
                this.$datasets.val(id);
            }

            if (this.datasetTab) {
                this.datasetTab.get('widget').set('id', id);
            }
        },

        /**
         * Called when the current dataset is modified.
         *
         * @this {DebugBar}
         * @param {Object} data
         */
        dataChangeHandler: function(data) {
            var self = this;
            $.each(this.dataMap, function(key, def) {
                var d = getDictValue(data, def[0], def[1]);
                if (key.indexOf(':') != -1) {
                    key = key.split(':');
                    self.getControl(key[0]).set(key[1], d);
                } else {
                    self.getControl(key).set('data', d);
                }
            });
            self.resize();
        },

        /**
         * Sets the handler to open past dataset
         *
         * @this {DebugBar}
         * @param {object} handler
         */
        setOpenHandler: function(handler) {
            this.openHandler = handler;
            this.openHandler.$el.attr('data-theme', this.$el.attr('data-theme'));
            if (handler !== null) {
                this.$openbtn.show();
            } else {
                this.$openbtn.hide();
            }
        },

        /**
         * Returns the handler to open past dataset
         *
         * @this {DebugBar}
         * @return {object}
         */
        getOpenHandler: function() {
            return this.openHandler;
        },

        enableAjaxHandlerTab: function() {
            this.datasetTab = new PhpDebugBar.DebugBar.Tab({"icon":"history", "title":"Request history", "widget": new PhpDebugBar.Widgets.DatasetWidget({
                    'debugbar': this
                })});
            this.datasetTab.$tab.addClass(csscls('tab-history'));
            this.datasetTab.$tab.attr('data-collector', '__datasets');
            this.datasetTab.$el.attr('data-collector', '__datasets');
            this.datasetTab.$tab.insertAfter(this.$openbtn).hide();
            this.datasetTab.$tab.click(() => {
                if (!this.isMinimized() && this.activePanelName == '__datasets') {
                    this.minimize();
                } else {
                    this.showTab('__datasets');
                }
            });
            this.datasetTab.$el.appendTo(this.$body);
            this.controls['__datasets'] = this.datasetTab;
        },
    });

    DebugBar.Tab = Tab;
    DebugBar.Indicator = Indicator;

    // ------------------------------------------------------------------

    /**
     * AjaxHandler
     *
     * Extract data from headers of an XMLHttpRequest and adds a new dataset
     *
     * @param {Bool} autoShow Whether to immediately show new datasets, optional (default: true)
     */
    var AjaxHandler = PhpDebugBar.AjaxHandler = function(debugbar, headerName, autoShow) {
        this.debugbar = debugbar;
        this.headerName = headerName || 'phpdebugbar';
        this.autoShow = typeof(autoShow) == 'undefined' ? true : autoShow;
        this.defaultAutoShow = this.autoShow;
        if (localStorage.getItem('phpdebugbar-ajaxhandler-autoshow') !== null) {
            this.autoShow = localStorage.getItem('phpdebugbar-ajaxhandler-autoshow') == '1';
        }
        if (debugbar.controls['__settings']) {
            debugbar.controls['__settings'].get('widget').set('autoshow', this.autoShow);
        }
    };

    $.extend(AjaxHandler.prototype, {

        /**
         * Handles a Fetch API Response or an XMLHttpRequest
         *
         * @this {AjaxHandler}
         * @param {Response|XMLHttpRequest} response
         * @return {Bool}
         */
        handle: function(response) {
            // Check if the debugbar header is available
            if (this.isFetch(response) && !response.headers.has(this.headerName + '-id')) {
                return true;
            } else if (this.isXHR(response) && response.getAllResponseHeaders().indexOf(this.headerName) === -1) {
                return true;
            }
            if (!this.loadFromId(response)) {
                return this.loadFromData(response);
            }
            return true;
        },

        getHeader: function(response, header) {
            if (this.isFetch(response)) {
                return response.headers.get(header)
            }

            return response.getResponseHeader(header)
        },

        isFetch: function(response) {
            return Object.prototype.toString.call(response) == '[object Response]'
        },

        isXHR: function(response) {
            return Object.prototype.toString.call(response) == '[object XMLHttpRequest]'
        },

        setAutoShow: function(autoshow) {
            this.autoShow = autoshow;
            localStorage.setItem('phpdebugbar-ajaxhandler-autoshow', autoshow ? '1' : '0');
        },

        /**
         * Checks if the HEADER-id exists and loads the dataset using the open handler
         *
         * @param {Response|XMLHttpRequest} response
         * @return {Bool}
         */
        loadFromId: function(response) {
            var id = this.extractIdFromHeaders(response);
            if (id && this.debugbar.openHandler) {
                this.debugbar.loadDataSet(id, "(ajax)", undefined, this.autoShow);
                return true;
            }
            return false;
        },

        /**
         * Extracts the id from the HEADER-id
         *
         * @param {Response|XMLHttpRequest} response
         * @return {String}
         */
        extractIdFromHeaders: function(response) {
            return this.getHeader(response, this.headerName + '-id');
        },

        /**
         * Checks if the HEADER exists and loads the dataset
         *
         * @param {Response|XMLHttpRequest} response
         * @return {Bool}
         */
        loadFromData: function(response) {
            var raw = this.extractDataFromHeaders(response);
            if (!raw) {
                return false;
            }

            var data = this.parseHeaders(raw);
            if (data.error) {
                throw new Error('Error loading debugbar data: ' + data.error);
            } else if(data.data) {
                this.debugbar.addDataSet(data.data, data.id, "(ajax)", this.autoShow);
            }
            return true;
        },

        /**
         * Extract the data as a string from headers of an XMLHttpRequest
         *
         * @this {AjaxHandler}
         * @param {Response|XMLHttpRequest} response
         * @return {string}
         */
        extractDataFromHeaders: function(response) {
            var data = this.getHeader(response, this.headerName);
            if (!data) {
                return;
            }
            for (var i = 1;; i++) {
                var header = this.getHeader(response, this.headerName + '-' + i);
                if (!header) {
                    break;
                }
                data += header;
            }
            return decodeURIComponent(data);
        },

        /**
         * Parses the string data into an object
         *
         * @this {AjaxHandler}
         * @param {string} data
         * @return {string}
         */
        parseHeaders: function(data) {
            return JSON.parse(data);
        },

        /**
         * Attaches an event listener to fetch
         *
         * @this {AjaxHandler}
         */
        bindToFetch: function() {
            var self = this;
            var proxied = window.fetch;

            if (proxied !== undefined && proxied.polyfill !== undefined) {
                return;
            }

            window.fetch = function () {
                var promise = proxied.apply(this, arguments);

                promise.then(function (response) {
                    self.handle(response);
                }).catch(function(reason) {
                    // Fetch request failed or aborted via AbortController.abort().
                    // Catch is required to not trigger React's error handler.
                });

                return promise;
            };
        },

        /**
         * @deprecated use bindToXHR instead
         */
        bindToJquery: function(jq) {
            var self = this;
            jq(document).ajaxComplete(function(e, xhr, settings) {
                if (!settings.ignoreDebugBarAjaxHandler) {
                    self.handle(xhr);
                }
            });
        },

        /**
         * Attaches an event listener to XMLHttpRequest
         *
         * @this {AjaxHandler}
         */
        bindToXHR: function() {
            var self = this;
            var proxied = XMLHttpRequest.prototype.open;
            XMLHttpRequest.prototype.open = function(method, url, async, user, pass) {
                var xhr = this;
                this.addEventListener("readystatechange", function() {
                    var skipUrl = self.debugbar.openHandler ? self.debugbar.openHandler.get('url') : null;
                    var href = (typeof url === 'string') ? url : url.href;

                    if (xhr.readyState == 4 && href.indexOf(skipUrl) !== 0) {
                        self.handle(xhr);
                    }
                }, false);
                proxied.apply(this, Array.prototype.slice.call(arguments));
            };
        }

    });

})(PhpDebugBar.$);
