/**
 * Copyright (C) 2004-2011 Jive Software. All rights reserved.
 *
 * 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
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * 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.conferences;

import org.jdesktop.swingx.JXList;
import org.jivesoftware.resource.Res;
import org.jivesoftware.resource.SparkRes;
import org.jivesoftware.smack.PresenceListener;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.StanzaError;
import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
import org.jivesoftware.smackx.muc.*;
import org.jivesoftware.smackx.muc.packet.MUCItem;
import org.jivesoftware.smackx.muc.packet.MUCUser;
import org.jivesoftware.spark.ChatManager;
import org.jivesoftware.spark.PresenceManager;
import org.jivesoftware.spark.SparkManager;
import org.jivesoftware.spark.UserManager;
import org.jivesoftware.spark.component.ImageTitlePanel;
import org.jivesoftware.spark.ui.ChatContainer;
import org.jivesoftware.spark.ui.ChatRoom;
import org.jivesoftware.spark.ui.ChatRoomNotFoundException;
import org.jivesoftware.spark.ui.rooms.ChatRoomImpl;
import org.jivesoftware.spark.ui.rooms.GroupChatRoom;
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.BareJid;
import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.EntityFullJid;
import org.jxmpp.jid.Jid;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.jid.parts.Resourcepart;
import org.jxmpp.stringprep.XmppStringprepException;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.List;
import java.util.*;

import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.jivesoftware.spark.ChatManager.ERROR_COLOR;
import static org.jivesoftware.spark.ChatManager.NOTIFICATION_COLOR;

/**
 * Display a room information, such as agents and room information.
 */
public class GroupChatParticipantList extends JPanel {
    private GroupChatRoom groupChatRoom;
    private final ImageTitlePanel agentInfoPanel;
    private final ChatManager chatManager;
    private MultiUserChat chat;
    private final LocalPreferences pref = SettingsManager.getLocalPreferences();

    /**
     * Map of participants displayName and JID
     */
    private final Map<CharSequence, EntityFullJid> userMap = new HashMap<>();

    private final UserManager userManager = SparkManager.getUserManager();

    private final DefaultListModel<JLabel> model = new DefaultListModel<>();

    private final JXList participantsList;

    private PresenceListener listener = null;

    private final Map<CharSequence, String> invitees = new HashMap<>();

    private boolean allowNicknameChange = true;

    private DiscoverInfo roomInformation;

    private final List<JLabel> users = new ArrayList<>();

    private final Map<EntityFullJid, MUCRole> usersToRoles = new HashMap<>();
    private final Map<EntityFullJid, MUCAffiliation> usersToAffiliation = new HashMap<>();

    public GroupChatParticipantList() {
        setLayout(new GridBagLayout());
        chatManager = SparkManager.getChatManager();

        agentInfoPanel = new ImageTitlePanel(Res.getString("message.participants.in.room"));
        participantsList = new JXList(model);
        participantsList.setCellRenderer(new ParticipantRenderer());

        // Set the room to track
        this.setOpaque(false);
        this.setBackground(Color.white);

        // Respond to Double-Click in Agent List to start a chat
        participantsList.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent evt) {
                if (evt.getClickCount() != 2) {
                    return;
                }
                String selectedUser = getSelectedUser();
                if (selectedUser == null) {
                    return;
                }
                startChat(groupChatRoom, userMap.get(Resourcepart.fromOrThrowUnchecked(selectedUser)));
            }

            @Override
            public void mouseReleased(final MouseEvent evt) {
                if (evt.isPopupTrigger()) {
                    checkPopup(evt);
                }
            }

            @Override
            public void mousePressed(final MouseEvent evt) {
                if (evt.isPopupTrigger()) {
                    checkPopup(evt);
                }
            }
        });

        JScrollPane scroller = new JScrollPane(participantsList);
        scroller.setPreferredSize(new Dimension(200, getHeight()));

        // Speed up scrolling. It was way too slow.
        scroller.getVerticalScrollBar().setBlockIncrement(200);
        scroller.getVerticalScrollBar().setUnitIncrement(20);
        scroller.setBackground(Color.white);
        scroller.getViewport().setBackground(Color.white);

        add(scroller, new GridBagConstraints(0, 0, 1, 1, 1.0, 1.0,
            GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
    }

    public void setChatRoom(final ChatRoom chatRoom) {
        this.groupChatRoom = (GroupChatRoom) chatRoom;
        chat = groupChatRoom.getMultiUserChat();
        chat.addInvitationRejectionListener((jid1, reason, message, rejection) -> {
            String displayName = userManager.getUserNicknameFromJID(jid1);
            userHasLeft(displayName);
            chatRoom.getTranscriptWindow().insertNotificationMessage(
                Res.getString("message.invitation.rejected", displayName, reason),
                NOTIFICATION_COLOR);
        });

        listener = p -> SwingUtilities.invokeLater(() -> {
            if (p.getError() != null) {
                if (p.getError().getCondition() == StanzaError.Condition.conflict) {
                    return;
                }
            }
            final EntityFullJid userid = p.getFrom().asEntityFullJidOrThrow();

            Resourcepart displayName = userid.getResourcepart();
            userMap.put(displayName, userid);
            if (p.isAvailable()) {
                addParticipant(userid, p);
                agentInfoPanel.setVisible(true);
                groupChatRoom.validate();
            } else {
                removeUser(displayName);
            }

            // When joining a room, check if the current user is an owner/admin. If so, the UI should allow the current
            // user to change settings of this MUC.
            final MUCUser mucUserEx = p.getExtension(MUCUser.class);
            // 110 = Inform user that presence refers to itself
            if (mucUserEx != null && mucUserEx.getStatus().contains(MUCUser.Status.create(110))) {
                final MUCItem item = mucUserEx.getItem();
                if (item != null) {
                    if (item.getAffiliation() == MUCAffiliation.admin || item.getAffiliation() == MUCAffiliation.owner) {
                        groupChatRoom.notifySettingsAccessRight();
                    }
                }
            }
        });

        chat.addParticipantListener(listener);
        ServiceDiscoveryManager discoManager = SparkManager.getDiscoManager();
        try {
            roomInformation = discoManager.discoverInfo(chat.getRoom());
        } catch (XMPPException | SmackException | InterruptedException e) {
            Log.debug("Unable to retrieve room information for " + chat.getRoom());
        }
    }

    public void addInvitee(EntityBareJid jid, String message) {
        // So the problem with this is that I have no idea what the users actual
        // jid is in most cases.
        final UserManager userManager = SparkManager.getUserManager();
        String displayName = userManager.getUserNicknameFromJID(jid);

        groupChatRoom.getTranscriptWindow().insertNotificationMessage(Res.getString("message.invitation.join", displayName), NOTIFICATION_COLOR);
        if (roomInformation != null
            && !roomInformation.containsFeature(MucFeature.NonAnonymous.getName())) {
            return;
        }
        final ImageIcon inviteIcon = SparkRes.getImageIcon(SparkRes.Icon.USER1_BACK_16x16);
        addUser(inviteIcon, Resourcepart.fromOrThrowUnchecked(displayName));
        invitees.put(displayName, message);
    }

    protected ImageIcon getImageIcon(EntityFullJid participantJID) {
        Resourcepart displayName = participantJID.getResourcepart();
        ImageIcon icon = SparkRes.getImageIcon(SparkRes.Icon.GREEN_BALL);
        icon.setDescription(displayName.toString());
        return icon;
    }

    protected void addParticipant(final EntityFullJid participantJID, Presence presence) {
        // Remove reference to invitees
        for (CharSequence displayName : invitees.keySet()) {
            EntityFullJid jid = SparkManager.getUserManager().getJIDFromDisplayName(displayName);
            Occupant occ = chat.getOccupant(jid);
            if (occ != null) {
                Jid actualJID = occ.getJid();
                if (actualJID.equals(jid)) {
                    removeUser(displayName);
                }
            }
        }

        Resourcepart nickname = participantJID.getResourcepart();
        MUCAffiliation affiliation = null;
        MUCRole role = null;
        final MUCUser extension = presence.getExtension(MUCUser.class);
        if (extension != null && extension.getItem() != null) {
            affiliation = extension.getItem().getAffiliation();
            role = extension.getItem().getRole();
        }

        if (affiliation == null) {
            affiliation = MUCAffiliation.none;
        }
        if (role == null) {
            role = MUCRole.none;
        }

        usersToRoles.put(participantJID, role);
        usersToAffiliation.put(participantJID, affiliation);

        Icon icon;
        if (pref.isShowingRoleIcons()) {
            icon = getIconForRole(role, affiliation);
        } else {
            icon = PresenceManager.getIconFromPresence(presence);
        }

        if (!exists(nickname)) {
            addUser(icon, nickname);
        } else {
            int index = getIndex(nickname);
            if (index != -1) {
                final JLabel userLabel = new JLabel(nickname.toString(), icon, SwingConstants.CENTER);
                userLabel.setForeground(groupChatRoom.getUserColor(nickname));
                model.setElementAt(userLabel, index);
            }
        }
    }

    /**
     * Returns corresponding Icons for each MUC-Role
     * icons are: <br>
     * Owner = Gold Star <br>
     * Admin = Silver Star <br>
     * Moderator = Bronze Star <br>
     * Member = Yellow <br>
     * Participant = Green <br>
     * Visitor = Blue <br>
     * N/A = Grey <br>
     */
    private Icon getIconForRole(MUCRole role, MUCAffiliation affiliation) {
        switch (affiliation) {
            case owner:
                return SparkRes.getImageIcon(SparkRes.Icon.STAR_OWNER);
            case admin:
                return SparkRes.getImageIcon(SparkRes.Icon.STAR_ADMIN);
            case member:
                if (role == MUCRole.moderator) {
                    return SparkRes.getImageIcon(SparkRes.Icon.STAR_MODERATOR);
                } else {
                    return SparkRes.getImageIcon(SparkRes.Icon.STAR_YELLOW_IMAGE);
                }
            default:
                switch (role) {
                    case participant:
                        return SparkRes.getImageIcon(SparkRes.Icon.STAR_GREEN_IMAGE);
                    case moderator:
                        return SparkRes.getImageIcon(SparkRes.Icon.STAR_MODERATOR);
                    case visitor:
                        return SparkRes.getImageIcon(SparkRes.Icon.STAR_BLUE_IMAGE);
                    default:
                        return SparkRes.getImageIcon(SparkRes.Icon.STAR_GREY_IMAGE);
                }
        }
    }

    public void userHasLeft(CharSequence displayName) {
        int index = getIndex(displayName);
        if (index != -1) {
            removeUser(displayName);
            userMap.remove(displayName);
        }
    }

    protected boolean exists(Resourcepart nickname) {
        String nicknameString = nickname.toString();
        // Check if nickname exists in UI user list
        for (int i = 0; i < model.getSize(); i++) {
            final JLabel userLabel = model.getElementAt(i);
            if (userLabel.getText().equals(nicknameString)) {
                return true;
            }
        }
        return false;
    }

    protected String getSelectedUser() {
        JLabel label = (JLabel) participantsList.getSelectedValue();
        if (label != null) {
            return label.getText();
        }
        return null;
    }

    protected void startChat(ChatRoom groupChat, EntityFullJid groupJID) {
        Resourcepart userNickname = groupJID.getResourcepart();
        String roomTitle = userNickname + " - " + groupChat.getBareJid();
        // TODO: Remove duplicate variable userNickname and nicknameOfUser.
        Resourcepart nickname = groupChat.getNickname();
        if (userNickname.equals(nickname)) {
            return;
        }
        ChatContainer chatContainer = chatManager.getChatContainer();
        ChatRoom chatRoom;
        try {
            chatRoom = chatContainer.getChatRoom(groupJID);
        } catch (ChatRoomNotFoundException e) {
            Log.debug("Could not find chat room - " + groupJID);
            // Create a new room
            chatRoom = new ChatRoomImpl(groupJID, userNickname, roomTitle);
            chatContainer.addChatRoom(chatRoom);
        }
        chatContainer.activateChatRoom(chatRoom);
    }

    public String getTabTitle() {
        return Res.getString("title.room.information");
    }

    public Icon getTabIcon() {
        return SparkRes.getImageIcon(SparkRes.Icon.SMALL_BUSINESS_MAN_VIEW);
    }

    public String getTabToolTip() {
        return Res.getString("title.room.information");
    }

    public JComponent getGUI() {
        return this;
    }

    protected void kickUser(Resourcepart nickname) {
        try {
            chat.kickParticipant(nickname, Res.getString("message.you.have.been.kicked"));
        } catch (XMPPException | SmackException | InterruptedException e) {
            groupChatRoom.insertText(Res.getString("message.kicked.error", nickname));
        }
    }

    protected void banUser(Resourcepart displayName) {
        try {
            EntityFullJid entityFullJid = userMap.get(displayName);
            Occupant occupant = chat.getOccupant(entityFullJid);
            if (occupant != null) {
                EntityBareJid bareJID = occupant.getJid().asEntityBareJidOrThrow();
                chat.banUser(bareJID, Res.getString("message.you.have.been.banned"));
            }
        } catch (XMPPException | SmackException | InterruptedException | IllegalStateException e) {
            groupChatRoom.getTranscriptWindow().
                insertNotificationMessage("No can do " + e.getMessage(), ERROR_COLOR);
        }
    }

    protected void unbanUser(String jidString) {
        try {
            BareJid jid = JidCreate.bareFrom(jidString);
            chat.grantMembership(jid);
        } catch (XMPPException | SmackException | XmppStringprepException | InterruptedException e) {
            groupChatRoom.getTranscriptWindow().
                insertNotificationMessage("No can do " + e.getMessage(), ERROR_COLOR);
        }
    }

    protected void grantVoice(Resourcepart nickname) {
        try {
            chat.grantVoice(nickname);
        } catch (XMPPException | SmackException | InterruptedException e) {
            groupChatRoom.getTranscriptWindow().
                insertNotificationMessage("No can do " + e.getMessage(), ERROR_COLOR);
        }
    }

    protected void revokeVoice(Resourcepart nickname) {
        try {
            chat.revokeVoice(nickname);
        } catch (XMPPException | SmackException | InterruptedException e) {
            groupChatRoom.getTranscriptWindow().
                insertNotificationMessage("No can do " + e.getMessage(), ERROR_COLOR);
        }
    }

    protected void grantModerator(Resourcepart nickname) {
        try {
            chat.grantModerator(nickname);
        } catch (XMPPException | SmackException | InterruptedException e) {
            groupChatRoom.getTranscriptWindow().insertNotificationMessage(
                "No can do " + e.getMessage(), ERROR_COLOR);
        }
    }

    protected void revokeModerator(Resourcepart nickname) {
        try {
            chat.revokeModerator(nickname);
        } catch (XMPPException | SmackException | InterruptedException e) {
            groupChatRoom.getTranscriptWindow().insertNotificationMessage(
                "No can do " + e.getMessage(), ERROR_COLOR);
        }
    }

    protected void grantMember(Resourcepart nickname) {
        try {
            Occupant o = userManager.getOccupant(groupChatRoom, nickname);
            BareJid bareJid = o.getJid().asBareJid();
            chat.grantMembership(bareJid);
        } catch (XMPPException | SmackException | InterruptedException e) {
            groupChatRoom.getTranscriptWindow().insertNotificationMessage(
                "No can do " + e.getMessage(), ERROR_COLOR);
        }
    }

    protected void revokeMember(Resourcepart nickname) {
        try {
            Occupant o = userManager.getOccupant(groupChatRoom, nickname);
            BareJid bareJid = o.getJid().asBareJid();
            chat.revokeMembership(bareJid);
        } catch (XMPPException | SmackException | InterruptedException e) {
            groupChatRoom.getTranscriptWindow().insertNotificationMessage(
                "No can do " + e.getMessage(), ERROR_COLOR);
        }
    }

    protected void grantAdmin(Resourcepart nickname) {
        try {
            Occupant o = userManager.getOccupant(groupChatRoom, nickname);
            BareJid bareJid = o.getJid().asBareJid();
            chat.grantAdmin(bareJid);
        } catch (XMPPException | SmackException | InterruptedException e) {
            groupChatRoom.getTranscriptWindow().insertNotificationMessage(
                "No can do " + e.getMessage(), ERROR_COLOR);
        }
    }

    protected void revokeAdmin(Resourcepart nickname) {
        try {
            Occupant o = userManager.getOccupant(groupChatRoom, nickname);
            BareJid bareJid = o.getJid().asBareJid();
            chat.revokeAdmin(bareJid);
        } catch (XMPPException | SmackException | InterruptedException e) {
            groupChatRoom.getTranscriptWindow().insertNotificationMessage(
                "No can do " + e.getMessage(), ERROR_COLOR);
        }
    }

    protected void grantOwner(Resourcepart nickname) {
        try {
            Occupant o = userManager.getOccupant(groupChatRoom, nickname);
            BareJid bareJid = o.getJid().asBareJid();
            chat.grantOwnership(bareJid);
        } catch (XMPPException | SmackException | InterruptedException e) {
            groupChatRoom.getTranscriptWindow().insertNotificationMessage(
                "No can do " + e.getMessage(), ERROR_COLOR);
        }
    }

    protected void revokeOwner(Resourcepart nickname) {
        try {
            Occupant o = userManager.getOccupant(groupChatRoom, nickname);
            BareJid bareJid = o.getJid().asBareJid();
            chat.revokeOwnership(bareJid);
        } catch (XMPPException | SmackException | InterruptedException e) {
            groupChatRoom.getTranscriptWindow().insertNotificationMessage(
                "No can do " + e.getMessage(), ERROR_COLOR);
        }
    }

    protected void checkPopup(MouseEvent evt) {
        Point p = evt.getPoint();
        final int index = participantsList.locationToIndex(p);
        final JPopupMenu popup = new JPopupMenu();
        if (index != -1) {
            participantsList.setSelectedIndex(index);
            final JLabel userLabel = model.getElementAt(index);
            final Resourcepart selectedUser = Resourcepart.fromOrThrowUnchecked(userLabel.getText());
            final EntityFullJid groupJID = userMap.get(selectedUser);
            final Resourcepart groupJIDNickname = groupJID.getResourcepart();
            final Resourcepart nickname = groupChatRoom.getNickname();
            final Occupant occupant = userManager.getOccupant(groupChatRoom, selectedUser);
            final boolean iamAdmin = SparkManager.getUserManager().isAdmin(groupChatRoom, chat.getNickname());
            final boolean iamOwner = SparkManager.getUserManager().isOwner(groupChatRoom, chat.getNickname());
            final boolean iamAdminOrOwner = iamAdmin || iamOwner;
            final boolean iamModerator = SparkManager.getUserManager().isModerator(groupChatRoom, chat.getNickname());
            final boolean userIsMember = SparkManager.getUserManager().isMember(occupant);
            final boolean userIsAdmin = userManager.isAdmin(groupChatRoom, occupant.getNick());
            final boolean userIsOwner = userManager.isOwner(occupant);
            final boolean userIsModerator = userManager.isModerator(occupant);
            boolean selectedMyself = nickname.equals(groupJIDNickname);

            // Handle invites
            if (groupJIDNickname == null) {
                Action inviteAgainAction = new AbstractAction() {
                    @Override
                    public void actionPerformed(ActionEvent actionEvent) {
                        String message = invitees.get(selectedUser);
                        EntityFullJid jid = userManager.getJIDFromDisplayName(selectedUser);
                        try {
                            chat.invite(jid.asEntityBareJid(), message);
                        } catch (SmackException.NotConnectedException | InterruptedException e) {
                            Log.warning("Unable to send stanza to " + jid, e);
                        }
                    }
                };

                inviteAgainAction.putValue(Action.NAME, Res.getString("menuitem.invite.again"));
                popup.add(inviteAgainAction);

                Action removeInvite = new AbstractAction() {
                    @Override
                    public void actionPerformed(ActionEvent actionEvent) {
                        int index = getIndex(selectedUser);
                        if (index != -1) {
                            model.removeElementAt(index);
                        }
                    }
                };
                removeInvite.putValue(Action.NAME, Res.getString("menuitem.remove"));
                popup.add(removeInvite);

                popup.show(participantsList, evt.getX(), evt.getY());
                return;
            }

            if (selectedMyself) {
                Action changeNicknameAction = new ChangeNicknameAction();
                changeNicknameAction.putValue(Action.NAME, Res.getString("menuitem.change.nickname"));
                changeNicknameAction.putValue(Action.SMALL_ICON, SparkRes.getImageIcon(SparkRes.Icon.TYPING_TRAY));
                if (allowNicknameChange) {
                    popup.add(changeNicknameAction);
                }
            }

            Action chatAction = new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent actionEvent) {
                    String selectedUser = getSelectedUser();
                    if (selectedUser == null) {
                        return;
                    }
                    startChat(groupChatRoom, userMap.get(Resourcepart.fromOrThrowUnchecked(selectedUser)));
                }
            };

            chatAction.putValue(Action.NAME, Res.getString("menuitem.start.a.chat"));
            chatAction.putValue(Action.SMALL_ICON, SparkRes.getImageIcon(SparkRes.Icon.SMALL_MESSAGE_IMAGE));
            if (!selectedMyself) {
                popup.add(chatAction);
            }

            Action blockAction = new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    String user = getSelectedUser();
                    ImageIcon icon;
                    if (groupChatRoom.isBlocked(groupJID)) {
                        groupChatRoom.removeBlockedUser(groupJID);
                        icon = getImageIcon(groupJID);
                    } else {
                        groupChatRoom.addBlockedUser(groupJID);
                        icon = SparkRes.getImageIcon(SparkRes.Icon.BRICKWALL_IMAGE);
                    }

                    JLabel label = new JLabel(user, icon, SwingConstants.CENTER);
                    model.setElementAt(label, index);
                }
            };

            blockAction.putValue(Action.NAME, Res.getString("menuitem.block.user"));
            blockAction.putValue(Action.SMALL_ICON, SparkRes.getImageIcon(SparkRes.Icon.BRICKWALL_IMAGE));
            if (!selectedMyself) {
                if (groupChatRoom.isBlocked(groupJID)) {
                    blockAction.putValue(Action.NAME, Res.getString("menuitem.unblock.user"));
                }
                popup.add(blockAction);
            }

            Action kickAction = new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent actionEvent) {
                    kickUser(selectedUser);
                }
            };

            kickAction.putValue(Action.NAME, Res.getString("menuitem.kick.user"));
            kickAction.putValue(Action.SMALL_ICON, SparkRes.getImageIcon(SparkRes.Icon.SMALL_DELETE));
            if (iamModerator && !userIsAdmin && !selectedMyself) {
                popup.add(kickAction);
            }

            // Handle Voice Operations
            Action voiceAction = new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent actionEvent) {
                    if (userManager.hasVoice(groupChatRoom, selectedUser)) {
                        revokeVoice(selectedUser);
                    } else {
                        grantVoice(selectedUser);
                    }
                    users.sort(labelComp);
                }
            };

            voiceAction.putValue(Action.NAME, Res.getString("menuitem.voice"));
            voiceAction.putValue(Action.SMALL_ICON, SparkRes.getImageIcon(SparkRes.Icon.MEGAPHONE_16x16));
            if (iamModerator && !userIsModerator && !selectedMyself) {
                if (userManager.hasVoice(groupChatRoom, selectedUser)) {
                    voiceAction.putValue(Action.NAME, Res.getString("menuitem.revoke.voice"));
                } else {
                    voiceAction.putValue(Action.NAME, Res.getString("menuitem.grant.voice"));
                }
                popup.add(voiceAction);
            }

            Action banAction = new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent actionEvent) {
                    banUser(selectedUser);
                }
            };
            banAction.putValue(Action.NAME, Res.getString("menuitem.ban.user"));
            banAction.putValue(Action.SMALL_ICON, SparkRes.getImageIcon(SparkRes.Icon.RED_FLAG_16x16));
            if (iamAdminOrOwner && !userIsModerator && !selectedMyself) {
                popup.add(banAction);
            }

            JMenu affiliationMenu = new JMenu(Res.getString("menuitem.affiliation"));
            affiliationMenu.setIcon(SparkRes.getImageIcon(SparkRes.Icon.MODERATOR_IMAGE));

            Action memberAction = new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    if (!userIsMember) {
                        grantMember(selectedUser);
                    } else {
                        revokeMember(selectedUser);
                    }
                    users.sort(labelComp);
                }
            };
            memberAction.putValue(Action.SMALL_ICON, SparkRes.getImageIcon(SparkRes.Icon.STAR_YELLOW_IMAGE));
            if (iamAdminOrOwner && !userIsMember) {
                memberAction.putValue(Action.NAME, Res.getString("menuitem.grant.member"));
                affiliationMenu.add(memberAction);
            } else if (iamAdminOrOwner && userIsMember && !selectedMyself) {
                memberAction.putValue(Action.NAME, Res.getString("menuitem.revoke.member"));
                affiliationMenu.add(memberAction);
            }


            Action moderatorAction = new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent actionEvent) {
                    if (!userIsModerator) {
                        grantModerator(selectedUser);
                    } else {
                        revokeModerator(selectedUser);
                    }
                    users.sort(labelComp);
                }
            };

            moderatorAction.putValue(Action.SMALL_ICON, SparkRes.getImageIcon(SparkRes.Icon.STAR_MODERATOR));

            if (iamAdminOrOwner && !userIsModerator && !userIsAdmin && !userIsOwner) {
                moderatorAction.putValue(Action.NAME, Res.getString("menuitem.grant.moderator"));
                affiliationMenu.add(moderatorAction);
            } else if (iamAdminOrOwner && userIsModerator && !selectedMyself) {
                moderatorAction.putValue(Action.NAME, Res.getString("menuitem.revoke.moderator"));
                affiliationMenu.add(moderatorAction);
            }

            Action adminAction = new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    if (!userIsAdmin) {
                        grantAdmin(selectedUser);
                    } else {
                        revokeAdmin(selectedUser);
                    }
                    users.sort(labelComp);
                }
            };
            adminAction.putValue(Action.SMALL_ICON, SparkRes.getImageIcon(SparkRes.Icon.STAR_ADMIN));
            if (iamAdminOrOwner && !userIsAdmin && !userIsOwner) {
                adminAction.putValue(Action.NAME, Res.getString("menuitem.grant.admin"));
                affiliationMenu.add(adminAction);
            } else if (iamAdminOrOwner && !selectedMyself) {
                adminAction.putValue(Action.NAME, Res.getString("menuitem.revoke.admin"));
                affiliationMenu.add(adminAction);
            }


            Action ownerAction = new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    if (!userIsOwner) {
                        grantOwner(selectedUser);
                    } else {
                        revokeOwner(selectedUser);
                    }
                    users.sort(labelComp);
                }
            };
            ownerAction.putValue(Action.SMALL_ICON, SparkRes.getImageIcon(SparkRes.Icon.STAR_OWNER));

            if (iamOwner && !userIsOwner) {
                ownerAction.putValue(Action.NAME, Res.getString("menuitem.grant.owner"));
                affiliationMenu.add(ownerAction);
            } else if (iamOwner && !selectedMyself) {
                ownerAction.putValue(Action.NAME, Res.getString("menuitem.revoke.owner"));
                affiliationMenu.add(ownerAction);
            }

            if (affiliationMenu.getItemCount() > 0) {
                popup.add(affiliationMenu);
            }

            // Handle Unbanning of users.
            Action unbanAction = new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent actionEvent) {
                    String jid = ((JMenuItem) actionEvent.getSource()).getText();
                    unbanUser(jid);
                }
            };

            if (iamAdmin || iamOwner) {
                JMenu unbanMenu = new JMenu(Res.getString("menuitem.unban"));
                List<Affiliate> bannedUsers = List.of();
                try {
                    bannedUsers = chat.getOutcasts();
                } catch (XMPPException | SmackException | InterruptedException e) {
                    Log.error("Error loading all banned users", e);
                }

                for (Affiliate bannedUser : bannedUsers) {
                    ImageIcon icon = SparkRes.getImageIcon(SparkRes.Icon.RED_BALL);
                    JMenuItem bannedItem = new JMenuItem(bannedUser.getJid().toString(), icon);
                    unbanMenu.add(bannedItem);
                    bannedItem.addActionListener(unbanAction);
                }
                if (unbanMenu.getMenuComponentCount() > 0) {
                    popup.add(unbanMenu);
                }
            }
        }

        Action inviteAction = new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent actionEvent) {
                ConferenceUtils.inviteUsersToRoom(groupChatRoom.getBareJid(), null, false);
            }
        };

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

        if (index != -1) {
            popup.addSeparator();
        }
        popup.add(inviteAction);

        Action copyUriGroupChat = new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                SparkManager.setClipboard("xmpp:" + chat.getRoom() + "?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);

        popup.show(participantsList, evt.getX(), evt.getY());
    }

    public void setNicknameChangeAllowed(boolean allowed) {
        allowNicknameChange = allowed;
    }

    public int getIndex(CharSequence name) {
        for (int i = 0; i < model.getSize(); i++) {
            JLabel label = model.getElementAt(i);
            if (label.getText().equals(name.toString())) {
                return i;
            }
        }
        return -1;
    }

    /**
     * Removes a user from the participant list based on their displayed name.
     *
     * @param displayName the users displayed name to remove.
     */
    public synchronized void removeUser(CharSequence displayName) {
        try {
            for (int i = 0; i < users.size(); i++) {
                JLabel label = users.get(i);
                if (label.getText().equals(displayName.toString())) {
                    users.remove(label);
                    model.removeElement(label);
                }
            }

            for (int i = 0; i < model.size(); i++) {
                JLabel label = model.getElementAt(i);
                if (label.getText().equals(displayName.toString())) {
                    users.remove(label);
                    model.removeElement(label);
                }
            }
        } catch (Exception e) {
            Log.error(e);
        }
    }

    /**
     * Adds a new user to the participant list.
     *
     * @param userIcon the icon to use initially.
     * @param nickname the users nickname.
     */
    public synchronized void addUser(Icon userIcon, Resourcepart nickname) {
        try {
            final JLabel user = new JLabel(nickname.toString(), userIcon, SwingConstants.CENTER);
            user.setForeground(groupChatRoom.getUserColor(nickname));
            users.add(user);
            // Sort users alpha.
            users.sort(labelComp);
            // Add to the correct position in the model.
            final int index = users.indexOf(user);
            model.insertElementAt(user, index);
        } catch (Exception e) {
            Log.error(e);
        }
    }

    /**
     * Sorts ContactItems.
     */
    final Comparator<JLabel> labelComp = new Comparator<>() {
        @Override
        public int compare(JLabel item1, JLabel item2) {
            if (pref.isShowingRoleIcons()) {
                return compareWithRole(item1, item2);
            } else {
                return compareWithoutRole(item1.getText(), item2.getText());
            }
        }

        private int compareWithoutRole(String s1, String s2) {
            return s1.compareToIgnoreCase(s2);
        }

        /**
         * Compares 2 items by their Role and Affiliation<br>
         * affiliation > role<br>
         * owner > admin > moderator > member > participant > visitor
         *
         * @return -1, 0 or 1
         */
        private int compareWithRole(JLabel item1, JLabel item2) {
            int user1 = 100;
            int user2 = 100;
            try {
                // append Room-JID to UserLabel
                Resourcepart nick1 = Resourcepart.from(item1.getText());
                Resourcepart nick2 = Resourcepart.from(item2.getText());
                EntityFullJid jid1 = JidCreate.entityFullFrom(chat.getRoom(), nick1);
                EntityFullJid jid2 = JidCreate.entityFullFrom(chat.getRoom(), nick2);
                user1 = getCompareValue(jid1);
                user2 = getCompareValue(jid2);
            } catch (Exception e) {
                //FIXME Sometimes there is no Occupant with that jid, dunno why
            }

            int result = 0;
            if (user1 == user2) {
                result = compareWithoutRole(item1.getText(), item2.getText());
            } else {
                // a=owner,b=admin, m=moderator, n=member , p=participant, v=visitor
                // a < b < m < n < p < v
                if (user1 < user2)
                    result = -1;
                if (user1 > user2)
                    result = 1;
            }
            return result;
        }
    };

    /**
     * check if we have an affiliation to this room<br>
     * and map it to an integer<br>
     * 0=owner,1=admin.....5=visitor<br>
     */
    private int getCompareValue(EntityFullJid jid) {
        MUCRole role = usersToRoles.get(jid);
        MUCAffiliation affiliation = usersToAffiliation.get(jid);
        switch (affiliation) {
            case owner:
                return 0;
            case admin:
                return 1;
            case member:
                return (role == MUCRole.moderator ? 2 : 3);
            case none:
                switch (role) {
                    case moderator:
                        return 2;
                    case participant:
                        return 4;
                    case visitor:
                        return 5;
                    default:
                        return 100;
                }
            default:
                return 100;
        }
    }

    /**
     * The <code>ParticipantRenderer</code> is an implementation of
     * ListCellRenderer to add icons w/ associated text in JComboBox and JList.
     */
    public static class ParticipantRenderer extends JLabel implements ListCellRenderer<Object> {
        public ParticipantRenderer() {
            setOpaque(true);
        }

        @Override
        public Component getListCellRendererComponent(JList list, Object value,
                                                      int index, boolean isSelected, boolean cellHasFocus) {
            JLabel label = (JLabel) value;
            if (isSelected) {
                setBackground(list.getSelectionBackground());
                setForeground(list.getSelectionForeground());
            } else {
                setBackground(label.getBackground());
                setForeground(label.getForeground());
            }
            setText(label.getText());
            setIcon(label.getIcon());
            return this;
        }
    }

    protected GroupChatRoom getGroupChatRoom() {
        return groupChatRoom;
    }

    protected ImageTitlePanel getAgentInfoPanel() {
        return agentInfoPanel;
    }

    protected MultiUserChat getChat() {
        return chat;
    }

    protected Map<CharSequence, EntityFullJid> getUserMap() {
        return userMap;
    }

    protected DefaultListModel<JLabel> getModel() {
        return model;
    }

    protected JXList getParticipantsList() {
        return participantsList;
    }

    protected PresenceListener getListener() {
        return listener;
    }

    protected Map<CharSequence, String> getInvitees() {
        return invitees;
    }

    protected boolean isAllowNicknameChange() {
        return allowNicknameChange;
    }

    protected DiscoverInfo getRoomInformation() {
        return roomInformation;
    }

    protected List<JLabel> getUsers() {
        return users;
    }

    protected Comparator<JLabel> getLabelComp() {
        return labelComp;
    }

    private class ChangeNicknameAction extends AbstractAction {
        @Override
        public void actionPerformed(ActionEvent actionEvent) {
            // retry to change nickname
            while (true) {
                String newNickname = JOptionPane.showInputDialog(groupChatRoom,
                    Res.getString("label.new.nickname") + ":",
                    Res.getString("title.change.nickname"),
                    JOptionPane.QUESTION_MESSAGE);
                if (isBlank(newNickname)) {
                    return;
                }
                newNickname = newNickname.trim();
                Resourcepart newNicknameResourcepart = Resourcepart.fromOrNull(newNickname);
                if (newNicknameResourcepart == null) {
                    // retry
                    continue;
                }
                String nick = chat.getNickname().toString();
                try {
                    chat.changeNickname(newNicknameResourcepart);
                    return;
                } catch (XMPPException.XMPPErrorException xmppError) {
                    switch (xmppError.getStanzaError().getCondition()) {
                        case not_acceptable:
                            // handle deny changing nick.
                            JOptionPane.showMessageDialog(groupChatRoom,
                                Res.getString("message.nickname.not.acceptable"),
                                Res.getString("title.change.nickname"),
                                JOptionPane.ERROR_MESSAGE);
                            return;
                        case conflict:
                            JOptionPane.showMessageDialog(groupChatRoom,
                                Res.getString("message.nickname.in.use"),
                                Res.getString("title.change.nickname"),
                                JOptionPane.ERROR_MESSAGE);
                            break;
                        default:
                            Log.error("Error changing nickname", xmppError);
                            return;
                    }
                } catch (SmackException | InterruptedException e1) {
                    Log.error("Error changing nickname", e1);
                    return;
                }
            }
        }
    }
}
