odoo.define('mail.Chatter', function (require) {
"use strict";

var Activity = require('mail.Activity');
var AttachmentBox = require('mail.AttachmentBox');
var ChatterComposer = require('mail.composer.Chatter');
var Dialog = require('web.Dialog');
var Followers = require('mail.Followers');
var ThreadField = require('mail.ThreadField');
var mailUtils = require('mail.utils');

var concurrency = require('web.concurrency');
var config = require('web.config');
var core = require('web.core');
var Widget = require('web.Widget');

var _t = core._t;
var QWeb = core.qweb;

// The purpose of this widget is to display the chatter area below the form view
//
// It instantiates the optional mail_thread, mail_activity and mail_followers widgets.
// It Ensures that those widgets are appended at the right place, and allows them to communicate
// with each other.
// It synchronizes the rendering of those widgets (as they may be asynchronous), to mitigate
// the flickering when switching between records
var Chatter = Widget.extend({
    template: 'mail.Chatter',
    custom_events: {
        delete_attachment: '_onDeleteAttachment',
        discard_record_changes: '_onDiscardRecordChanges',
        reload_attachment_box: '_onReloadAttachmentBox',
        reload_mail_fields: '_onReloadMailFields',
    },
    events: {
        'click .o_chatter_button_new_message': '_onOpenComposerMessage',
        'click .o_chatter_button_log_note': '_onOpenComposerNote',
        'click .o_chatter_button_attachment': '_onClickAttachmentButton',
        'click .o_chatter_button_schedule_activity': '_onScheduleActivity',
    },
    supportedFieldTypes: ['one2many'],

    /**
     * @override
     * @param {widget} parent
     * @param {Object} record
     * @param {Object} mailFields
     * @param {string} [mailFields.mail_activity]
     * @param {string} [mailFields.mail_followers]
     * @param {string} [mailFields.mail_thread]
     * @param {Object} options
     * @param {string} [options.viewType=record.viewType] current viewType in
     *   which the chatter is instantiated
     */
    init: function (parent, record, mailFields, options) {
        this._super.apply(this, arguments);
        this._setState(record);

        this.attachments = {};
        this.fields = {};

        this._areAttachmentsLoaded = false;
        this._disableAttachmentBox = !!options.disable_attachment_box;
        this._dp = new concurrency.DropPrevious();
        this._isAttachmentBoxOpen = false;
        // mention: get the prefetched partners and use them as mention suggestions
        // if there is a follower widget, the followers will be added to the
        // suggestions as well once fetched
        this._mentionPartnerSuggestions = this.call('mail_service', 'getMentionPartnerSuggestions');
        this._mentionSuggestions = this._mentionPartnerSuggestions;

        if (mailFields.mail_activity) {
            this.fields.activity = new Activity(this, mailFields.mail_activity, record, options);
        }
        if (mailFields.mail_followers) {
            this.fields.followers = new Followers(this, mailFields.mail_followers, record, options);
        }
        if (mailFields.mail_thread) {
            this.fields.thread = new ThreadField(this, mailFields.mail_thread, record, options);
            var fieldsInfo = record.fieldsInfo[options.viewType || record.viewType];
            var nodeOptions = fieldsInfo[mailFields.mail_thread].options || {};
            this.hasLogButton = options.display_log_button || nodeOptions.display_log_button;
            this.postRefresh = nodeOptions.post_refresh || 'never';
            this.reloadOnUploadAttachment = this.postRefresh === 'always';
        }
    },
    /**
     * @override
     */
    start: function () {
        this._$topbar = this.$('.o_chatter_topbar');
        if(!this._disableAttachmentBox) {
            this.$('.o_topbar_right_area').append(QWeb.render('mail.chatter.Attachment.Button', {
                displayCounter: !!this.fields.thread,
                count: this.record.data.message_attachment_count || 0,
            }));
        }
        // render and append the buttons
        this._$topbar.prepend(QWeb.render('mail.chatter.Buttons', {
            newMessageButton: !!this.fields.thread,
            logNoteButton: this.hasLogButton,
            scheduleActivityButton: !!this.fields.activity,
            isMobile: config.device.isMobile,
        }));
        // start and append the widgets
        var fieldDefs = _.invoke(this.fields, 'appendTo', $('<div>'));
        var def = this._dp.add($.when.apply($, fieldDefs));
        this._render(def).then(this._updateMentionSuggestions.bind(this));

        return this._super.apply(this, arguments);
    },

    //--------------------------------------------------------------------------
    // Public
    //--------------------------------------------------------------------------

    /**
     * @param {Object} record
     * @param {integer} [record.res_id=undefined]
     * @param {Object[]} [fieldNames=undefined]
     */
    update: function (record, fieldNames) {
        var self = this;

        // close the composer if we switch to another record as it is record dependent
        if (this.record.res_id !== record.res_id) {
            this._closeComposer(true);
            this._closeAttachments();
            this._areAttachmentsLoaded = false;
            this.attachments = {};
        }

        // update the state
        this._setState(record);

        // detach the thread and activity widgets (temporarily force the height to prevent flickering)
        // keep the followers in the DOM as it has a synchronous pre-rendering
        this.$el.height(this.$el.height());
        if (this.fields.activity) {
            this.fields.activity.$el.detach();
        }
        if (this.fields.thread && this.fields.thread.$el ) {
            this.fields.thread.$el.detach();
        }

        // reset and re-append the widgets (and reset 'height: auto' rule)
        // if fieldNames is given, only reset those fields, otherwise reset all fields
        var fieldsToReset;
        if (fieldNames) {
            fieldsToReset = _.filter(this.fields, function (field) {
                return _.contains(fieldNames, field.name);
            });
        } else {
            fieldsToReset = this.fields;
        }
        var fieldDefs = _.invoke(fieldsToReset, 'reset', record);
        var def = this._dp.add($.when.apply($, fieldDefs));
        this._render(def).then(function () {
            self.$el.height('auto');
            self._updateMentionSuggestions();
        });
        this._updateAttachmentCounter();
    },

    //--------------------------------------------------------------------------
    // Private
    //--------------------------------------------------------------------------

    /**
     * @private
     */
    _closeAttachments: function () {
        if (this.fields.attachments) {
            this.$('.o_chatter_button_attachment').removeClass('o_active_attach');
            this.fields.attachments.destroy();
            this._isAttachmentBoxOpen = false;
        }
    },
    /**
     * @private
     * @param {boolean} force
     */
    _closeComposer: function (force) {
        if (this._composer && (this._composer.isEmpty() || force)) {
            this.$el.removeClass('o_chatter_composer_active');
            this.$('.o_chatter_button_new_message, .o_chatter_button_log_note').removeClass('o_active');
            this._composer.do_hide();
            this._composer.clearComposer();
        }
    },
    /**
     * @private
     */
    _disableChatter: function () {
        this.$('.btn').prop('disabled', true); // disable buttons
    },
    /**
     * @private
     */
    _disableComposer: function () {
        this.$(".o_composer_button_send").prop('disabled', true);
    },
    /**
     * Discard changes on the record.
     *
     * @private
     * @returns {$.Deferred} resolved if successfully discarding changes on
     *   the record, rejected otherwise
     */
    _discardChanges: function () {
        var def = $.Deferred();
        this.trigger_up('discard_changes', {
            recordID: this.record.id,
            onSuccess: def.resolve.bind(def),
            onFailure: def.reject.bind(def),
        });
        return def;
    },
    /**
     * Discard changes on the record if the message will reload the record
     * after posting it
     *
     * @private
     * @param {Object} messageData
     * @return {$.Deferred} resolved if no reload or proceed to discard the
     *   changes on the record, rejected otherwise
     */
    _discardOnReload: function (messageData) {
        if (this._reloadAfterPost(messageData)) {
            return this._discardChanges();
        }
        return $.when();
    },
    /**
     * @private
     */
    _enableChatter: function () {
        this.$('.btn').prop('disabled', false); // enable buttons
    },
    /**
     * @private
     */
    _enableComposer: function () {
        this.$(".o_composer_button_send").prop('disabled', false);
    },
    /*
    * @private
    * @return {Deferred}
    */
    _fetchAttachments: function () {
        var self = this;
        var domain = [
            ['res_id', '=', this.record.res_id],
            ['res_model', '=', this.record.model],
        ];
        return this._rpc({
            model: 'ir.attachment',
            method: 'search_read',
            domain: domain,
            fields: ['id', 'name', 'datas_fname', 'mimetype'],
        }).then(function (result) {
            self._areAttachmentsLoaded = true;
            self.attachments = result;
        });

    },
    /**
     * @private
     */
    _openAttachmentBox: function () {
        if (this.fields.attachments) {
            this._closeAttachments();
        }
        this.fields.attachments = new AttachmentBox(this, this.record, this.attachments);
        var $anchor = this.$('.o_chatter_topbar');
        if (this._composer) {
            var $anchor = this.$('.o_thread_composer');
        } else {
            var $anchor = this.$('.o_chatter_topbar');
        }
        this.fields.attachments.insertAfter($anchor);
        this.$el.addClass('o_chatter_composer_active');
        this.$('.o_chatter_button_attachment').addClass('o_active_attach');

        this._isAttachmentBoxOpen = true;
    },
    /**
     * @private
     * @param {Object} options
     * @param {Object[]} [options.suggested_partners=[]]
     * @param {boolean} [options.isLog]
     */
    _openComposer: function (options) {
        var self = this;
        var oldComposer = this._composer;
        // create the new composer
        this._composer = new ChatterComposer(this, this.record.model, options.suggested_partners || [], {
            commandsEnabled: false,
            context: this.context,
            inputMinHeight: 50,
            isLog: options && options.isLog,
            recordName: this.recordName,
            defaultBody: oldComposer && oldComposer.$input && oldComposer.$input.val(),
            defaultMentionSelections: oldComposer && oldComposer.getMentionListenerSelections(),
            attachmentIds: (oldComposer && oldComposer.get('attachment_ids')) || [],
        });
        this._composer.on('input_focused', this, function () {
            this._composer.mentionSetPrefetchedPartners(this._mentionSuggestions || []);
        });
        this._composer.insertAfter(this.$('.o_chatter_topbar')).then(function () {
            // destroy existing composer
            if (oldComposer) {
                oldComposer.destroy();
            }
            self._composer.focus();
            self._composer.on('post_message', self, function (messageData) {
                self._discardOnReload(messageData).then(function () {
                    self._disableComposer();
                    self.fields.thread.postMessage(messageData).then(function () {
                        self._closeComposer(true);
                        if (self._reloadAfterPost(messageData)) {
                            self.trigger_up('reload');
                        } else if (messageData.attachment_ids.length) {
                            self._reloadAttachmentBox();
                            self.trigger_up('reload', {fieldNames: ['message_attachment_count'], keepChanges: true});
                        }
                    }).fail(function () {
                        self._enableComposer();
                    });
                });
            });
            self._composer.on('need_refresh', self, self.trigger_up.bind(self, 'reload'));
            self._composer.on('close_composer', null, self._closeComposer.bind(self, true));

            self.$el.addClass('o_chatter_composer_active');
            self.$('.o_chatter_button_new_message, .o_chatter_button_log_note').removeClass('o_active');
            self.$('.o_chatter_button_new_message').toggleClass('o_active', !self._composer.options.isLog);
            self.$('.o_chatter_button_log_note').toggleClass('o_active', self._composer.options.isLog);
        });
    },
    /**
     * State if the record will be reloaded after posting a message.
     * Useful to warn the user of unsaved changes if the record is dirty.
     *
     * @private
     * @param {Object} messageData
     * @param {Array} [messageData.partner_ids] list of recipients of a message
     * @return {boolean} true if record will be reloaded after posting the
     *   message, false otherwise
     */
    _reloadAfterPost: function (messageData) {
        return this.postRefresh === 'always' ||
                (
                   this.postRefresh === 'recipients' &&
                   messageData.partner_ids &&
                   messageData.partner_ids.length
                );
    },
    /**
     * @private
     */
    _reloadAttachmentBox: function () {
        this._areAttachmentsLoaded = false;
        if (this._isAttachmentBoxOpen) {
            this._fetchAttachments().then(this._openAttachmentBox.bind(this));
        }
        if (this.fields.thread) {
            this.trigger_up('reload', { fieldNames: ['message_attachment_count'], keepChanges: true });
        }
    },
    /**
     * @private
     * @param {Deferred} def
     * @returns {Deferred}
     */
    _render: function (def) {
        // the rendering of the chatter is aynchronous: relational data of its fields needs to be
        // fetched (in some case, it might be synchronous as they hold an internal cache).
        // this function takes a deferred as argument, which is resolved once all fields have
        // fetched their data
        // this function appends the fields where they should be once the given deferred is resolved
        // and if it takes more than 500ms, displays a spinner to indicate that it is loading
        var self = this;

        var $spinner = $(QWeb.render('Spinner'));
        concurrency.rejectAfter(concurrency.delay(500), def).then(function () {
            $spinner.appendTo(self.$el);
        });

        return def.then(function () {
            if (self.fields.activity) {
                self.fields.activity.$el.appendTo(self.$el);
            }
            if (self.fields.followers) {
                if (self._disableAttachmentBox) {
                    self.fields.followers.$el.appendTo(self.$('.o_topbar_right_area'));
                } else {
                    self.fields.followers.$el.insertBefore(self.$('.o_chatter_button_attachment'));
                }
            }
            if (self.fields.thread && self.fields.thread.$el) {
                self.fields.thread.$el.appendTo(self.$el);
            }
        }).always(function () {
            // disable widgets in create mode, otherwise enable
            self._isCreateMode ? self._disableChatter() : self._enableChatter();
            $spinner.remove();
        });
    },
    /**
     * @private
     * @param {Object} record
     * @param {integer} [record.res_id]
     * @param {string} [record.model]
     * @param {string} record.data.display_name
     */
    _setState: function (record) {

        this._isCreateMode = !record.res_id;

        if (!this.record || this.record.res_id !== record.res_id) {
            this.context = {
                default_res_id: record.res_id || false,
                default_model: record.model || false,
            };
            // reset the suggested_partners_def to ensure a reload of the
            // suggested partners when opening the composer on another record
            this.suggested_partners_def = undefined;
        }
        this.record = record;
        this.recordName = record.data.display_name;
    },
    /**
     * @private
     */
     _updateAttachmentCounter: function () {
        var count = this.record.data.message_attachment_count || 0;
        var $element = this.$('.o_chatter_attachment_button_count');
        if (Number($element.html()) !== count) {
            this._areAttachmentsLoaded = false;
            $element.html(count);
        }
     },
    /**
     * @private
     */
    _updateMentionSuggestions: function () {
        if (!this.fields.followers) {
            return;
        }
        var self = this;

        this._mentionSuggestions = [];

        // add the followers to the mention suggestions
        var followerSuggestions = [];
        var followers = this.fields.followers.getFollowers();
        _.each(followers, function (follower) {
            if (follower.res_model === 'res.partner') {
                followerSuggestions.push({
                    id: follower.res_id,
                    name: follower.name,
                    email: follower.email,
                });
            }
        });
        if (followerSuggestions.length) {
            this._mentionSuggestions.push(followerSuggestions);
        }

        // add the partners (followers filtered out) to the mention suggestions
        _.each(this._mentionPartnerSuggestions, function (partners) {
            self._mentionSuggestions.push(_.filter(partners, function (partner) {
                return !_.findWhere(followerSuggestions, { id: partner.id });
            }));
        });
    },

    //--------------------------------------------------------------------------
    // Handlers
    //--------------------------------------------------------------------------

    /**
     * @private
     * @param {OdooEvent} ev
     * @param {integer} ev.data.attachmentId
     * @param {String} ev.data.attachmentName
     */
    _onDeleteAttachment: function (ev) {
        ev.stopPropagation();
        var self = this;
        var options = {
            confirm_callback: function () {
                self._rpc({
                    model: 'ir.attachment',
                    method: 'unlink',
                    args: [parseInt(ev.data.attachmentId, 10)],
                })
                .then(function () {
                    self._reloadAttachmentBox();
                    if (self.fields.thread) {
                        self.fields.thread.removeAttachments([ev.data.attachmentId]);
                    }
                    self.trigger_up('reload');
                });
            }
        };
        var promptText = _.str.sprintf(_t("Do you really want to delete %s?"), _.escape(ev.data.attachmentName));
        Dialog.confirm(this, promptText, options);
    },
    /**
     * @private
     */
    _onClickAttachmentButton: function () {
        if(this._disableAttachmentBox) {
            return;
        }
        if (this._isAttachmentBoxOpen) {
            this._closeAttachments();
        } else {
            var def;
            if (!this._areAttachmentsLoaded) {
                def = this._fetchAttachments();
            }
            $.when(def).then(this._openAttachmentBox.bind(this));
        }
    },
    /**
     * Discard changes on the record.
     * This is notified by the composer, when opening the full-composer.
     *
     * @private
     * @param {OdooEvent} ev
     * @param {function} ev.data.proceed callback to tell to proceed
     */
    _onDiscardRecordChanges: function (ev) {
        this._discardChanges().then(ev.data.proceed);
    },
    _onOpenComposerMessage: function () {
        var self = this;
        if (!this.suggested_partners_def) {
            this.suggested_partners_def = $.Deferred();
            var method = 'message_get_suggested_recipients';
            var args = [[this.context.default_res_id], this.context];
            this._rpc({model: this.record.model, method: method, args: args})
                .then(function (result) {
                    if (!self.suggested_partners_def) {
                        return; // widget has been reset (e.g. we just switched to another record)
                    }
                    var suggested_partners = [];
                    var thread_recipients = result[self.context.default_res_id];
                    _.each(thread_recipients, function (recipient) {
                        var parsed_email = recipient[1] && mailUtils.parseEmail(recipient[1]);
                        suggested_partners.push({
                            checked: true,
                            partner_id: recipient[0],
                            full_name: recipient[1],
                            name: parsed_email[0],
                            email_address: parsed_email[1],
                            reason: recipient[2],
                        });
                    });
                    self.suggested_partners_def.resolve(suggested_partners);
                });
        }
        this.suggested_partners_def.then(function (suggested_partners) {
            self._openComposer({ isLog: false, suggested_partners: suggested_partners });
        });
    },
    /**
     * @private
     */
    _onOpenComposerNote: function () {
        this._openComposer({ isLog: true });
    },
    /**
     * @private
     */
    _onReloadAttachmentBox: function () {
        if (this.reloadOnUploadAttachment) {
            this.trigger_up('reload');
        }
        this._reloadAttachmentBox();
    },
    /**
     * @private
     * @param {OdooEvent} event
     * @param {string} event.name
     * @param {Object} event.data
     * @param {boolean} [event.data.activity]
     * @param {boolean} [event.data.followers]
     * @param {boolean} [event.data.thread]
     */
    _onReloadMailFields: function (event) {
        var fieldNames = [];
        if (this.fields.activity && event.data.activity) {
            fieldNames.push(this.fields.activity.name);
        }
        if (this.fields.followers && event.data.followers) {
            fieldNames.push(this.fields.followers.name);
        }
        if (this.fields.thread && event.data.thread) {
            fieldNames.push(this.fields.thread.name);
        }
        this.trigger_up('reload', {
            fieldNames: fieldNames,
            keepChanges: true,
        });
    },
    /**
     * @private
     */
    _onScheduleActivity: function () {
        this.fields.activity.scheduleActivity();
    },
});

return Chatter;

});
