/*
 * 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.apache.commons.lang3.Strings;
import org.jivesoftware.resource.Res;
import org.jivesoftware.resource.SparkRes;
import org.jivesoftware.smack.ConnectionListener;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.StanzaListener;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.MessageBuilder;
import org.jivesoftware.smack.packet.StanzaBuilder;
import org.jivesoftware.smack.roster.Roster;
import org.jivesoftware.smack.roster.RosterEntry;
import org.jivesoftware.smackx.chatstates.ChatState;
import org.jivesoftware.smackx.jiveproperties.packet.JivePropertiesExtension;
import org.jivesoftware.spark.ChatAreaSendField;
import org.jivesoftware.spark.ChatManager;
import org.jivesoftware.spark.SparkManager;
import org.jivesoftware.spark.component.BackgroundPanel;
import org.jivesoftware.spark.component.RolloverButton;
import org.jivesoftware.spark.plugin.ContextMenuListener;
import org.jivesoftware.spark.ui.rooms.GroupChatRoom;
import org.jivesoftware.spark.ui.transctipt.TranscriptWindow;
import org.jivesoftware.spark.util.GraphicUtils;
import org.jivesoftware.spark.util.SwingWorker;
import org.jivesoftware.spark.util.TaskEngine;
import org.jivesoftware.spark.util.UIComponentRegistry;
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.EntityBareJid;
import org.jxmpp.jid.EntityFullJid;
import org.jxmpp.jid.EntityJid;
import org.jxmpp.jid.parts.Resourcepart;

import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import java.awt.*;
import java.awt.event.*;
import java.io.File;
import java.time.Instant;
import java.util.*;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

import static java.awt.GridBagConstraints.*;

/**
 * The base implementation of all ChatRoom conversations. You would implement
 * this class to have most types of Chat.
 */
public abstract class ChatRoom extends BackgroundPanel implements ActionListener, StanzaListener, DocumentListener, ConnectionListener, FocusListener, ContextMenuListener, ChatFrameToFrontListener {
    private final LocalPreferences pref = SettingsManager.getLocalPreferences();

    private final JPanel chatPanel;
    private final JSplitPane splitPane;
    private final JSplitPane verticalSplit;

    private final JLabel notificationLabel;
    private final TranscriptWindow transcriptWindow;
    private final ChatAreaSendField chatAreaButton;
    private final ChatToolBar toolbar;
    private final JScrollPane textScroller;
    private final JPanel bottomPanel;

    private final JPanel editorWrapperBar;
    private final JPanel editorBarRight;
    private final JPanel editorBarLeft;
    private final JPanel chatWindowPanel;

    private int unreadMessageCount;

    private boolean mousePressed;

    private final CopyOnWriteArrayList<ChatRoomClosingListener> closingListeners = new CopyOnWriteArrayList<>();

    private ChatRoomTransferHandler transferHandler;

    private final List<String> packetIDList;
    private final CopyOnWriteArrayList<MessageListener> messageListeners;
    private final List<Message> transcript;
    private final CopyOnWriteArrayList<FileDropListener> fileDropListeners;

    private final MouseAdapter transcriptWindowMouseListener;

    private final KeyAdapter chatEditorKeyListener;
    private ChatFrame _chatFrame;
    private final RolloverButton _alwaysOnTopItem;
    private boolean _isAlwaysOnTopActive;

    // Chat state
    private TimerTask typingTimerTask;
    private long lastNotificationSentTime;
    private ChatState lastNotificationSent;
    private final long pauseTimePeriod = 2000;
    private final long inactiveTimePeriod = 120000;
    protected long lastActivity;
    /** Flag to show is the room is stale i.e. it was last active more than defaultChatLengthTimeout (15 min) */
    boolean stale;

    private static final Color COLOR_BOTTOM_PANEL_BORDER = new Color(197, 213, 230);

    protected ChatRoom() {
        chatPanel = new JPanel(new GridBagLayout());
        transcriptWindow = UIComponentRegistry.createTranscriptWindow();
        splitPane = new JSplitPane();
        packetIDList = new ArrayList<>();
        notificationLabel = new JLabel();
        toolbar = new ChatToolBar();
        bottomPanel = new JPanel();

        messageListeners = new CopyOnWriteArrayList<>();
        transcript = new ArrayList<>();

        editorWrapperBar = new JPanel(new BorderLayout());
        editorBarLeft = new JPanel(new FlowLayout(FlowLayout.LEFT, 1, 1));
        editorBarRight = new JPanel(new FlowLayout(FlowLayout.RIGHT, 1, 1));
        editorWrapperBar.add(editorBarLeft, BorderLayout.WEST);
        editorWrapperBar.add(editorBarRight, BorderLayout.EAST);
        fileDropListeners = new CopyOnWriteArrayList<>();

        transcriptWindowMouseListener = new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                if (e.getClickCount() != 2) {
                    getChatInputEditor().requestFocus();
                }
            }

            @Override
            public void mouseReleased(MouseEvent e) {
                mousePressed = false;
                if (transcriptWindow.getSelectedText() == null) {
                    getChatInputEditor().requestFocus();
                }
            }

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

        transcriptWindow.addMouseListener(transcriptWindowMouseListener);

        chatAreaButton = new ChatAreaSendField();
        chatAreaButton.setVisible(true);
        chatAreaButton.setEnabled(true);
        textScroller = new JScrollPane(transcriptWindow);
        textScroller.setBackground(transcriptWindow.getBackground());
        textScroller.getViewport().setBackground(Color.white);
        transcriptWindow.setBackground(Color.white);

        getChatInputEditor().setSelectedTextColor((Color) UIManager.get("ChatInput.SelectedTextColor"));
        getChatInputEditor().setSelectionColor((Color) UIManager.get("ChatInput.SelectionColor"));

        setLayout(new GridBagLayout());
        // Remove Default Beveled Borders
        splitPane.setBorder(null);
        splitPane.setOneTouchExpandable(false);
        // Add Vertical Split Pane
        verticalSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
        add(verticalSplit, new GridBagConstraints(0, 1, 1, 1, 1, 1, CENTER, BOTH, new Insets(0, 0, 0, 0), 0, 0));

        verticalSplit.setBorder(null);
        verticalSplit.setOneTouchExpandable(false);
        verticalSplit.setTopComponent(splitPane);

        textScroller.setAutoscrolls(true);

        // For the first 5*150ms we wait for transcript to load and move
        // ScrollPane to max position if size of ScrollPane changed
        textScroller.getVerticalScrollBar().addAdjustmentListener(new AdjustmentListener() {
            private boolean scrollAtStart = false;

            @Override
            public void adjustmentValueChanged(AdjustmentEvent e) {
                if (!scrollAtStart) {
                    scrollAtStart = true;
                    SwingWorker thread = new SwingWorker() {

                        @Override
                        public Object construct() {
                            int start = textScroller.getVerticalScrollBar().getMaximum();
                            int second;
                            int i = 0;
                            do {
                                try {
                                    Thread.sleep(150);
                                    second = textScroller.getVerticalScrollBar().getMaximum();
                                    if (start == second) {
                                        ++i;
                                    } else {
                                        scrollToBottom();
                                        getTranscriptWindow().repaint();
                                    }
                                    start = second;
                                } catch (InterruptedException ignored) {
                                }
                            } while (i < 5);
                            return null;
                        }
                    };
                    thread.start();
                }
            }
        });
        // Speed up scrolling. It was way too slow.
        textScroller.getVerticalScrollBar().setBlockIncrement(200);
        textScroller.getVerticalScrollBar().setUnitIncrement(20);

        chatWindowPanel = new JPanel();
        chatWindowPanel.setLayout(new GridBagLayout());
        chatWindowPanel.add(textScroller, new GridBagConstraints(0, 10, 1, 1, 1, 1, WEST, BOTH, new Insets(0, 0, 0, 0), 0, 0));
        chatWindowPanel.setOpaque(false);

        // Layout Components
        Insets chatPanelInsets = new Insets(0, 5, 0, 5);
        chatPanel.add(chatWindowPanel, new GridBagConstraints(0, 1, 1, 1, 1, 1, CENTER, BOTH, chatPanelInsets, 0, 0));
        // Add Chat Panel to Split Pane
        splitPane.setLeftComponent(chatPanel);
        // Add edit buttons to Chat Room
        editorBarLeft.setOpaque(false);
        chatPanel.setOpaque(false);

        bottomPanel.setOpaque(false);
        splitPane.setOpaque(false);
        bottomPanel.setLayout(new GridBagLayout());
        Insets chatAreaInsets = new Insets(0, 5, 5, 5);
        bottomPanel.add(chatAreaButton, new GridBagConstraints(0, 1, 5, 1, 1, 1, WEST, BOTH, chatAreaInsets, 0, 35));
        Insets editorWrapperInsets = new Insets(0, 5, 0, 5);
        bottomPanel.add(editorWrapperBar, new GridBagConstraints(0, 0, 5, 1, 0, 0, WEST, HORIZONTAL, editorWrapperInsets, 0, 0));

        // Set bottom panel border
        bottomPanel.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, COLOR_BOTTOM_PANEL_BORDER));
        verticalSplit.setOpaque(false);

        verticalSplit.setBottomComponent(bottomPanel);
        verticalSplit.setResizeWeight(1.0);
        verticalSplit.setDividerSize(2);

        // Add listener to send button
        chatAreaButton.getButton().addActionListener(this);
        // Add Key Listener to Send Field
        getChatInputEditor().getDocument().addDocumentListener(this);
        // Add Key Listener to Send Field
        chatEditorKeyListener = new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent e) {
                checkForEnter(e);
            }
        };

        getChatInputEditor().addKeyListener(chatEditorKeyListener);

        getChatInputEditor().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("ctrl F4"), "closeTheRoom");
        getChatInputEditor().getActionMap().put("closeTheRoom", new AbstractAction("closeTheRoom") {
            @Override
            public void actionPerformed(ActionEvent evt) {
                // Leave this chat.
                closeChatRoom();
            }
        });

        getChatInputEditor().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("ctrl SPACE"), "handleCompletion");
        getChatInputEditor().getActionMap().put("handleCompletion", new AbstractAction("handleCompletion") {
            @Override
            public void actionPerformed(ActionEvent evt) {
                // handle name completion.
                try {
                    handleNickNameCompletion();
                } catch (ChatRoomNotFoundException e) {
                    Log.error("ctlr-space nickname find", e);
                }
            }
        });

        _isAlwaysOnTopActive = pref.isChatWindowAlwaysOnTop();
        _alwaysOnTopItem = UIComponentRegistry.getButtonFactory().createAlwaysOnTop(_isAlwaysOnTopActive);

        _alwaysOnTopItem.addActionListener(actionEvent -> {
            if (!_isAlwaysOnTopActive) {
                pref.setChatWindowAlwaysOnTop(true);
                _chatFrame.setWindowAlwaysOnTop(true);
                _isAlwaysOnTopActive = true;
                _alwaysOnTopItem.setIcon(SparkRes.getImageIcon(SparkRes.Icon.FRAME_ALWAYS_ON_TOP_ACTIVE));

            } else {
                pref.setChatWindowAlwaysOnTop(false);
                _chatFrame.setWindowAlwaysOnTop(false);
                _isAlwaysOnTopActive = false;
                _alwaysOnTopItem.setIcon(SparkRes.getImageIcon(SparkRes.Icon.FRAME_ALWAYS_ON_TOP_DEACTIVE));
            }
        });

        editorBarRight.add(_alwaysOnTopItem);
        // Initially, set the right pane to null to keep it empty.
        getSplitPane().setRightComponent(null);

        notificationLabel.setIcon(SparkRes.getImageIcon(SparkRes.Icon.BLANK_IMAGE));
        getTranscriptWindow().addContextMenuListener(this);
        transferHandler = new ChatRoomTransferHandler(this);

        getTranscriptWindow().setTransferHandler(transferHandler);
        getChatInputEditor().setTransferHandler(transferHandler);

        addToolbar();
        // Add Connection Listener
        SparkManager.getConnection().addConnectionListener(this);
        // Add Focus Listener
        addFocusListener(this);

        setChatState(ChatState.active);
        createChatStateTimerTask();

        scrollToBottom();
    }

    protected void createChatStateTimerTask() {
        typingTimerTask = new TimerTask() {
            @Override
            public void run() {
                final long lastUpdate = System.currentTimeMillis() - lastNotificationSentTime;
                switch (lastNotificationSent) {
                    case paused:
                    case active:
                        if (lastUpdate > inactiveTimePeriod) {
                            setChatState(ChatState.inactive);
                        }
                        break;

                    case composing:
                        if (lastUpdate > pauseTimePeriod) {
                            setChatState(ChatState.paused);
                        }
                        break;
                }
            }
        };
        TaskEngine.getInstance().scheduleAtFixedRate(typingTimerTask, pauseTimePeriod / 2, pauseTimePeriod / 2);
    }

    /**
     * Sends a chat state to all peers.
     */
    protected abstract void sendChatState(ChatState state) throws SmackException.NotConnectedException, InterruptedException;

    /**
     * Sets the chat state, causing an update to be sent to all peers if the new
     * state warrants an update.
     *
     * @param state the chat state (never null).
     */
    public final void setChatState(ChatState state) {
        Objects.requireNonNull(state);
        // Only sent out a chat state notification when it is different from the last one that was transmitted...
        final boolean isDifferentState = lastNotificationSent != state;
        // ... unless it's 'composing' - that can be repeated every so many seconds.
        final boolean isStillComposing = state == ChatState.composing && System.currentTimeMillis() - lastNotificationSentTime > 2000;
        final long now = System.currentTimeMillis();
        if (isDifferentState || isStillComposing) {
            try {
                sendChatState(state);
            } catch (SmackException.NotConnectedException | InterruptedException e) {
                Log.warning("Unable to update the chat state to " + state, e);
            }
            lastNotificationSent = state;
            lastNotificationSentTime = now;
        }
    }

    /**
     * Handles the Nickname Completion dialog, when Pressing CTRL + SPACE<br>
     * it searches for matches in the current GroupChatList and also in the
     * Roster
     *
     * @throws ChatRoomNotFoundException when for some reason the GroupChatRoom
     * cannot be found, this should <u>not</u> happen, since we retrieve it from
     * the ActiveWindowTab and thus <u>can be ignored</u>
     */
    private void handleNickNameCompletion() throws ChatRoomNotFoundException {
        // Search for a name that starts with the same word as the last word in the chat input editor.
        final String text = getChatInputEditor().getText();
        if (text == null || text.isEmpty()) {
            return;
        }

        final int lastSpaceCharacterIndex = text.lastIndexOf(' '); // -1 when space does not occur.
        final String needle = text.substring(lastSpaceCharacterIndex + 1);

        final Set<String> matches = new TreeSet<>(String::compareToIgnoreCase);

        if (SparkManager.getChatManager().getChatContainer().getActiveChatRoom() instanceof GroupChatRoom) {
            final GroupChatRoom activeChatRoom = (GroupChatRoom) SparkManager.getChatManager().getChatContainer().getActiveChatRoom();
            for (EntityFullJid participant : activeChatRoom.getParticipants()) {
                String nickname = participant.getResourcepart().toString();
                if (Strings.CI.startsWith(nickname, needle)) {
                    matches.add(nickname);
                }
            }
        } else {
            Roster roster = SparkManager.getRoster();
            for (RosterEntry re : roster.getEntries()) {
                // Use the name if available, otherwise the localpart of the JID.
                final String username;
                if (re.getName() != null) {
                    username = re.getName();
                } else {
                    username = re.getJid().toString().substring(0, re.getJid().toString().indexOf('@'));
//TODO                    username = re.getJid().getLocalpartOrNull().asUnescapedString();
                }
                if (Strings.CI.startsWith(username, needle)) {
                    matches.add(username);
                }
            }
        }

        if (matches.size() == 1) {
            // If we only have 1 match, that match can be used immediately.
            getChatInputEditor().appendText(matches.iterator().next().substring(needle.length()));
        } else {
            // More than one match: create Popupmenu and let the user select one.
            final JPopupMenu popup = new JPopupMenu();
            for (final String match : matches) {
                final JMenuItem menuItem = new JMenuItem(match);
                popup.add(menuItem);
                menuItem.addActionListener(new AbstractAction() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        getChatInputEditor().appendText(match.substring(needle.length()));
                        popup.setVisible(false);
                    }
                });
            }

            popup.show(SparkManager.getChatManager().getChatContainer(),
                    getChatInputEditor().getCaret().getMagicCaretPosition().x,
                    SparkManager.getChatManager().getChatContainer().getHeight() - 20);
        }
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        sendMessage();
        // Clear send field and disable send button
        getChatInputEditor().clear();
        chatAreaButton.enableSendButton(false);
    }

    /**
     * Creates and sends a message object from the text in the Send Field, using
     * the default nickname specified in your Chat Preferences.
     */
    protected abstract void sendMessage();

    /**
     * Creates a Message object from the given text and delegates to the room
     * for sending.
     *
     * @param text the text to send.
     */
    public abstract void sendMessage(String text);

    /**
     * Sends the current message.
     */
    public abstract void sendMessage(MessageBuilder messageBuilder);

    /**
     * Returns the nickname of the current agent as specified in Chat Preferences.
     */
    public Resourcepart getNickname() {
        return pref.getNickname();
    }

    /**
     * The main entry point when receiving any messages. This will either handle
     * a message from a customer or delegate itself as an agent handler.
     *
     * @param message - the message received.
     * @return message modified by {@link ChatManager#filterIncomingMessage}
     */
    public Message insertMessage(Message message) {
        Objects.requireNonNull(message);
        // Fire Message
        MessageBuilder messageBuilder = MessageBuilder.buildMessageFrom(message, message.getStanzaId());
        SparkManager.getChatManager().filterIncomingMessage(this, messageBuilder);
        message = messageBuilder.build();
        SparkManager.getChatManager().fireGlobalMessageReceivedListeners(this, message);

        addToTranscript(message, true);
        fireMessageReceived(message);
        SparkManager.getWorkspace().getTranscriptPlugin().persistChatRoom(this);
        return message;
    }

    /**
     * Add a <code>ChatResponse</chat> to the current discussion chat area.
     *
     * @param message the message to add to the transcript list
     * @param updateDate true if you wish the date label to be updated with the
     * date and time the message was received.
     */
    public void addToTranscript(Message message, boolean updateDate) {
        // Create a message to persist.
        final Map<String, Object> properties = new HashMap<>(1);
        properties.put("date", new Date());
        Message newMessage = StanzaBuilder.buildMessageFrom(message, message.getStanzaId())
            .addExtension(new JivePropertiesExtension(properties))
            .build();
        transcript.add(newMessage);
        // Add current date if this is the current agent
        if (updateDate) {
            Instant lastMessageDate = transcriptWindow.getLastUpdated();
            if (lastMessageDate != null) {
                // Set new label date
                notificationLabel.setIcon(SparkRes.getImageIcon(SparkRes.Icon.SMALL_ABOUT_IMAGE));
                notificationLabel.setText(Res.getString("message.last.message.received", SparkManager.DATE_SECOND_FORMATTER.format(Date.from(lastMessageDate))));
            }
        }
        scrollToBottom();
    }

    /**
     * Scrolls the chat window to the bottom.
     */
    public void scrollToBottom() {
        if (mousePressed) {
            return;
        }
        int lengthOfChat = transcriptWindow.getDocument().getLength();
        transcriptWindow.setCaretPosition(lengthOfChat);
        try {
            final JScrollBar scrollBar = textScroller.getVerticalScrollBar();
            EventQueue.invokeLater(() -> scrollBar.setValue(scrollBar.getMaximum()));
        } catch (Exception e) {
            Log.error(e);
        }
    }

    /**
     * Checks to see if the Send button should be enabled.
     */
    protected void checkForText(DocumentEvent e) {
        final int length = e.getDocument().getLength();
        chatAreaButton.enableSendButton(length > 0);
    }

    /**
     * Requests valid focus to the SendField.
     */
    public void positionCursor() {
        getChatInputEditor().setCaretPosition(getChatInputEditor().getCaretPosition());
        chatAreaButton.getChatInputArea().requestFocusInWindow();
    }

    /**
     * Disable the chat room. This is called when a chat has been either
     * transferred over or the customer has left the chat room.
     */
    public abstract void leaveChatRoom();

    /**
     * Returns the SendField component.
     */
    public ChatInputEditor getChatInputEditor() {
        return chatAreaButton.getChatInputArea();
    }

    /**
     * Returns the chatWindow components.
     */
    public TranscriptWindow getTranscriptWindow() {
        return transcriptWindow;
    }

    /**
     * Checks to see if enter was pressed and validates room.
     */
    private void checkForEnter(KeyEvent e) {
        final KeyStroke keyStroke = KeyStroke.getKeyStroke(e.getKeyCode(), e.getModifiersEx());
        if (!keyStroke.equals(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, KeyEvent.SHIFT_DOWN_MASK))
                && e.getKeyChar() == KeyEvent.VK_ENTER) {
            e.consume();
            sendMessage();
            getChatInputEditor().setText("");
            getChatInputEditor().setCaretPosition(0);

            SparkManager.getWorkspace().getTranscriptPlugin().persistChatRoom(this);
        } else if (keyStroke.equals(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, KeyEvent.SHIFT_DOWN_MASK))) {
            final Document document = getChatInputEditor().getDocument();
            try {
                document.insertString(getChatInputEditor().getCaretPosition(), "\n", null);
                getChatInputEditor().requestFocusInWindow();
                chatAreaButton.enableSendButton(true);
            } catch (BadLocationException badLoc) {
                Log.error("Error when checking for enter:", badLoc);
            }
        }
    }

    /**
     * Add a {@link MessageListener} to the current ChatRoom.
     */
    public void addMessageListener(MessageListener listener) {
        messageListeners.addIfAbsent(listener);
    }

    /**
     * Remove the specified {@link MessageListener } from the current ChatRoom.
     */
    public void removeMessageListener(MessageListener listener) {
        messageListeners.remove(listener);
    }

    /**
     * Notifies all message listeners that the message received.
     */
    private void fireMessageReceived(Message message) {
        for (MessageListener listener : messageListeners) {
            try {
                listener.messageReceived(this, message);
            } catch (Exception e) {
                Log.error("A MessageListener (" + listener + ") threw an exception while processing a 'message received' event for message: " + message, e);
            }
        }
    }

    /**
     * Notifies all <code>MessageListener</code> that a message has been sent.
     */
    protected void fireMessageSent(Message message) {
        for (MessageListener listener : messageListeners) {
            try {
                listener.messageSent(this, message);
            } catch (Exception e) {
                Log.error("A MessageListener (" + listener + ") threw an exception while processing a 'message sent' event for message: " + message, e);
            }
        }
    }

    /**
     * Returns a map of the current Chat Transcript which is a list of all
     * ChatResponses and their order. You should retrieve this map to get any
     * current chat transcript state.
     */
    public List<Message> getTranscripts() {
        return transcript;
    }

    /**
     * Disables the ChatRoom toolbar.
     */
    public void disableToolbar() {
        final int count = editorBarLeft.getComponentCount();
        for (int i = 0; i < count; i++) {
            final Object o = editorBarLeft.getComponent(i);
            if (o instanceof RolloverButton) {
                final RolloverButton rb = (RolloverButton) o;
                rb.setEnabled(false);
            }
        }
    }

    /**
     * Enable the ChatRoom toolbar.
     */
    public void enableToolbar() {
        final int count = editorBarLeft.getComponentCount();
        for (int i = 0; i < count; i++) {
            final Object o = editorBarLeft.getComponent(i);
            if (o instanceof RolloverButton) {
                final RolloverButton rb = (RolloverButton) o;
                rb.setEnabled(true);
            }
        }
    }

    /**
     * Checks to see if the Send Button should be enabled depending on the
     * current update in SendField.
     *
     * @param event the DocumentEvent from the sendField.
     */
    @Override
    public void removeUpdate(DocumentEvent event) {
        checkForText(event);
    }

    /**
     * Checks to see if the Send button should be enabled.
     */
    @Override
    public void changedUpdate(DocumentEvent docEvent) {
        // Do nothing.
    }

    /**
     * Return the SplitPane used in this chat room.
     */
    public JSplitPane getSplitPane() {
        return splitPane;
    }

    /**
     * Returns the ChatPanel that contains the ChatWindow and SendField.
     */
    public JPanel getChatPanel() {
        return chatPanel;
    }

    /**
     * Close the ChatRoom.
     */
    public void closeChatRoom() {
        fireClosingListeners();
        setChatState(ChatState.gone);

        if (typingTimerTask != null) {
            TaskEngine.getInstance().cancelScheduledTask(typingTimerTask);
            typingTimerTask = null;
        }

        getTranscriptWindow().removeContextMenuListener(this);
        getTranscriptWindow().removeMouseListener(transcriptWindowMouseListener);
        getChatInputEditor().removeKeyListener(chatEditorKeyListener);
        this.removeAll();

        textScroller.getViewport().remove(transcriptWindow);

        // Remove Connection Listener
        SparkManager.getConnection().removeConnectionListener(this);
        getTranscriptWindow().setTransferHandler(null);
        getChatInputEditor().setTransferHandler(null);

        transferHandler = null;

        packetIDList.clear();
        messageListeners.clear();
        fileDropListeners.clear();
        getChatInputEditor().close();

        getChatInputEditor().getActionMap().remove("closeTheRoom");
        chatAreaButton.getButton().removeActionListener(this);
        bottomPanel.remove(chatAreaButton);
        _chatFrame.removeWindowToFrontListener(this);
    }

    /**
     * Get the <code>Icon</code> to be used in the tab holding this ChatRoom.
     */
    public abstract Icon getTabIcon();

    /**
     * Get the XMPP address of this room.
     */
    public abstract EntityBareJid getBareJid();

    public abstract EntityJid getJid();

    /**
     * Get the title to use in the tab holding this ChatRoom.
     */
    public abstract String getTabTitle();

    /**
     * Returns the title of this room to use. The title will be used in the
     * title bar of the ChatRoom.
     */
    public abstract String getRoomTitle();

    /**
     * Returns the <code>Message.Type</code> specific to this chat room.
     * GroupChat is Message.Type.groupchat Normal Chat is Message.TYPE.NORMAL
     */
    public abstract Message.Type getChatType();

    /**
     * Returns whether this ChatRoom is active. Note: carrying a
     * conversation rather than being disabled, as it would be transcript mode.
     */
    public abstract boolean isActive();

    /**
     * Returns the notification label. The notification label notifies the user
     * of chat room activity, such as the date of the last message and typing
     * notifications.
     */
    public JLabel getNotificationLabel() {
        return notificationLabel;
    }

    /**
     * Adds a packetID to the packedIDList. The packetIDLlist keeps track of all
     * messages coming into the chatroom.
     */
    public void addPacketID(String packetID) {
        packetIDList.add(packetID);
    }

    /**
     * Checks if the packetID has already been used.
     *
     * @param packetID the packetID to check for.
     */
    public boolean packetIDExists(String packetID) {
        return packetIDList.contains(packetID);
    }

    /**
     * Returns this instance of the chatroom.
     */
    public ChatRoom getChatRoom() {
        return this;
    }

    /**
     * Returns the toolbar used on top of the chat room.
     */
    public ChatToolBar getToolBar() {
        return toolbar;
    }

    protected void addToolbar() {
        add(toolbar, new GridBagConstraints(0, 0, 1, 1, 1, 0, EAST, HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));
    }

    /**
     * The current SendField has been updated somehow.
     */
    @Override
    public void insertUpdate(DocumentEvent e) {
        // Meant to be overridden
        checkForText(e);
        setChatState(ChatState.composing);
    }

    /**
     * Override to save the transcript in the preferred room style.
     */
    public void saveTranscript() {
        getTranscriptWindow().saveTranscript(getTabTitle() + ".html", getTranscripts(), null);
    }

    /**
     * Used for the top toolbar.
     */
    public static class ChatToolBar extends JPanel {

        private final JPanel buttonPanel;

        public ChatToolBar() {
            buttonPanel = new JPanel();
            buttonPanel.setLayout(new FlowLayout(FlowLayout.LEFT, 2, 0));
            // Set Layout
            setLayout(new GridBagLayout());
            buttonPanel.setOpaque(false);
            add(buttonPanel, new GridBagConstraints(1, 1, 1, 1, 0.0, 0.0, GridBagConstraints.NORTHEAST, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
            setOpaque(false);
        }

        /**
         * Adds a new ChatRoomButton the CommandBar.
         */
        public void addChatRoomButton(ChatRoomButton button) {
            buttonPanel.add(button);
            buttonPanel.setBorder(BorderFactory.createEmptyBorder(5, 0, 5, 0));
            // Make all JButtons the same size
            Component[] comps = buttonPanel.getComponents();
            final Component[] buttons = new Component[comps.length];
            for (int i = 0; i < comps.length; i++) {
                Component component = comps[i];
                if (component instanceof JButton) {
                    buttons[i] = component;
                }
            }
            GraphicUtils.makeSameSize(buttons);
        }

        /**
         * Removes the ChatRoomButton from the CommandBar.
         */
        public void removeChatRoomButton(ChatRoomButton button) {
            buttonPanel.remove(button);
        }
    }

    /**
     * Returns the number of unread messages in this ChatRoom.
     */
    public int getUnreadMessageCount() {
        return unreadMessageCount;
    }

    /**
     * Increases the number of unread messages by 1.
     */
    public void increaseUnreadMessageCount() {
        unreadMessageCount++;
    }

    /**
     * Resets the number of unread messages.
     */
    public void clearUnreadMessageCount() {
        unreadMessageCount = 0;
    }

    /**
     * Returns the bottom panel used in the ChatRoom.
     */
    public JPanel getBottomPanel() {
        return bottomPanel;
    }

    /**
     * Returns the Container which holds the ChatWindow.
     */
    public JPanel getChatWindowPanel() {
        return chatWindowPanel;
    }

    /**
     * Adds a new <code>FileDropListener</code> to allow for Drag and Drop
     * notifications of objects onto the ChatWindow.
     */
    public void addFileDropListener(FileDropListener listener) {
        fileDropListeners.addIfAbsent(listener);
    }

    /**
     * Remove the <code>FileDropListener</code> from ChatRoom.
     */
    public void removeFileDropListener(FileDropListener listener) {
        fileDropListeners.remove(listener);
    }

    /**
     * Notify all users that a collection of files has been dropped onto the
     * ChatRoom.
     */
    public void fireFileDropListeners(Collection<File> files) {
        for (FileDropListener listener : fileDropListeners) {
            try {
                listener.filesDropped(files, this);
            } catch (Exception e) {
                Log.error("A FileDropListener (" + listener + ") threw an exception while processing a 'files dropped' event.", e);
            }
        }
    }

    /**
     * Returns the panel which contains the toolbar items, such as spell
     * checker.
     */
    public JPanel getEditorBar() {
        return editorBarLeft;
    }

    /**
     * Returns the panel next to the editor bar<br>
     * for use with system buttons, like room controlling or toggle stay-on-top
     */
    public JPanel getRoomControllerBar() {
        return editorBarRight;
    }

    /**
     * Adds a <code>ChatRoomClosingListener</code> to this ChatRoom. A
     * ChatRoomClosingListener is notified whenever this room is closing.
     */
    public void addClosingListener(ChatRoomClosingListener listener) {
        closingListeners.addIfAbsent(listener);
    }

    /**
     * Removes a <code>ChatRoomClosingListener</code> from this ChatRoom.
     */
    public void removeClosingListener(ChatRoomClosingListener listener) {
        closingListeners.remove(listener);
    }

    /**
     * Notifies all <code>ChatRoomClosingListener</code> that this ChatRoom is
     * closing.
     */
    private void fireClosingListeners() {
        for (final ChatRoomClosingListener listener : closingListeners)
        {
            try {
                listener.closing();
            } catch (Exception e) {
                Log.error("A ChatRoomClosingListener (" + listener + ") threw an exception while processing a 'closing' event.", e);
            }
        }
        closingListeners.clear();
    }

    /**
     * Returns the ScrollPane that contains the TranscriptWindow.
     */
    public JScrollPane getScrollPaneForTranscriptWindow() {
        return textScroller;
    }

    /**
     * Enable or disable the "Send" button and message editor.
     */
    public void setChatInputEnabled(boolean enabled) {
        chatAreaButton.getChatInputArea().setEnabled(enabled);
        chatAreaButton.getButton().setEnabled(enabled);
    }

    /**
     * Returns the VerticalSplitPane used in this ChatRoom.
     */
    public JSplitPane getVerticalSplitPane() {
        return verticalSplit;
    }

    @Override
    public void focusGained(FocusEvent focusEvent) {
        validate();
        invalidate();
        repaint();
        if (focusEvent.getComponent().equals(getChatInputEditor())) {
            setChatState(ChatState.active);
        }
    }

    @Override
    public void poppingUp(Object component, JPopupMenu popup) {
        Action saveAction = new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent actionEvent) {
                saveTranscript();
            }
        };
        saveAction.putValue(Action.NAME, Res.getString("action.save"));
        saveAction.putValue(Action.SMALL_ICON, SparkRes.getImageIcon(SparkRes.Icon.SAVE_AS_16x16));
        popup.add(saveAction);
    }

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

    @Override
    public void focusLost(FocusEvent focusEvent) {
        if (focusEvent.getComponent().equals(getChatInputEditor())) {
            setChatState(ChatState.inactive);
        }
    }

    /**
     * The last time this chat room sent or received a message.
     */
    public long getLastActivity() {
        return lastActivity;
    }

    @Override
    public void updateStatus(boolean active) {
        _alwaysOnTopItem.setSelected(active);
    }

    @Override
    public void registeredToFrame(ChatFrame chatframe) {
        this._chatFrame = chatframe;
        _chatFrame.addWindowToFronListener(this);
    }

    protected JPanel getEditorWrapperBar() {
        return editorWrapperBar;
    }

    protected JPanel getEditorBarRight() {
        return editorBarRight;
    }

    protected JPanel getEditorBarLeft() {
        return editorBarLeft;
    }

    public void addChatRoomComponent(JComponent component) {
        editorBarLeft.add(component);
    }

    public void addChatRoomButton(ChatRoomButton button) {
        addChatRoomButton(button, false);
    }

    public void addChatRoomButton(ChatRoomButton button, boolean forceRepaint) {
        toolbar.addChatRoomButton(button);
        if (forceRepaint) {
            toolbar.invalidate();
            toolbar.repaint();
        }
    }

    public void showToolbar() {
        toolbar.setVisible(true);
    }

    public void hideToolbar() {
        toolbar.setVisible(false);
    }

    public void addEditorComponent(JComponent component) {
        editorBarLeft.add(component);
    }

    public void removeEditorComponent(JComponent component) {
        editorBarLeft.remove(component);
    }

    public void addControllerButton(RolloverButton button) {
        editorBarRight.add(button, 0);
    }
}
