// 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)
//
/*global define */

import "converse-chatview";
import "converse-profile";
import "converse-rosterview";
import _FormData from "formdata-polyfill";
import bootstrap from "bootstrap.native";
import converse from "@converse/headless/converse-core";
import fp from "@converse/headless/lodash.fp";
import tpl_brand_heading from "templates/converse_brand_heading.html";
import tpl_controlbox from "templates/controlbox.html";
import tpl_controlbox_toggle from "templates/controlbox_toggle.html";
import tpl_login_panel from "templates/login_panel.html";

const CHATBOX_TYPE = 'chatbox';
const { Strophe, Backbone, Promise, _, moment } = converse.env;
const u = converse.env.utils;

const CONNECTION_STATUS_CSS_CLASS = {
   'Error': 'error',
   'Connecting': 'info',
   'Connection failure': 'error',
   'Authenticating': 'info',
   'Authentication failure': 'error',
   'Connected': 'info',
   'Disconnected': 'error',
   'Disconnecting': 'warn',
   'Attached': 'info',
   'Redirect': 'info',
   'Reconnecting': 'warn'
};

const PRETTY_CONNECTION_STATUS = {
    0: 'Error',
    1: 'Connecting',
    2: 'Connection failure',
    3: 'Authenticating',
    4: 'Authentication failure',
    5: 'Connected',
    6: 'Disconnected',
    7: 'Disconnecting',
    8: 'Attached',
    9: 'Redirect',
   10: 'Reconnecting'
};

const REPORTABLE_STATUSES = [
    0, // ERROR'
    1, // CONNECTING
    2, // CONNFAIL
    3, // AUTHENTICATING
    4, // AUTHFAIL
    7, // DISCONNECTING
   10  // RECONNECTING
];

converse.plugins.add('converse-controlbox', {
    /* Plugin dependencies are other plugins which might be
     * overridden or relied upon, and therefore need to be loaded before
     * this plugin.
     *
     * If the setting "strict_plugin_dependencies" is set to true,
     * an error will be raised if the plugin is not found. By default it's
     * false, which means these plugins are only loaded opportunistically.
     *
     * NB: These plugins need to have already been loaded via require.js.
     */
    dependencies: ["converse-modal", "converse-chatboxes", "converse-rosterview", "converse-chatview"],

    enabled (_converse) {
        return _converse.view_mode !== 'embedded';
    },

    overrides: {
        // Overrides mentioned here will be picked up by converse.js's
        // plugin architecture they will replace existing methods on the
        // relevant objects or classes.
        //
        // New functions which don't exist yet can also be added.

        tearDown () {
            this.__super__.tearDown.apply(this, arguments);
            if (this.rosterview) {
                // Removes roster groups
                this.rosterview.model.off().reset();
                this.rosterview.each(function (groupview) {
                    groupview.removeAll();
                    groupview.remove();
                });
                this.rosterview.removeAll().remove();
            }
        },

        ChatBoxes: {
            model (attrs, options) {
                const { _converse } = this.__super__;
                if (attrs.id == 'controlbox') {
                    return new _converse.ControlBox(attrs, options);
                } else {
                    return this.__super__.model.apply(this, arguments);
                }
            },

            chatBoxMayBeShown (chatbox) {
                return this.__super__.chatBoxMayBeShown.apply(this, arguments) &&
                       chatbox.get('id') !== 'controlbox';
            },
        },

        ChatBoxViews: {
            closeAllChatBoxes () {
                const { _converse } = this.__super__;
                this.each(function (view) {
                    if (view.model.get('id') === 'controlbox' &&
                            (_converse.disconnection_cause !== _converse.LOGOUT || _converse.show_controlbox_by_default)) {
                        return;
                    }
                    view.close();
                });
                return this;
            },

            getChatBoxWidth (view) {
                const { _converse } = this.__super__;
                const controlbox = this.get('controlbox');
                if (view.model.get('id') === 'controlbox') {
                    /* We return the width of the controlbox or its toggle,
                     * depending on which is visible.
                     */
                    if (!controlbox || !u.isVisible(controlbox.el)) {
                        return u.getOuterWidth(_converse.controlboxtoggle.el, true);
                    } else {
                        return u.getOuterWidth(controlbox.el, true);
                    }
                } else {
                    return this.__super__.getChatBoxWidth.apply(this, arguments);
                }
            }
        },

        ChatBox: {
            validate (attrs, options) {
                const { _converse } = this.__super__;
                if (attrs.type === _converse.CONTROLBOX_TYPE) {
                    if (_converse.view_mode === 'embedded')  {
                        return 'Controlbox not relevant in embedded view mode';
                    }
                    return;
                }
                return this.__super__.validate.apply(this, arguments);
            },

            initialize () {
                if (this.get('id') === 'controlbox') {
                    this.set({'time_opened': moment(0).valueOf()});
                } else {
                    this.__super__.initialize.apply(this, arguments);
                }
            },
        },

        ChatBoxView: {
            insertIntoDOM () {
                const view = this.__super__._converse.chatboxviews.get("controlbox");
                if (view) {
                    view.el.insertAdjacentElement('afterend', this.el)
                } else {
                    this.__super__.insertIntoDOM.apply(this, arguments);
                }
                return this;
            }
        }
    },

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

        _converse.api.settings.update({
            allow_logout: true,
            default_domain: undefined,
            locked_domain: undefined,
            show_controlbox_by_default: false,
            sticky_controlbox: false
        });

        _converse.api.promises.add('controlboxInitialized');

        const addControlBox = () => _converse.chatboxes.add({'id': 'controlbox'});

        _converse.ControlBox = _converse.ChatBox.extend({
            defaults: {
                'bookmarked': false,
                'box_id': 'controlbox',
                'chat_state': undefined,
                'closed': !_converse.show_controlbox_by_default,
                'num_unread': 0,
                'type': _converse.CONTROLBOX_TYPE,
                'url': ''
            },

            initialize () {
                u.safeSave(this, {'time_opened': this.get('time_opened') || moment().valueOf()});
            }
        });


        _converse.ControlBoxView = _converse.ChatBoxView.extend({
            tagName: 'div',
            className: 'chatbox',
            id: 'controlbox',
            events: {
                'click a.close-chatbox-button': 'close'
            },

            initialize () {
                if (_.isUndefined(_converse.controlboxtoggle)) {
                    _converse.controlboxtoggle = new _converse.ControlBoxToggle();
                }
                _converse.controlboxtoggle.el.insertAdjacentElement('afterend', this.el);

                this.model.on('change:connected', this.onConnected, this);
                this.model.on('destroy', this.hide, this);
                this.model.on('hide', this.hide, this);
                this.model.on('show', this.show, this);
                this.model.on('change:closed', this.ensureClosedState, this);
                this.render();
                if (this.model.get('connected')) {
                    this.insertRoster();
                }
                _converse.emit('controlboxInitialized', this);
            },

            render () {
                if (this.model.get('connected')) {
                    if (_.isUndefined(this.model.get('closed'))) {
                        this.model.set('closed', !_converse.show_controlbox_by_default);
                    }
                }
                this.el.innerHTML = tpl_controlbox(_.extend(this.model.toJSON()));

                if (!this.model.get('closed')) {
                    this.show();
                } else {
                    this.hide();
                }
                if (!_converse.connection.connected ||
                        !_converse.connection.authenticated ||
                        _converse.connection.disconnecting) {
                    this.renderLoginPanel();
                } else if (this.model.get('connected') &&
                        (!this.controlbox_pane || !u.isVisible(this.controlbox_pane.el))) {
                    this.renderControlBoxPane();
                }
                return this;
            },

            onConnected () {
                if (this.model.get('connected')) {
                    this.render();
                    this.insertRoster();
                }
            },

            insertRoster () {
                if (_converse.authentication === _converse.ANONYMOUS) {
                    return;
                }
                /* Place the rosterview inside the "Contacts" panel. */
                _converse.api.waitUntil('rosterViewInitialized')
                    .then(() => this.controlbox_pane.el.insertAdjacentElement('beforeEnd', _converse.rosterview.el))
                    .catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
            },

             createBrandHeadingHTML () {
                return tpl_brand_heading({
                    'sticky_controlbox': _converse.sticky_controlbox
                });
            },

            insertBrandHeading () {
                const heading_el = this.el.querySelector('.brand-heading-container');
                if (_.isNull(heading_el)) {
                    const el = this.el.querySelector('.controlbox-head');
                    el.insertAdjacentHTML('beforeend', this.createBrandHeadingHTML());
                } else {
                    heading_el.outerHTML = this.createBrandHeadingHTML();
                }
            },

            renderLoginPanel () {
                this.el.classList.add("logged-out");
                if (_.isNil(this.loginpanel)) {
                    this.loginpanel = new _converse.LoginPanel({
                        'model': new _converse.LoginPanelModel()
                    });
                    const panes = this.el.querySelector('.controlbox-panes');
                    panes.innerHTML = '';
                    panes.appendChild(this.loginpanel.render().el);
                    this.insertBrandHeading();
                } else {
                    this.loginpanel.render();
                }
                this.loginpanel.initPopovers();
                return this;
            },

            renderControlBoxPane () {
                /* Renders the "Contacts" panel of the controlbox.
                 *
                 * This will only be called after the user has already been
                 * logged in.
                 */
                if (this.loginpanel) {
                    this.loginpanel.remove();
                    delete this.loginpanel;
                }
                this.el.classList.remove("logged-out");
                this.controlbox_pane = new _converse.ControlBoxPane();
                this.el.querySelector('.controlbox-panes').insertAdjacentElement(
                    'afterBegin',
                    this.controlbox_pane.el
                )
            },

            close (ev) {
                if (ev && ev.preventDefault) { ev.preventDefault(); }
                if (_converse.sticky_controlbox) {
                    return;
                }
                if (_converse.connection.connected && !_converse.connection.disconnecting) {
                    this.model.save({'closed': true});
                } else {
                    this.model.trigger('hide');
                }
                _converse.emit('controlBoxClosed', this);
                return this;
            },

            ensureClosedState () {
                if (this.model.get('closed')) {
                    this.hide();
                } else {
                    this.show();
                }
            },

            hide (callback) {
                if (_converse.sticky_controlbox) {
                    return;
                }
                u.addClass('hidden', this.el);
                _converse.emit('chatBoxClosed', this);
                if (!_converse.connection.connected) {
                    _converse.controlboxtoggle.render();
                }
                _converse.controlboxtoggle.show(callback);
                return this;
            },

            onControlBoxToggleHidden () {
                this.model.set('closed', false);
                this.el.classList.remove('hidden');
                _converse.emit('controlBoxOpened', this);
            },

            show () {
                _converse.controlboxtoggle.hide(
                    this.onControlBoxToggleHidden.bind(this)
                );
                return this;
            },

            showHelpMessages () {
                /* Override showHelpMessages in ChatBoxView, for now do nothing.
                 *
                 * Parameters:
                 *  (Array) msgs: Array of messages
                 */
                return;
            }
        });

        _converse.LoginPanelModel = Backbone.Model.extend({
            defaults: {
                // Passed-by-reference. Fine in this case because there's
                // only one such model.
                'errors': [],
            }
        });

        _converse.LoginPanel = Backbone.VDOMView.extend({
            tagName: 'div',
            id: "converse-login-panel",
            className: 'controlbox-pane fade-in',
            events: {
                'submit form#converse-login': 'authenticate',
                'change input': 'validate'
            },

            initialize (cfg) {
                this.model.on('change', this.render, this);
                this.listenTo(_converse.connfeedback, 'change', this.render);
                this.render();
            },

            toHTML () {
                const connection_status = _converse.connfeedback.get('connection_status');
                let feedback_class, pretty_status;
                if (_.includes(REPORTABLE_STATUSES, connection_status)) {
                    pretty_status = PRETTY_CONNECTION_STATUS[connection_status];
                    feedback_class = CONNECTION_STATUS_CSS_CLASS[pretty_status];
                }
                return tpl_login_panel(
                    _.extend(this.model.toJSON(), {
                        '__': __,
                        '_converse': _converse,
                        'ANONYMOUS': _converse.ANONYMOUS,
                        'EXTERNAL': _converse.EXTERNAL,
                        'LOGIN': _converse.LOGIN,
                        'PREBIND': _converse.PREBIND,
                        'auto_login': _converse.auto_login,
                        'authentication': _converse.authentication,
                        'connection_status': connection_status,
                        'conn_feedback_class': feedback_class,
                        'conn_feedback_subject': pretty_status,
                        'conn_feedback_message': _converse.connfeedback.get('message'),
                        'placeholder_username': (_converse.locked_domain || _converse.default_domain) &&
                                                __('Username') || __('user@domain'),
                        'show_trust_checkbox': _converse.trusted !== 'on' && _converse.trusted !== 'off'
                    })
                );
            },

            initPopovers () {
                _.forEach(this.el.querySelectorAll('[data-title]'), el => {
                    const popover = new bootstrap.Popover(el, {
                        'trigger': _converse.view_mode === 'mobile' && 'click' || 'hover',
                        'dismissible': _converse.view_mode === 'mobile' && true || false,
                        'container': this.el.parentElement.parentElement.parentElement
                    })
                });
            },

            validate () {
                const form = this.el.querySelector('form');
                const jid_element = form.querySelector('input[name=jid]');
                if (jid_element.value &&
                        !_converse.locked_domain &&
                        !_converse.default_domain &&
                        !u.isValidJID(jid_element.value)) {
                    jid_element.setCustomValidity(__('Please enter a valid XMPP address'));
                    return false;
                }
                jid_element.setCustomValidity('');
                return true;
            },

            authenticate (ev) {
                /* Authenticate the user based on a form submission event.
                 */
                if (ev && ev.preventDefault) { ev.preventDefault(); }
                if (_converse.authentication === _converse.ANONYMOUS) {
                    this.connect(_converse.jid, null);
                    return;
                }
                if (!this.validate()) { return; }

                const form_data = new FormData(ev.target);

                if (_converse.trusted === 'on' || _converse.trusted === 'off') {
                    _converse.config.save({
                        'trusted': _converse.trusted === 'on',
                        'storage': _converse.trusted === 'on' ? 'local' : 'session'
                    });
                } else {
                    _converse.config.save({
                        'trusted': form_data.get('trusted') && true || false,
                        'storage': form_data.get('trusted') ? 'local' : 'session'
                    });
                }

                let jid = form_data.get('jid');
                if (_converse.locked_domain) {
                    const last_part = '@' + _converse.locked_domain;
                    if (jid.endsWith(last_part)) {
                        jid = jid.substr(0, jid.length - last_part.length);
                    }
                    jid = Strophe.escapeNode(jid) + last_part;
                } else if (_converse.default_domain && !_.includes(jid, '@')) {
                    jid = jid + '@' + _converse.default_domain;
                }
                this.connect(jid, form_data.get('password'));
            },

            connect (jid, password) {
                if (jid) {
                    const resource = Strophe.getResourceFromJid(jid);
                    if (!resource) {
                        jid = jid.toLowerCase() + _converse.generateResource();
                    } else {
                        jid = Strophe.getBareJidFromJid(jid).toLowerCase()+'/'+resource;
                    }
                }
                if (_.includes(["converse/login", "converse/register"],
                        Backbone.history.getFragment())) {
                    _converse.router.navigate('', {'replace': true});
                }
                _converse.connection.reset();
                _converse.connection.connect(jid, password, _converse.onConnectStatusChanged);
            }
        });


        _converse.ControlBoxPane = Backbone.NativeView.extend({
            tagName: 'div',
            className: 'controlbox-pane',

            initialize () {
                _converse.xmppstatusview = new _converse.XMPPStatusView({
                    'model': _converse.xmppstatus
                });
                this.el.insertAdjacentElement(
                    'afterBegin',
                    _converse.xmppstatusview.render().el
                );
            }
        });


        _converse.ControlBoxToggle = Backbone.NativeView.extend({
            tagName: 'a',
            className: 'toggle-controlbox hidden',
            id: 'toggle-controlbox',
            events: {
                'click': 'onClick'
            },
            attributes: {
                'href': "#"
            },

            initialize () {
                _converse.chatboxviews.insertRowColumn(this.render().el);
                _converse.api.waitUntil('initialized')
                    .then(this.render.bind(this))
                    .catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
            },

            render () {
                // We let the render method of ControlBoxView decide whether
                // the ControlBox or the Toggle must be shown. This prevents
                // artifacts (i.e. on page load the toggle is shown only to then
                // seconds later be hidden in favor of the controlbox).
                this.el.innerHTML = tpl_controlbox_toggle({
                    'label_toggle': _converse.connection.connected ? __('Chat Contacts') : __('Toggle chat')
                })
                return this;
            },

            hide (callback) {
                u.hideElement(this.el);
                callback();
            },

            show (callback) {
                u.fadeIn(this.el, callback);
            },

            showControlBox () {
                let controlbox = _converse.chatboxes.get('controlbox');
                if (!controlbox) {
                    controlbox = addControlBox();
                }
                if (_converse.connection.connected) {
                    controlbox.save({closed: false});
                } else {
                    controlbox.trigger('show');
                }
            },

            onClick (e) {
                e.preventDefault();
                if (u.isVisible(_converse.root.querySelector("#controlbox"))) {
                    const controlbox = _converse.chatboxes.get('controlbox');
                    if (_converse.connection.connected) {
                        controlbox.save({closed: true});
                    } else {
                        controlbox.trigger('hide');
                    }
                } else {
                    this.showControlBox();
                }
            }
        });

        _converse.on('chatBoxViewsInitialized', () => {
            const that = _converse.chatboxviews;
            _converse.chatboxes.on('add', item => {
                if (item.get('type') === _converse.CONTROLBOX_TYPE) {
                    const view = that.get(item.get('id'));
                    if (view) {
                        view.model = item;
                        view.initialize();
                    } else {
                        that.add(item.get('id'), new _converse.ControlBoxView({model: item}));
                    }
                }
            });
        });

        _converse.on('clearSession', () => {
            if (_converse.config.get('trusted')) {
                const chatboxes = _.get(_converse, 'chatboxes', null);
                if (!_.isNil(chatboxes)) {
                    const controlbox = chatboxes.get('controlbox');
                    if (controlbox &&
                            controlbox.collection &&
                            controlbox.collection.browserStorage) {
                        controlbox.save({'connected': false});
                    }
                }
            }
        });

        Promise.all([
            _converse.api.waitUntil('connectionInitialized'),
            _converse.api.waitUntil('chatBoxViewsInitialized')
        ]).then(addControlBox).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));

        _converse.on('chatBoxesFetched', () => {
            const controlbox = _converse.chatboxes.get('controlbox') || addControlBox();
            controlbox.save({connected:true});
        });

        const disconnect =  function () {
            /* Upon disconnection, set connected to `false`, so that if
             * we reconnect, "onConnected" will be called,
             * to fetch the roster again and to send out a presence stanza.
             */
            const view = _converse.chatboxviews.get('controlbox');
            view.model.set({'connected': false});
            return view;
        };
        _converse.on('disconnected', () => disconnect().renderLoginPanel());
        _converse.on('will-reconnect', disconnect);


        /************************ BEGIN API ************************/
        _.extend(_converse.api, {
            /**
             * The "controlbox" namespace groups methods pertaining to the
             * controlbox view
             *
             * @namespace _converse.api.controlbox
             * @memberOf _converse.api
             */
            'controlbox': {
                /**
                 * Retrieves the controlbox view.
                 *
                 * @example
                 * const view = _converse.api.controlbox.get();
                 *
                 * @returns {Backbone.View} View representing the controlbox
                 */
                get () {
                    return _converse.chatboxviews.get('controlbox');
                }
            }
        });
    }
});
