// Converse.js (A browser based XMPP chat client)
// http://conversejs.org
//
// Copyright (c) 2012-2017, Jan-Carel Brand <jc@opkode.com>
// Licensed under the Mozilla Public License (MPLv2)
//

/* This is a Converse.js plugin which add support for XEP-0030: Service Discovery */

/*global Backbone, define, window */
(function (root, factory) {
    define(["converse-core", "sizzle", "strophe.disco"], factory);
}(this, function (converse, sizzle) {

    const { Backbone, Promise, Strophe, b64_sha1, utils, _ } = converse.env;

    converse.plugins.add('converse-disco', {

        initialize () {
            /* The initialize function gets called as soon as the plugin is
             * loaded by converse.js's plugin machinery.
             */
            const { _converse } = this;

            function onDiscoItems (stanza) {
                _.each(stanza.querySelectorAll('query item'), (item) => {
                    if (item.getAttribute("node")) {
                        // XXX: ignore nodes for now.
                        // See: https://xmpp.org/extensions/xep-0030.html#items-nodes
                        return;
                    }
                    const jid = item.getAttribute('jid');
                    const entities = _converse.disco_entities;
                    if (_.isUndefined(entities.get(jid))) {
                        entities.create({'jid': jid});
                    }
                });
            }

            // Promises exposed by this plugin
            _converse.api.promises.add('discoInitialized');

            _converse.DiscoEntity = Backbone.Model.extend({
                /* A Disco Entity is a JID addressable entity that can be queried
                 * for features.
                 *
                 * See XEP-0030: https://xmpp.org/extensions/xep-0030.html
                 */
                idAttribute: 'jid',

                initialize () {
                    this.waitUntilFeaturesDiscovered = utils.getResolveablePromise();

                    this.features = new Backbone.Collection();
                    this.features.browserStorage = new Backbone.BrowserStorage[_converse.storage](
                        b64_sha1(`converse.features-${this.get('jid')}`)
                    );
                    this.features.on('add', this.onFeatureAdded, this);

                    this.identities = new Backbone.Collection();
                    this.identities.browserStorage = new Backbone.BrowserStorage[_converse.storage](
                        b64_sha1(`converse.identities-${this.get('jid')}`)
                    );
                    this.fetchFeatures();

                },

                getIdentity (category, type) {
                    /* Returns a Promise which resolves with a map indicating
                     * whether a given identity is provided.
                     *
                     * Parameters:
                     *    (String) category - The identity category
                     *    (String) type - The identity type
                     */
                    const entity = this;
                    return new Promise((resolve, reject) => {
                        function fulfillPromise () {
                            const model = entity.identities.findWhere({
                                'category': category,
                                'type': type
                            });
                            resolve(model);
                        }
                        entity.waitUntilFeaturesDiscovered
                            .then(fulfillPromise)
                            .catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
                    });
                },

                hasFeature (feature) {
                    /* Returns a Promise which resolves with a map indicating
                     * whether a given feature is supported.
                     *
                     * Parameters:
                     *    (String) feature - The feature that might be supported.
                     */
                    const entity = this;
                    return new Promise((resolve, reject) => {
                        function fulfillPromise () {
                            const model = entity.features.findWhere({'var': feature });
                            if (model) {
                                resolve({
                                    'supported': true,
                                    'feature': model
                                });
                            } else {
                                resolve({
                                    'supported': false,
                                    'feature': null
                                });
                            }
                        }
                        entity.waitUntilFeaturesDiscovered
                            .then(fulfillPromise)
                            .catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
                    });
                },

                onFeatureAdded (feature) {
                    feature.entity = this;
                    _converse.emit('serviceDiscovered', feature);
                },

                fetchFeatures () {
                    if (this.features.browserStorage.records.length === 0) {
                        this.queryInfo();
                    } else {
                        this.features.fetch({
                            add: true,
                            success: () => {
                                this.waitUntilFeaturesDiscovered.resolve();
                                this.trigger('featuresDiscovered');
                            }
                        });
                        this.identities.fetch({add: true});
                    }
                },

                queryInfo () {
                    _converse.connection.disco.info(this.get('jid'), null, this.onInfo.bind(this));
                },

                queryForItems () {
                    if (_.isEmpty(this.identities.where({'category': 'server'}))) {
                        // Don't fetch features and items if this is not a
                        // server or a conference component.
                        return;
                    }
                    _converse.connection.disco.items(this.get('jid'), null, onDiscoItems);
                },

                onInfo (stanza) {
                    _.forEach(stanza.querySelectorAll('identity'), (identity) => {
                        this.identities.create({
                            'category': identity.getAttribute('category'),
                            'type': identity.getAttribute('type'),
                            'name': identity.getAttribute('name')
                        });
                    });
                    if (stanza.querySelector('feature[var="'+Strophe.NS.DISCO_ITEMS+'"]')) {
                        this.queryForItems();
                    }
                    _.forEach(stanza.querySelectorAll('feature'), (feature) => {
                        this.features.create({
                            'var': feature.getAttribute('var'),
                            'from': stanza.getAttribute('from')
                        });
                    });
                    this.waitUntilFeaturesDiscovered.resolve();
                    this.trigger('featuresDiscovered');
                }
            });

            _converse.DiscoEntities = Backbone.Collection.extend({
                model: _converse.DiscoEntity,

                initialize () {
                    this.browserStorage = new Backbone.BrowserStorage[_converse.storage](
                        b64_sha1(`converse.disco-entities-${_converse.bare_jid}`)
                    );
                    this.fetchEntities().then(
                        _.partial(_converse.emit, 'discoInitialized'),
                        _.partial(_converse.emit, 'discoInitialized')
                    ).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
                },

                fetchEntities () {
                    return new Promise((resolve, reject) => {
                        this.fetch({
                            add: true,
                            success: function (collection) {
                                if (collection.length === 0 || !collection.get(_converse.domain)) {
                                    this.create({'jid': _converse.domain});
                                }
                                resolve();
                            }.bind(this),
                            error () {
                                reject (new Error("Could not fetch disco entities"));
                            }
                        });
                    });
                }
            });

            function addClientFeatures () {
                /* The strophe.disco.js plugin keeps a list of features which
                 * it will advertise to any #info queries made to it.
                 *
                 * See: http://xmpp.org/extensions/xep-0030.html#info
                 */

                // See http://xmpp.org/registrar/disco-categories.html
                _converse.connection.disco.addIdentity('client', 'web', 'Converse.js');

                _converse.connection.disco.addFeature(Strophe.NS.BOSH);
                _converse.connection.disco.addFeature(Strophe.NS.CHATSTATES);
                _converse.connection.disco.addFeature(Strophe.NS.DISCO_INFO);
                _converse.connection.disco.addFeature(Strophe.NS.ROSTERX); // Limited support
                if (_converse.message_carbons) {
                    _converse.connection.disco.addFeature(Strophe.NS.CARBONS);
                }
                _converse.emit('addClientFeatures');
                return this;
            }

            function initializeDisco () {
                addClientFeatures();
                _converse.disco_entities = new _converse.DiscoEntities();
            }
            _converse.api.listen.on('reconnected', initializeDisco);
            _converse.api.listen.on('connected', initializeDisco);

            _converse.api.listen.on('beforeTearDown', () => {
                if (_converse.disco_entities) {
                    _converse.disco_entities.each((entity) => {
                        entity.features.reset();
                        entity.features.browserStorage._clear();
                    });
                    _converse.disco_entities.reset();
                    _converse.disco_entities.browserStorage._clear();
                }
            });

            /* We extend the default converse.js API to add methods specific to service discovery */
            _.extend(_converse.api, {
                'disco': {
                    'entities': {
                        'get' (entity_jid, create=false) {
                            const entity = _converse.disco_entities.get(entity_jid);
                            if (entity || !create) {
                                return entity;
                            }
                            return _converse.disco_entities.create({'jid': entity_jid});
                        }
                    },

                    'supports' (feature, entity_jid) {
                        /* Returns a Promise which resolves with a map indicating
                         * whether a given feature is supported.
                         *
                         * Parameters:
                         *    (String) feature - The feature that might be
                         *          supported. In the XML stanza, this is the `var`
                         *          attribute of the `<feature>` element. For
                         *          example: 'http://jabber.org/protocol/muc'
                         *    (String) entity_jid - The JID of the entity which might support the feature.
                         */
                        return _converse.api.waitUntil('discoInitialized').then(() => {
                            const entity = _converse.api.disco.entities.get(entity_jid, true);
                            return entity.hasFeature(feature);
                        }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
                    },

                    'getIdentity' (category, type, entity_jid) {
                        /* Returns a Promise which resolves with a map indicating
                         * whether an identity with a given type is provided by
                         * the entity.
                         *
                         * Parameters:
                         *    (String) category - The identity category.
                         *          In the XML stanza, this is the `category`
                         *          attribute of the `<identity>` element.
                         *          For example: 'pubsub'
                         *    (String) type - The identity type.
                         *          In the XML stanza, this is the `type`
                         *          attribute of the `<identity>` element.
                         *          For example: 'pep'
                         */
                        return _converse.api.waitUntil('discoInitialized').then(() => {
                            const entity = _converse.api.disco.entities.get(entity_jid, true);
                            return entity.getIdentity(category, type);
                        }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
                    }
                }
            });
        }
    });
}));
