/**
 * <p>
 * Copyright (C) 2004-2011 Jive Software. All rights reserved.
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jivesoftware.spark.ui.rooms;

import org.jivesoftware.resource.Res;
import org.jivesoftware.resource.SparkRes;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.filter.*;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.MessageBuilder;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smack.packet.StanzaBuilder;
import org.jivesoftware.smack.packet.StanzaError;
import org.jivesoftware.smackx.chatstates.ChatState;
import org.jivesoftware.smackx.delay.packet.DelayInformation;
import org.jivesoftware.smackx.muc.MultiUserChat;
import org.jivesoftware.smackx.muc.ParticipantStatusListener;
import org.jivesoftware.smackx.muc.UserStatusListener;
import org.jivesoftware.smackx.muc.packet.Destroy;
import org.jivesoftware.smackx.muc.packet.MUCUser;
import org.jivesoftware.smackx.vcardtemp.packet.VCard;
import org.jivesoftware.smackx.xdata.form.Form;
import org.jivesoftware.smackx.xevent.MessageEventManager;
import org.jivesoftware.spark.ChatManager;
import org.jivesoftware.spark.SparkManager;
import org.jivesoftware.spark.component.RolloverButton;
import org.jivesoftware.spark.plugin.ContextMenuListener;
import org.jivesoftware.spark.ui.*;
import org.jivesoftware.spark.ui.conferences.AnswerFormDialog;
import org.jivesoftware.spark.ui.conferences.ConferenceUtils;
import org.jivesoftware.spark.ui.conferences.DataFormDialog;
import org.jivesoftware.spark.ui.conferences.GroupChatParticipantList;
import org.jivesoftware.spark.util.ModelUtil;
import org.jivesoftware.spark.util.UIComponentRegistry;
import org.jivesoftware.spark.util.XEP0392Utils;
import org.jivesoftware.spark.util.log.Log;
import org.jivesoftware.sparkimpl.settings.local.LocalPreferences;
import org.jivesoftware.sparkimpl.settings.local.SettingsManager;
import org.jxmpp.jid.*;
import org.jxmpp.jid.parts.Resourcepart;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.MouseEvent;
import java.net.URL;
import java.util.*;
import java.util.List;
import java.util.regex.Pattern;

import static java.awt.GridBagConstraints.HORIZONTAL;
import static java.awt.GridBagConstraints.NORTHWEST;
import static java.util.regex.Pattern.CASE_INSENSITIVE;
import static org.jivesoftware.spark.ChatManager.ERROR_COLOR;
import static org.jivesoftware.spark.ChatManager.NOTIFICATION_COLOR;

/**
 * GroupChatRoom is the conference chat room UI used to have Multi-User Chats.
 */
public class GroupChatRoom extends ChatRoom {
    private static final Color COLOR_HIGHLIGHT_MINE_BG = new Color(244, 248, 255);
    private static final Color COLOR_HIGHLIGHT_MENTION_BG = new Color(255, 255, 153);
    private static final Color COLOR_ALPHA = new Color(0, 0, 0, 0);

    private final LocalPreferences pref = SettingsManager.getLocalPreferences();
    private final MultiUserChat chat;
    private final SubjectPanel subjectPanel;
    private final List<EntityFullJid> currentUserList = new ArrayList<>();
    private final List<EntityFullJid> blockedUsers = new ArrayList<>();
    private final GroupChatParticipantList roomInfo;
    private final RolloverButton settings;
    /** compiled regexp by Nick and JID for {@link #getMessageBackground} */
    private Pattern myNicknameMatch;
    private Icon tabIcon = SparkRes.getImageIcon(SparkRes.Icon.CONFERENCE_IMAGE_16x16);
    private String password = null;
    private String tabTitle;
    private boolean isActive = true;
    private Message lastMessage;
    private boolean chatStatEnabled;

    public GroupChatRoom(final MultiUserChat chat) {
        this.chat = chat;
        EntityBareJid roomBareJid = getBareJid();
        Log.debug("Loading Group Chat Room for " + roomBareJid);
        // Create the filter and register with the current connection, making sure to filter by room
        final StanzaFilter fromFilter = FromMatchesFilter.createBare(roomBareJid);
        final StanzaFilter orFilter = new OrFilter(new StanzaTypeFilter(Presence.class), new StanzaTypeFilter(Message.class));
        final StanzaFilter andFilter = new AndFilter(orFilter, fromFilter);
        // Add packet Listener.
        SparkManager.getConnection().addAsyncStanzaListener(this, andFilter);
        // We are just using a generic Group Chat.
        tabTitle = roomBareJid.getLocalpart().asUnescapedString();
        // Room Information
        roomInfo = UIComponentRegistry.createGroupChatParticipantList();
        getSplitPane().setRightComponent(roomInfo.getGUI());
        getSplitPane().setResizeWeight(0.8);

        roomInfo.setChatRoom(this);

        setupListeners();

        VCard vcard = SparkManager.getVCardManager().getVCard(roomBareJid);
        byte[] avatar = vcard != null ? vcard.getAvatar() : null;
        subjectPanel = new SubjectPanel(roomBareJid, tabTitle, chat.getSubject(), avatar);
        // Do not show the top toolbar
        getToolBar().add(subjectPanel,
            new GridBagConstraints(0, 1, 1, 1, 1, 0,
                NORTHWEST, HORIZONTAL, new Insets(0, 2, 0, 2), 0, 0));
        // Add ContextMenuListener
        getTranscriptWindow().addContextMenuListener(new ContextMenuListener() {
            @Override
            public void poppingUp(Object component, JPopupMenu popup) {
                popup.addSeparator();
                Action inviteAction = new AbstractAction() {
                    @Override
                    public void actionPerformed(ActionEvent actionEvent) {
                        ConferenceUtils.inviteUsersToRoom(roomBareJid, null, false);
                    }
                };

                inviteAction.putValue(Action.NAME, Res.getString("menuitem.invite.users"));
                inviteAction.putValue(Action.SMALL_ICON, SparkRes.getImageIcon(SparkRes.Icon.SMALL_MESSAGE_IMAGE));

                popup.add(inviteAction);

                Action copyUriGroupChat = new AbstractAction() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        SparkManager.setClipboard("xmpp:" + roomBareJid + "?join");
                    }
                };

                copyUriGroupChat.putValue(Action.NAME, Res.getString("button.copy.to.clipboard"));
                copyUriGroupChat.putValue(Action.SMALL_ICON, SparkRes.getImageIcon(SparkRes.Icon.COPY_16x16));
                popup.add(copyUriGroupChat);

                Action configureAction = new AbstractAction() {
                    @Override
                    public void actionPerformed(ActionEvent actionEvent) {
                        DataFormDialog.openDataFormDialog(chat);
                    }
                };

                configureAction.putValue(Action.NAME, Res.getString("title.configure.room"));
                configureAction.putValue(Action.SMALL_ICON, SparkRes.getImageIcon(SparkRes.Icon.SETTINGS_IMAGE_16x16));
                if (SparkManager.getUserManager().isOwner((GroupChatRoom) getChatRoom(), getNickname())) {
                    popup.add(configureAction);
                }

                Action subjectChangeAction = new AbstractAction() {
                    @Override
                    public void actionPerformed(ActionEvent actionEvent) {
                        String newSubject = JOptionPane.showInputDialog(
                            getChatRoom(),
                            Res.getString("message.enter.new.subject") + ":",
                            Res.getString("title.change.subject"),
                            JOptionPane.QUESTION_MESSAGE);
                        if (ModelUtil.hasLength(newSubject)) {
                            try {
                                chat.changeSubject(newSubject);
                            } catch (XMPPException | SmackException | InterruptedException e) {
                                Log.error(e);
                            }
                        }
                    }
                };

                subjectChangeAction.putValue(Action.NAME, Res.getString("menuitem.change.subject"));
                subjectChangeAction.putValue(Action.SMALL_ICON, SparkRes.getImageIcon(SparkRes.Icon.SMALL_MESSAGE_EDIT_IMAGE));
                popup.add(subjectChangeAction);

                // Define actions to modify/view room information
                Action destroyRoomAction = new AbstractAction() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        int ok = JOptionPane.showConfirmDialog(
                            getChatRoom(),
                            Res.getString("message.confirm.destruction.of.room"),
                            Res.getString("title.confirmation"),
                            JOptionPane.YES_NO_OPTION,
                            JOptionPane.QUESTION_MESSAGE);
                        if (ok == JOptionPane.NO_OPTION) {
                            return;
                        }

                        String reason = JOptionPane.showInputDialog(
                            getChatRoom(),
                            Res.getString("message.room.destruction.reason"),
                            Res.getString("title.enter.reason"),
                            JOptionPane.QUESTION_MESSAGE);
                        if (ModelUtil.hasLength(reason)) {
                            try {
                                chat.destroy(reason, null);
                                getChatRoom().leaveChatRoom();
                            } catch (XMPPException | SmackException | InterruptedException e1) {
                                Log.warning("Unable to destroy room", e1);
                            }
                        }
                    }
                };

                destroyRoomAction.putValue(Action.NAME, Res.getString("menuitem.destroy.room"));
                destroyRoomAction.putValue(Action.SMALL_ICON, SparkRes.getImageIcon(SparkRes.Icon.SMALL_DELETE));
                if (SparkManager.getUserManager().isOwner((GroupChatRoom) getChatRoom(), getNickname())) {
                    popup.add(destroyRoomAction);
                }
            }

            @Override
            public boolean handleDefaultAction(MouseEvent e) {
                return false;
            }
        });

        // set the last activity to be right now
        lastActivity = System.currentTimeMillis();

        final GroupChatRoomTransferHandler transferHandler = new GroupChatRoomTransferHandler(this);
        getTranscriptWindow().setTransferHandler(transferHandler);
        // Adds the Settings and Subject Button to the right Toolbar
        settings = UIComponentRegistry.getButtonFactory().createSettingsButton();
        settings.setVisible(false);
        final RolloverButton thema = UIComponentRegistry.getButtonFactory().createTemaButton();
        final RolloverButton register = UIComponentRegistry.getButtonFactory().createRegisterButton();

        addControllerButton(settings);
        addControllerButton(thema);
        addControllerButton(register);

        settings.addActionListener(new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent event) {
                DataFormDialog.openDataFormDialog(chat);
            }
        });

        thema.addActionListener(new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                final String newSubject = JOptionPane.showInputDialog(getChatRoom(),
                    Res.getString("message.enter.new.subject") + ":",
                    Res.getString("title.change.subject"),
                    JOptionPane.QUESTION_MESSAGE);
                if (ModelUtil.hasLength(newSubject)) {
                    try {
                        chat.changeSubject(newSubject);
                    } catch (XMPPException | SmackException | InterruptedException xmpee) {
                        getTranscriptWindow().insertNotificationMessage(xmpee.getMessage(), ERROR_COLOR);
                        scrollToBottom();
                    }
                }
            }
        });

        register.addActionListener(event -> {
            try {
                final Form form = chat.getRegistrationForm();
                if (form != null) {
                    final ChatFrame chatFrame = SparkManager.getChatManager().getChatContainer().getChatFrame();
                    new AnswerFormDialog(chatFrame, chat, form.getDataForm());
                } else {
                    getTranscriptWindow().insertNotificationMessage(Res.getString("message.registration_not_needed"), ERROR_COLOR);
                }
            } catch (XMPPException | SmackException | InterruptedException xmpe) {
                getTranscriptWindow().insertNotificationMessage(xmpe.getMessage(), ERROR_COLOR);
                scrollToBottom();
            }
        });
        Log.debug("Loaded Group Chat Room for " + roomBareJid);
    }

    private Pattern compileMyNicknameMatch() {
        String myUserName = SparkManager.getSessionManager().getUsername();
        return Pattern.compile(Pattern.quote(myUserName) + "|" + Pattern.quote(getNickname().toString()), CASE_INSENSITIVE);
    }

    public Message getLastMessage() {
        return lastMessage;
    }

    /**
     * Have the user leave this chat room and then close it.
     */
    @Override
    public void closeChatRoom() {
        // Specify the end time.
        super.closeChatRoom();
        // Remove Listener
        SparkManager.getConnection().removeAsyncStanzaListener(this);
        final ChatContainer container = SparkManager.getChatManager().getChatContainer();
        container.leaveChatRoom(this);
        container.closeTab(this);
    }

    /**
     * Determines the background color to use for messages.
     *
     * @param nickname Nickname associated with the message.
     * @param body     Body of the message to scan for reasons to highlight.
     * @return Color of message background.
     */
    private Color getMessageBackground(Resourcepart nickname, String body) {
        final Resourcepart myNickName = getNickname();
        if (myNicknameMatch == null) {
            // TODO recalculate on nick change
            myNicknameMatch = compileMyNicknameMatch();
        }
        // Should we even highlight this packet?
        if (pref.isMucHighNameEnabled() && myNickName.equals(nickname)) {
            return COLOR_HIGHLIGHT_MINE_BG;
        } else if (pref.isMucHighTextEnabled() && myNicknameMatch.matcher(body).find()) {
            return COLOR_HIGHLIGHT_MENTION_BG;
        } else {
            return COLOR_ALPHA;
        }
    }

    /**
     * Sends a message.
     *
     * @param messageBuilder - the message to send.
     */
    @Override
    public void sendMessage(MessageBuilder messageBuilder) {
        messageBuilder.ofType(Message.Type.groupchat);
        messageBuilder.to(getBareJid());
        Message message = messageBuilder.build();
        try {
            MessageEventManager.addNotificationsRequests(message, true, true, true, true);
            addPacketID(message.getStanzaId());

            SparkManager.getChatManager().filterOutgoingMessage(this, messageBuilder);
            SparkManager.getChatManager().fireGlobalMessageSentListeners(this, message);

            chat.sendMessage(messageBuilder);
        } catch (Exception ex) {
            Log.error("Unable to send message in conference chat.", ex);
        }

        // Notify users that the message has been sent.
        fireMessageSent(message);
        addToTranscript(message, false);

        getChatInputEditor().clear();
        getTranscriptWindow().validate();
        getTranscriptWindow().repaint();

        getChatInputEditor().setCaretPosition(0);
        getChatInputEditor().requestFocusInWindow();
        scrollToBottom();

        lastActivity = System.currentTimeMillis();
    }

    @Override
    public EntityBareJid getBareJid() {
        return chat.getRoom();
    }

    @Override
    public EntityJid getJid() {
        return chat.getRoom();
    }

    /**
     * Retrieve the nickname of the user in this groupchat.
     */
    @Override
    public Resourcepart getNickname() {
        return chat.getNickname();
    }

    /**
     * Return the Icon that should be used in the tab of this GroupChat Pane.
     */
    @Override
    public Icon getTabIcon() {
        return tabIcon;
    }

    /**
     * Sets the icon that should be used in the tab of this GroupChat Pane.
     */
    public void setTabIcon(ImageIcon tabIcon) {
        this.tabIcon = tabIcon;
    }

    /**
     * Return the title that should be used in the tab.
     */
    @Override
    public String getTabTitle() {
        return tabTitle;
    }

    /**
     * Sets the title to use on the tab describing the Conference room.
     */
    public void setTabTitle(CharSequence tabTitle) {
        this.tabTitle = tabTitle.toString();
    }

    /**
     * Return the title of this room.
     */
    @Override
    public String getRoomTitle() {
        return getTabTitle();
    }

    /**
     * Return the type of chat we are in.
     */
    @Override
    public Message.Type getChatType() {
        return Message.Type.groupchat;
    }

    /**
     * Implementation of leaveChatRoom.
     */
    @Override
    public void leaveChatRoom() {
        if (!isActive) {
            return;
        }
        SparkManager.getConnection().removeAsyncStanzaListener(this);
        getChatInputEditor().showAsDisabled();
        // Do not allow other to try and invite or transfer chat
        disableToolbar();
        getToolBar().setVisible(false);
        // Update Room Notice To Inform Agent that he has left the chat.
        getTranscriptWindow().insertNotificationMessage(Res.getString("message.user.left.room", getNickname()), NOTIFICATION_COLOR);
        SwingUtilities.invokeLater(() -> {
            try {
                chat.leave();
            } catch (Exception e) {
                Log.error("Closing Group Chat Room error.", e);
            }
        });
        // Set the window as greyed out.
        getTranscriptWindow().showWindowDisabled();
        // Update Notification Label
        getNotificationLabel().setText(Res.getString("message.chat.session.ended", SparkManager.DATE_SECOND_FORMATTER.format(new java.util.Date())));
        getNotificationLabel().setIcon(null);
        getNotificationLabel().setEnabled(false);

        getSplitPane().setRightComponent(null);
        getSplitPane().setDividerSize(0);
        isActive = false;
    }

    /**
     * Returns whether or not this ChatRoom is active. To be active means to
     * have the agent still engaged in a conversation with a customer.
     *
     * @return true if the ChatRoom is active.
     */
    @Override
    public boolean isActive() {
        return isActive;
    }

    /**
     * Implementation of processPacket to handle muc related packets.
     *
     * @param stanza the packet.
     */
    @Override
    public void processStanza(final Stanza stanza) {
        if (stanza instanceof Presence) {
            SwingUtilities.invokeLater(() -> handlePresencePacket(stanza));
        }
        if (stanza instanceof Message) {
            SwingUtilities.invokeLater(() -> {
                handleMessagePacket(stanza);
                // Set last activity
                lastActivity = System.currentTimeMillis();
            });
        }
    }

    /**
     * Handle all MUC related packets.
     */
    private void handleMessagePacket(Stanza stanza) {
        // Do something with the incoming packet here.
        final Message message = (Message) stanza;
        lastMessage = message;
        if (message.getType() == Message.Type.groupchat) {
            // Do not accept Administrative messages.
            final DomainBareJid host = SparkManager.getSessionManager().getServerAddress();
            if (host.equals(message.getFrom())) {
                return;
            }
            if (ModelUtil.hasLength(message.getBody())) {
                final Resourcepart from = message.getFrom().getResourceOrThrow();
                final DelayInformation inf = message.getExtension(DelayInformation.class);
                final Date sentDate = inf != null ? inf.getStamp() : new Date();
                if (inf != null) {
                    // This is part of the MUC history. No need to add it to the transcript again.
                    // Add to the UI component that shows the chat.
                    getTranscriptWindow().insertHistoryMessage(from.toString(), message.getBody(), sentDate);
                } else {
                    // A 'regular' chat message.
                    if (isBlocked(message.getFrom().asEntityFullJidIfPossible())) {
                        return;
                    }
                    final boolean isFromRoom = !message.getFrom().hasNoResource();
                    if (!isFromRoom && SparkManager.getUserManager().getOccupant(this, from) == null) {
                        return;
                    }
                    // Update transcript
                    insertMessage(message);
                }
            }
        } else if (message.getType() == Message.Type.chat) {
            try {
                SparkManager.getChatManager().getChatContainer().getChatRoom(message.getFrom().asEntityJidOrThrow());
            } catch (ChatRoomNotFoundException e) {
                final Resourcepart userNickname = message.getFrom().getResourceOrEmpty();
                final String roomTitle = userNickname + " - " + getBareJid();
                // Check to see if this is a message notification.
                if (message.getBody() != null) {
                    // Create the new room
                    ChatRoom chatRoom = new ChatRoomImpl(message.getFrom().asEntityJidOrThrow(), userNickname, roomTitle);
                    SparkManager.getChatManager().getChatContainer().addChatRoom(chatRoom);
                    chatRoom.insertMessage(message);
                }
            }
        } else if (message.getError() != null) {
            String errorMessage = "";
            if (message.getError().getCondition() == StanzaError.Condition.forbidden && message.getSubject() != null) {
                errorMessage = Res.getString("message.subject.change.error");
            } else if (message.getError().getCondition() == StanzaError.Condition.forbidden) {
                errorMessage = Res.getString("message.forbidden.error");
            }
            if (ModelUtil.hasLength(errorMessage)) {
                getTranscriptWindow().insertNotificationMessage(errorMessage, ERROR_COLOR);
            }
        }
        // Scroll To bottom every time a message is received
        scrollToBottom();
    }

    @Override
    public Message insertMessage(Message message) {
        message = super.insertMessage(message);
        final Resourcepart from = message.getFrom().getResourceOrThrow();
        // Add to the UI component that shows the chat.
        getTranscriptWindow().insertMessage(from, message, getColor(from), getMessageBackground(from, message.getBody()));
        return message;
    }

    /**
     * Handle all presence packets being sent to this Group Chat Room.
     */
    private void handlePresencePacket(Stanza stanza) {
        final Presence presence = (Presence) stanza;
        if (presence.getError() != null) {
            return;
        }
        final EntityFullJid from = presence.getFrom().asEntityFullJidIfPossible();
        if (from == null) {
            return;
        }

        final Resourcepart nickname = from.getResourcepart();
        final MUCUser mucUser = stanza.getExtension(MUCUser.class);
        Set<MUCUser.Status> status = null;
        if (mucUser != null) {
            status = mucUser.getStatus();
            final Destroy destroy = mucUser.getDestroy();
            if (destroy != null) {
                JOptionPane.showMessageDialog(this,
                    (destroy.getReason() != null ? Res.getString("message.room.destroyed", destroy.getReason()) : Res.getString("message.room.destroyed.no.reason")),
                    Res.getString("title.room.destroyed"),
                    JOptionPane.INFORMATION_MESSAGE);
                leaveChatRoom();
                return;
            }
        }

        if (presence.getType() == Presence.Type.unavailable && status != null && !status.contains(MUCUser.Status.NEW_NICKNAME_303)) {
            synchronized (currentUserList) {
                if (currentUserList.contains(from)) {
                    if (pref.isShowJoinLeaveMessagesEnabled()) {
                        getTranscriptWindow().insertNotificationMessage(Res.getString("message.user.left.room", nickname), NOTIFICATION_COLOR);
                        scrollToBottom();
                    }
                    currentUserList.remove(from);
                }
            }
        } else {
            synchronized (currentUserList) {
                if (!currentUserList.contains(from)) {
                    currentUserList.add(from);
                    setChatInputEnabled(true);
                    if (pref.isShowJoinLeaveMessagesEnabled()) {
                        getTranscriptWindow().insertNotificationMessage(
                            Res.getString("message.user.joined.room", nickname),
                            NOTIFICATION_COLOR);
                        scrollToBottom();
                    }
                }
            }
        }
    }

    /**
     * Set up the participant listeners and status change listeners.
     */
    private void setupListeners() {
        chat.addParticipantStatusListener(new ParticipantStatusListener() {
            @Override
            public void kicked(EntityFullJid participant, Jid actor, String reason) {
                if (reason == null || reason.isEmpty()) {
                    insertText(Res.getString("message.user.kicked.from.room.no.reason", participant.getResourcepart(), actor));
                } else {
                    insertText(Res.getString("message.user.kicked.from.room", participant.getResourcepart(), actor, reason));
                }
            }

            @Override
            public void voiceGranted(EntityFullJid participant) {
                insertText(Res.getString("message.user.given.voice", participant.getResourcepart()));
            }

            @Override
            public void voiceRevoked(EntityFullJid participant) {
                insertText(Res.getString("message.user.voice.revoked", participant.getResourcepart()));
            }

            @Override
            public void banned(EntityFullJid participant, Jid actor, String reason) {
                if (reason == null || reason.isEmpty()) {
                    insertText(Res.getString("message.user.banned.no.reason", participant.getResourcepart()));
                } else {
                    insertText(Res.getString("message.user.banned", participant.getResourcepart(), reason));
                }
            }

            @Override
            public void membershipGranted(EntityFullJid participant) {
                insertText(Res.getString("message.user.granted.membership", participant.getResourcepart()));
            }

            @Override
            public void membershipRevoked(EntityFullJid participant) {
                insertText(Res.getString("message.user.revoked.membership", participant.getResourcepart()));
            }

            @Override
            public void moderatorGranted(EntityFullJid participant) {
                insertText(Res.getString("message.user.granted.moderator", participant.getResourcepart()));
            }

            @Override
            public void moderatorRevoked(EntityFullJid participant) {
                insertText(Res.getString("message.user.revoked.moderator", participant.getResourcepart()));
            }

            @Override
            public void ownershipGranted(EntityFullJid participant) {
                insertText(Res.getString("message.user.granted.owner", participant.getResourcepart()));
            }

            @Override
            public void ownershipRevoked(EntityFullJid participant) {
                insertText(Res.getString("message.user.revoked.owner", participant.getResourcepart()));
            }

            @Override
            public void adminGranted(EntityFullJid participant) {
                insertText(Res.getString("message.user.granted.admin", participant.getResourcepart()));
            }

            @Override
            public void adminRevoked(EntityFullJid participant) {
                insertText(Res.getString("message.user.revoked.admin", participant.getResourcepart()));
            }

            @Override
            public void nicknameChanged(EntityFullJid participant, Resourcepart nickname) {
                insertText(Res.getString("message.user.nickname.changed", participant.getResourcepart(), nickname));
            }
        });

        chat.addUserStatusListener(new UserStatusListener() {
            @Override
            public void kicked(Jid s, String reason) {
                if (ModelUtil.hasLength(reason)) {
                    insertText(reason);
                } else {
                    insertText(Res.getString("message.your.kicked", s));
                }

                setChatInputEnabled(false);
                getSplitPane().setRightComponent(null);
                leaveChatRoom();
            }

            @Override
            public void voiceGranted() {
                insertText(Res.getString("message.your.voice.granted"));
                setChatInputEnabled(true);
            }

            @Override
            public void voiceRevoked() {
                insertText(Res.getString("message.your.voice.revoked"));
                setChatInputEnabled(false);
            }

            @Override
            public void banned(Jid s, String reason) {
                insertText(Res.getString("message.your.banned"));
            }

            @Override
            public void membershipGranted() {
                insertText(Res.getString("message.your.membership.granted"));
            }

            @Override
            public void membershipRevoked() {
                insertText(Res.getString("message.your.membership.revoked"));
            }

            @Override
            public void moderatorGranted() {
                insertText(Res.getString("message.your.moderator.granted"));
            }

            @Override
            public void moderatorRevoked() {
                insertText(Res.getString("message.your.moderator.revoked"));
            }

            @Override
            public void ownershipGranted() {
                insertText(Res.getString("message.your.ownership.granted"));
            }

            @Override
            public void ownershipRevoked() {
                insertText(Res.getString("message.your.ownership.revoked"));
            }

            @Override
            public void adminGranted() {
                insertText(Res.getString("message.your.admin.granted"));
            }

            @Override
            public void adminRevoked() {
                insertText(Res.getString("message.your.revoked.granted"));
            }
        });

        chat.addSubjectUpdatedListener((subject, by) -> {
            subjectPanel.setSubject(subject);
            if (subject.isEmpty()) {
                return;
            }
            Resourcepart nickname = Resourcepart.EMPTY;
            if (by != null) {
                nickname = by.getResourcepart();
            }
            final String insertMessage = Res.getString("message.subject.has.been.changed.to", subject, nickname);
            getTranscriptWindow().insertNotificationMessage(insertMessage, NOTIFICATION_COLOR);
        });
    }

    /**
     * Changes the subject that is displayed for this room. Does not send an update to the XMPP server (UI only).
     */
    public void setRoomSubject(String subject) {
        subjectPanel.setSubject(subject);
    }

    /**
     * Inserts a notification message within the TranscriptWindow.
     *
     * @param text the text to insert.
     */
    public void insertText(String text) {
        getTranscriptWindow().insertNotificationMessage(text, NOTIFICATION_COLOR);
    }

    /**
     * Returns the user format (e.g. darkcave@macbeth.shakespeare.lit/thirdwitch) of each user in the room.
     */
    public Collection<EntityFullJid> getParticipants() {
        synchronized (currentUserList) {
            return new ArrayList<>(currentUserList);
        }
    }

    /**
     * Sends the message that is currently in the Send field.
     */
    @Override
    public void sendMessage() {
        sendMessage(getChatInputEditor().getText());
    }

    @Override
    public void sendMessage(String text) {
        // Remove control characters
        text = text.replaceAll("[\\u0001-\\u0008\\u000B-\\u001F]", "");
        // If the body is empty, just return and do nothing
        if (!ModelUtil.hasLength(text)) {
            return;
        }

        MessageBuilder messageBuilder = StanzaBuilder.buildMessage()
            .setBody(text);

        sendMessage(messageBuilder);
    }

    /**
     * Returns a MultiUserChat object associated with this room.
     */
    public MultiUserChat getMultiUserChat() {
        return chat;
    }

    /**
     * Adds a user to the blocked user list.
     * Blocked users are NOT a MUC-related item,
     * but rather used by the client to not display messages from certain people.
     *
     * @param userJid the room jid of the user (ex.spark@conference.jivesoftware.com/Dan)
     */
    public void addBlockedUser(EntityFullJid userJid) {
        blockedUsers.add(userJid);
    }

    /**
     * Removes a user from the blocked user list.
     *
     * @param usersJID the jid of the user (ex. spark@conference.jivesoftware.com/Dan)
     */
    public void removeBlockedUser(EntityFullJid usersJID) {
        blockedUsers.remove(usersJID);
    }

    /**
     * Returns true if the user is in the blocked user list.
     *
     * @param userJid the jid of the user (ex. spark@conference.jivesoftware.com/Dan)
     */
    public boolean isBlocked(EntityFullJid userJid) {
        return blockedUsers.contains(userJid);
    }

    /**
     * Invite a user to this conference room.
     *
     * @param jid     the jid of the user to invite.
     * @param message the message to send with the invitation.
     */
    public void inviteUser(EntityBareJid jid, String message) {
        message = message != null && !message.isEmpty() ? message : Res.getString("message.please.join.in.conference");
        try {
            getMultiUserChat().invite(jid, message);
            roomInfo.addInvitee(jid, message);
        } catch (SmackException.NotConnectedException | InterruptedException e) {
            Log.warning("Unable to invite " + jid + " to room " + getBareJid(), e);
        }
    }

    /**
     * Returns the {@link GroupChatParticipantList} which displays all users within a conference room.
     */
    public GroupChatParticipantList getConferenceRoomInfo() {
        return roomInfo;
    }

    @Override
    public void authenticated(XMPPConnection xmppConnection, boolean b) {
        final EntityBareJid roomJID = getBareJid();
        final String roomName = tabTitle;
        final Resourcepart nickname = getNickname();
        final String password = this.password;
        try {
            chat.leave();
        } catch (Exception e) {
            Log.error(e);
        }

        isActive = false;
        EventQueue.invokeLater(() -> {
            closeChatRoom();
            ConferenceUtils.joinConferenceOnSeparateThread(roomName, roomJID, nickname, password);
        });
    }

    @Override
    public void connectionClosed() {
        handleDisconnect();
    }

    @Override
    public void connectionClosedOnError(Exception ex) {
        handleDisconnect();

        getTranscriptWindow().showWindowDisabled();
        getSplitPane().setRightComponent(null);
        getTranscriptWindow().insertNotificationMessage(Res.getString("message.disconnected.group.chat.error"), ERROR_COLOR);
    }

    /**
     * Sets the Password for this GroupChat if available, to rejoin the chat after a reconnection without prompting the user
     */
    public void setPassword(String password) {
        this.password = password;
    }

    /**
     * Is called whenever Spark was unexpectedly disconnected.
     */
    private void handleDisconnect() {
        setChatInputEnabled(false);
        SparkManager.getChatManager().getChatContainer().fireChatRoomStateUpdated(this);
    }

    /**
     * Returns the Color to use. Use Color.blue for yourself
     */
    public Color getColor(Resourcepart nickname) {
        if (nickname.equals(this.getNickname())) {
            return ChatManager.TO_COLOR;
        } else {
            return getUserColor(nickname);
        }
    }

    public Color getUserColor(Resourcepart nickname) {
        if (!pref.isMucRandomColors()) {
            return ChatManager.FROM_COLOR;
        }
        Color userColor = XEP0392Utils.colorOfMucParticipant(nickname);
        return userColor;
    }

    public void notifySettingsAccessRight() {
        if (SparkManager.getUserManager().isOwner((GroupChatRoom) getChatRoom(), getNickname())) {
            settings.setVisible(true);
        }
    }

    public boolean isChatStatEnabled() {
        return chatStatEnabled;
    }

    public void setChatStatEnabled(boolean chatStatEnabled) {
        this.chatStatEnabled = chatStatEnabled;
    }

    @Override
    protected void sendChatState(ChatState state) throws SmackException.NotConnectedException, InterruptedException {
        if (!chatStatEnabled || !SparkManager.getConnection().isConnected()) {
            return;
        }

        // XEP-0085: SHOULD NOT send 'gone' in a MUC.
        if (state == ChatState.gone) {
            return;
        }

        final Resourcepart myNickname = getNickname();
        // Send composing notifications to all other occupants
        for (final EntityFullJid occupant : chat.getOccupants()) {
            final Resourcepart occupantNickname = occupant.getResourcepart();
            if (occupantNickname != null && !occupantNickname.equals(myNickname)) {
                SparkManager.getMessageEventManager().sendComposingNotification(occupant, "djn");
            }
        }
    }
}
