/**
 * 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;

import org.jivesoftware.resource.Res;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.StanzaBuilder;
import org.jivesoftware.spark.PresenceManager;
import org.jivesoftware.spark.SparkManager;
import org.jivesoftware.spark.Workspace;
import org.jivesoftware.spark.component.VerticalFlowLayout;
import org.jivesoftware.spark.component.panes.CollapsiblePane;
import org.jivesoftware.spark.component.renderer.JPanelRenderer;
import org.jivesoftware.spark.util.*;
import org.jivesoftware.spark.util.SwingWorker;
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 javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

import static org.jivesoftware.spark.ChatManager.TESTING_JID;
import static org.jivesoftware.spark.ui.ContactItem.CONTACT_ITEM_COMPARATOR;

/**
 * Container representing a RosterGroup within the Contact List.
 */
public class ContactGroup extends CollapsiblePane implements MouseListener {
    /**
     * Nested group delimiter (XEP-0083)
     */
    private static final String GROUP_DELIMITER = "::";
    private final List<ContactItem> contactItems = new ArrayList<>();
    private final List<ContactGroup> contactGroups = new ArrayList<>();
    private final CopyOnWriteArrayList<ContactGroupListener> listeners = new CopyOnWriteArrayList<>();
    private final List<ContactItem> offlineContacts = new ArrayList<>();

    private final String groupName;
    private final boolean sharedGroup;
    private final boolean offlineGroup;
    private final boolean unfiledGroup;
    private final DefaultListModel<ContactItem> model;
    private final JList<? extends ContactItem> contactItemList;
    private final JPanel listPanel;

    // Used to display no contacts in list.
    private final ContactItem noContacts = UIComponentRegistry.createContactGroupItem(Res.getString("group.empty"));
    private final ListMotionListener motionListener = new ListMotionListener();
    private boolean canShowPopup;
    private boolean mouseDragged = false;
    private final LocalPreferences preferences = SettingsManager.getLocalPreferences();
    private ContactList contactList = Workspace.getInstance().getContactList();
    private DisplayWindowTask timerTask = null;

    /**
     * Create a new ContactGroup.
     *
     * @param groupName the name of the new ContactGroup.
     */
    public ContactGroup(String groupName, boolean sharedGroup, boolean offlineGroup, boolean unfiledGroup) {
        this.groupName = groupName;
        this.sharedGroup = sharedGroup;
        this.offlineGroup = offlineGroup;
        this.unfiledGroup = unfiledGroup;
        // Initialize Model and UI
        model = new DefaultListModel<>();
        contactItemList = new JList<>(model);

        setTitle(getGroupTitle());
        if (sharedGroup) {
            setToolTipText(Res.getString("message.is.shared.group", getGroupName()));
        }
        // Use JPanel Renderer
        contactItemList.setCellRenderer(new JPanelRenderer());

        listPanel = new JPanel(new VerticalFlowLayout(VerticalFlowLayout.TOP, 0, 0, true, false));
        listPanel.add(contactItemList, listPanel);
        this.setContentPane(listPanel);

        if (!isOfflineGroup()) {
            contactItemList.setDragEnabled(true);
            contactItemList.setTransferHandler(new ContactGroupTransferHandler());
        }

        // Allow for mouse events to take place on the title bar
        getTitlePane().addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                checkPopup(e);
            }

            @Override
            public void mouseReleased(MouseEvent e) {
                checkPopup(e);
            }

            @Override
            public void mouseClicked(MouseEvent e) {
                if (e.getButton() == MouseEvent.BUTTON1) {
                    contactList = Workspace.getInstance().getContactList();
                    contactList.saveState();
                }
            }

            public void checkPopup(MouseEvent e) {
                if (e.isPopupTrigger()) {
                    e.consume();
                    fireContactGroupPopupEvent(e);
                }
            }
        });

        // Items should have selection listener
        contactItemList.addMouseListener(this);

        contactItemList.addKeyListener(new KeyListener() {
            @Override
            public void keyTyped(KeyEvent keyEvent) {
            }

            @Override
            public void keyPressed(KeyEvent keyEvent) {
                if (keyEvent.getKeyChar() == KeyEvent.VK_ENTER) {
                    ContactItem item = contactItemList.getSelectedValue();
                    fireContactItemDoubleClicked(item);
                }
                ContactList.activeKeyEvent = keyEvent;
            }

            @Override
            public void keyReleased(KeyEvent keyEvent) {
                ContactList.activeKeyEvent = null;
            }
        });

        noContacts.getNicknameLabel().setFont(new Font("Dialog", Font.PLAIN, preferences.getContactListFontSize()));
        noContacts.getNicknameLabel().setForeground(Color.GRAY);
        model.addElement(noContacts);
        // Add Popup Window
        addPopupWindow();
    }

    /**
     * Adds a new offline contact.
     *
     * @param alias    the alias of the offline contact.
     * @param nickname the nickname of the offline contact.
     * @param jid      the jid of the offline contact.
     * @param status   the current status of the offline contact.
     */
    public void addOfflineContactItem(final String alias, final String nickname, final BareJid jid, final String status) {
        if (EventQueue.isDispatchThread()) {
            // Build new ContactItem
            final ContactItem offlineItem = UIComponentRegistry.createContactItem(alias, nickname, jid);
            offlineItem.setGroupName(getGroupName());

            final Presence offlinePresence = StanzaBuilder.buildPresence()
                .ofType(Presence.Type.unavailable)
                .build();
            offlineItem.setPresence(offlinePresence);
            // set offline icon
            offlineItem.setIcon(PresenceManager.getIconFromPresence(offlinePresence));
            offlineItem.setStatusText(status);
            // Add to offline contacts.
            offlineContacts.add(offlineItem);

            insertOfflineContactItem(offlineItem);
        } else {
            try {
                // invokeAndWait, because the contacts must be added before they can moved to offline group
                EventQueue.invokeAndWait(() -> {
                    // Build new ContactItem
                    final ContactItem offlineItem = UIComponentRegistry.createContactItem(alias, nickname, jid);
                    offlineItem.setGroupName(getGroupName());

                    final Presence offlinePresence = StanzaBuilder.buildPresence()
                        .ofType(Presence.Type.unavailable)
                        .build();
                    offlineItem.setPresence(offlinePresence);

                    // set offline icon
                    offlineItem.setIcon(PresenceManager.getIconFromPresence(offlinePresence));

                    // Set status if applicable.
                    if (ModelUtil.hasLength(status)) {
                        offlineItem.setStatusText(status);
                    }
                    // Add to offline contacts.
                    offlineContacts.add(offlineItem);

                    insertOfflineContactItem(offlineItem);
                });
            } catch (Exception ex) {
                Log.error(ex);
            }
        }
    }

    /**
     * Add a new offline <code>ContactItem</code> into the UI model.
     */
    public void insertOfflineContactItem(ContactItem offlineItem) {
        if (model.contains(offlineItem)) {
            return;
        }
        if (!preferences.isOfflineGroupVisible()) {
            offlineContacts.sort(CONTACT_ITEM_COMPARATOR);
            int index = offlineContacts.indexOf(offlineItem);
            int totalListSize = contactItems.size();
            int newPos = totalListSize + index;
            if (newPos > model.size()) {
                newPos = model.size();
            }
            model.insertElementAt(offlineItem, newPos);
            if (model.contains(noContacts)) {
                model.removeElement(noContacts);
            }
        }
    }

    /**
     * Removes an offline <code>ContactItem</code> from the Offline contact
     * model and ui.
     */
    public void removeOfflineContactItem(ContactItem item) {
        offlineContacts.remove(item);
        //removeContactItem(item);
    }

    /**
     * Removes an offline <code>ContactItem</code> from the offline contact model and ui.
     */
    public void removeOfflineContactItem(BareJid jid) {
        for (ContactItem item : new ArrayList<>(offlineContacts)) {
            if (item.getJid().equals(jid)) {
                removeOfflineContactItem(item);
            }
        }
    }

    /**
     * Toggles the visibility of Offline Contacts.
     */
    public void toggleOfflineVisibility(boolean show) {
        for (ContactItem item : new ArrayList<>(offlineContacts)) {
            if (show) {
                insertOfflineContactItem(item);
            } else {
                model.removeElement(item);
            }
        }
        if (model.getSize() == 0) {
            model.addElement(noContacts);
        }
    }

    /**
     * Adds a <code>ContactItem</code> to the ContactGroup.
     */
    public void addContactItem(ContactItem item) {
        // Remove from offline group if it exists
        removeOfflineContactItem(item.getJid());
        if (model.contains(noContacts)) {
            model.remove(0);
        }
        if (isOfflineGroup()) {
            setOfflineGroupNameFont(item);
        }
        item.setGroupName(getGroupName());
        contactItems.add(item);
        List<ContactItem> tempItems = getContactItems();
        int index = tempItems.indexOf(item);
        model.insertElementAt(item, index);
        @SuppressWarnings("unchecked")
        List<ContactItem> objs = (List<ContactItem>) contactItemList.getSelectedValuesList();
        int[] intList = new int[objs.size()];
        for (int i = 0; i < objs.size(); i++) {
            ContactItem contact = objs.get(i);
            intList[i] = model.indexOf(contact);
        }
        if (intList.length > 0) {
            contactItemList.setSelectedIndices(intList);
        }
        fireContactItemAdded(item);
    }

    protected void setOfflineGroupNameFont(ContactItem item) {
        item.getNicknameLabel().setFont(new Font("Dialog", Font.PLAIN, item.getFontSize()));
        item.getNicknameLabel().setForeground(Color.GRAY);
    }

    /**
     * Call whenever the UI needs to be updated.
     */
    public void fireContactGroupUpdated() {
        contactItemList.validate();
        contactItemList.repaint();
        updateTitle();
    }

    /**
     * Adds a new subgroup to this Contact group.
     */
    public void addContactGroup(ContactGroup contactGroup) {
        final JPanel panel = new JPanel(new GridBagLayout());
        panel.add(contactGroup, new GridBagConstraints(0, 0, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(2, 15, 0, 0), 0, 0));
        panel.setBackground(Color.white);
        contactGroup.setSubPane(true);

        // contactGroup.setStyle(CollapsiblePane.TREE_STYLE);
        contactGroups.add(contactGroup);
        contactGroups.sort(ContactList.GROUP_COMPARATOR);
        listPanel.add(panel, contactGroups.indexOf(contactGroup));
    }

    /**
     * Removes a child ContactGroup.
     */
    public void removeContactGroup(ContactGroup contactGroup) {
        Component[] comps = listPanel.getComponents();
        for (Component comp : comps) {
            if (comp instanceof JPanel) {
                JPanel panel = (JPanel) comp;
                if (panel.getComponentCount() > 0) {
                    ContactGroup group = (ContactGroup) panel.getComponent(0);
                    if (group == contactGroup) {
                        listPanel.remove(panel);
                        break;
                    }
                }
            }
        }
        contactGroups.remove(contactGroup);
    }

    public void setPanelBackground(Color color) {
        Component[] comps = listPanel.getComponents();
        for (Component comp : comps) {
            if (comp instanceof JPanel) {
                JPanel panel = (JPanel) comp;
                panel.setBackground(color);
            }
        }
    }

    /**
     * Returns a ContactGroup based on its name.
     *
     * @param groupName the name of the group.
     * @return the ContactGroup or null.
     */
    public ContactGroup getContactGroup(String groupName) {
        for (ContactGroup group : new ArrayList<>(contactGroups)) {
            if (group.getGroupName().equals(groupName)) {
                return group;
            }
        }
        return null;
    }

    /**
     * Removes a <code>ContactItem</code>.
     */
    public void removeContactItem(ContactItem item) {
        contactItems.remove(item);
        if (contactItems.isEmpty()) {
            removeContactGroup(this);
        }
        model.removeElement(item);
        updateTitle();
        fireContactItemRemoved(item);
    }

    /**
     * Returns a <code>ContactItem</code> by the displayed name the user has been assigned.
     *
     * @param displayName the displayed name of the user.
     * @return the ContactItem or null.
     */
    public ContactItem getContactItemByDisplayName(CharSequence displayName) {
        if (displayName == null) {
            return null;
        }
        for (ContactItem item : new ArrayList<>(contactItems)) {
            if (displayName.equals(item.getDisplayName())) {
                return item;
            }
        }
        return null;
    }

    /**
     * Returns a <code>ContactItem</code> from offlineContacts by the displayed name the user has been assigned.
     *
     * @param displayName the displayed name of the user.
     * @return the ContactItem or null.
     */
    public ContactItem getOfflineContactItemByDisplayName(String displayName) {
        for (ContactItem item : new ArrayList<>(offlineContacts)) {
            if (item.getDisplayName().equals(displayName)) {
                return item;
            }
        }
        return null;
    }

    /**
     * Returns a <code>ContactItem</code> by the displayed name the user has been assigned.
     *
     * @param displayName the displayed name of the user.
     * @param searchInOffline should we search <code>ContactItem</code> in offline contacts
     * @return the ContactItem.
     */
    public ContactItem getContactItemByDisplayName(String displayName, boolean searchInOffline) {
        if (searchInOffline) {
            ContactItem item = getContactItemByDisplayName(displayName);
            if (item == null) {
                item = getOfflineContactItemByDisplayName(displayName);
            }
            return item;
        }
        return getContactItemByDisplayName(displayName);
    }

    /**
     * Returns a <code>ContactItem</code> by the user's bare JID.
     */
    public ContactItem getContactItemByJID(BareJid bareJID) {
        for (ContactItem item : new ArrayList<>(contactItems)) {
            if (item != null && item.getJid().equals(bareJID)) {
                return item;
            }
        }
        return null;
    }

    /**
     * Returns a <code>ContactItem</code> from offlineContacts by the user's bare JID.
     */
    public ContactItem getOfflineContactItemByJID(BareJid bareJID) {
        for (ContactItem item : new ArrayList<>(offlineContacts)) {
            if (item.getJid().equals(bareJID)) {
                return item;
            }
        }
        return null;
    }

    /**
     * Returns a <code>ContactItem</code> by the users bare bareJID.
     *
     * @param bareJID the bareJID of the user.
     * @param searchInOffline should we search <code>ContactItem</code> in offline contacts
     * @return the ContactItem.
     */
    public ContactItem getContactItemByJID(BareJid bareJID, boolean searchInOffline) {
        if (searchInOffline) {
            ContactItem item = getContactItemByJID(bareJID);
            if (item == null) {
                item = getOfflineContactItemByJID(bareJID);
            }
            return item;
        }
        return getContactItemByJID(bareJID);
    }

    /**
     * Returns all <code>ContactItem</cod>s in the ContactGroup.
     */
    public List<ContactItem> getContactItems() {
        final List<ContactItem> list = new ArrayList<>(contactItems);
        list.sort(CONTACT_ITEM_COMPARATOR);
        return list;
    }

    /**
     * Returns the name of the ContactGroup.
     */
    public String getGroupName() {
        return groupName;
    }


    @Override
    public void mouseClicked(MouseEvent e) {
        ContactItem item = contactItemList.getSelectedValue();
        if (item == null) {
            return;
        }
        if (e.getClickCount() == 2) {
            fireContactItemDoubleClicked(item);
        } else if (e.getClickCount() == 1) {
            fireContactItemClicked(item);
        }
    }

    @Override
    public void mouseEntered(MouseEvent e) {
        int loc = contactItemList.locationToIndex(e.getPoint());
        Object o = model.getElementAt(loc);
        if (!(o instanceof ContactItem)) {
            return;
        }
        contactItemList.setCursor(GraphicUtils.HAND_CURSOR);
    }

    @Override
    public void mouseExited(MouseEvent e) {
        Object o;
        try {
            int loc = contactItemList.locationToIndex(e.getPoint());
            if (loc == -1) {
                return;
            }
            o = model.getElementAt(loc);
            if (!(o instanceof ContactItem)) {
                UIComponentRegistry.getContactInfoWindow().hideWindow();
                return;
            }
        } catch (Exception e1) {
            Log.error(e1);
            return;
        }
        contactItemList.setCursor(GraphicUtils.DEFAULT_CURSOR);
    }

    @Override
    public void mousePressed(MouseEvent e) {
        checkPopup(e);
    }

    @Override
    public void mouseReleased(MouseEvent e) {
        checkPopup(e);
    }

    private void checkPopup(MouseEvent e) {
        if (e.isPopupTrigger()) {
            // Otherwise, handle single selection
            int index = contactItemList.locationToIndex(e.getPoint());
            if (index != -1) {
                int[] indexes = contactItemList.getSelectedIndices();
                boolean selected = false;
                for (int o : indexes) {
                    if (index == o) {
                        selected = true;
                        break;
                    }
                }

                if (!selected) {
                    contactItemList.setSelectedIndex(index);
                    fireContactItemClicked(contactItemList.getSelectedValue());
                }
            }
            List<ContactItem> selectedItems = SparkManager.getChatManager().getSelectedContactItems();
            if (selectedItems.size() > 1) {
                firePopupEvent(e, selectedItems);
            } else if (selectedItems.size() == 1) {
                final ContactItem contactItem = selectedItems.get(0);
                firePopupEvent(e, contactItem);
            }
        }
    }

    public void addContactGroupListener(ContactGroupListener listener) {
        listeners.addIfAbsent(listener);
    }

    public void removeContactGroupListener(ContactGroupListener listener) {
        listeners.remove(listener);
    }

    private void fireContactItemClicked(ContactItem item) {
        for (final ContactGroupListener listener : listeners) {
            try {
                listener.contactItemClicked(item);
            } catch (Exception e) {
                Log.error("A ContactGroupListener (" + listener + ") threw an exception while processing a 'contactItemClicked' event for item: " + item, e);
            }
        }
    }

    private void fireContactItemDoubleClicked(ContactItem item) {
        if(item.getJid().equals(TESTING_JID)){
            return;
        }
        for (final ContactGroupListener listener : listeners) {
            try {
                listener.contactItemDoubleClicked(item);
            } catch (Exception e) {
                Log.error("A ContactGroupListener (" + listener + ") threw an exception while processing a 'contactItemDoubleClicked' event for item: " + item, e);
            }
        }
    }

    private void firePopupEvent(MouseEvent event, ContactItem item) {
        for (final ContactGroupListener listener : listeners) {
            try {
                listener.showPopup(event, item);
            } catch (Exception e) {
                Log.error("A ContactGroupListener (" + listener + ") threw an exception while processing a 'showPopup' event for item: " + item + ", event: " + event, e);
            }
        }
    }

    private void firePopupEvent(MouseEvent event, Collection<ContactItem> items) {
        for (final ContactGroupListener listener : listeners) {
            try {
                listener.showPopup(event, items);
            } catch (Exception e) {
                Log.error("A ContactGroupListener (" + listener + ") threw an exception while processing a 'showPopup' event for items: " + items + ", event: " + event, e);
            }
        }
    }

    private void fireContactGroupPopupEvent(MouseEvent event) {
        for (final ContactGroupListener listener : listeners) {
            try {
                listener.contactGroupPopup(event, this);
            } catch (Exception e) {
                Log.error("A ContactGroupListener (" + listener + ") threw an exception while processing a 'contactGroupPopup' event: " + event, e);
            }
        }
    }

    private void fireContactItemAdded(ContactItem item) {
        for (final ContactGroupListener listener : listeners) {
            try {
                listener.contactItemAdded(item);
            } catch (Exception e) {
                Log.error("A ContactGroupListener (" + listener + ") threw an exception while processing a 'contactItemAdded' event for item: " + item, e);
            }
        }
    }

    private void fireContactItemRemoved(ContactItem item) {
        for (final ContactGroupListener listener : listeners) {
            try {
                listener.contactItemRemoved(item);
            } catch (Exception e) {
                Log.error("A ContactGroupListener (" + listener + ") threw an exception while processing a 'contactItemRemoved' event for item: " + item, e);
            }
        }
    }

    private void updateTitle() {
        if (isOfflineGroup()) {
            return;
        }

        int count = 0;
        for (ContactItem it : getContactItems()) {
            if (it.isAvailable()) {
                count++;
            }
        }

        setTitle(getGroupTitle() + " (" + count + " " + Res.getString("online") + ")");
        if (model.getSize() == 0) {
            model.addElement(noContacts);
        }
    }

    /**
     * Returns the containing <code>JList</code> of the ContactGroup.
     */
    public JList<? extends ContactItem> getList() {
        return contactItemList;
    }

    /**
     * Clears all selections within this group.
     */
    public void clearSelection() {
        contactItemList.clearSelection();
    }

    public void removeAllContacts() {
        // Remove all users from online group.
        for (ContactItem item : getContactItems()) {
            removeContactItem(item);
        }
        // Remove all users from offline group.
        for (ContactItem item : getOfflineContacts()) {
            removeOfflineContactItem(item);
        }
    }

    /**
     * Returns true if the ContactGroup contains available users.
     */
    public boolean hasAvailableContacts() {
        for (ContactGroup group : contactGroups) {
            if (group.hasAvailableContacts()) {
                return true;
            }
        }
        for (ContactItem item : getContactItems()) {
            if (item.getPresence() != null) {
                return true;
            }
        }
        return false;
    }

    public Collection<ContactItem> getOfflineContacts() {
        final List<ContactItem> list = new ArrayList<>(offlineContacts);
        list.sort(CONTACT_ITEM_COMPARATOR);
        return list;
    }

    /**
     * Returns true if ContactGroup is a Shared Group.
     */
    public boolean isSharedGroup() {
        return sharedGroup;
    }

    /**
     * Returns true if this ContactGroup is the Offline Group.
     */
    public boolean isOfflineGroup() {
        return offlineGroup;
    }

    /**
     * Returns true if this ContactGroup is the Unfiled Group.
     */
    public boolean isUnfiledGroup() {
        return unfiledGroup;
    }

    @Override
    public String toString() {
        return getGroupName();
    }

    /**
     * Returns all Selected Contacts within the ContactGroup.
     */
    public List<ContactItem> getSelectedContacts() {
        @SuppressWarnings("unchecked")
        List<ContactItem> selections = (List<ContactItem>) contactItemList.getSelectedValuesList();
        return selections;
    }

    public JPanel getContainerPanel() {
        return listPanel;
    }

    public Collection<ContactGroup> getContactGroups() {
        return contactGroups;
    }

    /**
     * Lets make sure that the panel doesn't stretch past the
     * scrollpane view pane.
     *
     * @return the preferred dimension
     */
    @Override
    public Dimension getPreferredSize() {
        final Dimension size = super.getPreferredSize();
        size.width = 0;
        return size;
    }

    /**
     * Returns the "pretty" title of the ContactGroup.
     */
    public String getGroupTitle() {
        int lastIndex = groupName.lastIndexOf(GROUP_DELIMITER);
        if (lastIndex != -1) {
            return groupName.substring(lastIndex + 2);
        }
        return groupName;
    }

    /**
     * Returns true if this group is nested.
     */
    public boolean isSubGroup() {
        return getGroupName().contains(GROUP_DELIMITER);
    }

    /**
     * Returns the underlying container for the JList.
     */
    public JPanel getListPanel() {
        return listPanel;
    }

    /**
     * Adds an internal popup listener.
     */
    private void addPopupWindow() {
        contactItemList.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseEntered(MouseEvent mouseEvent) {
                canShowPopup = true;
                timerTask = new DisplayWindowTask(mouseEvent);
                TaskEngine.getInstance().schedule(timerTask, 750, 1000);
            }

            @Override
            public void mouseExited(MouseEvent mouseEvent) {
                if (timerTask != null) {
                    TaskEngine.getInstance().cancelScheduledTask(timerTask);
                }
                canShowPopup = false;
                UIComponentRegistry.getContactInfoWindow().hideWindow();
            }
        });
        contactItemList.addMouseMotionListener(motionListener);
    }

    private class DisplayWindowTask extends SwingTimerTask {
        private MouseEvent event;
        private boolean newPopupShown = false;

        public DisplayWindowTask(MouseEvent e) {
            event = e;
        }

        @Override
        public void doRun() {
            if (canShowPopup) {
                if (!newPopupShown && !mouseDragged) {
                    displayWindow(event);
                    newPopupShown = true;
                }
            }
        }

        public void setEvent(MouseEvent event) {
            this.event = event;
        }

        public void setNewPopupShown(boolean popupChanged) {
            this.newPopupShown = popupChanged;
        }

        public boolean isNewPopupShown() {
            return newPopupShown;
        }
    }

    private class ListMotionListener extends MouseMotionAdapter {
        @Override
        public void mouseMoved(MouseEvent e) {
            if (!canShowPopup) {
                return;
            }
            if (e == null) {
                return;
            }
            timerTask.setEvent(e);
            if (needToChangePopup(e) && timerTask.isNewPopupShown()) {
                UIComponentRegistry.getContactInfoWindow().hideWindow();
                timerTask.setNewPopupShown(false);
            }
            mouseDragged = false;
        }

        @Override
        public void mouseDragged(MouseEvent e) {
            if (timerTask.isNewPopupShown()) {
                UIComponentRegistry.getContactInfoWindow().hideWindow();
            }
            mouseDragged = true;
        }
    }


    /**
     * Displays the <code>ContactInfoWindow</code>.
     */
    private void displayWindow(MouseEvent e) {
        if (!preferences.areVCardsVisible()) {
            return;
        }
        ContactGroup parent = this;
        SwingWorker worker = new SwingWorker() {
            @Override
            public Object construct() {
                UIComponentRegistry.getContactInfoWindow().display(parent, e);
                return null;
            }
        };
        worker.start();
    }

    private boolean needToChangePopup(MouseEvent e) {
        ContactInfoWindow contactInfoWindow = UIComponentRegistry.getContactInfoWindow();
        int loc = getList().locationToIndex(e.getPoint());
        ContactItem item = getList().getModel().getElementAt(loc);
        ContactItem currentlyShownItem = contactInfoWindow.getContactItem();
        return (item == null || currentlyShownItem == null) || !currentlyShownItem.getJid().equals(item.getJid());
    }

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

    protected JList<? extends ContactItem> getContactItemList() {
        return contactItemList;
    }
}
