import Handler from './handler.js';
import TimedHandler from './timed-handler.js';
import Builder, { $build, $iq, $pres } from './builder.js';
import log from './log.js';
import { ErrorCondition, NS, Status } from './constants.js';
import SASLAnonymous from './sasl-anon.js';
import SASLExternal from './sasl-external.js';
import SASLOAuthBearer from './sasl-oauthbearer.js';
import SASLPlain from './sasl-plain.js';
import SASLSHA1 from './sasl-sha1.js';
import SASLSHA256 from './sasl-sha256.js';
import SASLSHA384 from './sasl-sha384.js';
import SASLSHA512 from './sasl-sha512.js';
import SASLXOAuth2 from './sasl-xoauth2.js';
import {
    addCookies,
    forEachChild,
    getBareJidFromJid,
    getDomainFromJid,
    getNodeFromJid,
    getResourceFromJid,
    getText,
    handleError,
} from './utils.js';
import { SessionError } from './errors.js';
import Bosh from './bosh.js';
import WorkerWebsocket from './worker-websocket.js';
import Websocket from './websocket.js';

/**
 * @typedef {import("./sasl.js").default} SASLMechanism
 * @typedef {import("./request.js").default} Request
 *
 * @typedef {Object} ConnectionOptions
 * @property {Cookies} [cookies]
 *  Allows you to pass in cookies that will be included in HTTP requests.
 *  Relevant to both the BOSH and Websocket transports.
 *
 *  The passed in value must be a map of cookie names and string values.
 *
 *  > { "myCookie": {
 *  >     "value": "1234",
 *  >     "domain": ".example.org",
 *  >     "path": "/",
 *  >     "expires": expirationDate
 *  >     }
 *  > }
 *
 *  Note that cookies can't be set in this way for domains other than the one
 *  that's hosting Strophe (i.e. cross-domain).
 *  Those cookies need to be set under those domains, for example they can be
 *  set server-side by making a XHR call to that domain to ask it to set any
 *  necessary cookies.
 * @property {SASLMechanism[]} [mechanisms]
 *  Allows you to specify the SASL authentication mechanisms that this
 *  instance of Connection (and therefore your XMPP client) will support.
 *
 *  The value must be an array of objects with {@link SASLMechanism}
 *  prototypes.
 *
 *  If nothing is specified, then the following mechanisms (and their
 *  priorities) are registered:
 *
 *      Mechanism       Priority
 *      ------------------------
 *      SCRAM-SHA-512   72
 *      SCRAM-SHA-384   71
 *      SCRAM-SHA-256   70
 *      SCRAM-SHA-1     60
 *      PLAIN           50
 *      OAUTHBEARER     40
 *      X-OAUTH2        30
 *      ANONYMOUS       20
 *      EXTERNAL        10
 *
 * @property {boolean} [explicitResourceBinding]
 *  If `explicitResourceBinding` is set to `true`, then the XMPP client
 *  needs to explicitly call {@link Connection.bind} once the XMPP
 *  server has advertised the `urn:ietf:propertys:xml:ns:xmpp-bind` feature.
 *
 *  Making this step explicit allows client authors to first finish other
 *  stream related tasks, such as setting up an XEP-0198 Stream Management
 *  session, before binding the JID resource for this session.
 *
 * @property {'ws'|'wss'} [protocol]
 *  _Note: This option is only relevant to Websocket connections, and not BOSH_
 *
 *  If you want to connect to the current host with a WebSocket connection you
 *  can tell Strophe to use WebSockets through the "protocol" option.
 *  Valid values are `ws` for WebSocket and `wss` for Secure WebSocket.
 *  So to connect to "wss://CURRENT_HOSTNAME/xmpp-websocket" you would call
 *
 *      const conn = new Strophe.Connection(
 *          "/xmpp-websocket/",
 *          {protocol: "wss"}
 *      );
 *
 *  Note that relative URLs _NOT_ starting with a "/" will also include the path
 *  of the current site.
 *
 *  Also because downgrading security is not permitted by browsers, when using
 *  relative URLs both BOSH and WebSocket connections will use their secure
 *  variants if the current connection to the site is also secure (https).
 *
 * @property {string} [worker]
 *  _Note: This option is only relevant to Websocket connections, and not BOSH_
 *
 *  Set this option to URL from where the shared worker script should be loaded.
 *
 *  To run the websocket connection inside a shared worker.
 *  This allows you to share a single websocket-based connection between
 *  multiple Connection instances, for example one per browser tab.
 *
 *  The script to use is the one in `src/shared-connection-worker.js`.
 *
 * @property {boolean} [sync]
 *  Used to control whether BOSH HTTP requests will be made synchronously or not.
 *  The default behaviour is asynchronous. If you want to make requests
 *  synchronous, make "sync" evaluate to true.
 *
 *  > const conn = new Strophe.Connection("/http-bind/", {sync: true});
 *
 *  You can also toggle this on an already established connection.
 *
 *  > conn.options.sync = true;
 *
 * @property {string[]} [customHeaders]
 *  Used to provide custom HTTP headers to be included in the BOSH HTTP requests.
 *
 * @property {boolean} [keepalive]
 *  Used to instruct Strophe to maintain the current BOSH session across
 *  interruptions such as webpage reloads.
 *
 *  It will do this by caching the sessions tokens in sessionStorage, and when
 *  "restore" is called it will check whether there are cached tokens with
 *  which it can resume an existing session.
 *
 * @property {boolean} [withCredentials]
 *  Used to indicate wether cookies should be included in HTTP requests (by default
 *  they're not).
 *  Set this value to `true` if you are connecting to a BOSH service
 *  and for some reason need to send cookies to it.
 *  In order for this to work cross-domain, the server must also enable
 *  credentials by setting the `Access-Control-Allow-Credentials` response header
 *  to "true". For most usecases however this setting should be false (which
 *  is the default).
 *  Additionally, when using `Access-Control-Allow-Credentials`, the
 *  `Access-Control-Allow-Origin` header can't be set to the wildcard "*", but
 *  instead must be restricted to actual domains.
 *
 * @property {string} [contentType]
 *  Used to change the default Content-Type, which is "text/xml; charset=utf-8".
 *  Can be useful to reduce the amount of CORS preflight requests that are sent
 *  to the server.
 */


/**
 * _Private_ variable Used to store plugin names that need
 * initialization during Connection construction.
 * @type {Object.<string, Object>}
 */
const connectionPlugins = {};

/**
 * **XMPP Connection manager**
 *
 * This class is the main part of Strophe.  It manages a BOSH or websocket
 * connection to an XMPP server and dispatches events to the user callbacks
 * as data arrives.
 *
 * It supports various authentication mechanisms (e.g. SASL PLAIN, SASL SCRAM),
 * and more can be added via
 * {@link Connection#registerSASLMechanisms|registerSASLMechanisms()}.
 *
 * After creating a Connection object, the user will typically
 * call {@link Connection#connect|connect()} with a user supplied callback
 * to handle connection level events like authentication failure,
 * disconnection, or connection complete.
 *
 * The user will also have several event handlers defined by using
 * {@link Connection#addHandler|addHandler()} and
 * {@link Connection#addTimedHandler|addTimedHandler()}.
 * These will allow the user code to respond to interesting stanzas or do
 * something periodically with the connection. These handlers will be active
 * once authentication is finished.
 *
 * To send data to the connection, use {@link Connection#send|send()}.
 *
 * @memberof Strophe
 */
class Connection {
    /**
     * @typedef {Object.<string, string>} Cookie
     * @typedef {Cookie|Object.<string, Cookie>} Cookies
     */


    /**
     * Create and initialize a {@link Connection} object.
     *
     * The transport-protocol for this connection will be chosen automatically
     * based on the given service parameter. URLs starting with "ws://" or
     * "wss://" will use WebSockets, URLs starting with "http://", "https://"
     * or without a protocol will use [BOSH](https://xmpp.org/extensions/xep-0124.html).
     *
     * To make Strophe connect to the current host you can leave out the protocol
     * and host part and just pass the path:
     *
     *  const conn = new Strophe.Connection("/http-bind/");
     *
     * @param {string} service - The BOSH or WebSocket service URL.
     * @param {ConnectionOptions} options - A object containing configuration options
     */
    constructor(service, options = {}) {
        // The service URL
        this.service = service;
        // Configuration options
        this.options = options;

        this.setProtocol();

        /* The connected JID. */
        this.jid = '';
        /* the JIDs domain */
        this.domain = null;
        /* stream:features */
        this.features = null;

        // SASL
        /**
         * @typedef {Object.<string, any>} SASLData
         * @property {Object} [SASLData.keys]
         */

        /** @type {SASLData} */
        this._sasl_data = {};
        this.do_bind = false;
        this.do_session = false;

        /** @type {Object.<string, SASLMechanism>} */
        this.mechanisms = {};

        /** @type {TimedHandler[]} */
        this.timedHandlers = [];

        /** @type {Handler[]} */
        this.handlers = [];

        /** @type {TimedHandler[]} */
        this.removeTimeds = [];

        /** @type {Handler[]} */
        this.removeHandlers = [];

        /** @type {TimedHandler[]} */
        this.addTimeds = [];

        /** @type {Handler[]} */
        this.addHandlers = [];

        this.protocolErrorHandlers = {
            /** @type {Object.<number, Function>} */
            'HTTP': {},
            /** @type {Object.<number, Function>} */
            'websocket': {},
        };

        this._idleTimeout = null;
        this._disconnectTimeout = null;

        this.authenticated = false;
        this.connected = false;
        this.disconnecting = false;
        this.do_authentication = true;
        this.paused = false;
        this.restored = false;

        /** @type {(Element|'restart')[]} */
        this._data = [];
        this._uniqueId = 0;

        this._sasl_success_handler = null;
        this._sasl_failure_handler = null;
        this._sasl_challenge_handler = null;

        // Max retries before disconnecting
        this.maxRetries = 5;

        // Call onIdle callback every 1/10th of a second
        this._idleTimeout = setTimeout(() => this._onIdle(), 100);

        addCookies(this.options.cookies);
        this.registerSASLMechanisms(this.options.mechanisms);

        // A client must always respond to incoming IQ "set" and "get" stanzas.
        // See https://datatracker.ietf.org/doc/html/rfc6120#section-8.2.3
        //
        // This is a fallback handler which gets called when no other handler
        // was called for a received IQ "set" or "get".
        this.iqFallbackHandler = new Handler(
            /**
             * @param {Element} iq
             */
            (iq) =>
                this.send(
                    $iq({ type: 'error', id: iq.getAttribute('id') })
                        .c('error', { 'type': 'cancel' })
                        .c('service-unavailable', { 'xmlns': NS.STANZAS })
                ),
            null,
            'iq',
            ['get', 'set']
        );

        // initialize plugins
        for (const k in connectionPlugins) {
            if (Object.prototype.hasOwnProperty.call(connectionPlugins, k)) {
                const F = function () {};
                F.prototype = connectionPlugins[k];
                // @ts-ignore
                this[k] = new F();
                // @ts-ignore
                this[k].init(this);
            }
        }
    }

    /**
     * Extends the Connection object with the given plugin.
     * @param {string} name - The name of the extension.
     * @param {Object} ptype - The plugin's prototype.
     */
    static addConnectionPlugin(name, ptype) {
        connectionPlugins[name] = ptype;
    }

    /**
     * Select protocal based on this.options or this.service
     */
    setProtocol() {
        const proto = this.options.protocol || '';
        if (this.options.worker) {
            this._proto = new WorkerWebsocket(this);
        } else if (
            this.service.indexOf('ws:') === 0 ||
            this.service.indexOf('wss:') === 0 ||
            proto.indexOf('ws') === 0
        ) {
            this._proto = new Websocket(this);
        } else {
            this._proto = new Bosh(this);
        }
    }

    /**
     * Reset the connection.
     *
     * This function should be called after a connection is disconnected
     * before that connection is reused.
     */
    reset() {
        this._proto._reset();

        // SASL
        this.do_session = false;
        this.do_bind = false;

        // handler lists
        this.timedHandlers = [];
        this.handlers = [];
        this.removeTimeds = [];
        this.removeHandlers = [];
        this.addTimeds = [];
        this.addHandlers = [];

        this.authenticated = false;
        this.connected = false;
        this.disconnecting = false;
        this.restored = false;
        this._data = [];
        /** @type {Request[]} */
        this._requests = [];
        this._uniqueId = 0;
    }

    /**
     * Pause the request manager.
     *
     * This will prevent Strophe from sending any more requests to the
     * server.  This is very useful for temporarily pausing
     * BOSH-Connections while a lot of send() calls are happening quickly.
     * This causes Strophe to send the data in a single request, saving
     * many request trips.
     */
    pause() {
        this.paused = true;
    }

    /**
     * Resume the request manager.
     *
     * This resumes after pause() has been called.
     */
    resume() {
        this.paused = false;
    }

    /**
     * Generate a unique ID for use in <iq/> elements.
     *
     * All <iq/> stanzas are required to have unique id attributes.  This
     * function makes creating these easy.  Each connection instance has
     * a counter which starts from zero, and the value of this counter
     * plus a colon followed by the suffix becomes the unique id. If no
     * suffix is supplied, the counter is used as the unique id.
     *
     * Suffixes are used to make debugging easier when reading the stream
     * data, and their use is recommended.  The counter resets to 0 for
     * every new connection for the same reason.  For connections to the
     * same server that authenticate the same way, all the ids should be
     * the same, which makes it easy to see changes.  This is useful for
     * automated testing as well.
     *
     * @param {string} suffix - A optional suffix to append to the id.
     * @returns {string} A unique string to be used for the id attribute.
     */
    getUniqueId(suffix) {
        const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
            const r = (Math.random() * 16) | 0,
                v = c === 'x' ? r : (r & 0x3) | 0x8;
            return v.toString(16);
        });
        if (typeof suffix === 'string' || typeof suffix === 'number') {
            return uuid + ':' + suffix;
        } else {
            return uuid + '';
        }
    }

    /**
     * Register a handler function for when a protocol (websocker or HTTP)
     * error occurs.
     *
     * NOTE: Currently only HTTP errors for BOSH requests are handled.
     * Patches that handle websocket errors would be very welcome.
     *
     * @example
     *  function onError(err_code){
     *    //do stuff
     *  }
     *
     *  const conn = Strophe.connect('http://example.com/http-bind');
     *  conn.addProtocolErrorHandler('HTTP', 500, onError);
     *  // Triggers HTTP 500 error and onError handler will be called
     *  conn.connect('user_jid@incorrect_jabber_host', 'secret', onConnect);
     *
     * @param {'HTTP'|'websocket'} protocol - 'HTTP' or 'websocket'
     * @param {number} status_code - Error status code (e.g 500, 400 or 404)
     * @param {Function} callback - Function that will fire on Http error
     */
    addProtocolErrorHandler(protocol, status_code, callback) {
        this.protocolErrorHandlers[protocol][status_code] = callback;
    }

    /**
     * @typedef {Object} Password
     * @property {string} Password.name
     * @property {string} Password.ck
     * @property {string} Password.sk
     * @property {number} Password.iter
     * @property {string} Password.salt
     */

    /**
     * Starts the connection process.
     *
     * As the connection process proceeds, the user supplied callback will
     * be triggered multiple times with status updates.  The callback
     * should take two arguments - the status code and the error condition.
     *
     * The status code will be one of the values in the Strophe.Status
     * constants.  The error condition will be one of the conditions
     * defined in RFC 3920 or the condition 'strophe-parsererror'.
     *
     * The Parameters _wait_, _hold_ and _route_ are optional and only relevant
     * for BOSH connections. Please see XEP 124 for a more detailed explanation
     * of the optional parameters.
     *
     * @param {string} jid - The user's JID.  This may be a bare JID,
     *     or a full JID.  If a node is not supplied, SASL OAUTHBEARER or
     *     SASL ANONYMOUS authentication will be attempted (OAUTHBEARER will
     *     process the provided password value as an access token).
     *   (String or Object) pass - The user's password, or an object containing
     *     the users SCRAM client and server keys, in a fashion described as follows:
     *
     *     { name: String, representing the hash used (eg. SHA-1),
     *       salt: String, base64 encoded salt used to derive the client key,
     *       iter: Int,    the iteration count used to derive the client key,
     *       ck:   String, the base64 encoding of the SCRAM client key
     *       sk:   String, the base64 encoding of the SCRAM server key
     *     }
     * @param {string|Password} pass - The user password
     * @param {Function} callback - The connect callback function.
     * @param {number} [wait] - The optional HTTPBIND wait value.  This is the
     *     time the server will wait before returning an empty result for
     *     a request.  The default setting of 60 seconds is recommended.
     * @param {number} [hold] - The optional HTTPBIND hold value.  This is the
     *     number of connections the server will hold at one time.  This
     *     should almost always be set to 1 (the default).
     * @param {string} [route] - The optional route value.
     * @param {string} [authcid] - The optional alternative authentication identity
     *     (username) if intending to impersonate another user.
     *     When using the SASL-EXTERNAL authentication mechanism, for example
     *     with client certificates, then the authcid value is used to
     *     determine whether an authorization JID (authzid) should be sent to
     *     the server. The authzid should NOT be sent to the server if the
     *     authzid and authcid are the same. So to prevent it from being sent
     *     (for example when the JID is already contained in the client
     *     certificate), set authcid to that same JID. See XEP-178 for more
     *     details.
     *  @param {number} [disconnection_timeout=3000] - The optional disconnection timeout
     *     in milliseconds before _doDisconnect will be called.
     */
    connect(jid, pass, callback, wait, hold, route, authcid, disconnection_timeout = 3000) {
        this.jid = jid;
        /** Authorization identity */
        this.authzid = getBareJidFromJid(this.jid);
        /** Authentication identity (User name) */
        this.authcid = authcid || getNodeFromJid(this.jid);
        /** Authentication identity (User password) */
        this.pass = pass;

        /**
         * The SASL SCRAM client and server keys. This variable will be populated with a non-null
         * object of the above described form after a successful SCRAM connection
         */
        this.scram_keys = null;

        this.connect_callback = callback;
        this.disconnecting = false;
        this.connected = false;
        this.authenticated = false;
        this.restored = false;
        this.disconnection_timeout = disconnection_timeout;

        // parse jid for domain
        this.domain = getDomainFromJid(this.jid);

        this._changeConnectStatus(Status.CONNECTING, null);

        this._proto._connect(wait, hold, route);
    }

    /**
     * Attach to an already created and authenticated BOSH session.
     *
     * This function is provided to allow Strophe to attach to BOSH
     * sessions which have been created externally, perhaps by a Web
     * application.  This is often used to support auto-login type features
     * without putting user credentials into the page.
     *
     * @param {string|Function} jid - The full JID that is bound by the session.
     * @param {string} [sid] - The SID of the BOSH session.
     * @param {number} [rid] - The current RID of the BOSH session.  This RID
     *     will be used by the next request.
     * @param {Function} [callback] - The connect callback function.
     * @param {number} [wait] - The optional HTTPBIND wait value.  This is the
     *     time the server will wait before returning an empty result for
     *     a request.  The default setting of 60 seconds is recommended.
     *     Other settings will require tweaks to the Strophe.TIMEOUT value.
     * @param {number} [hold] - The optional HTTPBIND hold value.  This is the
     *     number of connections the server will hold at one time.  This
     *     should almost always be set to 1 (the default).
     * @param {number} [wind] - The optional HTTBIND window value.  This is the
     *     allowed range of request ids that are valid.  The default is 5.
     */
    attach(jid, sid, rid, callback, wait, hold, wind) {
        if (this._proto instanceof Bosh && typeof jid === 'string') {
            return this._proto._attach(jid, sid, rid, callback, wait, hold, wind);
        } else if (this._proto instanceof WorkerWebsocket && typeof jid === 'function') {
            const callback = jid;
            return this._proto._attach(callback);
        } else {
            throw new SessionError('The "attach" method is not available for your connection protocol');
        }
    }

    /**
     * Attempt to restore a cached BOSH session.
     *
     * This function is only useful in conjunction with providing the
     * "keepalive":true option when instantiating a new {@link Connection}.
     *
     * When "keepalive" is set to true, Strophe will cache the BOSH tokens
     * RID (Request ID) and SID (Session ID) and then when this function is
     * called, it will attempt to restore the session from those cached
     * tokens.
     *
     * This function must therefore be called instead of connect or attach.
     *
     * For an example on how to use it, please see examples/restore.js
     *
     * @param {string} jid - The user's JID.  This may be a bare JID or a full JID.
     * @param {Function} callback - The connect callback function.
     * @param {number} [wait] - The optional HTTPBIND wait value.  This is the
     *     time the server will wait before returning an empty result for
     *     a request.  The default setting of 60 seconds is recommended.
     * @param {number} [hold] - The optional HTTPBIND hold value.  This is the
     *     number of connections the server will hold at one time.  This
     *     should almost always be set to 1 (the default).
     * @param {number} [wind] - The optional HTTBIND window value.  This is the
     *     allowed range of request ids that are valid.  The default is 5.
     */
    restore(jid, callback, wait, hold, wind) {
        if (!(this._proto instanceof Bosh) || !this._sessionCachingSupported()) {
            throw new SessionError('The "restore" method can only be used with a BOSH connection.');
        }

        if (this._sessionCachingSupported()) {
            this._proto._restore(jid, callback, wait, hold, wind);
        }
    }

    /**
     * Checks whether sessionStorage and JSON are supported and whether we're
     * using BOSH.
     */
    _sessionCachingSupported() {
        if (this._proto instanceof Bosh) {
            if (!JSON) {
                return false;
            }
            try {
                sessionStorage.setItem('_strophe_', '_strophe_');
                sessionStorage.removeItem('_strophe_');
            } catch (_e) {
                return false;
            }
            return true;
        }
        return false;
    }

    /**
     * User overrideable function that receives XML data coming into the
     * connection.
     *
     * The default function does nothing.  User code can override this with
     * > Connection.xmlInput = function (elem) {
     * >   (user code)
     * > };
     *
     * Due to limitations of current Browsers' XML-Parsers the opening and closing
     * <stream> tag for WebSocket-Connoctions will be passed as selfclosing here.
     *
     * BOSH-Connections will have all stanzas wrapped in a <body> tag. See
     * <Bosh.strip> if you want to strip this tag.
     *
     * @param {Node|MessageEvent} _elem - The XML data received by the connection.
     */
    xmlInput(_elem) {
        return;
    }

    /**
     * User overrideable function that receives XML data sent to the
     * connection.
     *
     * The default function does nothing.  User code can override this with
     * > Connection.xmlOutput = function (elem) {
     * >   (user code)
     * > };
     *
     * Due to limitations of current Browsers' XML-Parsers the opening and closing
     * <stream> tag for WebSocket-Connoctions will be passed as selfclosing here.
     *
     * BOSH-Connections will have all stanzas wrapped in a <body> tag. See
     * <Bosh.strip> if you want to strip this tag.
     *
     * @param {Element} _elem - The XMLdata sent by the connection.
     */
    xmlOutput(_elem) {
        return;
    }

    /**
     * User overrideable function that receives raw data coming into the
     * connection.
     *
     * The default function does nothing.  User code can override this with
     * > Connection.rawInput = function (data) {
     * >   (user code)
     * > };
     *
     * @param {string} _data - The data received by the connection.
     */
    rawInput(_data) {
        return;
    }

    /**
     * User overrideable function that receives raw data sent to the
     * connection.
     *
     * The default function does nothing.  User code can override this with
     * > Connection.rawOutput = function (data) {
     * >   (user code)
     * > };
     *
     * @param {string} _data - The data sent by the connection.
     */
    rawOutput(_data) {
        return;
    }

    /**
     * User overrideable function that receives the new valid rid.
     *
     * The default function does nothing. User code can override this with
     * > Connection.nextValidRid = function (rid) {
     * >    (user code)
     * > };
     *
     * @param {number} _rid - The next valid rid
     */
    nextValidRid(_rid) {
        return;
    }

    /**
     * Send a stanza.
     *
     * This function is called to push data onto the send queue to
     * go out over the wire.  Whenever a request is sent to the BOSH
     * server, all pending data is sent and the queue is flushed.
     *
     * @param {Element|Builder|Element[]|Builder[]} stanza - The stanza to send
     */
    send(stanza) {
        if (stanza === null) return;

        if (Array.isArray(stanza)) {
            stanza.forEach((s) => this._queueData(s instanceof Builder ? s.tree() : s));
        } else {
            const el = stanza instanceof Builder ? stanza.tree() : stanza;
            this._queueData(el);
        }
        this._proto._send();
    }

    /**
     * Immediately send any pending outgoing data.
     *
     * Normally send() queues outgoing data until the next idle period
     * (100ms), which optimizes network use in the common cases when
     * several send()s are called in succession. flush() can be used to
     * immediately send all pending data.
     */
    flush() {
        // cancel the pending idle period and run the idle function
        // immediately
        clearTimeout(this._idleTimeout);
        this._onIdle();
    }

    /**
     * Helper function to send presence stanzas. The main benefit is for
     * sending presence stanzas for which you expect a responding presence
     * stanza with the same id (for example when leaving a chat room).
     *
     * @param {Element} stanza - The stanza to send.
     * @param {Function} [callback] - The callback function for a successful request.
     * @param {Function} [errback] - The callback function for a failed or timed
     *    out request.  On timeout, the stanza will be null.
     * @param {number} [timeout] - The time specified in milliseconds for a
     *    timeout to occur.
     * @return {string} The id used to send the presence.
     */
    sendPresence(stanza, callback, errback, timeout) {
        /** @type {TimedHandler} */
        let timeoutHandler = null;

        const el = stanza instanceof Builder ? stanza.tree() : stanza;

        let id = el.getAttribute('id');
        if (!id) {
            // inject id if not found
            id = this.getUniqueId('sendPresence');
            el.setAttribute('id', id);
        }

        if (typeof callback === 'function' || typeof errback === 'function') {
            const handler = this.addHandler(
                /** @param {Element} stanza */
                (stanza) => {
                    // remove timeout handler if there is one
                    if (timeoutHandler) this.deleteTimedHandler(timeoutHandler);

                    if (stanza.getAttribute('type') === 'error') {
                        errback?.(stanza);
                    } else if (callback) {
                        callback(stanza);
                    }
                },
                null,
                'presence',
                null,
                id
            );

            // if timeout specified, set up a timeout handler.
            if (timeout) {
                timeoutHandler = this.addTimedHandler(timeout, () => {
                    // get rid of normal handler
                    this.deleteHandler(handler);
                    // call errback on timeout with null stanza
                    errback?.(null);
                    return false;
                });
            }
        }
        this.send(el);
        return id;
    }

    /**
     * Helper function to send IQ stanzas.
     *
     * @param {Element|Builder} stanza - The stanza to send.
     * @param {Function} [callback] - The callback function for a successful request.
     * @param {Function} [errback] - The callback function for a failed or timed
     *     out request.  On timeout, the stanza will be null.
     * @param {number} [timeout] - The time specified in milliseconds for a
     *     timeout to occur.
     * @return {string} The id used to send the IQ.
     */
    sendIQ(stanza, callback, errback, timeout) {
        /** @type {TimedHandler} */
        let timeoutHandler = null;

        const el = stanza instanceof Builder ? stanza.tree() : stanza;

        let id = el.getAttribute('id');
        if (!id) {
            // inject id if not found
            id = this.getUniqueId('sendIQ');
            el.setAttribute('id', id);
        }

        if (typeof callback === 'function' || typeof errback === 'function') {
            const handler = this.addHandler(
                /** @param {Element} stanza */
                (stanza) => {
                    // remove timeout handler if there is one
                    if (timeoutHandler) this.deleteTimedHandler(timeoutHandler);

                    const iqtype = stanza.getAttribute('type');
                    if (iqtype === 'result') {
                        callback?.(stanza);
                    } else if (iqtype === 'error') {
                        errback?.(stanza);
                    } else {
                        const error = new Error(`Got bad IQ type of ${iqtype}`);
                        error.name = 'StropheError';
                        throw error;
                    }
                },
                null,
                'iq',
                ['error', 'result'],
                id
            );

            // if timeout specified, set up a timeout handler.
            if (timeout) {
                timeoutHandler = this.addTimedHandler(timeout, () => {
                    // get rid of normal handler
                    this.deleteHandler(handler);
                    // call errback on timeout with null stanza
                    errback?.(null);
                    return false;
                });
            }
        }
        this.send(el);
        return id;
    }

    /**
     * Queue outgoing data for later sending.  Also ensures that the data
     * is a DOMElement.
     * @private
     * @param {Element} element
     */
    _queueData(element) {
        if (element === null || !element.tagName || !element.childNodes) {
            const error = new Error('Cannot queue non-DOMElement.');
            error.name = 'StropheError';
            throw error;
        }
        this._data.push(element);
    }

    /**
     * Send an xmpp:restart stanza.
     * @private
     */
    _sendRestart() {
        this._data.push('restart');
        this._proto._sendRestart();
        this._idleTimeout = setTimeout(() => this._onIdle(), 100);
    }

    /**
     * Add a timed handler to the connection.
     *
     * This function adds a timed handler.  The provided handler will
     * be called every period milliseconds until it returns false,
     * the connection is terminated, or the handler is removed.  Handlers
     * that wish to continue being invoked should return true.
     *
     * Because of method binding it is necessary to save the result of
     * this function if you wish to remove a handler with
     * deleteTimedHandler().
     *
     * Note that user handlers are not active until authentication is
     * successful.
     *
     * @param {number} period - The period of the handler.
     * @param {Function} handler - The callback function.
     * @return {TimedHandler} A reference to the handler that can be used to remove it.
     */
    addTimedHandler(period, handler) {
        const thand = new TimedHandler(period, handler);
        this.addTimeds.push(thand);
        return thand;
    }

    /**
     * Delete a timed handler for a connection.
     *
     * This function removes a timed handler from the connection.  The
     * handRef parameter is *not* the function passed to addTimedHandler(),
     * but is the reference returned from addTimedHandler().
     * @param {TimedHandler} handRef - The handler reference.
     */
    deleteTimedHandler(handRef) {
        // this must be done in the Idle loop so that we don't change
        // the handlers during iteration
        this.removeTimeds.push(handRef);
    }

    /**
     * @typedef {Object} HandlerOptions
     * @property {boolean} [HandlerOptions.matchBareFromJid]
     * @property {boolean} [HandlerOptions.ignoreNamespaceFragment]
     */

    /**
     * Add a stanza handler for the connection.
     *
     * This function adds a stanza handler to the connection.  The
     * handler callback will be called for any stanza that matches
     * the parameters.  Note that if multiple parameters are supplied,
     * they must all match for the handler to be invoked.
     *
     * The handler will receive the stanza that triggered it as its argument.
     * *The handler should return true if it is to be invoked again;
     * returning false will remove the handler after it returns.*
     *
     * As a convenience, the ns parameters applies to the top level element
     * and also any of its immediate children.  This is primarily to make
     * matching /iq/query elements easy.
     *
     * ### Options
     *
     * With the options argument, you can specify boolean flags that affect how
     * matches are being done.
     *
     * Currently two flags exist:
     *
     * * *matchBareFromJid*:
     *     When set to true, the from parameter and the
     *     from attribute on the stanza will be matched as bare JIDs instead
     *     of full JIDs. To use this, pass {matchBareFromJid: true} as the
     *     value of options. The default value for matchBareFromJid is false.
     *
     * * *ignoreNamespaceFragment*:
     *     When set to true, a fragment specified on the stanza's namespace
     *     URL will be ignored when it's matched with the one configured for
     *     the handler.
     *
     *     This means that if you register like this:
     *
     *     >   connection.addHandler(
     *     >       handler,
     *     >       'http://jabber.org/protocol/muc',
     *     >       null, null, null, null,
     *     >       {'ignoreNamespaceFragment': true}
     *     >   );
     *
     *     Then a stanza with XML namespace of
     *     'http://jabber.org/protocol/muc#user' will also be matched. If
     *     'ignoreNamespaceFragment' is false, then only stanzas with
     *     'http://jabber.org/protocol/muc' will be matched.
     *
     * ### Deleting the handler
     *
     * The return value should be saved if you wish to remove the handler
     * with `deleteHandler()`.
     *
     * @param {Function} handler - The user callback.
     * @param {string} ns - The namespace to match.
     * @param {string} name - The stanza name to match.
     * @param {string|string[]} type - The stanza type (or types if an array) to match.
     * @param {string} [id] - The stanza id attribute to match.
     * @param {string} [from] - The stanza from attribute to match.
     * @param {HandlerOptions} [options] - The handler options
     * @return {Handler} A reference to the handler that can be used to remove it.
     */
    addHandler(handler, ns, name, type, id, from, options) {
        const hand = new Handler(handler, ns, name, type, id, from, options);
        this.addHandlers.push(hand);
        return hand;
    }

    /**
     * Delete a stanza handler for a connection.
     *
     * This function removes a stanza handler from the connection.  The
     * handRef parameter is *not* the function passed to addHandler(),
     * but is the reference returned from addHandler().
     *
     * @param {Handler} handRef - The handler reference.
     */
    deleteHandler(handRef) {
        // this must be done in the Idle loop so that we don't change
        // the handlers during iteration
        this.removeHandlers.push(handRef);
        // If a handler is being deleted while it is being added,
        // prevent it from getting added
        const i = this.addHandlers.indexOf(handRef);
        if (i >= 0) {
            this.addHandlers.splice(i, 1);
        }
    }

    /**
     * Register the SASL mechanisms which will be supported by this instance of
     * Connection (i.e. which this XMPP client will support).
     * @param {SASLMechanism[]} mechanisms - Array of objects with SASLMechanism prototypes
     */
    registerSASLMechanisms(mechanisms) {
        this.mechanisms = {};
        (
            mechanisms || [
                SASLAnonymous,
                SASLExternal,
                SASLOAuthBearer,
                SASLXOAuth2,
                SASLPlain,
                SASLSHA1,
                SASLSHA256,
                SASLSHA384,
                SASLSHA512,
            ]
        ).forEach((m) => this.registerSASLMechanism(m));
    }

    /**
     * Register a single SASL mechanism, to be supported by this client.
     * @param {any} Mechanism - Object with a Strophe.SASLMechanism prototype
     */
    registerSASLMechanism(Mechanism) {
        const mechanism = new Mechanism();
        this.mechanisms[mechanism.mechname] = mechanism;
    }

    /**
     * Start the graceful disconnection process.
     *
     * This function starts the disconnection process.  This process starts
     * by sending unavailable presence and sending BOSH body of type
     * terminate.  A timeout handler makes sure that disconnection happens
     * even if the BOSH server does not respond.
     * If the Connection object isn't connected, at least tries to abort all pending requests
     * so the connection object won't generate successful requests (which were already opened).
     *
     * The user supplied connection callback will be notified of the
     * progress as this process happens.
     *
     * @param {string} [reason] - The reason the disconnect is occuring.
     */
    disconnect(reason) {
        this._changeConnectStatus(Status.DISCONNECTING, reason);
        if (reason) {
            log.info('Disconnect was called because: ' + reason);
        } else {
            log.debug('Disconnect was called');
        }
        if (this.connected) {
            let pres = null;
            this.disconnecting = true;
            if (this.authenticated) {
                pres = $pres({
                    'xmlns': NS.CLIENT,
                    'type': 'unavailable',
                });
            }
            // setup timeout handler
            this._disconnectTimeout = this._addSysTimedHandler(
                this.disconnection_timeout,
                this._onDisconnectTimeout.bind(this)
            );
            this._proto._disconnect(pres);
        } else {
            log.debug('Disconnect was called before Strophe connected to the server');
            this._proto._abortAllRequests();
            this._doDisconnect();
        }
    }

    /**
     * _Private_ helper function that makes sure plugins and the user's
     * callback are notified of connection status changes.
     * @param {number} status - the new connection status, one of the values
     *     in Strophe.Status
     * @param {string|null} [condition] - the error condition
     * @param {Element} [elem] - The triggering stanza.
     */
    _changeConnectStatus(status, condition, elem) {
        // notify all plugins listening for status changes
        for (const k in connectionPlugins) {
            if (Object.prototype.hasOwnProperty.call(connectionPlugins, k)) {
                // @ts-ignore
                const plugin = this[k];
                if (plugin.statusChanged) {
                    try {
                        plugin.statusChanged(status, condition);
                    } catch (err) {
                        log.error(`${k} plugin caused an exception changing status: ${err}`);
                    }
                }
            }
        }
        // notify the user's callback
        if (this.connect_callback) {
            try {
                this.connect_callback(status, condition, elem);
            } catch (e) {
                handleError(e);
                log.error(`User connection callback caused an exception: ${e}`);
            }
        }
    }

    /**
     * _Private_ function to disconnect.
     *
     * This is the last piece of the disconnection logic.  This resets the
     * connection and alerts the user's connection callback.
     * @param {string|null} [condition] - the error condition
     */
    _doDisconnect(condition) {
        if (typeof this._idleTimeout === 'number') {
            clearTimeout(this._idleTimeout);
        }

        // Cancel Disconnect Timeout
        if (this._disconnectTimeout !== null) {
            this.deleteTimedHandler(this._disconnectTimeout);
            this._disconnectTimeout = null;
        }

        log.debug('_doDisconnect was called');
        this._proto._doDisconnect();

        this.authenticated = false;
        this.disconnecting = false;
        this.restored = false;

        // delete handlers
        this.handlers = [];
        this.timedHandlers = [];
        this.removeTimeds = [];
        this.removeHandlers = [];
        this.addTimeds = [];
        this.addHandlers = [];

        // tell the parent we disconnected
        this._changeConnectStatus(Status.DISCONNECTED, condition);
        this.connected = false;
    }

    /**
     * _Private_ handler to processes incoming data from the the connection.
     *
     * Except for _connect_cb handling the initial connection request,
     * this function handles the incoming data for all requests.  This
     * function also fires stanza handlers that match each incoming
     * stanza.
     * @param {Element | Request} req - The request that has data ready.
     * @param {string} [raw] - The stanza as raw string.
     */
    _dataRecv(req, raw) {
        const elem = /** @type {Element} */ (
            '_reqToData' in this._proto ? this._proto._reqToData(/** @type {Request} */ (req)) : req
        );
        if (elem === null) {
            return;
        }

        if (this.xmlInput !== Connection.prototype.xmlInput) {
            if (elem.nodeName === this._proto.strip && elem.childNodes.length) {
                this.xmlInput(elem.childNodes[0]);
            } else {
                this.xmlInput(elem);
            }
        }
        if (this.rawInput !== Connection.prototype.rawInput) {
            if (raw) {
                this.rawInput(raw);
            } else {
                this.rawInput(Builder.serialize(elem));
            }
        }

        // remove handlers scheduled for deletion
        while (this.removeHandlers.length > 0) {
            const hand = this.removeHandlers.pop();
            const i = this.handlers.indexOf(hand);
            if (i >= 0) {
                this.handlers.splice(i, 1);
            }
        }

        // add handlers scheduled for addition
        while (this.addHandlers.length > 0) {
            this.handlers.push(this.addHandlers.pop());
        }

        // handle graceful disconnect
        if (this.disconnecting && this._proto._emptyQueue()) {
            this._doDisconnect();
            return;
        }

        const type = elem.getAttribute('type');
        if (type !== null && type === 'terminate') {
            // Don't process stanzas that come in after disconnect
            if (this.disconnecting) {
                return;
            }
            // an error occurred
            let cond = elem.getAttribute('condition');
            const conflict = elem.getElementsByTagName('conflict');
            if (cond !== null) {
                if (cond === 'remote-stream-error' && conflict.length > 0) {
                    cond = 'conflict';
                }
                this._changeConnectStatus(Status.CONNFAIL, cond);
            } else {
                this._changeConnectStatus(Status.CONNFAIL, ErrorCondition.UNKNOWN_REASON);
            }
            this._doDisconnect(cond);
            return;
        }

        // send each incoming stanza through the handler chain
        forEachChild(
            elem,
            null,
            /** @param {Element} child */
            (child) => {
                const matches = [];
                this.handlers = this.handlers.reduce((handlers, handler) => {
                    try {
                        if (handler.isMatch(child) && (this.authenticated || !handler.user)) {
                            if (handler.run(child)) {
                                handlers.push(handler);
                            }
                            matches.push(handler);
                        } else {
                            handlers.push(handler);
                        }
                    } catch (e) {
                        // if the handler throws an exception, we consider it as false
                        log.warn('Removing Strophe handlers due to uncaught exception: ' + e.message);
                    }

                    return handlers;
                }, []);

                // If no handler was fired for an incoming IQ with type="set",
                // then we return an IQ error stanza with service-unavailable.
                if (!matches.length && this.iqFallbackHandler.isMatch(child)) {
                    this.iqFallbackHandler.run(child);
                }
            }
        );
    }

    /**
     * @callback connectionCallback
     * @param {Connection} connection
     */

    /**
     * _Private_ handler for initial connection request.
     *
     * This handler is used to process the initial connection request
     * response from the BOSH server. It is used to set up authentication
     * handlers and start the authentication process.
     *
     * SASL authentication will be attempted if available, otherwise
     * the code will fall back to legacy authentication.
     *
     * @param {Element | Request} req - The current request.
     * @param {connectionCallback} _callback - low level (xmpp) connect callback function.
     *     Useful for plugins with their own xmpp connect callback (when they
     *     want to do something special).
     * @param {string} [raw] - The stanza as raw string.
     */
    _connect_cb(req, _callback, raw) {
        log.debug('_connect_cb was called');
        this.connected = true;

        let bodyWrap;
        try {
            bodyWrap = /** @type {Element} */ (
                '_reqToData' in this._proto ? this._proto._reqToData(/** @type {Request} */ (req)) : req
            );
        } catch (e) {
            if (e.name !== ErrorCondition.BAD_FORMAT) {
                throw e;
            }
            this._changeConnectStatus(Status.CONNFAIL, ErrorCondition.BAD_FORMAT);
            this._doDisconnect(ErrorCondition.BAD_FORMAT);
        }
        if (!bodyWrap) {
            return;
        }

        if (this.xmlInput !== Connection.prototype.xmlInput) {
            if (bodyWrap.nodeName === this._proto.strip && bodyWrap.childNodes.length) {
                this.xmlInput(bodyWrap.childNodes[0]);
            } else {
                this.xmlInput(bodyWrap);
            }
        }
        if (this.rawInput !== Connection.prototype.rawInput) {
            if (raw) {
                this.rawInput(raw);
            } else {
                this.rawInput(Builder.serialize(bodyWrap));
            }
        }

        const conncheck = this._proto._connect_cb(bodyWrap);
        if (conncheck === Status.CONNFAIL) {
            return;
        }

        // Check for the stream:features tag
        let hasFeatures;
        if (bodyWrap.getElementsByTagNameNS) {
            hasFeatures = bodyWrap.getElementsByTagNameNS(NS.STREAM, 'features').length > 0;
        } else {
            hasFeatures =
                bodyWrap.getElementsByTagName('stream:features').length > 0 ||
                bodyWrap.getElementsByTagName('features').length > 0;
        }
        if (!hasFeatures) {
            this._proto._no_auth_received(_callback);
            return;
        }

        const matched = Array.from(bodyWrap.getElementsByTagName('mechanism'))
            .map((m) => this.mechanisms[m.textContent])
            .filter((m) => m);

        if (matched.length === 0) {
            if (bodyWrap.getElementsByTagName('auth').length === 0) {
                // There are no matching SASL mechanisms and also no legacy
                // auth available.
                this._proto._no_auth_received(_callback);
                return;
            }
        }
        if (this.do_authentication !== false) {
            this.authenticate(matched);
        }
    }

    /**
     * Sorts an array of objects with prototype SASLMechanism according to
     * their priorities.
     * @param {SASLMechanism[]} mechanisms - Array of SASL mechanisms.
     */
    sortMechanismsByPriority(mechanisms) {
        // Sorting mechanisms according to priority.
        for (let i = 0; i < mechanisms.length - 1; ++i) {
            let higher = i;
            for (let j = i + 1; j < mechanisms.length; ++j) {
                if (mechanisms[j].priority > mechanisms[higher].priority) {
                    higher = j;
                }
            }
            if (higher !== i) {
                const swap = mechanisms[i];
                mechanisms[i] = mechanisms[higher];
                mechanisms[higher] = swap;
            }
        }
        return mechanisms;
    }

    /**
     * Set up authentication
     *
     * Continues the initial connection request by setting up authentication
     * handlers and starting the authentication process.
     *
     * SASL authentication will be attempted if available, otherwise
     * the code will fall back to legacy authentication.
     *
     * @param {SASLMechanism[]} matched - Array of SASL mechanisms supported.
     */
    authenticate(matched) {
        if (!this._attemptSASLAuth(matched)) {
            this._attemptLegacyAuth();
        }
    }

    /**
     * Iterate through an array of SASL mechanisms and attempt authentication
     * with the highest priority (enabled) mechanism.
     *
     * @private
     * @param {SASLMechanism[]} mechanisms - Array of SASL mechanisms.
     * @return {Boolean} mechanism_found - true or false, depending on whether a
     *  valid SASL mechanism was found with which authentication could be started.
     */
    _attemptSASLAuth(mechanisms) {
        mechanisms = this.sortMechanismsByPriority(mechanisms || []);
        let mechanism_found = false;
        for (let i = 0; i < mechanisms.length; ++i) {
            if (!mechanisms[i].test(this)) {
                continue;
            }
            this._sasl_success_handler = this._addSysHandler(
                this._sasl_success_cb.bind(this),
                null,
                'success',
                null,
                null
            );
            this._sasl_failure_handler = this._addSysHandler(
                this._sasl_failure_cb.bind(this),
                null,
                'failure',
                null,
                null
            );
            this._sasl_challenge_handler = this._addSysHandler(
                this._sasl_challenge_cb.bind(this),
                null,
                'challenge',
                null,
                null
            );

            this._sasl_mechanism = mechanisms[i];
            this._sasl_mechanism.onStart(this);

            const request_auth_exchange = $build('auth', {
                'xmlns': NS.SASL,
                'mechanism': this._sasl_mechanism.mechname,
            });
            if (this._sasl_mechanism.isClientFirst) {
                const response = this._sasl_mechanism.clientChallenge(this);
                request_auth_exchange.t(btoa(/** @type {string} */ (response)));
            }
            this.send(request_auth_exchange.tree());
            mechanism_found = true;
            break;
        }
        return mechanism_found;
    }

    /**
     * _Private_ handler for the SASL challenge
     * @private
     * @param {Element} elem
     */
    async _sasl_challenge_cb(elem) {
        const challenge = atob(getText(elem));
        const response = await this._sasl_mechanism.onChallenge(this, challenge);
        const stanza = $build('response', { 'xmlns': NS.SASL });
        if (response) stanza.t(btoa(response));
        this.send(stanza.tree());
        return true;
    }

    /**
     * Attempt legacy (i.e. non-SASL) authentication.
     * @private
     */
    _attemptLegacyAuth() {
        if (getNodeFromJid(this.jid) === null) {
            // we don't have a node, which is required for non-anonymous
            // client connections
            this._changeConnectStatus(Status.CONNFAIL, ErrorCondition.MISSING_JID_NODE);
            this.disconnect(ErrorCondition.MISSING_JID_NODE);
        } else {
            // Fall back to legacy authentication
            this._changeConnectStatus(Status.AUTHENTICATING, null);
            this._addSysHandler(this._onLegacyAuthIQResult.bind(this), null, null, null, '_auth_1');
            this.send(
                $iq({
                    'type': 'get',
                    'to': this.domain,
                    'id': '_auth_1',
                })
                    .c('query', { xmlns: NS.AUTH })
                    .c('username', {})
                    .t(getNodeFromJid(this.jid))
                    .tree()
            );
        }
    }

    /**
     * _Private_ handler for legacy authentication.
     *
     * This handler is called in response to the initial <iq type='get'/>
     * for legacy authentication.  It builds an authentication <iq/> and
     * sends it, creating a handler (calling back to _auth2_cb()) to
     * handle the result
     * @private
     * @return {false} `false` to remove the handler.
     */
    _onLegacyAuthIQResult() {
        const pass = typeof this.pass === 'string' ? this.pass : '';

        // build plaintext auth iq
        const iq = $iq({ type: 'set', id: '_auth_2' })
            .c('query', { xmlns: NS.AUTH })
            .c('username', {})
            .t(getNodeFromJid(this.jid))
            .up()
            .c('password')
            .t(pass);

        if (!getResourceFromJid(this.jid)) {
            // since the user has not supplied a resource, we pick
            // a default one here.  unlike other auth methods, the server
            // cannot do this for us.
            this.jid = getBareJidFromJid(this.jid) + '/strophe';
        }
        iq.up().c('resource', {}).t(getResourceFromJid(this.jid));

        this._addSysHandler(this._auth2_cb.bind(this), null, null, null, '_auth_2');
        this.send(iq.tree());
        return false;
    }

    /**
     * _Private_ handler for succesful SASL authentication.
     * @private
     * @param {Element} elem - The matching stanza.
     * @return {false} `false` to remove the handler.
     */
    _sasl_success_cb(elem) {
        if (this._sasl_data['server-signature']) {
            let serverSignature;
            const success = atob(getText(elem));
            const attribMatch = /([a-z]+)=([^,]+)(,|$)/;
            const matches = success.match(attribMatch);
            if (matches[1] === 'v') {
                serverSignature = matches[2];
            }
            if (serverSignature !== this._sasl_data['server-signature']) {
                // remove old handlers
                this.deleteHandler(this._sasl_failure_handler);
                this._sasl_failure_handler = null;
                if (this._sasl_challenge_handler) {
                    this.deleteHandler(this._sasl_challenge_handler);
                    this._sasl_challenge_handler = null;
                }
                this._sasl_data = {};
                return this._sasl_failure_cb(null);
            }
        }
        log.info('SASL authentication succeeded.');

        if (this._sasl_data.keys) {
            this.scram_keys = this._sasl_data.keys;
        }

        if (this._sasl_mechanism) {
            this._sasl_mechanism.onSuccess();
        }
        // remove old handlers
        this.deleteHandler(this._sasl_failure_handler);
        this._sasl_failure_handler = null;
        if (this._sasl_challenge_handler) {
            this.deleteHandler(this._sasl_challenge_handler);
            this._sasl_challenge_handler = null;
        }
        /** @type {Handler[]} */
        const streamfeature_handlers = [];

        /**
         * @param {Handler[]} handlers
         * @param {Element} elem
         */
        const wrapper = (handlers, elem) => {
            while (handlers.length) {
                this.deleteHandler(handlers.pop());
            }
            this._onStreamFeaturesAfterSASL(elem);
            return false;
        };

        streamfeature_handlers.push(
            this._addSysHandler(
                /** @param {Element} elem */
                (elem) => wrapper(streamfeature_handlers, elem),
                null,
                'stream:features',
                null,
                null
            )
        );

        streamfeature_handlers.push(
            this._addSysHandler(
                /** @param {Element} elem */
                (elem) => wrapper(streamfeature_handlers, elem),
                NS.STREAM,
                'features',
                null,
                null
            )
        );

        // we must send an xmpp:restart now
        this._sendRestart();
        return false;
    }

    /**
     * @private
     * @param {Element} elem - The matching stanza.
     * @return {false} `false` to remove the handler.
     */
    _onStreamFeaturesAfterSASL(elem) {
        // save stream:features for future usage
        this.features = elem;
        for (let i = 0; i < elem.childNodes.length; i++) {
            const child = elem.childNodes[i];
            if (child.nodeName === 'bind') {
                this.do_bind = true;
            }
            if (child.nodeName === 'session') {
                this.do_session = true;
            }
        }

        if (!this.do_bind) {
            this._changeConnectStatus(Status.AUTHFAIL, null);
            return false;
        } else if (!this.options.explicitResourceBinding) {
            this.bind();
        } else {
            this._changeConnectStatus(Status.BINDREQUIRED, null);
        }
        return false;
    }

    /**
     * Sends an IQ to the XMPP server to bind a JID resource for this session.
     *
     * https://tools.ietf.org/html/rfc6120#section-7.5
     *
     * If `explicitResourceBinding` was set to a truthy value in the options
     * passed to the Connection constructor, then this function needs
     * to be called explicitly by the client author.
     *
     * Otherwise it'll be called automatically as soon as the XMPP server
     * advertises the "urn:ietf:params:xml:ns:xmpp-bind" stream feature.
     */
    bind() {
        if (!this.do_bind) {
            log.info(`Connection.prototype.bind called but "do_bind" is false`);
            return;
        }
        this._addSysHandler(this._onResourceBindResultIQ.bind(this), null, null, null, '_bind_auth_2');

        const resource = getResourceFromJid(this.jid);
        if (resource) {
            this.send(
                $iq({ type: 'set', id: '_bind_auth_2' })
                    .c('bind', { xmlns: NS.BIND })
                    .c('resource', {})
                    .t(resource)
                    .tree()
            );
        } else {
            this.send($iq({ type: 'set', id: '_bind_auth_2' }).c('bind', { xmlns: NS.BIND }).tree());
        }
    }

    /**
     * _Private_ handler for binding result and session start.
     * @private
     * @param {Element} elem - The matching stanza.
     * @return {false} `false` to remove the handler.
     */
    _onResourceBindResultIQ(elem) {
        if (elem.getAttribute('type') === 'error') {
            log.warn('Resource binding failed.');
            const conflict = elem.getElementsByTagName('conflict');
            let condition;
            if (conflict.length > 0) {
                condition = ErrorCondition.CONFLICT;
            }
            this._changeConnectStatus(Status.AUTHFAIL, condition, elem);
            return false;
        }
        // TODO - need to grab errors
        const bind = elem.getElementsByTagName('bind');
        if (bind.length > 0) {
            const jidNode = bind[0].getElementsByTagName('jid');
            if (jidNode.length > 0) {
                this.authenticated = true;
                this.jid = getText(jidNode[0]);
                if (this.do_session) {
                    this._establishSession();
                } else {
                    this._changeConnectStatus(Status.CONNECTED, null);
                }
            }
        } else {
            log.warn('Resource binding failed.');
            this._changeConnectStatus(Status.AUTHFAIL, null, elem);
            return false;
        }
    }

    /**
     * Send IQ request to establish a session with the XMPP server.
     *
     * See https://xmpp.org/rfcs/rfc3921.html#session
     *
     * Note: The protocol for session establishment has been determined as
     * unnecessary and removed in RFC-6121.
     * @private
     */
    _establishSession() {
        if (!this.do_session) {
            throw new Error(
                `Connection.prototype._establishSession ` +
                    `called but apparently ${NS.SESSION} wasn't advertised by the server`
            );
        }
        this._addSysHandler(this._onSessionResultIQ.bind(this), null, null, null, '_session_auth_2');

        this.send($iq({ type: 'set', id: '_session_auth_2' }).c('session', { xmlns: NS.SESSION }).tree());
    }

    /**
     * _Private_ handler for the server's IQ response to a client's session
     * request.
     *
     * This sets Connection.authenticated to true on success, which
     * starts the processing of user handlers.
     *
     * See https://xmpp.org/rfcs/rfc3921.html#session
     *
     * Note: The protocol for session establishment has been determined as
     * unnecessary and removed in RFC-6121.
     * @private
     * @param {Element} elem - The matching stanza.
     * @return {false} `false` to remove the handler.
     */
    _onSessionResultIQ(elem) {
        if (elem.getAttribute('type') === 'result') {
            this.authenticated = true;
            this._changeConnectStatus(Status.CONNECTED, null);
        } else if (elem.getAttribute('type') === 'error') {
            this.authenticated = false;
            log.warn('Session creation failed.');
            this._changeConnectStatus(Status.AUTHFAIL, null, elem);
            return false;
        }
        return false;
    }

    /**
     * _Private_ handler for SASL authentication failure.
     * @param {Element} [elem] - The matching stanza.
     * @return {false} `false` to remove the handler.
     */
    _sasl_failure_cb(elem) {
        // delete unneeded handlers
        if (this._sasl_success_handler) {
            this.deleteHandler(this._sasl_success_handler);
            this._sasl_success_handler = null;
        }
        if (this._sasl_challenge_handler) {
            this.deleteHandler(this._sasl_challenge_handler);
            this._sasl_challenge_handler = null;
        }

        if (this._sasl_mechanism) this._sasl_mechanism.onFailure();
        this._changeConnectStatus(Status.AUTHFAIL, null, elem);
        return false;
    }

    /**
     * _Private_ handler to finish legacy authentication.
     *
     * This handler is called when the result from the jabber:iq:auth
     * <iq/> stanza is returned.
     * @private
     * @param {Element} elem - The stanza that triggered the callback.
     * @return {false} `false` to remove the handler.
     */
    _auth2_cb(elem) {
        if (elem.getAttribute('type') === 'result') {
            this.authenticated = true;
            this._changeConnectStatus(Status.CONNECTED, null);
        } else if (elem.getAttribute('type') === 'error') {
            this._changeConnectStatus(Status.AUTHFAIL, null, elem);
            this.disconnect('authentication failed');
        }
        return false;
    }

    /**
     * _Private_ function to add a system level timed handler.
     *
     * This function is used to add a TimedHandler for the
     * library code.  System timed handlers are allowed to run before
     * authentication is complete.
     * @param {number} period - The period of the handler.
     * @param {Function} handler - The callback function.
     */
    _addSysTimedHandler(period, handler) {
        const thand = new TimedHandler(period, handler);
        thand.user = false;
        this.addTimeds.push(thand);
        return thand;
    }

    /**
     * _Private_ function to add a system level stanza handler.
     *
     * This function is used to add a Handler for the
     * library code.  System stanza handlers are allowed to run before
     * authentication is complete.
     * @param {Function} handler - The callback function.
     * @param {string} ns - The namespace to match.
     * @param {string} name - The stanza name to match.
     * @param {string} type - The stanza type attribute to match.
     * @param {string} id - The stanza id attribute to match.
     */
    _addSysHandler(handler, ns, name, type, id) {
        const hand = new Handler(handler, ns, name, type, id);
        hand.user = false;
        this.addHandlers.push(hand);
        return hand;
    }

    /**
     * _Private_ timeout handler for handling non-graceful disconnection.
     *
     * If the graceful disconnect process does not complete within the
     * time allotted, this handler finishes the disconnect anyway.
     * @return {false} `false` to remove the handler.
     */
    _onDisconnectTimeout() {
        log.debug('_onDisconnectTimeout was called');
        this._changeConnectStatus(Status.CONNTIMEOUT, null);
        this._proto._onDisconnectTimeout();
        // actually disconnect
        this._doDisconnect();
        return false;
    }

    /**
     * _Private_ handler to process events during idle cycle.
     *
     * This handler is called every 100ms to fire timed handlers that
     * are ready and keep poll requests going.
     */
    _onIdle() {
        // add timed handlers scheduled for addition
        // NOTE: we add before remove in the case a timed handler is
        // added and then deleted before the next _onIdle() call.
        while (this.addTimeds.length > 0) {
            this.timedHandlers.push(this.addTimeds.pop());
        }

        // remove timed handlers that have been scheduled for deletion
        while (this.removeTimeds.length > 0) {
            const thand = this.removeTimeds.pop();
            const i = this.timedHandlers.indexOf(thand);
            if (i >= 0) {
                this.timedHandlers.splice(i, 1);
            }
        }

        // call ready timed handlers
        const now = new Date().getTime();
        const newList = [];
        for (let i = 0; i < this.timedHandlers.length; i++) {
            const thand = this.timedHandlers[i];
            if (this.authenticated || !thand.user) {
                const since = thand.lastCalled + thand.period;
                if (since - now <= 0) {
                    if (thand.run()) {
                        newList.push(thand);
                    }
                } else {
                    newList.push(thand);
                }
            }
        }
        this.timedHandlers = newList;
        clearTimeout(this._idleTimeout);
        this._proto._onIdle();

        // reactivate the timer only if connected
        if (this.connected) {
            this._idleTimeout = setTimeout(() => this._onIdle(), 100);
        }
    }
}

export default Connection;
