// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

/**
 * This module handles the message area of the messaging area.
 *
 * @module     core_message/message_area_messages
 * @package    core_message
 * @copyright  2016 Mark Nelson <markn@moodle.com>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/custom_interaction_events',
        'core/auto_rows', 'core_message/message_area_actions', 'core/modal_factory', 'core/modal_events',
        'core/str', 'core_message/message_area_events', 'core/backoff_timer'],
    function($, Ajax, Templates, Notification, CustomEvents, AutoRows, Actions, ModalFactory,
             ModalEvents, Str, Events, BackOffTimer) {

        /** @type {int} The message area default height. */
        var MESSAGES_AREA_DEFAULT_HEIGHT = 500;

        /** @type {int} The response default height. */
        var MESSAGES_RESPONSE_DEFAULT_HEIGHT = 50;

        /** @type {Object} The list of selectors for the message area. */
        var SELECTORS = {
            BLOCKTIME: "[data-region='blocktime']",
            CANCELDELETEMESSAGES: "[data-action='cancel-delete-messages']",
            CONTACT: "[data-region='contact']",
            CONVERSATIONS: "[data-region='contacts'][data-region-content='conversations']",
            DELETEALLMESSAGES: "[data-action='delete-all-messages']",
            DELETEMESSAGES: "[data-action='delete-messages']",
            LOADINGICON: '.loading-icon',
            MESSAGE: "[data-region='message']",
            MESSAGERESPONSE: "[data-region='response']",
            MESSAGES: "[data-region='messages']",
            MESSAGESAREA: "[data-region='messages-area']",
            MESSAGINGAREA: "[data-region='messaging-area']",
            SENDMESSAGE: "[data-action='send-message']",
            SENDMESSAGETEXT: "[data-region='send-message-txt']",
            SHOWCONTACTS: "[data-action='show-contacts']",
            STARTDELETEMESSAGES: "[data-action='start-delete-messages']",
        };

        /** @type {int} The number of milliseconds in a second. */
        var MILLISECONDSINSEC = 1000;

        /**
         * Messages class.
         *
         * @param {Messagearea} messageArea The messaging area object.
         */
        function Messages(messageArea) {
            this.messageArea = messageArea;
            this._init();
        }

        /** @type {Boolean} checks if we are sending a message */
        Messages.prototype._isSendingMessage = false;

        /** @type {Boolean} checks if we are currently loading messages */
        Messages.prototype._isLoadingMessages = false;

        /** @type {int} the number of messagess displayed */
        Messages.prototype._numMessagesDisplayed = 0;

        /** @type {array} the messages displayed or about to be displayed on the page */
        Messages.prototype._messageQueue = [];

        /** @type {int} the number of messages to retrieve */
        Messages.prototype._numMessagesToRetrieve = 20;

        /** @type {Modal} the confirmation modal */
        Messages.prototype._confirmationModal = null;

        /** @type {int} the timestamp for the most recent visible message */
        Messages.prototype._latestMessageTimestamp = 0;

        /** @type {BackOffTimer} the backoff timer */
        Messages.prototype._backoffTimer = null;

        /** @type {Messagearea} The messaging area object. */
        Messages.prototype.messageArea = null;

        /**
         * Initialise the event listeners.
         *
         * @private
         */
        Messages.prototype._init = function() {
            CustomEvents.define(this.messageArea.node, [
                CustomEvents.events.activate,
                CustomEvents.events.up,
                CustomEvents.events.down,
                CustomEvents.events.enter,
            ]);

            // We have a responsive media query based on height that reduces this size on screens shorter than 670.
            if ($(window).height() <= 670) {
                MESSAGES_AREA_DEFAULT_HEIGHT = 400;
            }

            AutoRows.init(this.messageArea.node);

            this.messageArea.onCustomEvent(Events.CONVERSATIONSELECTED, this._viewMessages.bind(this));
            this.messageArea.onCustomEvent(Events.SENDMESSAGE, this._viewMessages.bind(this));
            this.messageArea.onCustomEvent(Events.CHOOSEMESSAGESTODELETE, this._chooseMessagesToDelete.bind(this));
            this.messageArea.onCustomEvent(Events.CANCELDELETEMESSAGES, this._hideDeleteAction.bind(this));
            this.messageArea.onDelegateEvent(CustomEvents.events.activate, SELECTORS.SENDMESSAGE,
                this._sendMessage.bind(this));
            this.messageArea.onDelegateEvent(CustomEvents.events.activate, SELECTORS.STARTDELETEMESSAGES,
                this._startDeleting.bind(this));
            this.messageArea.onDelegateEvent(CustomEvents.events.activate, SELECTORS.DELETEMESSAGES,
                this._deleteMessages.bind(this));
            this.messageArea.onDelegateEvent(CustomEvents.events.activate, SELECTORS.DELETEALLMESSAGES,
                this._deleteAllMessages.bind(this));
            this.messageArea.onDelegateEvent(CustomEvents.events.activate, SELECTORS.CANCELDELETEMESSAGES,
                this._triggerCancelMessagesToDelete.bind(this));
            this.messageArea.onDelegateEvent(CustomEvents.events.activate, SELECTORS.MESSAGE,
                this._toggleMessage.bind(this));
            this.messageArea.onDelegateEvent(CustomEvents.events.activate, SELECTORS.SHOWCONTACTS,
                this._hideMessagingArea.bind(this));

            this.messageArea.onDelegateEvent(CustomEvents.events.up, SELECTORS.MESSAGE,
                this._selectPreviousMessage.bind(this));
            this.messageArea.onDelegateEvent(CustomEvents.events.down, SELECTORS.MESSAGE,
                this._selectNextMessage.bind(this));

            this.messageArea.onDelegateEvent('focus', SELECTORS.SENDMESSAGETEXT, this._setMessaging.bind(this));
            this.messageArea.onDelegateEvent('blur', SELECTORS.SENDMESSAGETEXT, this._clearMessaging.bind(this));

            $(document).on(AutoRows.events.ROW_CHANGE, this._adjustMessagesAreaHeight.bind(this));

            // Check if any messages have been displayed on page load.
            var messages = this.messageArea.find(SELECTORS.MESSAGES);
            if (messages.length) {
                this._addScrollEventListener(messages.find(SELECTORS.MESSAGE).length);
                this._latestMessageTimestamp = messages.find(SELECTORS.MESSAGE + ':last').data('timecreated');
            }

            // Create a timer to poll the server for new messages.
            this._backoffTimer = new BackOffTimer(this._loadNewMessages.bind(this),
                BackOffTimer.getIncrementalCallback(this.messageArea.pollmin * MILLISECONDSINSEC, MILLISECONDSINSEC,
                    this.messageArea.pollmax * MILLISECONDSINSEC, this.messageArea.polltimeout * MILLISECONDSINSEC));

            // Start the timer.
            this._backoffTimer.start();
        };

        /**
         * View the message panel.
         *
         * @param {Event} event
         * @param {int} userid
         * @return {Promise} The promise resolved when the messages have been loaded.
         * @private
         */
        Messages.prototype._viewMessages = function(event, userid) {
            // We are viewing another user, or re-loading the panel, so set number of messages displayed to 0.
            this._numMessagesDisplayed = 0;
            // Stop the existing timer so we can set up the new user's messages.
            this._backoffTimer.stop();
            // Reset the latest timestamp when we change the messages view.
            this._latestMessageTimestamp = 0;

            // Mark all the messages as read.
            var markMessagesAsRead = Ajax.call([{
                methodname: 'core_message_mark_all_messages_as_read',
                args: {
                    useridto: this.messageArea.getCurrentUserId(),
                    useridfrom: userid
                }
            }]);

            // Keep track of the number of messages received.
            var numberreceived = 0;
            // Show loading template.
            return Templates.render('core/loading', {}).then(function(html, js) {
                Templates.replaceNodeContents(this.messageArea.find(SELECTORS.MESSAGESAREA), html, js);
                return markMessagesAsRead[0];
            }.bind(this)).then(function() {
                var conversationnode = this.messageArea.find(SELECTORS.CONVERSATIONS + " " +
                    SELECTORS.CONTACT + "[data-userid='" + userid + "']");
                if (conversationnode.hasClass('unread')) {
                    // Remove the class.
                    conversationnode.removeClass('unread');
                    // Trigger an event letting the notification popover (and whoever else) know.
                    $(document).trigger('messagearea:conversationselected', userid);
                }
                return this._getMessages(userid);
            }.bind(this)).then(function(data) {
                numberreceived = data.messages.length;
                // We have the data - lets render the template with it.
                return Templates.render('core_message/message_area_messages_area', data);
            }).then(function(html, js) {
                Templates.replaceNodeContents(this.messageArea.find(SELECTORS.MESSAGESAREA), html, js);
                this._addScrollEventListener(numberreceived);
                // Restart the poll timer.
                this._backoffTimer.restart();
                this.messageArea.find(SELECTORS.SENDMESSAGETEXT).focus();
            }.bind(this)).fail(Notification.exception);
        };

        /**
         * Loads messages while scrolling.
         *
         * @return {Promise|boolean} The promise resolved when the messages have been loaded.
         * @private
         */
        Messages.prototype._loadMessages = function() {
            if (this._isLoadingMessages) {
                return false;
            }

            this._isLoadingMessages = true;

            // Keep track of the number of messages received.
            var numberreceived = 0;
            // Show loading template.
            return Templates.render('core/loading', {}).then(function(html, js) {
                Templates.prependNodeContents(this.messageArea.find(SELECTORS.MESSAGES),
                    "<div style='text-align:center'>" + html + "</div>", js);
                return this._getMessages(this._getUserId());
            }.bind(this)).then(function(data) {
                numberreceived = data.messages.length;
                // We have the data - lets render the template with it.
                return Templates.render('core_message/message_area_messages', data);
            }).then(function(html, js) {
                // Remove the loading icon.
                this.messageArea.find(SELECTORS.MESSAGES + " " +
                    SELECTORS.LOADINGICON).remove();
                // Check if we got something to do.
                if (numberreceived > 0) {
                    var newHtml = $('<div>' + html + '</div>');
                    if (this._hasMatchingBlockTime(this.messageArea.node, newHtml, true)) {
                        this.messageArea.node.find(SELECTORS.BLOCKTIME + ':first').remove();
                    }
                    // Get height before we add the messages.
                    var oldheight = this.messageArea.find(SELECTORS.MESSAGES)[0].scrollHeight;
                    // Show the new content.
                    Templates.prependNodeContents(this.messageArea.find(SELECTORS.MESSAGES), html, js);
                    // Get height after we add the messages.
                    var newheight = this.messageArea.find(SELECTORS.MESSAGES)[0].scrollHeight;
                    // Make sure scroll bar is at the location before we loaded more messages.
                    this.messageArea.find(SELECTORS.MESSAGES).scrollTop(newheight - oldheight);
                    // Increment the number of messages displayed.
                    this._numMessagesDisplayed += numberreceived;
                }
                // Mark that we are no longer busy loading data.
                this._isLoadingMessages = false;
            }.bind(this)).fail(Notification.exception);
        };

        /**
         * Loads and renders messages newer than the most recently seen messages.
         *
         * @return {Promise|boolean} The promise resolved when the messages have been loaded.
         * @private
         */
        Messages.prototype._loadNewMessages = function() {
            if (this._isLoadingMessages) {
                return false;
            }

            // If we have no user id yet then bail early.
            if (!this._getUserId()) {
                return false;
            }

            this._isLoadingMessages = true;

            // Only scroll the message window if the user hasn't scrolled up.
            var shouldScrollBottom = false;
            var messages = this.messageArea.find(SELECTORS.MESSAGES);
            if (messages.length !== 0) {
                var scrollTop = messages.scrollTop();
                var innerHeight = messages.innerHeight();
                var scrollHeight = messages[0].scrollHeight;

                if (scrollTop + innerHeight >= scrollHeight) {
                    shouldScrollBottom = true;
                }
            }

            // Keep track of the number of messages received.
            return this._getMessages(this._getUserId(), true).then(function(data) {
                return this._addMessagesToDom(data.messages, shouldScrollBottom);
            }.bind(this)).always(function() {
                // Mark that we are no longer busy loading data.
                this._isLoadingMessages = false;
            }.bind(this)).fail(Notification.exception);
        };

        /**
         * Handles returning a list of messages to display.
         *
         * @param {int} userid
         * @param {bool} fromTimestamp Load messages from the latest known timestamp
         * @return {Promise} The promise resolved when the contact area has been rendered
         * @private
         */
        Messages.prototype._getMessages = function(userid, fromTimestamp) {
            var args = {
                currentuserid: this.messageArea.getCurrentUserId(),
                otheruserid: userid,
                limitfrom: this._numMessagesDisplayed,
                limitnum: this._numMessagesToRetrieve,
                newest: true
            };

            // If we're trying to load new messages since the message UI was
            // rendered. Used for ajax polling while user is on the message UI.
            if (fromTimestamp) {
                args.timefrom = this._latestMessageTimestamp;
                // Remove limit and offset. We want all new messages.
                args.limitfrom = 0;
                args.limitnum = 0;
            }

            // Call the web service to get our data.
            var promises = Ajax.call([{
                methodname: 'core_message_data_for_messagearea_messages',
                args: args,
            }]);

            // Do stuff when we get data back.
            return promises[0].then(function(data) {
                var messages = data.messages;

                // Did we get any new messages?
                if (messages && messages.length) {
                    var latestMessage = messages[messages.length - 1];

                    // Update our record of the latest known message for future requests.
                    if (latestMessage.timecreated > this._latestMessageTimestamp) {
                        // Next request should be for the second after the most recent message we've seen.
                        this._latestMessageTimestamp = latestMessage.timecreated + 1;
                    }
                }

                return data;
            }.bind(this)).fail(function(ex) {
                // Stop the timer if we received an error so that we don't keep spamming the server.
                this._backoffTimer.stop();
                Notification.exception(ex);
            }.bind(this));
        };

        /**
         * Handles sending a message.
         *
         * @return {Promise|boolean} The promise resolved once the message has been sent.
         * @private
         */
        Messages.prototype._sendMessage = function() {
            var element = this.messageArea.find(SELECTORS.SENDMESSAGETEXT);
            var text = element.val().trim();

            // Do not do anything if it is empty.
            if (text === '') {
                return false;
            }

            // If we are sending a message, don't do anything, be patient!
            if (this._isSendingMessage) {
                return false;
            }

            // Ok, mark that we are sending a message.
            this._isSendingMessage = true;

            // Call the web service to save our message.
            var promises = Ajax.call([{
                methodname: 'core_message_send_instant_messages',
                args: {
                    messages: [
                        {
                            touserid: this._getUserId(),
                            text: text
                        }
                    ]
                }
            }]);

            element.prop('disabled', true);

            // Update the DOM when we get some data back.
            return promises[0].then(function(response) {
                if (response.length < 0) {
                    // Even errors should return valid data.
                    throw new Error('Invalid response');
                }
                if (response[0].errormessage) {
                    throw new Error(response[0].errormessage);
                }
                // Fire an event to say the message was sent.
                this.messageArea.trigger(Events.MESSAGESENT, [this._getUserId(), text]);
                // Update the messaging area.
                return this._addLastMessageToDom();
            }.bind(this)).then(function() {
                // Ok, we are no longer sending a message.
                this._isSendingMessage = false;
            }.bind(this)).always(function() {
                element.prop('disabled', false);
                element.focus();
            }).fail(Notification.exception);
        };

        /**
         * Handles selecting messages to delete.
         *
         * @private
         */
        Messages.prototype._chooseMessagesToDelete = function() {
            this.messageArea.find(SELECTORS.MESSAGESAREA).addClass('editing');
            this.messageArea.find(SELECTORS.MESSAGE)
                .attr('role', 'checkbox')
                .attr('aria-checked', 'false');
        };

        /**
         * Handles deleting messages.
         *
         * @private
         */
        Messages.prototype._deleteMessages = function() {
            var userid = this.messageArea.getCurrentUserId();
            var checkboxes = this.messageArea.find(SELECTORS.MESSAGE + "[aria-checked='true']");
            var requests = [];
            var messagestoremove = [];

            // Go through all the checked checkboxes and prepare them for deletion.
            checkboxes.each(function(id, element) {
                var node = $(element);
                var messageid = node.data('messageid');
                var isread = node.data('messageread') ? 1 : 0;
                messagestoremove.push(node);
                requests.push({
                    methodname: 'core_message_delete_message',
                    args: {
                        messageid: messageid,
                        userid: userid,
                        read: isread
                    }
                });
            });
            if (requests.length > 0) {
                $.when(Ajax.call(requests)).then(function() {
                    // Store the last message on the page, and the last message being deleted.
                    var updatemessage = null;
                    var messages = this.messageArea.find(SELECTORS.MESSAGE);
                    var lastmessage = messages.last();
                    var lastremovedmessage = messagestoremove[messagestoremove.length - 1];
                    // Remove the messages from the DOM.
                    $.each(messagestoremove, function(key, message) {
                        // Remove the message.
                        message.remove();
                    });
                    // If the last message was deleted then we need to provide the new last message.
                    if (lastmessage.data('id') === lastremovedmessage.data('id')) {
                        updatemessage = this.messageArea.find(SELECTORS.MESSAGE).last();
                    }
                    // Now we have removed all the messages from the DOM lets remove any block times we may need to as well.
                    $.each(messagestoremove, function(key, message) {
                        // First - let's make sure there are no more messages in that time block.
                        var blocktime = message.data('blocktime');
                        if (this.messageArea.find(SELECTORS.MESSAGE +
                            "[data-blocktime='" + blocktime + "']").length === 0) {
                            this.messageArea.find(SELECTORS.BLOCKTIME +
                                "[data-blocktime='" + blocktime + "']").remove();
                        }
                    }.bind(this));

                    // If there are no messages at all, then remove conversation panel.
                    if (this.messageArea.find(SELECTORS.MESSAGE).length === 0) {
                        this.messageArea.find(SELECTORS.CONVERSATIONS + " " +
                            SELECTORS.CONTACT + "[data-userid='" + this._getUserId() + "']").remove();
                    }

                    // Trigger event letting other modules know messages were deleted.
                    this.messageArea.trigger(Events.MESSAGESDELETED, [this._getUserId(), updatemessage]);
                }.bind(this)).catch(Notification.exception);
            } else {
                // Trigger event letting other modules know messages were deleted.
                this.messageArea.trigger(Events.MESSAGESDELETED, this._getUserId());
            }

            // Hide the items responsible for deleting messages.
            this._hideDeleteAction();
        };

        /**
         * Handles adding a scrolling event listener.
         *
         * @param {int} numberreceived The number of messages received
         * @private
         */
        Messages.prototype._addScrollEventListener = function(numberreceived) {
            // Scroll to the bottom.
            this._scrollBottom();
            // Set the number of messages displayed.
            this._numMessagesDisplayed = numberreceived;
            // Now enable the ability to infinitely scroll through messages.
            CustomEvents.define(this.messageArea.find(SELECTORS.MESSAGES), [
                CustomEvents.events.scrollTop
            ]);
            // Assign the event for scrolling.
            this.messageArea.onCustomEvent(CustomEvents.events.scrollTop, this._loadMessages.bind(this));
        };

        /**
         * Handles deleting a conversation.
         *
         * @private
         */
        Messages.prototype._deleteAllMessages = function() {
            if (this._confirmationModal) {
                // Just show the existing modal.
                this._confirmationModal.show();
                return;
            }

            var stringsPromise = Str.get_strings([
                {key: 'confirm'},
                {key: 'deleteallconfirm', component: 'message'},
                {key: 'delete'}
            ]);
            var deleteModalPromise = ModalFactory.create(
                {
                    type: ModalFactory.types.SAVE_CANCEL
                },
                this.messageArea.find(SELECTORS.DELETEALLMESSAGES)
            );

            $.when(stringsPromise, deleteModalPromise).then(function(s, modal) {
                modal.setTitle(s[0]);
                modal.setBody(s[1]);
                modal.setSaveButtonText(s[2]);

                this._confirmationModal = modal;
                // Only delete the conversation if the user agreed in the confirmation modal.
                modal.getRoot().on(ModalEvents.save, function() {
                    var otherUserId = this._getUserId();
                    var request = {
                        methodname: 'core_message_delete_conversation',
                        args: {
                            userid: this.messageArea.getCurrentUserId(),
                            otheruserid: otherUserId
                        }
                    };

                    // Delete the conversation.
                    Ajax.call([request])[0].then(function() {
                        // Clear the message area.
                        this.messageArea.find(SELECTORS.MESSAGESAREA).empty();
                        // Let the app know a conversation was deleted.
                        this.messageArea.trigger(Events.CONVERSATIONDELETED, otherUserId);
                        this._hideDeleteAction();
                    }.bind(this)).catch(Notification.exception);
                }.bind(this));

                // Display the confirmation.
                modal.show();
            }.bind(this)).catch(Notification.exception);
        };

        /**
         * Handles hiding the delete checkboxes and replacing the response area.
         *
         * @private
         */
        Messages.prototype._hideDeleteAction = function() {
            this.messageArea.find(SELECTORS.MESSAGE)
                .removeAttr('role')
                .removeAttr('aria-checked');
            this.messageArea.find(SELECTORS.MESSAGESAREA).removeClass('editing');
        };

        /**
         * Triggers the CANCELDELETEMESSAGES event.
         *
         * @private
         */
        Messages.prototype._triggerCancelMessagesToDelete = function() {
            // Trigger event letting other modules know message deletion was canceled.
            this.messageArea.trigger(Events.CANCELDELETEMESSAGES);
        };

        /**
         * Handles adding messages to the DOM.
         *
         * @param {array} messages An array of messages to be added to the DOM.
         * @param {boolean} shouldScrollBottom True will scroll to the bottom of the message window and show the new messages.
         * @return {Promise} The promise resolved when the messages have been added to the DOM.
         * @private
         */
        Messages.prototype._addMessagesToDom = function(messages, shouldScrollBottom) {
            var numberreceived = 0;
            var messagesArea = this.messageArea.find(SELECTORS.MESSAGES);
            messages = messages.filter(function(message) {
                var id = "" + message.id + message.isread;
                // If the message is already queued to be rendered, remove from the list of messages.
                if (this._messageQueue[id]) {
                    return false;
                }
                // Filter out any messages already rendered.
                var result = messagesArea.find(SELECTORS.MESSAGE + '[data-id="' + id + '"]');
                // Any message we are rendering should go in the messageQueue.
                if (!result.length) {
                    this._messageQueue[id] = true;
                }
                return !result.length;
            }.bind(this));
            numberreceived = messages.length;
            // We have the data - lets render the template with it.
            return Templates.render('core_message/message_area_messages', {messages: messages}).then(function(html, js) {
                // Check if we got something to do.
                if (numberreceived > 0) {
                    var newHtml = $('<div>' + html + '</div>');
                    if (this._hasMatchingBlockTime(this.messageArea.node, newHtml, false)) {
                        newHtml.find(SELECTORS.BLOCKTIME + ':first').remove();
                    }
                    // Show the new content.
                    Templates.appendNodeContents(this.messageArea.find(SELECTORS.MESSAGES), newHtml, js);
                    // Scroll the new message into view.
                    if (shouldScrollBottom) {
                        this._scrollBottom();
                    }
                    // Increment the number of messages displayed.
                    this._numMessagesDisplayed += numberreceived;
                    // Reset the poll timer because the user may be active.
                    this._backoffTimer.restart();
                }
            }.bind(this));
        };

        /**
         * Handles adding the last message to the DOM.
         *
         * @return {Promise} The promise resolved when the message has been added to the DOM.
         * @private
         */
        Messages.prototype._addLastMessageToDom = function() {
            // Call the web service to return how the message should look.
            var promises = Ajax.call([{
                methodname: 'core_message_data_for_messagearea_get_most_recent_message',
                args: {
                    currentuserid: this.messageArea.getCurrentUserId(),
                    otheruserid: this._getUserId()
                }
            }]);

            // Add the message.
            return promises[0].then(function(data) {
                return this._addMessagesToDom([data], true);
            }.bind(this)).always(function() {
                // Empty the response text area.text
                this.messageArea.find(SELECTORS.SENDMESSAGETEXT).val('').trigger('input');
            }.bind(this)).fail(Notification.exception);
        };

        /**
         * Returns the ID of the other user in the conversation.
         *
         * @return {int} The user id
         * @private
         */
        Messages.prototype._getUserId = function() {
            return this.messageArea.find(SELECTORS.MESSAGES).data('userid');
        };

        /**
         * Scrolls to the bottom of the messages.
         *
         * @private
         */
        Messages.prototype._scrollBottom = function() {
            // Scroll to the bottom.
            var messages = this.messageArea.find(SELECTORS.MESSAGES);
            if (messages.length !== 0) {
                messages.scrollTop(messages[0].scrollHeight);
            }
        };

        /**
         * Select the previous message in the list.
         *
         * @param {event} e The jquery event
         * @param {object} data Extra event data
         * @private
         */
        Messages.prototype._selectPreviousMessage = function(e, data) {
            var currentMessage = $(e.target).closest(SELECTORS.MESSAGE);

            do {
                currentMessage = currentMessage.prev();
            } while (currentMessage.length && !currentMessage.is(SELECTORS.MESSAGE));

            currentMessage.focus();

            data.originalEvent.preventDefault();
            data.originalEvent.stopPropagation();
        };

        /**
         * Select the next message in the list.
         *
         * @param {event} e The jquery event
         * @param {object} data Extra event data
         * @private
         */
        Messages.prototype._selectNextMessage = function(e, data) {
            var currentMessage = $(e.target).closest(SELECTORS.MESSAGE);

            do {
                currentMessage = currentMessage.next();
            } while (currentMessage.length && !currentMessage.is(SELECTORS.MESSAGE));

            currentMessage.focus();

            data.originalEvent.preventDefault();
            data.originalEvent.stopPropagation();
        };

        /**
         * Flag the response area as messaging.
         *
         * @param {event} e The jquery event
         * @private
         */
        Messages.prototype._setMessaging = function(e) {
            $(e.target).closest(SELECTORS.MESSAGERESPONSE).addClass('messaging');
        };

        /**
         * Clear the response area as messaging flag.
         *
         * @param {event} e The jquery event
         * @private
         */
        Messages.prototype._clearMessaging = function(e) {
            $(e.target).closest(SELECTORS.MESSAGERESPONSE).removeClass('messaging');
        };

        /**
         * Turn on delete message mode.
         *
         * @param {event} e The jquery event
         * @private
         */
        Messages.prototype._startDeleting = function(e) {
            var actions = new Actions(this.messageArea);
            actions.chooseMessagesToDelete();

            e.preventDefault();
        };

        /**
         * Check if the message area is in editing mode.
         *
         * @return {bool}
         * @private
         */
        Messages.prototype._isEditing = function() {
            return this.messageArea.find(SELECTORS.MESSAGESAREA).hasClass('editing');
        };

        /**
         * Check or uncheck the message if the message area is in editing mode.
         *
         * @param {event} e The jquery event
         * @private
         */
        Messages.prototype._toggleMessage = function(e) {
            if (!this._isEditing()) {
                return;
            }

            var message = $(e.target).closest(SELECTORS.MESSAGE);

            if (message.attr('aria-checked') === 'true') {
                message.attr('aria-checked', 'false');
            } else {
                message.attr('aria-checked', 'true');
            }
        };

        /**
         * Adjust the height of the messages area to match the changed height of
         * the response area.
         *
         * @private
         */
        Messages.prototype._adjustMessagesAreaHeight = function() {
            var messagesArea = this.messageArea.find(SELECTORS.MESSAGES);
            var messagesResponse = this.messageArea.find(SELECTORS.MESSAGERESPONSE);

            var currentMessageResponseHeight = messagesResponse.outerHeight();
            var diffResponseHeight = currentMessageResponseHeight - MESSAGES_RESPONSE_DEFAULT_HEIGHT;
            var newMessagesAreaHeight = MESSAGES_AREA_DEFAULT_HEIGHT - diffResponseHeight;

            messagesArea.outerHeight(newMessagesAreaHeight);
        };

        /**
         * Handle the event that triggers sending a message from the messages area.
         *
         * @param {event} e The jquery event
         * @param {object} data Additional event data
         * @private
         */
        Messages.prototype._sendMessageHandler = function(e, data) {
            data.originalEvent.preventDefault();

            this._sendMessage();
        };

        /**
         * Hide the messaging area. This only applies on smaller screen resolutions.
         *
         * @private
         */
        Messages.prototype._hideMessagingArea = function() {
            this.messageArea.find(SELECTORS.MESSAGINGAREA)
                .removeClass('show-messages')
                .addClass('hide-messages');
        };

        /**
         * Checks if a day separator needs to be removed.
         *
         * Example - scrolling up and loading previous messages that belong to the
         * same day as the last message that was previously shown, meaning we can
         * remove the original separator.
         *
         * @param {jQuery} domHtml The HTML in the DOM.
         * @param {jQuery} newHtml The HTML to compare to the DOM
         * @param {boolean} loadingPreviousMessages Are we loading previous messages?
         * @return {boolean}
         * @private
         */
        Messages.prototype._hasMatchingBlockTime = function(domHtml, newHtml, loadingPreviousMessages) {
            var blockTime, blockTimePos, newBlockTime, newBlockTimePos;

            if (loadingPreviousMessages) {
                blockTimePos = ':first';
                newBlockTimePos = ':last';
            } else {
                blockTimePos = ':last';
                newBlockTimePos = ':first';
            }

            blockTime = domHtml.find(SELECTORS.BLOCKTIME + blockTimePos);
            newBlockTime = newHtml.find(SELECTORS.BLOCKTIME + newBlockTimePos);

            if (blockTime.length && newBlockTime.length) {
                return blockTime.data('blocktime') == newBlockTime.data('blocktime');
            }

            return false;
        };

        return Messages;
    }
);
