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

import org.jivesoftware.MainWindow;
import org.jivesoftware.Spark;
import org.jivesoftware.resource.Res;
import org.jivesoftware.resource.SparkRes;
import org.jivesoftware.smack.StanzaListener;
import org.jivesoftware.smack.filter.AndFilter;
import org.jivesoftware.smack.filter.FromMatchesFilter;
import org.jivesoftware.smack.filter.StanzaTypeFilter;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.spark.ChatManager;
import org.jivesoftware.spark.SparkManager;
import org.jivesoftware.spark.component.tabbedPane.SparkTab;
import org.jivesoftware.spark.component.tabbedPane.SparkTabbedPane;
import org.jivesoftware.spark.component.tabbedPane.SparkTabbedPaneListener;
import org.jivesoftware.spark.ui.conferences.GroupChatRoomListener;
import org.jivesoftware.spark.ui.rooms.ChatRoomImpl;
import org.jivesoftware.spark.ui.rooms.GroupChatRoom;
import org.jivesoftware.spark.util.SwingTimerTask;
import org.jivesoftware.spark.util.SwingWorker;
import org.jivesoftware.spark.util.TaskEngine;
import org.jivesoftware.spark.util.log.Log;
import org.jivesoftware.sparkimpl.plugin.alerts.SparkToaster;
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 javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.regex.Pattern;

import static org.jivesoftware.spark.util.StringUtils.keyStroke2String;

/**
 * Contains all <code>ChatRoom</code> objects within Spark.
 *
 * @author Derek DeMoro
 */
public class ChatContainer extends SparkTabbedPane implements MessageListener, ChangeListener, KeyListener {
    private final CopyOnWriteArrayList<ChatRoomListener> chatRoomListeners = new CopyOnWriteArrayList<>();
    private final List<ChatRoom> chatRoomList = new ArrayList<>();
    private final Map<EntityJid, StanzaListener> presenceMap = new HashMap<>();
    private final String WELCOME_TITLE = SparkRes.getString(SparkRes.WELCOME);
    private ChatFrame chatFrame;
    private final TimerTask focusTask;
    private final LocalPreferences pref = SettingsManager.getLocalPreferences();

    public ChatContainer() {
        // Assign location
        super(SettingsManager.getLocalPreferences().isTabOnTop() ? JTabbedPane.TOP : JTabbedPane.BOTTOM);
        // Set minimum size
        setMinimumSize(new Dimension(400, 200));
        // Don't allow tabs to shrink and allow scrolling.
        enableDragAndDrop();
        //add Group chat room listener
        addChatRoomListener(new GroupChatRoomListener());
        addSparkTabbedPaneListener(new SparkTabbedPaneListener() {
            @Override
            public void tabRemoved(SparkTab tab, Component component, int index) {
                stateChanged(null);
                if (component instanceof ChatRoom) {
                    cleanupChatRoom((ChatRoom) component);
                } else if (component instanceof ContainerComponent) {
                    ((ContainerComponent) component).closing();
                }
            }

            @Override
            public void tabAdded(SparkTab tab, Component component, int index) {
                stateChanged(null);
            }

            @Override
            public void tabSelected(SparkTab tab, Component component, int index) {
                stateChanged(null);
                // Notify ChatRoomListeners that the tab has been activated.
                if (component instanceof ChatRoom) {
                    fireChatRoomActivated((ChatRoom) component);
                }
            }

            @Override
            public void allTabsRemoved() {
                if (chatFrame == null) {
                    return;
                }
                chatFrame.setTitle("");
                chatFrame.setVisible(false);
                chatFrame = null;
            }

            @Override
            public boolean canTabClose(SparkTab tab, Component component) {
                return true;
            }
        });

        setCloseButtonEnabled(true);
        // Add Key Navigation
        addKeyNavigation();
        this.setFocusable(false);
        setOpaque(true);
        setBackground(Color.white);
        // Create task for focusing chat.
        focusTask = new SwingTimerTask() {
            @Override
            public void doRun() {
                try {
                    //chatFrame.requestFocus();
                    ChatRoom chatRoom = getActiveChatRoom();
                    chatRoom.getChatInputEditor().requestFocusInWindow();
                    updateActiveTab();
                } catch (ChatRoomNotFoundException ignored) {
                    // Ignore. There may legitimately not be a chat room.
                }
            }
        };
    }

    /**
     * Adds navigation capability to chat rooms. Users can navigate using the
     * alt-left or right arrow keys.
     */
    private void addKeyNavigation() {
        KeyStroke leftStroke = KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0);
        String leftStrokeString = keyStroke2String(leftStroke);
        // Handle Left Arrow
        this.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke("alt " + leftStrokeString), "navigateLeft");
        this.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("alt " + leftStrokeString), "navigateLeft");
        this.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke("alt " + leftStrokeString), "navigateLeft");
        this.getActionMap().put("navigateLeft", new AbstractAction("navigateLeft") {
            @Override
            public void actionPerformed(ActionEvent evt) {
                navigateLeft();
            }
        });

        KeyStroke rightStroke = KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0);
        String rightStrokeString = keyStroke2String(rightStroke);
        // Handle Right Arrow
        this.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke("alt " + rightStrokeString), "navigateRight");
        this.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("alt " + rightStrokeString), "navigateRight");
        this.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke("alt " + rightStrokeString), "navigateRight");

        this.getActionMap().put("navigateRight", new AbstractAction("navigateRight") {
            @Override
            public void actionPerformed(ActionEvent evt) {
                navigateRight();
            }
        });

        this.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke("ESCAPE"), "escape");
        this.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke("ESCAPE"), "escape");

        this.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke("Ctrl W"), "escape");
        this.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_W, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()), "escape");

        this.getActionMap().put("escape", new AbstractAction("escape") {
            @Override
            public void actionPerformed(ActionEvent evt) {
                closeActiveRoom();
            }
        });

        this.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_W, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx() | InputEvent.SHIFT_DOWN_MASK), "shiftCmdW");
        this.getActionMap().put("shiftCmdW", new AbstractAction("shiftCmdW") {
            @Override
            public void actionPerformed(ActionEvent evt) {
                closeAllChatRooms();
            }
        });

        // Add KeyMappings
        getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_F, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()), "searchContacts");
        getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke("control F"), "searchContacts");
        getActionMap().put("searchContacts", new AbstractAction("searchContacts") {

            @Override
            public void actionPerformed(ActionEvent evt) {
                SwingWorker worker = new SwingWorker() {

                    @Override
                    public Object construct() {
                        return 42;
                    }

                    @Override
                    public void finished() {
                        SparkManager.getUserManager()
                                .searchContacts("", SparkManager.getChatManager()
                                        .getChatContainer().getChatFrame());
                    }
                };
                worker.start();
            }
        });
    }

    /**
     * Adds a new ChatRoom to Spark.
     */
    public synchronized void addChatRoom(final ChatRoom room) {
        createFrameIfNeeded();

        room.setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, Color.LIGHT_GRAY));
        AndFilter presenceFilter = new AndFilter(new StanzaTypeFilter(Presence.class), FromMatchesFilter.createBare(room.getBareJid()));

        // Next, create a packet listener. We use an anonymous inner class for brevity.
        StanzaListener myListener = stanza -> SwingUtilities.invokeLater(() -> {
            try {
                handleRoomPresence((Presence) stanza);
            } catch (Throwable t) {
                Log.warning("Unable to handle incoming stanza (room presence): " + stanza, t);
            }
        });

        room.registeredToFrame(chatFrame);

        SparkManager.getConnection().addAsyncStanzaListener(myListener, presenceFilter);

        // Add to PresenceMap
        presenceMap.put(room.getJid(), myListener);

        String tooltip;
        if (room instanceof ChatRoomImpl) {
            String nickname = SparkManager.getUserManager().getUserNicknameFromJID(((ChatRoomImpl) room).getParticipantJID());
            tooltip = "<html><body><b>Contact:&nbsp;</b>" + nickname + "<br><b>JID:&nbsp;</b>" + room.getJid();
        } else {
            tooltip = room.getBareJid().toString();
        }
        // Create ChatRoom UI and dock
        SparkTab tab = addTab(room.getTabTitle(), room.getTabIcon(), room, tooltip);
        tab.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseReleased(MouseEvent e) {
                checkTabPopup(e);
            }

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

        room.addMessageListener(this);

        // Remove brand panel
        final String title = getTabAt(0).getActualText();
        if (title.equals(WELCOME_TITLE)) {
            chatFrame.setTitle(room.getRoomTitle());
        }

        final TimerTask visibleTask = new SwingTimerTask() {
            @Override
            public void doRun() {
                checkVisibility(room);
            }
        };

        TaskEngine.getInstance().schedule(visibleTask, 100);
        // Add to ChatRoomList
        chatRoomList.add(room);
        // Notify users that the chat room has been opened.
        fireChatRoomOpened(room);
        focusChat();
        // Add Room listeners to override issue with input maps and keybinding on the mac.
        if (Spark.isMac()) {
            room.getChatInputEditor().addKeyListener(this);
        }
    }

    public void addContainerComponent(ContainerComponent comp) {
        createFrameIfNeeded();

        addTab(comp.getTabTitle(), comp.getTabIcon(), comp.getGUI(), comp.getToolTipDescription());
        chatFrame.setTitle(comp.getFrameTitle());
        checkVisibility(comp.getGUI());

        if (getSelectedComponent() != comp) {
            // Notify Decorators
            SparkManager.getChatManager().notifySparkTabHandlers(comp.getGUI());
        }
    }

    /**
     * Handles the presence of a one to one chat room.
     */
    private void handleRoomPresence(final Presence p) {
        final EntityBareJid roomname = p.getFrom().asEntityBareJidIfPossible();
        ChatRoom chatRoom;
        try {
            chatRoom = getChatRoom(roomname);
        } catch (ChatRoomNotFoundException e1) {
            Log.debug("Could not locate chat room " + roomname);
            return;
        }

        final Resourcepart userid = p.getFrom().getResourceOrNull();
        if (p.getType() == Presence.Type.unavailable) {
            fireUserHasLeft(chatRoom, userid);
        } else if (p.isAvailable()) {
            fireUserHasJoined(chatRoom, userid);
        }
        // Change tab icon
        if (chatRoom instanceof ChatRoomImpl) {
            // Notify state change.
            int tabLoc = indexOfComponent(chatRoom);
            if (tabLoc != -1) {
                SparkManager.getChatManager().notifySparkTabHandlers(chatRoom);
            }
        }
    }

    private void checkVisibility(Component component) {
        if (!chatFrame.isVisible() && SparkManager.getMainWindow().isFocusOwner()) {
            chatFrame.setState(Frame.NORMAL);
            chatFrame.setVisible(true);
        } else if (chatFrame.isVisible() && !chatFrame.isInFocus()) {
            startFlashing(component, false, null, null);
        } else if (chatFrame.isVisible() && chatFrame.getState() == Frame.ICONIFIED) {
            // Set to new tab.
            int tabLocation = indexOfComponent(component);
            setSelectedIndex(tabLocation);
            // If the ContactList is in the tray, we need better notification by flashing
            // the chatframe.
            startFlashing(component, false, null, null);
        } // Handle when chat frame is visible but the Contact List is not.
        else if (chatFrame.isVisible() && !SparkManager.getMainWindow().isVisible()) {
            startFlashing(component, false, null, null);
        } else if (!chatFrame.isVisible()) {
            // Set to new tab.
            int tabLocation = indexOfComponent(component);
            setSelectedIndex(tabLocation);

            if (Spark.isWindows()) {
                chatFrame.setFocusableWindowState(false);
                chatFrame.setState(Frame.ICONIFIED);
            }
            chatFrame.setFocusableWindowState(true);
            // If the ContactList is in the tray, we need better notification by flashing
            // the chatframe.
            if (!SparkManager.getMainWindow().isVisible()) {
                startFlashing(component, false, null, null);
            } else if (chatFrame.getState() == Frame.ICONIFIED) {
                startFlashing(component, false, null, null);
            }

            if (component instanceof ChatRoom) {
                chatFrame.setTitle(((ChatRoom) component).getRoomTitle());
            }
        }
    }

    private void handleMessageNotification(final ChatRoom chatRoom, boolean customMsg, String customMsgText, String customMsgTitle) {
        ChatRoom activeChatRoom = null;
        boolean groupMessageChecked = false;
        try {
            activeChatRoom = getActiveChatRoom();
        } catch (ChatRoomNotFoundException e1) {
            Log.error(e1);
        }

        if (chatFrame.isVisible() && (chatFrame.getState() == Frame.ICONIFIED || chatFrame.getInactiveTime() > 20000)) {
            int tabLocation = indexOfComponent(chatRoom);
            //setSelectedIndex(tabLocation);
            groupChatMessageCheck(chatRoom, customMsg, customMsgText, customMsgTitle);
            return;
        }
        if (chatFrame.isVisible() && chatFrame.getState() == Frame.NORMAL) {
            groupChatMessageCheck(chatRoom, customMsg, customMsgText, customMsgTitle);
            groupMessageChecked = true;
        }

        if (!chatFrame.isVisible() && SparkManager.getMainWindow().isFocusOwner()) {
            chatFrame.setState(Frame.NORMAL);
            chatFrame.setVisible(true);
        } else if (chatFrame.isVisible() && !chatFrame.isInFocus()) {
            if (!groupMessageChecked) {
                groupChatMessageCheck(chatRoom, customMsg, customMsgText, customMsgTitle);
            }
        } else if (chatFrame.isVisible() && chatFrame.getState() == Frame.ICONIFIED) {
            // Set to new tab.
            int tabLocation = indexOfComponent(chatRoom);
            setSelectedIndex(tabLocation);
            // If the ContactList is in the tray, we need better notification by flashing
            // the chatframe.
            if (!groupMessageChecked) {
                groupChatMessageCheck(chatRoom, customMsg, customMsgText, customMsgTitle);
            }
        } // Handle when chat frame is visible but the Contact List is not.
        else if (chatFrame.isVisible() && !SparkManager.getMainWindow().isVisible() && !chatFrame.isInFocus()) {
            if (!groupMessageChecked) {
                groupChatMessageCheck(chatRoom, customMsg, customMsgText, customMsgTitle);
            }
        } else if (!chatFrame.isVisible()) {
            // Set to new tab.
            int tabLocation = indexOfComponent(chatRoom);
            setSelectedIndex(tabLocation);

            if (Spark.isWindows()) {
                chatFrame.setExtendedState(Frame.ICONIFIED);
            }
            chatFrame.setVisible(true);

            // If the ContactList is in the tray, we need better notification by flashing
            // the chatframe.
            if (!SparkManager.getMainWindow().isVisible()) {
                if (!groupMessageChecked) {
                    groupChatMessageCheck(chatRoom, customMsg, customMsgText, customMsgTitle);
                }
            } else if (chatFrame.getState() == Frame.ICONIFIED) {
                if (!groupMessageChecked) {
                    groupChatMessageCheck(chatRoom, customMsg, customMsgText, customMsgTitle);
                }
            }
            chatFrame.setTitle(chatRoom.getRoomTitle());
        } else if (chatRoom != activeChatRoom) {
            if (!groupMessageChecked) {
                groupChatMessageCheck(chatRoom, customMsg, customMsgText, customMsgTitle);
            }
        }
    }

    /**
     * Removes the ChatRoom resources.
     */
    private synchronized void cleanupChatRoom(ChatRoom room) {
        if (room.isActive()) {
            room.leaveChatRoom();
            room.closeChatRoom();
        }

        final StanzaListener listener = presenceMap.get(room.getJid());
        if (listener != null) {
            SparkManager.getConnection().removeAsyncStanzaListener(listener);
        }

        fireChatRoomClosed(room);
        room.removeMessageListener(this);
        // Remove mappings
        presenceMap.remove(room.getJid());
        chatRoomList.remove(room);
        room.getChatInputEditor().removeKeyListener(this);
        // Clear all Text :)
        room.getTranscriptWindow().cleanup();
    }

    /**
     * Close all chat rooms.
     */
    public void closeAllChatRooms() {
        if (MainWindow.getInstance().isDocked() || !pref.isAutoCloseChatRoomsEnabled()) {
            return;
        }
        // copy room list to avoid concurrent modification during iterations and removing
        List<ChatRoom> allChatRooms = new ArrayList<>(chatRoomList);
        for (ChatRoom chatRoom : allChatRooms) {
            chatRoom.closeChatRoom();
        }
        // close in reverse order
        for (int i = getTabCount() - 1; i >= 0; i--) {
            Component comp = getComponentAt(i);
            if (comp instanceof ContainerComponent) {
                ((ContainerComponent) comp).closing();
            }
            closeTab(comp);
        }
    } 

    /**
     * Leaves a ChatRoom. Leaving a chat room does everything but close the room
     * itself.
     */
    public void leaveChatRoom(ChatRoom room) {
        // Notify that the chatroom has been left.
        fireChatRoomLeft(room);
        room.leaveChatRoom();
        final StanzaListener listener = presenceMap.get(room.getJid());
        if (listener != null && SparkManager.getConnection().isConnected()) {
            SparkManager.getConnection().removeAsyncStanzaListener(listener);
        }
    }

    /**
     * Returns a ChatRoom by name.
     *
     * @param jid the name of the ChatRoom.
     * @return the ChatRoom
     * @throws ChatRoomNotFoundException if the room was not found.
     */
    public ChatRoom getChatRoom(Jid jid) throws ChatRoomNotFoundException {
        for (int i = 0; i < getTabCount(); i++) {
            ChatRoom room = null;
            try {
                room = getChatRoom(i);
            } catch (ChatRoomNotFoundException ignored) {
            }
            if (jid instanceof EntityBareJid) {
                if (room != null && room.getJid().equals(jid) && isMUC(jid) && room.isActive()) {
                    return room;
                }
                if (room != null && room.getBareJid().equals(jid) && !isMUC(jid) && room.isActive()) {
                    return room;
                }
            } else {
                if (room != null && room.getJid().equals(jid) && room.isActive()) {
                    return room;
                }
            }
        }
        throw new ChatRoomNotFoundException(jid + " not found.");
    }

    /**
     * Check if the JID of conference
     */
    private static boolean isMUC(Jid jid){
        if (jid.hasResource()) {
            return false;
        }
        ChatManager chatManager = SparkManager.getChatManager();
        List<DomainBareJid> conferenceServices = chatManager.getConferenceServices();
        return conferenceServices != null && conferenceServices.contains(jid.asDomainBareJid());
    }

    /**
     * Returns a ChatRoom in the specified tab location.
     *
     * @param location the tab location.
     * @throws ChatRoomNotFoundException thrown if the room is not found.
     */
    public ChatRoom getChatRoom(int location) throws ChatRoomNotFoundException {
        if (getTabCount() < location) {
            throw new ChatRoomNotFoundException();
        }
        try {
            Component comp = getComponentAt(location);
            if (comp instanceof ChatRoom) {
                return (ChatRoom) comp;
            }
        } catch (ArrayIndexOutOfBoundsException outOfBoundsEx) {
            Log.error("Error getting Chat Room", outOfBoundsEx);
        }
        throw new ChatRoomNotFoundException();
    }

    /**
     * Returns the ChatRoom that is active in the tabbed pane.
     *
     * @throws ChatRoomNotFoundException is thrown if no chat room is found.
     */
    public ChatRoom getActiveChatRoom() throws ChatRoomNotFoundException {
        int location = getSelectedIndex();
        if (location != -1) {
            return getChatRoom(location);
        }
        throw new ChatRoomNotFoundException();
    }

    /**
     * Returns the Component that is active in the tabbed pane.
     */
    public Component getActiveRoom() {
        int location = getSelectedIndex();
        if (location != -1) {
            return getComponentAt(location);
        }
        return null;
    }

    /**
     * Activates the specified ChatRoom.
     */
    public void activateChatRoom(ChatRoom room) {
        int tabLocation = indexOfComponent(room);
        setSelectedIndex(tabLocation);
        chatFrame.bringFrameIntoFocus();
        focusChat();
    }

    /**
     * Activates the component in tabbed pane.
     *
     * @param component the component contained within the tab to activate.
     */
    public void activateComponent(Component component) {
        int tabLocation = indexOfComponent(component);
        if (tabLocation != -1) {
            setSelectedIndex(tabLocation);
        }
        chatFrame.bringFrameIntoFocus();
        focusChat();
    }

    /**
     * Used for Tray Notifications.
     *
     * @param room the ChatRoom where the message was received.
     * @param message the message received.
     */
    @Override
    public void messageReceived(ChatRoom room, Message message) {
        room.increaseUnreadMessageCount();
        SparkManager.getWorkspace().getTranscriptPlugin().persistChatRoom(room);
        fireNotifyOnMessage(room, false, null, null);
        ChatManager.getInstance().fireMessageReceived(message);
    }

    /**
     * Used for Tray Notifications.
     *
     * @param chatRoom the ChatRoom where the message was received.
     */
    public void fireNotifyOnMessage(final ChatRoom chatRoom, final boolean customMsg, final String customMsgText, final String customMsgTitle) {
        SwingUtilities.invokeLater(() -> handleMessageNotification(chatRoom, customMsg, customMsgText, customMsgTitle));
    }

    @Override
    public void messageSent(ChatRoom room, Message message) {
        fireChatRoomStateUpdated(room);
    }

    /**
     * Notification that the tab pane has been modified.
     * Generally by changing of the tabs.
     */
    @Override
    public void stateChanged(ChangeEvent e) {
        // Stop the flashing only if the chat frame is in focus.
        if (chatFrame.isInFocus()) {
            stopFlashing();
        }

        final Object o = getSelectedComponent();
        if (o instanceof ChatRoom) {
            final ChatRoom room = (ChatRoom) o;
            focusChat();
            // Set the title of the room.
            chatFrame.setTitle(room.getRoomTitle());
            chatFrame.setIconImage(SparkManager.getMainWindow().getIconImage());
        } else if (o instanceof ContainerComponent) {
            final ContainerComponent comp = (ContainerComponent) o;
            chatFrame.setTitle(comp.getFrameTitle());
            chatFrame.setIconImage(comp.getTabIcon().getImage());
            SparkManager.getChatManager().notifySparkTabHandlers(comp.getGUI());
        }
    }

    private void stopFlashing() {
        try {
            // Get current tab
            int selectedIndex = getSelectedIndex();
            if (selectedIndex != -1) {
                Component comp = getComponentAt(selectedIndex);
                if (comp != null) {
                    stopFlashing(comp);
                }
            }
        } catch (Exception e) {
            Log.error(e);
        }
    }

    /**
     * Closes a tab of a room.
     *
     * @param component the component inside of the tab to close.
     */
    public void closeTab(Component component) {
        int location = indexOfComponent(component);
        if (location == -1) {
            return;
        }
        if (getTabCount() == 0) {
            chatFrame.setTitle("");
        }
        this.close(this.getTabAt(location));
    }

    public void closeActiveRoom() {
        ChatRoom room;
        try {
            room = getActiveChatRoom();
        } catch (ChatRoomNotFoundException e1) {
            Component comp = getActiveRoom();
            if (comp != null) {
                boolean canClose = ((ContainerComponent) comp).closing();
                if (canClose) {
                    closeTab(comp);
                }
            }
            return;
        }

        // Confirm end session
        boolean isGroupChat = room.getChatType() == Message.Type.groupchat;
        if (isGroupChat) {
            final int ok = JOptionPane.showConfirmDialog(chatFrame, Res.getString("message.end.conversation"),
                    Res.getString("title.confirmation"), JOptionPane.YES_NO_OPTION);
            if (ok == JOptionPane.OK_OPTION) {
                room.closeChatRoom();
            }
        } else {
            room.closeChatRoom();
        }
    }

    @Override
    public String toString() {
        StringBuilder buf = new StringBuilder();
        for (ChatRoom room : chatRoomList) {
            buf.append("Roomname=").append(room.getBareJid()).append("\n");
        }
        return buf.toString();
    }

    /**
     * Returns true if there are any Rooms present.
     */
    public boolean hasRooms() {
        int count = getSelectedIndex();
        return count != -1;
    }

    /**
     * Adds a ChatRoom listener to ChatRooms. The listener will be called when
     * either a ChatRoom has been added, removed, or activated.
     */
    public void addChatRoomListener(ChatRoomListener listener) {
        chatRoomListeners.addIfAbsent(listener);
    }

    /**
     * Removes the specified <code>ChatRoomListener</code>.
     */
    public void removeChatRoomListener(ChatRoomListener listener) {
        chatRoomListeners.remove(listener);
    }

    /**
     * Notifies users that a <code>ChatRoom</code> has been opened.
     */
    protected void fireChatRoomOpened(ChatRoom room) {
        for (final ChatRoomListener listener : chatRoomListeners) {
            try {
                listener.chatRoomOpened(room);
            } catch (Exception e) {
                Log.error("A ChatRoomListener (" + listener + ") threw an exception while processing a 'opened' event for room: " + room, e);
            }
        }
    }

    /**
     * Notifies users that a <code>ChatRoom</code> has been left.
     */
    protected void fireChatRoomLeft(ChatRoom room) {
        for (final ChatRoomListener listener : chatRoomListeners) {
            try {
                listener.chatRoomLeft(room);
            } catch (Exception e) {
                Log.error("A ChatRoomListener (" + listener + ") threw an exception while processing a 'left' event for room: " + room, e);
            }
        }
    }

    /**
     * Notifies users that a <code>ChatRoom</code> has been closed.
     */
    protected void fireChatRoomClosed(ChatRoom room) {
        for (final ChatRoomListener listener : chatRoomListeners) {
            try {
                listener.chatRoomClosed(room);
            } catch (Exception e) {
                Log.error("A ChatRoomListener (" + listener + ") threw an exception while processing a 'closed' event for room: " + room, e);
            }
        }
    }

    /**
     * Notifies users that a <code>ChatRoom</code> has been activated.
     */
    protected void fireChatRoomActivated(ChatRoom room) {
        for (final ChatRoomListener listener : chatRoomListeners) {
            try {
                listener.chatRoomActivated(room);
            } catch (Exception e) {
                Log.error("A ChatRoomListener (" + listener + ") threw an exception while processing a 'activated' event for room: " + room, e);
            }
        }
    }

    /**
     * Notifies users that a user has joined a <code>ChatRoom</code>.
     *
     * @param room - the <code>ChatRoom</code> that a user has joined.
     * @param userid - the nickname of the person.
     */
    protected void fireUserHasJoined(final ChatRoom room, final Resourcepart userid) {
        SwingUtilities.invokeLater(() -> {
            for (final ChatRoomListener listener : chatRoomListeners) {
                try {
                    if (userid != null) {
                        listener.userHasJoined(room, userid.toString());
                    }
                } catch (Exception e) {
                    Log.error("A ChatRoomListener (" + listener + ") threw an exception while processing a 'user joined' event for user '" + userid + "' in room: " + room, e);
                }
            }
        });
    }

    /**
     * Notifies users that a user has left a <code>ChatRoom</code>.
     *
     * @param room - the <code>ChatRoom</code> that a user has left.
     * @param userid - the nickname of the person.
     */
    protected void fireUserHasLeft(final ChatRoom room, final Resourcepart userid) {
        SwingUtilities.invokeLater(() -> {
            for (final ChatRoomListener listener : chatRoomListeners) {
                try {
                    listener.userHasLeft(room, userid.toString());
                } catch (Exception e) {
                    Log.error("A ChatRoomListener (" + listener + ") threw an exception while processing a 'user left' event for user '" + userid + "' in room: " + room, e);
                }
            }
        });
    }

    /**
     * Starts flashing of MainWindow.
     *
     * @param comp the Component to check if a message has been inserted but the
     * room is not the selected room.
     */
    public void startFlashing(final Component comp, final boolean customMsg,
            final String customMsgText, final String customMsgTitle) {
        SwingUtilities.invokeLater(() -> {
            try {
                final int index = indexOfComponent(comp);
                if (index != -1) {
                    // Check notifications.
                    if (pref.isChatRoomNotificationsOn() || !(comp instanceof GroupChatRoom)) {
                        if (comp instanceof ChatRoom) {
                            if (comp instanceof GroupChatRoom) {
                                GroupChatRoom groupChatRoom = (GroupChatRoom) comp;
                                if (groupChatRoom.getLastMessage() != null) {
                                    EntityFullJid lastMessageFrom = groupChatRoom.getLastMessage().getFrom().asEntityFullJidIfPossible();
                                    if (!groupChatRoom.isBlocked(lastMessageFrom)) {
                                        checkNotificationPreferences(groupChatRoom, customMsg, customMsgText, customMsgTitle);
                                    }
                                }
                            } else {
                                checkNotificationPreferences((ChatRoom) comp, customMsg, customMsgText, customMsgTitle);
                            }
                        }
                    }
                    // Notify decorators
                    SparkManager.getChatManager().notifySparkTabHandlers(comp);
                }

                boolean flashAllowed = pref.isChatRoomNotificationsOn()
                        || !(comp instanceof GroupChatRoom);
                if (!chatFrame.isInFocus() && flashAllowed) {
                    SparkManager.getNativeManager().flashWindow(chatFrame);
                }
            } catch (Exception ex) {
                Log.error("Issue in ChatRooms with tab location.", ex);
            }
        });
    }

    public void fireChatRoomStateUpdated(final ChatRoom room) {
        final int index = indexOfComponent(room);
        if (index != -1) {
            SparkManager.getChatManager().notifySparkTabHandlers(room);
        }
    }

    /**
     * Checks to see if the <code>ChatFrame</code> should stop flashing.
     *
     * @param component the component that should be notified.
     */
    public void stopFlashing(final Component component) {
        SwingUtilities.invokeLater(() -> {
            try {
                // Stop the flashing
                SparkManager.getNativeManager().stopFlashing(chatFrame);
                // Notify decorators
                SparkManager.getChatManager().notifySparkTabHandlers(component);
            } catch (Exception ex) {
                Log.error("Could not stop flashing because " + ex.getMessage(), ex);
            }
        });
    }

    /**
     * Handles Notification preferences for incoming messages and rooms.
     */
    private void checkNotificationPreferences(final ChatRoom room, boolean customMsg, String customMsgText, String customMsgTitle) {
        if (pref.isWindowTakesFocus()) {
            chatFrame.setState(Frame.NORMAL);
            chatFrame.setVisible(true);
        }

        if (pref.isShowToasterPopup()) {
            SparkToaster toaster = new SparkToaster();
            toaster.setCustomAction(new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent actionEvent) {
                    chatFrame.setState(Frame.NORMAL);
                    chatFrame.setVisible(true);
                    int tabLocation = indexOfComponent(room);
                    if (tabLocation != -1) {
                        setSelectedIndex(tabLocation);
                    }
                }
            });

            toaster.setDisplayTime(5000);
            toaster.setBorder(BorderFactory.createLineBorder(Color.lightGray, 1, true));

            String nickname = room.getRoomTitle();

            toaster.setToasterHeight(150);
            toaster.setToasterWidth(200);

            int size = room.getTranscripts().size();
            if (customMsg) {
                toaster.setTitle(customMsgTitle);
                toaster.showToaster(room.getTabIcon(), customMsgText);
            } else {
                toaster.setTitle(nickname);
                if (size > 0) {
                    Message lastChatMessage = room.getTranscripts().get(size - 1);
                    toaster.showToaster(room.getTabIcon(), lastChatMessage.getBody());
                }
            }
        }
    }

    /**
     * Performs several group chat checks
     *
     * chatRoom the chat room that needs to be passed customMsg whether or not
     * this is a custom message customMsgText if any custom message should
     * appear in the popup customMsgTitle whether or not the toaster should have
     * any popup
     */
    private void groupChatMessageCheck(ChatRoom chatRoom, boolean customMsg, String customMsgText, String customMsgTitle) {
        // predefine if this is a group chat message or not
        boolean isGroupChat = chatRoom.getChatType() == Message.Type.groupchat;
        List<Message> transcripts = chatRoom.getTranscripts();
        final Message lastChatMessage = !transcripts.isEmpty() ? transcripts.get(transcripts.size() - 1) : null;
        final String lastMessageBody = lastChatMessage != null ? lastChatMessage.getBody() : null;
        // is this a group chat message and is my name in it?
        if (isGroupChat) {
            // is a group chat, perform some functions
            String fromNickName = "";
            String finalRoomName = "";
            if (lastChatMessage != null) {
                finalRoomName = chatRoom.getRoomTitle();
                Jid lastChatMessageFrom = lastChatMessage.getFrom();
                if (lastChatMessageFrom == null) {
                    fromNickName = finalRoomName;
                } else {
                    if (lastChatMessageFrom.hasNoResource()) {
                        // We have no name after "/" in JID (must be like: test@conference.jabber.kg/kos)
                        fromNickName = finalRoomName;
                    } else {
                        fromNickName = lastChatMessageFrom.getResourceOrEmpty().toString();
                    }
                }
            }
            String myNickName = chatRoom.getNickname().toString();
            if (!myNickName.equals(fromNickName)) {
                if (lastMessageBody != null && !lastMessageBody.isEmpty() && pref.isMucHighToastEnabled()) {
                    // allowed to check for new messages containing name
                    String myUserName = SparkManager.getSessionManager().getUsername();
                    Pattern usernameMatch = Pattern.compile(myUserName, Pattern.CASE_INSENSITIVE);
                    Pattern nicknameMatch = Pattern.compile(myNickName, Pattern.CASE_INSENSITIVE);
                    if (usernameMatch.matcher(lastMessageBody).find() || nicknameMatch.matcher(lastMessageBody).find()) {
                        // match, send new message
                        String customMsgTextS = Res.getString("group.chat.name.match") + " " + finalRoomName + " by " + fromNickName + " (" + lastMessageBody + ")";
                        String customMsgTitleS = Res.getString("group.chat.name.notification");
                        startFlashing(chatRoom, true, customMsgTextS, customMsgTitleS);
                    } else {
                        // regular group message
                        String customMsgTextS = fromNickName + " says: " + lastMessageBody;
                        startFlashing(chatRoom, true, customMsgTextS, finalRoomName);
                    }
                } else {
                    // regular group message
                    String customMsgTextS = fromNickName + " says: " + lastMessageBody;
                    startFlashing(chatRoom, true, customMsgTextS, finalRoomName);
                }
            }
        } else if (customMsg) {
            // probably a file transfer request
            startFlashing(chatRoom, true, customMsgText, customMsgTitle);
        } else {
            // normal personal chat
            String finalRoomName = chatRoom.getRoomTitle();
            String customMsgTextS = lastMessageBody != null ? lastMessageBody : "";
            startFlashing(chatRoom, true, customMsgTextS, finalRoomName);
        }
    }

    public void setChatRoomTitle(ChatRoom room, String title) {
        int index = indexOfComponent(room);
        if (index != -1) {
            SparkTab tab = getTabAt(index);
            fireChatRoomStateUpdated(room);
            tab.setTabTitle(room.getTabTitle());
        }
    }

    private void createFrameIfNeeded() {
        if (chatFrame != null) {
            return;
        }
        chatFrame = pref.isDockingEnabled() ? MainWindow.getInstance() : new ChatFrame();

        if (SparkManager.getMainWindow().isActive() || pref.isWindowTakesFocus()) {
            chatFrame.setState(Frame.NORMAL);
        } else {
            chatFrame.setAutoRequestFocus(false);
            chatFrame.setState(Frame.ICONIFIED);
        }

        chatFrame.setVisible(true);
        chatFrame.addWindowListener(new WindowAdapter() {
            @Override
            public void windowActivated(WindowEvent windowEvent) {
                stopFlashing();
                int sel = getSelectedIndex();
                if (sel == -1) {
                    return;
                }
                final ChatRoom room;
                try {
                    room = getChatRoom(sel);
                    focusChat();
                    // Set the title of the room.
                    chatFrame.setTitle(room.getRoomTitle());
                } catch (ChatRoomNotFoundException ignored) {
                    // Nothing to do
                }
            }

            @Override
            public void windowClosing(WindowEvent windowEvent) {
                ChatContainer chatContainer = SparkManager.getChatManager().getChatContainer();
                if (chatContainer.getTotalNumberOfUnreadMessages() > 0 &&
                    pref.isCloseUnreadMessage()) {
                    final int selectedOption = JOptionPane.showConfirmDialog(null,
                        Res.getString("message.close.unread.window"),
                        Res.getString("title.alert"),
                        JOptionPane.YES_NO_OPTION);
                    if (selectedOption == JOptionPane.YES_OPTION) {
                        chatContainer.closeAllChatRooms();
                    }
                } else {
                    chatContainer.closeAllChatRooms();
                }
            }
        });
        // Start timer
        handleStaleChats();
    }

    /**
     * Brings the chat into focus.
     */
    public void focusChat() {
        TaskEngine.getInstance().schedule(focusTask, 50);
    }

    public Collection<ChatRoom> getChatRooms() {
        return new ArrayList<>(chatRoomList);
    }

    public ChatFrame getChatFrame() {
        return chatFrame;
    }

    public void blinkFrameIfNecessary(final JFrame frame) {
        final MainWindow mainWindow = SparkManager.getMainWindow();
        if (mainWindow.isFocusOwner()) {
            frame.setVisible(true);
        } else {
            // Set to new tab.
            if (Spark.isWindows()) {
                frame.setState(Frame.ICONIFIED);
                SparkManager.getNativeManager().flashWindow(frame);
                frame.setVisible(true);
                frame.addWindowListener(new WindowAdapter() {
                    @Override
                    public void windowActivated(WindowEvent e) {
                        SparkManager.getNativeManager().stopFlashing(frame);
                    }
                });
            }
        }
    }

    private void checkTabPopup(MouseEvent e) {
        final SparkTab tab = (SparkTab) e.getSource();
        if (!e.isPopupTrigger()) {
            return;
        }

        final JPopupMenu popup = new JPopupMenu();

        // Handle closing this room.
        Action closeThisAction = new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                ChatRoom chatRoom = (ChatRoom) getComponentInTab(tab);
                if (chatRoom != null) {
                    closeTab(chatRoom);
                }
            }
        };
        closeThisAction.putValue(Action.NAME, Res.getString("message.close.this.chat"));
        popup.add(closeThisAction);

        if (getChatRooms().size() > 1) {
            // Handle closing other rooms.
            Action closeOthersAction = new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    ChatRoom chatRoom = (ChatRoom) getComponentInTab(tab);
                    if (chatRoom == null) {
                        return;
                    }
                    for (ChatRoom cRoom : getChatRooms()) {
                        if (chatRoom != cRoom) {
                            closeTab(cRoom);
                        }
                    }
                }
            };

            closeOthersAction.putValue(Action.NAME, Res.getString("message.close.other.chats"));
            popup.add(closeOthersAction);

            Action closeOldAction = new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    for (ChatRoom chatRoom : getChatRooms()) {
                        if (chatRoom.stale) {
                            closeTab(chatRoom);
                        }
                    }
                }
            };
            closeOldAction.putValue(Action.NAME, Res.getString("message.close.stale.chats"));
            popup.add(closeOldAction);
        }

        popup.show(tab, e.getX(), e.getY());
    }

    public boolean isStaleChatRoom(ChatRoom chatRoom) {
        return chatRoom.stale;
    }

    /**
     * Checks every room every minute to see if it's timed out.
     */
    private void handleStaleChats() {
        int delay = 1000;   // delay for 1 second.
        int period = 60000;  // repeat every minute.
        int timeoutInMs = pref.getChatLengthDefaultTimeout() * 60_000;

        final TimerTask task = new SwingTimerTask() {
            @Override
            public void doRun() {
                long currentTime = System.currentTimeMillis();
                for (ChatRoom chatRoom : getChatRooms()) {
                    markRoomAsStale(chatRoom, currentTime, timeoutInMs);
                    if (!chatRoom.stale) {
                        continue;
                    }
                    // Notify decorators
                    SparkManager.getChatManager().notifySparkTabHandlers(chatRoom);
                }
            }
        };

        TaskEngine.getInstance().scheduleAtFixedRate(task, delay, period);
    }

    private static void markRoomAsStale(ChatRoom chatRoom, long currentTime, int timeoutInMs) {
        int unreadCount = chatRoom.getUnreadMessageCount();
        if (unreadCount > 0) {
            chatRoom.stale = true;
        }
        long lastActivity = chatRoom.getLastActivity();
        long diff = currentTime - lastActivity;
        chatRoom.stale = timeoutInMs <= diff;
    }

    private void navigateRight() {
        int selectedIndex = getSelectedIndex();
        if (selectedIndex > -1) {
            int count = getTabCount();
            if (selectedIndex == (count - 1)) {
                setSelectedIndex(0);
            } else {
                setSelectedIndex(selectedIndex + 1);
            }
        }
    }

    private void navigateLeft() {
        int selectedIndex = getSelectedIndex();
        if (selectedIndex > 0) {
            setSelectedIndex(selectedIndex - 1);
        } else {
            setSelectedIndex(getTabCount() - 1);
        }
    }

    // Handle key listener events for mac only :)
    @Override
    public void keyTyped(KeyEvent keyEvent) {
        // Nothing to do.
    }

    @Override
    public void keyPressed(KeyEvent keyEvent) {
        if (keyEvent.isMetaDown()) {
            if (keyEvent.getKeyCode() == KeyEvent.VK_RIGHT) {
                navigateRight();
            } else if (keyEvent.getKeyCode() == KeyEvent.VK_LEFT) {
                navigateLeft();
            }
        }
    }

    @Override
    public void keyReleased(KeyEvent keyEvent) {
        // Nothing to do.
    }

    /**
     * Returns the total number of unread messages in Spark.
     */
    public int getTotalNumberOfUnreadMessages() {
        int messageCount = 0;
        for (ChatRoom chatRoom : chatRoomList) {
            messageCount += chatRoom.getUnreadMessageCount();
        }

        return messageCount;
    }

    public void closeAllGroupChatRooms() {
        for (ChatRoom chatRoom : chatRoomList) {
            boolean isGroup = chatRoom.getChatType() == Message.Type.groupchat;
            if (isGroup) {
                closeTab(chatRoom);
                chatRoom.closeChatRoom();
            }
        }
    }

    public boolean hasGroupChatRooms() {
        for (ChatRoom chatRoom : chatRoomList) {
            boolean isGroup = chatRoom.getChatType() == Message.Type.groupchat;
            if (isGroup) {
                return true;
            }
        }
        return false;
    }
}
