text: /**
   * JavaScript Xmpp Chat namespace
   *
   * @namespace jsxc
   */
  jsxc = {
     /** Version of jsxc */
     version: '< $ app.version $ >',
  
     /** True if i'm the master */
     master: false,
  
     /** True if the role allocation is finished */
     role_allocation: false,
  
     /** Timeout for keepalive */
     to: [],
  
     /** Timeout after normal keepalive starts */
     toBusy: null,
  
     /** Timeout for notification */
     toNotification: null,
  
     /** Timeout delay for notification */
     toNotificationDelay: 500,
  
     /** Interval for keep-alive */
     keepaliveInterval: null,
  
     /** True if restore is complete */
     restoreCompleted: false,
  
     /** True if login through box */
     triggeredFromBox: false,
  
     /** True if logout through element click */
     triggeredFromElement: false,
  
     /** True if logout through logout click */
     triggeredFromLogout: false,
  
     /** last values which we wrote into localstorage (IE workaround) */
     ls: [],
  
     /**
      * storage event is even fired if I write something into storage (IE
      * workaround) 0: conform, 1: not conform, 2: not shure
      */
     storageNotConform: null,
  
     /** Timeout for storageNotConform test */
     toSNC: null,
  
     /** My bar id */
     bid: null,
  
     /** Current state */
     currentState: null,
  
     /** Current UI state */
     currentUIState: null,
  
     /** Some constants */
     CONST: {
        NOTIFICATION_DEFAULT: 'default',
        NOTIFICATION_GRANTED: 'granted',
        NOTIFICATION_DENIED: 'denied',
        STATUS: ['offline', 'dnd', 'xa', 'away', 'chat', 'online'],
        SOUNDS: {
           MSG: 'incomingMessage.wav',
           CALL: 'Rotary-Phone6.mp3',
           NOTICE: 'Ping1.mp3'
        },
        REGEX: {
           JID: new RegExp('\\b[^"&\'\\/:<>@\\s]+@[\\w-_.]+\\b', 'ig'),
           URL: new RegExp(/(https?:\/\/|www\.)[^\s<>'"]+/gi),
           GEOURI: new RegExp(/geo:(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)(?:,-?\d+(?:\.\d+)?)?(?:;crs=[\w-]+)?(?:;u=(\d+(?:\.\d+)?))?(?:;[\w-]+=(?:[\w-_.!~*'()]|%[\da-f][\da-f])+)*/)
        },
        NS: {
           CARBONS: 'urn:xmpp:carbons:2',
           FORWARD: 'urn:xmpp:forward:0',
           HINTS: 'urn:xmpp:hints'
        },
        HIDDEN: 'hidden',
        SHOWN: 'shown',
        STATE: {
           INITIATING: 0,
           PREVCONFOUND: 1,
           SUSPEND: 2,
           TRYTOINTERCEPT: 3,
           INTERCEPTED: 4,
           ESTABLISHING: 5,
           READY: 6
        },
        UISTATE: {
           INITIATING: 0,
           READY: 1
        }
     },
  
     /**
      * Parse a unix timestamp and return a formatted time string
      *
      * @memberOf jsxc
      * @param {Object} unixtime
      * @returns time of day and/or date
      */
     getFormattedTime: function(unixtime) {
        var msgDate = new Date(parseInt(unixtime));
        var day = ('0' + msgDate.getDate()).slice(-2);
        var month = ('0' + (msgDate.getMonth() + 1)).slice(-2);
        var year = msgDate.getFullYear();
        var hours = ('0' + msgDate.getHours()).slice(-2);
        var minutes = ('0' + msgDate.getMinutes()).slice(-2);
        var dateNow = new Date();
  
        var date = (typeof msgDate.toLocaleDateString === 'function') ? msgDate.toLocaleDateString() : day + '.' + month + '.' + year;
        var time = (typeof msgDate.toLocaleTimeString === 'function') ? msgDate.toLocaleTimeString() : hours + ':' + minutes;
  
        // compare dates only
        dateNow.setHours(0, 0, 0, 0);
        msgDate.setHours(0, 0, 0, 0);
  
        if (dateNow.getTime() !== msgDate.getTime()) {
           return date + ' ' + time;
        }
        return time;
     },
  
     /**
      * Write debug message to console and to log.
      *
      * @memberOf jsxc
      * @param {String} msg Debug message
      * @param {Object} data
      * @param {String} Could be warn|error|null
      */
     debug: function(msg, data, level) {
        if (level) {
           msg = '[' + level + '] ' + msg;
        }
  
        if (data) {
           if (jsxc.storage.getItem('debug') === true) {
              console.log(msg, data);
           }
  
           // try to convert data to string
           var d;
           try {
              // clone html snippet
              d = $("<span>").prepend($(data).clone()).html();
           } catch (err) {
              try {
                 d = JSON.stringify(data);
              } catch (err2) {
                 d = 'see js console';
              }
           }
  
           jsxc.log = jsxc.log + '$ ' + msg + ': ' + d + '\n';
        } else {
           console.log(msg);
           jsxc.log = jsxc.log + '$ ' + msg + '\n';
        }
     },
  
     /**
      * Write warn message.
      *
      * @memberOf jsxc
      * @param {String} msg Warn message
      * @param {Object} data
      */
     warn: function(msg, data) {
        jsxc.debug(msg, data, 'WARN');
     },
  
     /**
      * Write error message.
      *
      * @memberOf jsxc
      * @param {String} msg Error message
      * @param {Object} data
      */
     error: function(msg, data) {
        jsxc.debug(msg, data, 'ERROR');
     },
  
     /** debug log */
     log: '',
  
     /**
      * This function initializes important core functions and event handlers.
      * Afterwards it performs the following actions in the given order:
      *
      * <ol>
      *  <li>If (loginForm.ifFound = 'force' and form was found) or (jid or rid or
      * 	sid was not found) intercept form, and listen for credentials.</li>
      *  <li>Attach with jid, rid and sid from storage, if no form was found or
      * 	loginForm.ifFound = 'attach'</li>
      *  <li>Attach with jid, rid and sid from options.xmpp, if no form was found or
      * 	loginForm.ifFound = 'attach'</li>
      * </ol>
      *
      * @memberOf jsxc
      * @param {object} options See {@link jsxc.options}
      */
     init: function(options) {
        jsxc.runMigrations();
  
        jsxc.changeState(jsxc.CONST.STATE.INITIATING);
  
        if (options && options.loginForm && typeof options.loginForm.attachIfFound === 'boolean' && !options.loginForm.ifFound) {
           // translate deprated option attachIfFound found to new ifFound
           options.loginForm.ifFound = (options.loginForm.attachIfFound) ? 'attach' : 'pause';
        }
  
        if (options) {
           // override default options
           $.extend(true, jsxc.options, options);
        }
  
        // Check localStorage
        if (!jsxc.storage.hasSupport()) {
           jsxc.error("Browser doesn't support localStorage. JSXC will be disabled.");
           return;
        }
  
        /**
         * Getter method for options. Saved options will override default one.
         *
         * @param {string} key option key
         * @returns default or saved option value
         */
        jsxc.options.get = function(key) {
           if (jsxc.bid) {
              var local = jsxc.storage.getUserItem('options') || {};
  
              return (typeof local[key] !== 'undefined') ? local[key] : jsxc.options[key];
           }
  
           return jsxc.options[key];
        };
  
        /**
         * Setter method for options. Will write into localstorage.
         *
         * @param {string} key option key
         * @param {object} value option value
         */
        jsxc.options.set = function(key, value) {
           jsxc.storage.updateItem('options', key, value, true);
        };
  
        jsxc.storageNotConform = jsxc.storage.getItem('storageNotConform');
        if (jsxc.storageNotConform === null) {
           jsxc.storageNotConform = 2;
        }
  
        // detect language
        var lang;
        if (jsxc.storage.getItem('lang') !== null) {
           lang = jsxc.storage.getItem('lang');
        } else if (jsxc.options.autoLang && navigator.languages && navigator.languages.length > 0) {
           lang = navigator.languages[0];
        } else if (jsxc.options.autoLang && navigator.language) {
           lang = navigator.language;
        } else {
           lang = jsxc.options.defaultLang;
        }
  
        var availableLanguages = Object.keys(window.jsxcLanguageResources);
  
        if (availableLanguages.indexOf(lang) < 0) {
           var languagePrefix = lang.slice(0, 2);
  
           if (availableLanguages.indexOf(languagePrefix) > -1) {
              lang = languagePrefix;
           } else {
              var prefixMatch = availableLanguages.filter(function(l) {
                 return l.slice(0, 2) === languagePrefix;
              });
  
              if (prefixMatch.length > 0) {
                 lang = prefixMatch[0];
              }
           }
        }
  
        // initialize i18next translator
        window.i18next.init({
           lng: lang,
           fallbackLng: 'en',
           resources: window.jsxcLanguageResources,
           returnNull: false,
           debug: jsxc.storage.getItem('debug') === true,
           interpolation: {
              prefix: '__',
              suffix: '__'
           }
        }, function() {
           window.jqueryI18next.init(window.i18next, $, {
              tName: 't',
              i18nName: 'i18next',
              handleName: 'localize',
           });
        });
  
        if (jsxc.storage.getItem('debug') === true) {
           jsxc.options.otr.debug = true;
        }
  
        // Register event listener for the storage event
        window.addEventListener('storage', jsxc.storage.onStorage, false);
  
        $(document).on('attached.jsxc', jsxc.registerLogout);
        $(document).on('disconnected.jsxc', jsxc.relogin);
  
        var isStorageAttachParameters = jsxc.storage.getItem('rid') && jsxc.storage.getItem('sid') && jsxc.storage.getItem('jid');
        var isOptionsAttachParameters = jsxc.options.xmpp.rid && jsxc.options.xmpp.sid && jsxc.options.xmpp.jid;
        var isForceLoginForm = jsxc.options.loginForm && jsxc.options.loginForm.ifFound === 'force' && jsxc.isLoginForm();
  
        // Check if we have to establish a new connection
        if ((!isStorageAttachParameters && !isOptionsAttachParameters) || isForceLoginForm) {
  
           if (jsxc.relogin()) {
              return;
           }
  
           jsxc.prepareNewConnection();
  
        } else if (!jsxc.isLoginForm() || (jsxc.options.loginForm && jsxc.options.loginForm.ifFound === 'attach')) {
  
           // Restore old connection
           jsxc.changeState(jsxc.CONST.STATE.PREVCONFOUND);
  
           if (typeof jsxc.storage.getItem('alive') === 'undefined') {
              jsxc.onMaster();
           } else {
              jsxc.checkMaster();
           }
        }
     },
  
     runMigrations: function() {
        var lastUsedVersion = jsxc.storage.getItem('version');
  
        if (!lastUsedVersion) {
           var keys = Object.keys(localStorage).filter(function(key) {
              var isKeepMatch = key.match(/^jsxc:[^:]+:(key|history|msg|priv_fingerprint):?/);
  
              return (key.match(/^jsxc:/) && !isKeepMatch) || key.match(/^strophe\.caps\./)
           });
  
           keys.forEach(function(key) {
              localStorage.removeItem(key);
           });
  
           jsxc.debug('I turned out your storage and deleted ' + keys.length + ' entries.');
        }
  
        if (lastUsedVersion !== jsxc.version) {
           jsxc.storage.setItem('version', jsxc.version);
        }
     },
  
     prepareNewConnection: function() {
        // clean up rid and sid
        jsxc.storage.removeItem('rid');
        jsxc.storage.removeItem('sid');
  
        // Looking for a login form
        if (!jsxc.isLoginForm()) {
           jsxc.changeState(jsxc.CONST.STATE.SUSPEND);
  
           if (jsxc.options.displayRosterMinimized()) {
              // Show minimized roster
              jsxc.storage.setUserItem('roster', 'hidden');
              jsxc.gui.roster.init();
              jsxc.gui.roster.noConnection();
           }
  
           return;
        }
  
        jsxc.changeState(jsxc.CONST.STATE.TRYTOINTERCEPT);
  
        if (typeof jsxc.options.formFound === 'function') {
           jsxc.options.formFound.call();
        }
  
        // create jquery object
        var form = jsxc.options.loginForm.form = $(jsxc.options.loginForm.form);
        var events = form.data('events') || {
           submit: []
        };
        var submits = [];
  
        // save attached submit events and remove them. Will be reattached
        // in jsxc.submitLoginForm
        $.each(events.submit, function(index, val) {
           submits.push(val.handler);
        });
  
        form.data('submits', submits);
        form.off('submit');
  
        // Add jsxc login action to form
        form.submit(function(ev) {
           ev.preventDefault();
  
           jsxc.prepareLogin(function(settings) {
              if (settings !== false) {
                 // settings.xmpp.onlogin is deprecated since v2.1.0
                 var enabled = (settings.loginForm && settings.loginForm.enable) || (settings.xmpp && settings.xmpp.onlogin);
                 enabled = enabled === "true" || enabled === true;
  
                 if (enabled) {
                    jsxc.options.loginForm.triggered = true;
  
                    jsxc.xmpp.login(jsxc.options.xmpp.jid, jsxc.options.xmpp.password);
  
                    return;
                 }
              }
  
              jsxc.submitLoginForm();
           });
  
           // Trigger submit in jsxc.xmpp.connected()
           return false;
        });
  
        jsxc.changeState(jsxc.CONST.STATE.INTERCEPTED);
     },
  
     /**
      * Attach to previous session if jid, sid and rid are available
      * in storage or options (default behaviour also for {@link jsxc.init}).
      *
      * @memberOf jsxc
      */
     /**
      * Start new chat session with given jid and password.
      *
      * @memberOf jsxc
      * @param {string} jid Jabber Id
      * @param {string} password Jabber password
      */
     /**
      * Attach to new chat session with jid, sid and rid.
      *
      * @memberOf jsxc
      * @param {string} jid Jabber Id
      * @param {string} sid Session Id
      * @param {string} rid Request Id
      */
     start: function() {
        var args = arguments;
  
        if (jsxc.role_allocation && !jsxc.master) {
           jsxc.debug('There is an other master tab');
  
           return false;
        }
  
        if (jsxc.xmpp.conn && jsxc.xmpp.conn.authenticated) {
           jsxc.debug('We are already connected and authenticated');
  
           return false;
        }
  
        if (jsxc.currentState !== jsxc.CONST.STATE.INTERCEPTED && jsxc.currentState !== jsxc.CONST.STATE.SUSPEND) {
           if (!jsxc.busy) {
              jsxc.debug('I am currently busy and will try again later. Please be patient.');
           }
  
           jsxc.busy = true;
  
           setTimeout(function() {
              jsxc.start.apply(jsxc, args);
           }, 400);
  
           return;
        }
  
        jsxc.busy = false;
  
        if (args.length === 3) {
           $(document).one('attached.jsxc', function() {
              // save rid after first attachment
              jsxc.xmpp.onRidChange(jsxc.xmpp.conn._proto.rid);
  
              jsxc.onMaster();
           });
        }
  
        jsxc.checkMaster(function() {
           jsxc.xmpp.login.apply(this, args);
        });
     },
  
     relogin: function() {
        jsxc.debug('Try to relogin');
  
        var jid = jsxc.storage.getItem('jid');
        jsxc.bid = jsxc.bid || (jid ? jsxc.jidToBid(jid) : null);
  
        if (!jsxc.bid || jsxc.storage.getUserItem('forcedLogout')) {
           jsxc.debug('Logout was forced or I found no valid jid');
  
           return false;
        }
  
        var xmppOptions = jsxc.options.get('xmpp');
  
        if (xmppOptions.url && (xmppOptions.jid || (xmppOptions.username && xmppOptions.domain)) && xmppOptions.password) {
           xmppOptions.jid = xmppOptions.jid || (xmppOptions.username + '@' + xmppOptions.domain);
  
           jsxc.start(xmppOptions.jid, xmppOptions.password);
  
           return true;
        }
  
        var loadSettingsAllKnowing = jsxc.storage.getUserItem('loadSettingsAllKnowing');
  
        if (xmppOptions.url && loadSettingsAllKnowing) {
           jsxc.options.loadSettings(null, null, function(settings) {
              jsxc._prepareLogin(null, null, function(settings) {
                 if (settings !== false && jsxc.options.xmpp.jid && jsxc.options.xmpp.password) {
                    $(document).on('connfail.jsxc', reloginFailed);
                    $(document).on('authfail.jsxc', reloginFailed);
                    $(document).on('connected.jsxc', removeReloginHandler);
  
                    jsxc.start(jsxc.options.xmpp.jid, jsxc.options.xmpp.password);
  
                    delete jsxc.options.xmpp.password;
                 } else {
                    reloginFailed();
                 }
  
                 function reloginFailed() {
                    jsxc.debug('Could not relogin.');
  
                    removeReloginHandler();
  
                    jsxc.storage.removeUserItem('loadSettingsAllKnowing');
  
                    jsxc.prepareNewConnection();
                 }
  
                 function removeReloginHandler() {
                    $(document).off('connfail.jsxc', reloginFailed);
                    $(document).off('authfail.jsxc', reloginFailed);
                    $(document).off('connected.jsxc', removeReloginHandler);
                 }
              }, settings);
           });
  
           return true;
        }
  
        jsxc.debug('I am not able to relogin');
  
        return false;
     },
  
     registerLogout: function() {
        // Looking for logout element
        if (jsxc.options.logoutElement !== null && $(jsxc.options.logoutElement).length > 0) {
           var logout = function(ev) {
              ev.stopPropagation();
              ev.preventDefault();
  
              jsxc.options.logoutElement = $(this);
              jsxc.triggeredFromLogout = true;
  
              jsxc.xmpp.logout();
           };
  
           jsxc.options.logoutElement = $(jsxc.options.logoutElement);
  
           jsxc.options.logoutElement.off('click', null, logout).one('click', logout);
        }
     },
  
     /**
      * Returns true if login form is found.
      *
      * @memberOf jsxc
      * @returns {boolean} True if login form was found.
      */
     isLoginForm: function() {
        return jsxc.options.loginForm.form && jsxc.el_exists(jsxc.options.loginForm.form) && jsxc.el_exists(jsxc.options.loginForm.jid) && jsxc.el_exists(jsxc.options.loginForm.pass);
     },
  
     /**
      * Load settings and prepare jid.
      *
      * @memberOf jsxc
      * @param {string} username
      * @param {string} password
      * @param {function} cb Called after login is prepared with result as param
      */
     prepareLogin: function(username, password, cb) {
        if (typeof username === 'function') {
           cb = username;
           username = null;
        }
        username = username || $(jsxc.options.loginForm.jid).val();
        password = password || $(jsxc.options.loginForm.pass).val();
  
        if (!jsxc.triggeredFromBox && (jsxc.options.loginForm.onConnecting === 'dialog' || typeof jsxc.options.loginForm.onConnecting === 'undefined')) {
           jsxc.gui.showWaitAlert($.t('Logging_in'));
        }
  
        var settings;
  
        if (typeof jsxc.options.loadSettings === 'function') {
           settings = jsxc.options.loadSettings.call(this, username, password, function(s) {
              jsxc._prepareLogin(username, password, cb, s);
           });
  
           if (typeof settings !== 'undefined') {
              jsxc._prepareLogin(username, password, cb, settings);
           }
        } else {
           jsxc._prepareLogin(username, password, cb);
        }
     },
  
     /**
      * Process xmpp settings and save loaded settings.
      *
      * @private
      * @memberOf jsxc
      * @param {string} username
      * @param {string} password
      * @param {function} cb Called after login is prepared with result as param
      * @param {object} [loadedSettings] additonal options
      */
     _prepareLogin: function(username, password, cb, loadedSettings) {
        if (loadedSettings === false) {
           jsxc.warn('No settings provided');
  
           cb(false);
           return;
        }
  
        // prevent to modify the original object
        var settings = $.extend(true, {}, jsxc.options);
  
        if (loadedSettings) {
           // overwrite current options with loaded settings;
           settings = $.extend(true, settings, loadedSettings);
        } else {
           loadedSettings = {};
        }
  
        if (typeof settings.xmpp.username === 'string') {
           username = settings.xmpp.username;
        }
  
        if (typeof settings.xmpp.password === 'string') {
           password = settings.xmpp.password;
  
           delete settings.xmpp.password;
        }
  
        var resource = (settings.xmpp.resource) ? '/' + settings.xmpp.resource : '';
        var domain = settings.xmpp.domain;
        var jid;
  
        if (username.match(/@(.*)$/)) {
           jid = (username.match(/\/(.*)$/)) ? username : username + resource;
        } else {
           jid = username + '@' + domain + resource;
        }
  
        if (typeof jsxc.options.loginForm.preJid === 'function') {
           jid = jsxc.options.loginForm.preJid(jid);
        }
  
        jsxc.bid = jsxc.jidToBid(jid);
  
        settings.xmpp.username = jid.split('@')[0];
        settings.xmpp.domain = jid.split('@')[1].split('/')[0];
        settings.xmpp.resource = jid.split('@')[1].split('/')[1] || "";
  
        if (!loadedSettings.xmpp) {
           // force xmpp settings to be saved to storage
           loadedSettings.xmpp = {};
        }
  
        jsxc.storage.setUserItem('loadSettingsAllKnowing', (!!loadedSettings.xmpp.jid || (!!loadedSettings.xmpp.username && !!loadedSettings.xmpp.domain)) && !!loadedSettings.xmpp.password);
  
        // save loaded settings to storage
        $.each(loadedSettings, function(key) {
           var old = jsxc.options.get(key);
           var val = settings[key];
           val = $.extend(true, old, val);
  
           jsxc.options.set(key, val);
        });
  
        jsxc.options.xmpp.jid = jid;
        jsxc.options.xmpp.password = password;
  
        cb(settings);
     },
  
     /**
      * Called if the script is a slave
      */
     onSlave: function() {
        jsxc.debug('I am the slave.');
  
        jsxc.role_allocation = true;
        jsxc.bid = jsxc.jidToBid(jsxc.storage.getItem('jid'));
  
        jsxc.gui.init();
        $('#jsxc_roster').removeClass('jsxc_noConnection');
  
        jsxc.registerLogout();
        jsxc.gui.avatar.update($('#jsxc_roster > .jsxc_bottom'), jsxc.jidToBid(jsxc.storage.getItem('jid')), 'own');
  
        jsxc.gui.restore();
     },
  
     /**
      * Called if the script is the master
      */
     onMaster: function() {
        jsxc.debug('I am master.');
  
        jsxc.master = true;
  
        // Init local storage
        jsxc.storage.setItem('alive', 0);
        jsxc.storage.setItem('alive_busy', 0);
  
        // Sending keepalive signal
        jsxc.startKeepAlive();
  
        jsxc.role_allocation = true;
  
        jsxc.xmpp.login();
     },
  
     /**
      * Checks if there is a master
      *
      * @param {function} [cb] Called if no master was found.
      */
     checkMaster: function(cb) {
        jsxc.debug('check master');
  
        cb = (cb && typeof cb === 'function') ? cb : jsxc.onMaster;
  
        if (typeof jsxc.storage.getItem('alive') === 'undefined') {
           cb.call();
        } else {
           jsxc.to.push(window.setTimeout(cb, 1000));
           jsxc.keepAlive('slave');
        }
     },
  
     masterActions: function() {
  
        if (!jsxc.xmpp.conn || !jsxc.xmpp.conn.authenticated) {
           return;
        }
  
        //prepare notifications
        var noti = jsxc.storage.getUserItem('notification');
        noti = (typeof noti === 'number') ? noti : 2;
        if (jsxc.options.notification && noti > 0 && jsxc.notification.hasSupport()) {
           if (jsxc.notification.hasPermission()) {
              jsxc.notification.init();
           } else {
              jsxc.notification.prepareRequest();
           }
        } else {
           // No support => disable
           jsxc.options.notification = false;
        }
  
        if (jsxc.options.get('otr').enable) {
           // create or load DSA key
           jsxc.otr.createDSA();
        }
  
        jsxc.gui.avatar.update($('#jsxc_roster > .jsxc_bottom'), jsxc.jidToBid(jsxc.storage.getItem('jid')), 'own');
     },
  
     /**
      * Start sending keep-alive signal
      */
     startKeepAlive: function() {
        jsxc.keepaliveInterval = window.setInterval(jsxc.keepAlive, jsxc.options.timeout - 1000);
     },
  
     /**
      * Sends the keep-alive signal to signal that the master is still there.
      */
     keepAlive: function(role) {
        var next = parseInt(jsxc.storage.getItem('alive')) + 1;
        role = role || 'master';
  
        jsxc.storage.setItem('alive', next + ':' + role);
     },
  
     /**
      * Send one keep-alive signal with higher timeout, and than resume with
      * normal signal
      */
     keepBusyAlive: function() {
        if (jsxc.toBusy) {
           window.clearTimeout(jsxc.toBusy);
        }
  
        if (jsxc.keepaliveInterval) {
           window.clearInterval(jsxc.keepaliveInterval);
        }
  
        jsxc.storage.ink('alive_busy');
        jsxc.toBusy = window.setTimeout(jsxc.startKeepAlive, jsxc.options.busyTimeout - 1000);
     },
  
     /**
      * Generates a random integer number between 0 and max
      *
      * @param {Integer} max
      * @return {Integer} random integer between 0 and max
      */
     random: function(max) {
        return Math.floor(Math.random() * max);
     },
  
     /**
      * Checks if there is a element with the given selector
      *
      * @param {String} selector jQuery selector
      * @return {Boolean}
      */
     el_exists: function(selector) {
        return $(selector).length > 0;
     },
  
     /**
      * Creates a CSS compatible string from a JID
      *
      * @param {type} jid Valid Jabber ID
      * @returns {String} css Compatible string
      */
     jidToCid: function(jid) {
        jsxc.warn('jsxc.jidToCid is deprecated!');
  
        var cid = Strophe.getBareJidFromJid(jid).replace('@', '-').replace(/\./g, '-').toLowerCase();
  
        return cid;
     },
  
     /**
      * Create comparable bar jid.
      *
      * @memberOf jsxc
      * @param jid
      * @returns comparable bar jid
      */
     jidToBid: function(jid) {
        return Strophe.unescapeNode(Strophe.getBareJidFromJid(jid).toLowerCase());
     },
  
     /**
      * Restore roster
      */
     restoreRoster: function() {
        var buddies = jsxc.storage.getUserItem('buddylist');
  
        if (!buddies || buddies.length === 0) {
           jsxc.debug('No saved buddylist.');
  
           jsxc.gui.roster.empty();
  
           return;
        }
  
        $.each(buddies, function(index, value) {
           jsxc.gui.roster.add(value);
        });
  
        jsxc.gui.roster.loaded = true;
        $(document).trigger('cloaded.roster.jsxc');
     },
  
     /**
      * Restore all windows
      */
     restoreWindows: function() {
        var windows = jsxc.storage.getUserItem('windowlist');
  
        if (windows === null) {
           return;
        }
  
        $.each(windows, function(index, bid) {
           var win = jsxc.storage.getUserItem('window', bid);
  
           if (!win) {
              jsxc.debug('Associated window-element is missing: ' + bid);
              return true;
           }
  
           jsxc.gui.window.init(bid);
  
           if (!win.minimize) {
              jsxc.gui.window.show(bid);
           } else {
              jsxc.gui.window.hide(bid);
           }
  
           jsxc.gui.window.setText(bid, win.text);
        });
     },
  
     /**
      * This method submits the specified login form.
      */
     submitLoginForm: function() {
        var form = $(jsxc.options.loginForm.form).off('submit');
  
        // Attach original events
        var submits = form.data('submits') || [];
        $.each(submits, function(index, val) {
           form.submit(val);
        });
  
        if (form.find('#submit').length > 0) {
           form.find('#submit').click();
        } else if (form.get(0) && typeof form.get(0).submit === 'function') {
           form.submit();
        } else if (form.find('[type="submit"]').length > 0) {
           form.find('[type="submit"]').click();
        } else {
           jsxc.warn('Could not submit login form.');
        }
     },
  
     /**
      * Escapes some characters to HTML character
      */
     escapeHTML: function(text) {
        text = text.replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>');
        return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
     },
  
     /**
      * Removes all html tags.
      *
      * @memberOf jsxc
      * @param text
      * @returns stripped text
      */
     removeHTML: function(text) {
        return $('<span>').html(text).text();
     },
  
     /**
      * Executes only one of the given events
      *
      * @param {string} obj.key event name
      * @param {function} obj.value function to execute
      * @returns {string} namespace of all events
      */
     switchEvents: function(obj) {
        var ns = Math.random().toString(36).substr(2, 12);
        var self = this;
  
        $.each(obj, function(key, val) {
           $(document).one(key + '.' + ns, function() {
              $(document).off('.' + ns);
  
              val.apply(self, arguments);
           });
        });
  
        return ns;
     },
  
     /**
      * Checks if tab is hidden.
      *
      * @returns {boolean} True if tab is hidden
      */
     isHidden: function() {
        var hidden = false;
  
        if (typeof document.hidden !== 'undefined') {
           hidden = document.hidden;
        } else if (typeof document.webkitHidden !== 'undefined') {
           hidden = document.webkitHidden;
        } else if (typeof document.mozHidden !== 'undefined') {
           hidden = document.mozHidden;
        } else if (typeof document.msHidden !== 'undefined') {
           hidden = document.msHidden;
        }
  
        // handle multiple tabs
        if (hidden && jsxc.master) {
           jsxc.storage.ink('hidden', 0);
        } else if (!hidden && !jsxc.master) {
           jsxc.storage.ink('hidden');
        }
  
        return hidden;
     },
  
     /**
      * Checks if tab has focus.
      *
      * @returns {boolean} True if tabs has focus
      */
     hasFocus: function() {
        var focus = true;
  
        if (typeof document.hasFocus === 'function') {
           focus = document.hasFocus();
        }
  
        if (!focus && jsxc.master) {
           jsxc.storage.ink('focus', 0);
        } else if (focus && !jsxc.master) {
           jsxc.storage.ink('focus');
        }
  
        return focus;
     },
  
     /**
      * Executes the given function in jsxc namespace.
      *
      * @memberOf jsxc
      * @param {string} fnName Function name
      * @param {array} fnParams Function parameters
      * @returns Function return value
      */
     exec: function(fnName, fnParams) {
        var fnList = fnName.split('.');
        var fn = jsxc[fnList[0]];
        var i;
        for (i = 1; i < fnList.length; i++) {
           fn = fn[fnList[i]];
        }
  
        if (typeof fn === 'function') {
           return fn.apply(null, fnParams);
        }
     },
  
     /**
      * Hash string into 32-bit signed integer.
      *
      * @memberOf jsxc
      * @param {string} str input string
      * @returns {integer} 32-bit signed integer
      */
     hashStr: function(str) {
        var hash = 0,
           i;
  
        if (str.length === 0) {
           return hash;
        }
  
        for (i = 0; i < str.length; i++) {
           hash = ((hash << 5) - hash) + str.charCodeAt(i);
           hash |= 0; // Convert to 32bit integer
        }
  
        return hash;
     },
  
     isExtraSmallDevice: function() {
        return $(window).width() < 500;
     },
  
     changeState: function(state) {
        jsxc.currentState = state;
  
        jsxc.debug('State changed to ' + Object.keys(jsxc.CONST.STATE)[state]);
  
        $(document).trigger('stateChange.jsxc', state);
     },
  
     changeUIState: function(state) {
        jsxc.currentUIState = state;
  
        jsxc.debug('UI State changed to ' + Object.keys(jsxc.CONST.UISTATE)[state]);
  
        $(document).trigger('stateUIChange.jsxc', state);
     },
  
     getLastMsg: function(bid) {
        var history = jsxc.storage.getUserItem('history', bid) || [];
        var i = 0;
  
        while (history.length > i) {
           var message = new jsxc.Message(history[i]);
           if (message.direction !== jsxc.Message.SYS) {
              return {
                 text: message.msg,
                 date: message.stamp
              };
           }
           i++;
        }
     },
  
     enableDebugMode: function() {
        jsxc.storage.setItem('debug', true);
     },
  
     disableDebugMode: function() {
        jsxc.storage.setItem('debug', false);
     },
  
     deleteAllData: function() {
        if (!jsxc.storage.getItem('debug')) {
           jsxc.warn('This action is only available in debug mode.');
  
           return 0;
        }
  
        var prefix = jsxc.storage.PREFIX + jsxc.storage.SEP;
        var prefixRegex = new RegExp('^' + prefix);
        var keys = Object.keys(localStorage);
        var count = 0;
  
        $.each(keys, function(index, key) {
           if (prefixRegex.test(key) && key !== prefix + 'debug') {
              localStorage.removeItem(key);
              count++;
           }
        });
  
        return count;
     }
  };
  