/*
 * Jitsi, the OpenSource Java VoIP and Instant Messaging client.
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package net.java.sip.communicator.impl.gui.main.chat;

import java.awt.*;
import java.awt.datatransfer.*;
import java.awt.event.*;
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.Map.Entry;
import java.util.Map;
import java.util.regex.*;

import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;
import javax.swing.text.html.*;
import javax.swing.text.html.HTML.*;

import net.java.sip.communicator.impl.gui.*;
import net.java.sip.communicator.impl.gui.customcontrols.*;
import net.java.sip.communicator.impl.gui.main.chat.history.*;
import net.java.sip.communicator.impl.gui.main.chat.menus.*;
import net.java.sip.communicator.impl.gui.utils.*;
import net.java.sip.communicator.service.gui.*;
import net.java.sip.communicator.service.replacement.*;
import net.java.sip.communicator.service.replacement.smilies.*;
import net.java.sip.communicator.util.*;
import net.java.sip.communicator.util.skin.*;
import net.java.sip.communicator.plugin.desktoputil.*;
import net.java.sip.communicator.plugin.desktoputil.SwingWorker;

/**
 * The <tt>ChatConversationPanel</tt> is the panel, where all sent and received
 * messages appear. All data is stored in an HTML document. An external CSS file
 * is applied to the document to provide the look&feel. All smileys and link
 * strings are processed and finally replaced by corresponding images and HTML
 * links.
 *
 * @author Yana Stamcheva
 * @author Lubomir Marinov
 * @author Adam Netocny
 */
public class ChatConversationPanel
    extends SCScrollPane
    implements  HyperlinkListener,
                MouseListener,
                ClipboardOwner,
                Skinnable
{
    /**
     * The <tt>Logger</tt> used by the <tt>ChatConversationPanel</tt> class and
     * its instances for logging output.
     */
    private static final Logger logger
        = Logger.getLogger(ChatConversationPanel.class);

    /**
     * The closing tag of the <code>PLAINTEXT</code> HTML element.
     */
    private static final String END_PLAINTEXT_TAG = "</PLAINTEXT>";

    /**
     * The opening tag of the <code>PLAINTEXT</code> HTML element.
     */
    private static final String START_PLAINTEXT_TAG = "<PLAINTEXT>";

    /**
     * The regular expression (in the form of compiled <tt>Pattern</tt>) which
     * matches URLs for the purposed of turning them into links.
     */
    private static final Pattern URL_PATTERN
        = Pattern.compile(
            "("
            + "(\\bwww\\.[^\\s<>\"]+\\.[^\\s<>\"]+/*[?#]*(\\w+[&=;?]\\w+)*\\b)" // wwwURL
            + "|"
            + "(\\bjitsi\\:[^\\s<>\"]+\\.[^\\s<>\"]*\\b)" // internalURL
            + "|"
            + "(\\b\\w+://[^\\s<>\"]+/*[?#]*(\\w+[&=;?]\\w+)*\\b)" // protocolURL
            + ")");

    /**
     * List for observing text messages.
     */
    private Set<ChatLinkClickedListener> chatLinkClickedListeners = 
        new HashSet<ChatLinkClickedListener>();

    /**
     * The component rendering chat conversation panel text.
     */
    private final JTextPane chatTextPane = new MyTextPane();

    /**
     * The editor kit used by the text component.
     */
    private final HTMLEditorKit editorKit;

    /**
     * The document used by the text component.
     */
    private HTMLDocument document;

    /**
     * The parent container.
     */
    private final ChatConversationContainer chatContainer;

    /**
     * The menu shown on right button mouse click.
     */
    private final ChatRightButtonMenu rightButtonMenu;

    /**
     * The currently shown href.
     */
    private String currentHref;

    /**
     * The copy link item, contained in the right mouse click menu.
     */
    private final JMenuItem copyLinkItem;

    /**
     * The open link item, contained in the right mouse click menu.
     */
    private final JMenuItem openLinkItem;

    /**
     * The right mouse click menu separator.
     */
    private final JSeparator copyLinkSeparator = new JSeparator();

    /**
     * The timestamp of the last incoming message.
     */
    private long lastIncomingMsgTimestamp;

    /**
     * Indicates if this component is rendering a history conversation.
     */
    private final boolean isHistory;

    /**
     * The html text content type.
     */
    public static final String HTML_CONTENT_TYPE = "text/html";

    /**
     * The plain text content type.
     */
    public static final String TEXT_CONTENT_TYPE = "text/plain";

    /**
     * The indicator which determines whether an automatic scroll to the bottom
     * of {@link #chatTextPane} is to be performed.
     */
    private boolean scrollToBottomIsPending = false;

    private final static String INCOMING_MESSAGE_IMAGE_PATH
        = GuiActivator.getResources().getImageURL(
            "service.gui.lookandfeel.INCOMING_MESSAGE_BACKGROUND").toString();

    /**
     * The implementation of the routine which scrolls {@link #chatTextPane} to its
     * bottom.
     */
    private final Runnable scrollToBottomRunnable = new Runnable()
    {
        /*
         * Implements Runnable#run().
         */
        public void run()
        {
            JScrollBar verticalScrollBar = getVerticalScrollBar();

            if (verticalScrollBar != null)
            {
                // We need to call both methods in order to be sure to scroll
                // to the bottom of the text even when the user has selected
                // something (changed the caret) or when a new tab has been
                // added or the window has been resized.
                verticalScrollBar.setValue(verticalScrollBar.getMaximum());
                chatTextPane.setCaretPosition(document.getLength());
            }
        }
    };

    /**
     * Creates an instance of <tt>ChatConversationPanel</tt>.
     *
     * @param chatContainer The parent <tt>ChatConversationContainer</tt>.
     */
    public ChatConversationPanel(ChatConversationContainer chatContainer)
    {
        editorKit = new SIPCommHTMLEditorKit(this);

        this.chatContainer = chatContainer;

        isHistory = (chatContainer instanceof HistoryWindow);

        this.rightButtonMenu = new ChatRightButtonMenu(this);

        this.document = (HTMLDocument) editorKit.createDefaultDocument();

        this.chatTextPane.setEditorKitForContentType("text/html", editorKit);
        this.chatTextPane.setEditorKit(editorKit);
        this.chatTextPane.setEditable(false);
        this.chatTextPane.setDocument(document);
        this.chatTextPane.setDragEnabled(true);

        chatTextPane.putClientProperty(
            JEditorPane.HONOR_DISPLAY_PROPERTIES, Boolean.TRUE);
        Constants.loadSimpleStyle(
            document.getStyleSheet(), chatTextPane.getFont());

        this.chatTextPane.addHyperlinkListener(this);
        this.chatTextPane.addMouseListener(this);
        this.chatTextPane.setCursor(
            Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR));

        this.setWheelScrollingEnabled(true);

        this.setViewportView(chatTextPane);

        this.setBorder(null);

        this.setHorizontalScrollBarPolicy(
            JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);

        ToolTipManager.sharedInstance().registerComponent(chatTextPane);

        String copyLinkString
            = GuiActivator.getResources().getI18NString("service.gui.COPY_LINK");

        copyLinkItem
            = new JMenuItem(copyLinkString,
                new ImageIcon(ImageLoader.getImage(ImageLoader.COPY_ICON)));

        copyLinkItem.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                StringSelection stringSelection = new StringSelection(
                    currentHref);
                Clipboard clipboard = Toolkit.getDefaultToolkit()
                    .getSystemClipboard();
                clipboard.setContents(stringSelection,
                    ChatConversationPanel.this);
            }
        });

        String openLinkString
            = GuiActivator.getResources().getI18NString(
                "service.gui.OPEN_IN_BROWSER");

        openLinkItem =
            new JMenuItem(
                openLinkString,
                new ImageIcon(ImageLoader.getImage(ImageLoader.BROWSER_ICON)));

        openLinkItem.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                GuiActivator.getBrowserLauncher().openURL(currentHref);

                // after opening the link remove the currentHref to avoid
                // clicking on the window to gain focus to open the link again
                ChatConversationPanel.this.currentHref = "";
            }
        });

        openLinkItem.setMnemonic(
            GuiActivator.getResources().getI18nMnemonic(
                "service.gui.OPEN_IN_BROWSER"));

        copyLinkItem.setMnemonic(
            GuiActivator.getResources().getI18nMnemonic(
                "service.gui.COPY_LINK"));

        /*
         * When we append a new message (regardless of whether it is a string or
         * an UI component), we want to make it visible in the viewport of this
         * JScrollPane so that the user can see it.
         */
        ComponentListener componentListener = new ComponentAdapter()
        {
            @Override
            public void componentResized(ComponentEvent e)
            {
                synchronized (scrollToBottomRunnable)
                {
                    if (!scrollToBottomIsPending)
                        return;
                    scrollToBottomIsPending = false;

                    /*
                     * Yana Stamcheva, pointed out that Java 5 (on Linux only?)
                     * needs invokeLater for JScrollBar.
                     */
                    SwingUtilities.invokeLater(scrollToBottomRunnable);
                }
            }
        };

        chatTextPane.addComponentListener(componentListener);
        getViewport().addComponentListener(componentListener);
    }

    /**
     * Overrides Component#setBounds(int, int, int, int) in order to determine
     * whether an automatic scroll of #chatTextPane to its bottom will be
     * necessary at a later time in order to keep its vertical scroll bar to its
     * bottom after the realization of the resize if it is at its bottom before
     * the resize.
     */
    @Override
    public void setBounds(int x, int y, int width, int height)
    {
        synchronized (scrollToBottomRunnable)
        {
            JScrollBar verticalScrollBar = getVerticalScrollBar();

            if (verticalScrollBar != null)
            {
                BoundedRangeModel verticalScrollBarModel
                    = verticalScrollBar.getModel();

                if ((verticalScrollBarModel.getValue()
                                + verticalScrollBarModel.getExtent()
                            >= verticalScrollBarModel.getMaximum())
                        || !verticalScrollBar.isVisible())
                    scrollToBottomIsPending = true;
            }
        }

        super.setBounds(x, y, width, height);
    }

    /**
     * Initializes the editor by adding a header containing the date.
     * TODO: remove if not used anymore
     */
//    private void initEditor()
//    {
//        Element root = this.document.getDefaultRootElement();
//
//        Date date = new Date(System.currentTimeMillis());
//
//        String chatHeader = "<h1>" + GuiUtils.formatDate(date) + " " + "</h1>";
//
//        try
//        {
//            this.document.insertAfterStart(root, chatHeader);
//        }
//        catch (BadLocationException e)
//        {
//            logger.error("Insert in the HTMLDocument failed.", e);
//        }
//        catch (IOException e)
//        {
//            logger.error("Insert in the HTMLDocument failed.", e);
//        }
//    }

    /**
     * Retrieves the contents of the sent message with the given ID.
     * 
     * @param messageUID The ID of the message to retrieve.
     * @return The contents of the message, or null if the message is not found.
     */
    public String getMessageContents(String messageUID)
    {
        Element root = document.getDefaultRootElement();
        Element e = document.getElement(root, Attribute.ID, messageUID);
        if (e == null)
        {
            logger.warn("Could not find message with ID" + messageUID);
            return null;
        }

        int elemLen = e.getEndOffset() - e.getStartOffset();
        String res = null;
        try
        {
            res = document.getText(e.getStartOffset(), elemLen);
        }
        catch (BadLocationException exc)
        {
            logger.warn("Could not get message contents for message "
                    + "with ID" + messageUID, exc);
        }
        return res;
    }

    /**
     * Creates a tag that shows the last edit time of a message, in the format
     *  (Edited at ...).
     * If <tt>date < 0</tt>, returns an empty tag that serves as a placeholder
     * for future corrections of this message.
     * 
     * @param messageUID The ID of the edited message.
     * @param date The date when the message was last edited, or -1 to generate
     * an empty tag.
     * @return The string representation of the tag.
     */
    private String generateEditedAtTag(String messageUID, long date)
    {
        StringBuilder res = new StringBuilder();
        // Use a <cite /> tag here as most of the other inline tags (e.g. h1-7,
        // b, i) cause different problems when used in setOuterHTML.
        res.append("<cite id='");
        res.append(messageUID);
        res.append("-editedAt'> ");
        if (date > 0)
        {
            res.append("&nbsp;");
            String contents = GuiActivator.getResources().getI18NString(
                    "service.gui.EDITED_AT",
                    new String[] { GuiUtils.formatTime(date) }
            );
            res.append(contents);
        }
        res.append("</cite>");
        return res.toString();
    }

    /**
     * Processes the message given by the parameters.
     *
     * @param chatMessage the message
     * @param keyword a substring of <tt>chatMessage</tt> to be highlighted upon
     * display of <tt>chatMessage</tt> in the UI
     * @return the processed message
     */
    public String processMessage(ChatMessage chatMessage, String keyword)
    {
        String contactName = chatMessage.getContactName();
        String contactDisplayName = chatMessage.getContactDisplayName();
        if (contactDisplayName == null
                || contactDisplayName.trim().length() <= 0)
            contactDisplayName = contactName;

        String contentType = chatMessage.getContentType();
        long date = chatMessage.getDate();
        String messageType = chatMessage.getMessageType();
        String messageTitle = chatMessage.getMessageTitle();
        String message = chatMessage.getMessage();
        String messageUID = chatMessage.getMessageUID();

        String msgID = "message";
        String msgHeaderID = "messageHeader";
        String chatString = "";
        String endHeaderTag = "";
        String dateString = getDateString(date);
        String idAttr = messageUID == null ? "" : " id='" + messageUID + "'";
        String dateAttr = " date='" + date + "'";
        String editedAtTag = generateEditedAtTag(messageUID, -1);

        String startHistoryDivTag
            = "<DIV identifier=\"" + msgID + "\" style=\"color:#707070;\">";
        String startSystemDivTag
            = "<DIV identifier=\"systemMessage\" style=\"color:#627EB7;\">";
        String endDivTag = "</DIV>";

        String startPlainTextTag;
        String endPlainTextTag;

        if (HTML_CONTENT_TYPE.equals(contentType))
        {
            startPlainTextTag = "";
            endPlainTextTag = "";
        }
        else
        {
            startPlainTextTag = START_PLAINTEXT_TAG;
            endPlainTextTag = END_PLAINTEXT_TAG;
        }

        if (messageType.equals(Chat.INCOMING_MESSAGE))
        {
            this.lastIncomingMsgTimestamp = System.currentTimeMillis();

            StringBuffer headerBuffer = new StringBuffer();

            headerBuffer.append("<h2 identifier=\"" + msgHeaderID + "\"");
            headerBuffer.append(dateAttr + ">");
            headerBuffer.append("<a style=\"color:#488fe7;");
            headerBuffer.append("font-weight:bold;");
            headerBuffer.append("text-decoration:none;\" ");
            headerBuffer.append("href=\"" + contactName + "\">");
            headerBuffer.append(dateString + contactDisplayName + " at "
                + GuiUtils.formatTime(date) + editedAtTag);
            headerBuffer.append("</a></h2>");

            chatString = createIncomingMessageTag(msgID + "\"" + idAttr,
                headerBuffer.toString(),
                startPlainTextTag
                + formatMessage(message, contentType, keyword)
                + endPlainTextTag);
System.out.println("CHAT STRING OSHTE TUUUUUK=======" + chatString);
//            chatString      = "<h2 identifier=\"" + msgHeaderID + "\""
//                                    + " date=\"" + date + "\">"
//                                    + "<a style=\"color:#488fe7;"
//                                    + "font-weight:bold;"
//                                    + "text-decoration:none;\" "
//                                    + "href=\"" + contactName + "\">";
//
//            endHeaderTag = "</a></h2>";

//            String startDivTag = "<DIV identifier=\"" + msgID
//                + "\" style=\""+ createIncomingMessageStyle()+"\">";
//
//            chatString
//                += dateString + contactDisplayName + " at "
//                    + GuiUtils.formatTime(date)
//                    + endHeaderTag + startDivTag + startPlainTextTag
//                    + formatMessage(message, contentType, keyword)
//                    + endPlainTextTag + endDivTag;

        }
        else if (messageType.equals(Chat.SMS_MESSAGE))
        {
            chatString      = "<h2 identifier=\""
                            + msgHeaderID
                            + "\" date=\""
                            + date + "\">";

            endHeaderTag = "</h2>";

            String startDivTag = "<DIV identifier=\"" + msgID
                + "\" style=\""+ createSmsMessageStyle()+"\">";

            chatString
                += "SMS: " + dateString + contactName + " at "
                    + GuiUtils.formatTime(date) + endHeaderTag + startDivTag
                    + startPlainTextTag
                    + formatMessage(message, contentType, keyword)
                    + endPlainTextTag + endDivTag;
        }
        else if (messageType.equals(Chat.OUTGOING_MESSAGE))
        {
            chatString      = "<h3 identifier=\"" + msgHeaderID + "\""
                                    + dateAttr + ">"
                                    + "<a style=\"color:#6a6868;"
                                    + "font-weight:bold;"
                                    + "text-decoration:none;\" "
                                    + "href=\"" + contactName + "\">";

            endHeaderTag = "</a></h3>";

            String startDivTag = "<DIV identifier=\"" + msgID
                + "\" style=\""+ createOutgoingMessageStyle()+"\">";

            chatString
                += dateString + contactDisplayName + " at "
                    + GuiUtils.formatTime(date) + editedAtTag + endHeaderTag
                    + startDivTag + startPlainTextTag
                    + formatMessage(message, contentType, keyword)
                    + endPlainTextTag + endDivTag;
        }
        else if (messageType.equals(Chat.STATUS_MESSAGE))
        {
            chatString =    "<h4 identifier=\"statusMessage\" date=\""
                            + date + "\">";
            endHeaderTag = "</h4>";

            chatString
                += GuiUtils.formatTime(date) + " " + contactName + " " + message
                    + endHeaderTag;
        }
        else if (messageType.equals(Chat.ACTION_MESSAGE))
        {
            chatString =    "<p identifier=\"actionMessage\" date=\""
                            + date + "\">";
            endHeaderTag = "</p>";

            chatString += "* " + GuiUtils.formatTime(date)
                + " " + contactName + " "
                + message
                + endHeaderTag;
        }
        else if (messageType.equals(Chat.SYSTEM_MESSAGE))
        {
            chatString
                += startSystemDivTag + startPlainTextTag
                    + formatMessage(message, contentType, keyword)
                    + endPlainTextTag + endDivTag;
        }
        else if (messageType.equals(Chat.ERROR_MESSAGE))
        {
            chatString      = "<h6 identifier=\""
                            + msgHeaderID
                            + "\" date=\""
                            + date + "\">";

            endHeaderTag = "</h6>";

            String errorIcon = "<IMG SRC='"
                + ImageLoader.getImageUri(ImageLoader.EXCLAMATION_MARK)
                + "' </IMG>";

            chatString += errorIcon
                + messageTitle
                + endHeaderTag + "<h5>" + message + "</h5>";
        }
        else if (messageType.equals(Chat.HISTORY_INCOMING_MESSAGE))
        {
            chatString      = "<h2 identifier=\"" + msgHeaderID + "\""
                                + dateAttr + ">"
                                + "<a style=\"color:#488fe7;"
                                + "font-weight:bold;"
                                + "text-decoration:none;\" "
                                + "href=\"" + contactName + "\">";

            endHeaderTag = "</a></h2>";

            chatString
                += dateString + contactDisplayName
                    + " at " + GuiUtils.formatTime(date) + endHeaderTag
                    + editedAtTag + startHistoryDivTag + startPlainTextTag
                    + formatMessage(message, contentType, keyword)
                    + endPlainTextTag + endDivTag;
        }
        else if (messageType.equals(Chat.HISTORY_OUTGOING_MESSAGE))
        {
            chatString      = "<h3 identifier=\"" + msgHeaderID + "\""
                                + dateAttr + ">"
                                + "<a style=\"color:#6a6868;"
                                + "font-weight:bold;"
                                + "text-decoration:none;\" "
                                + "href=\"" + contactName + "\">";

            endHeaderTag = "</h3>";

            chatString
                += dateString
                    + contactDisplayName + " at " + GuiUtils.formatTime(date)
                    + editedAtTag + endHeaderTag
                    + startHistoryDivTag + startPlainTextTag
                    + formatMessage(message, contentType, keyword)
                    + endPlainTextTag + endDivTag;
        }

        return chatString;
    }

    /**
     * Processes the message given by the parameters.
     *
     * @param chatMessage the message.
     * @return the formatted message
     */
    public String processMessage(ChatMessage chatMessage)
    {
        return processMessage(chatMessage, null);
    }

    /**
     * Replaces the contents of the message with ID of the corrected message
     * specified in chatMessage, with this message.
     * 
     * @param chatMessage A <tt>ChatMessage</tt> that contains all the required
     * information to correct the old message.
     */
    public void correctMessage(ChatMessage chatMessage)
    {
        String correctedUID = chatMessage.getCorrectedMessageUID();
        Element root = document.getDefaultRootElement();
        Element e = document.getElement(root, Attribute.ID, correctedUID);
        if (e == null)
        {
            logger.warn("Could not find message with ID " + correctedUID);
            return;
        }
        int len = e.getEndOffset() - e.getStartOffset();
        
        StringBuilder newContents = new StringBuilder();
        String bgColor = GuiActivator.getResources().getColorString(
                "service.gui.CHAT_EDIT_MESSAGE_BACKGROUND");
        newContents.append("<div identifier='message' id='");
        newContents.append(chatMessage.getMessageUID());
        newContents.append("' bgcolor='");
        newContents.append(bgColor);
        newContents.append("'>");
        if (chatMessage.getContentType().equals(TEXT_CONTENT_TYPE))
        {
            newContents.append(START_PLAINTEXT_TAG);
            newContents.append(chatMessage.getMessage());
            newContents.append(END_PLAINTEXT_TAG);
        }
        else
        {
            newContents.append(chatMessage.getMessage());
        }
        newContents.append("</div>");
        
        Element header = document.getElement(root, Attribute.ID,
                correctedUID + "-editedAt");
        
        try
        {
            if (header != null)
            {
                String newHeaderContents = generateEditedAtTag(
                        chatMessage.getMessageUID(), chatMessage.getDate());
                document.setOuterHTML(header, newHeaderContents);
            }
            document.setOuterHTML(e, newContents.toString());
        }
        catch (BadLocationException ex)
        {
            logger.error("Could not replace chat message", ex);
        }
        catch (IOException ex)
        {
            logger.error("Could not replace chat message", ex);
        }
    }

    /**
     * Appends the given string at the end of the contained in this panel
     * document.
     *
     * @param chatString the string to append
     */
    public void appendMessageToEnd(String chatString, String contentType)
    {
        synchronized (scrollToBottomRunnable)
        {
            Element root = document.getDefaultRootElement();

//          Need to call explicitly scrollToBottom, because for some
//          reason the componentResized event isn't fired every time we
//          add text.
//          Replaced by the code on line: 573.
//
//            scrollToBottomIsPending = true;
System.out.println("CHAT STRING+=========" + chatString);
            try
            {
                document
                    .insertAfterEnd(
                        root.getElement(root.getElementCount() - 1),
                        chatString);

                // Need to call explicitly scrollToBottom, because for some
                // reason the componentResized event isn't fired every time we
                // add text.
                SwingUtilities.invokeLater(scrollToBottomRunnable);
            }
            catch (BadLocationException e)
            {
                logger.error("Insert in the HTMLDocument failed.", e);
            }
            catch (IOException e)
            {
                logger.error("Insert in the HTMLDocument failed.", e);
            }
            if (!isHistory)
                ensureDocumentSize();

            // Process replacements.
            final Element elem;
            /*
             * Check to make sure element isn't the first element in the HTML
             * document.
             */
            if (!(root.getElementCount() < 2))
            {
                elem = root.getElement(root.getElementCount() - 2);
            }
            else
                elem = root.getElement(1);

            /*
             * Replacements will be processed only if it is enabled in the
             * property
             */
            if (GuiActivator.getConfigurationService().getBoolean(
                ReplacementProperty.REPLACEMENT_ENABLE, true)
                || GuiActivator.getConfigurationService().getBoolean(
                    ReplacementProperty.getPropertyName("SMILEY"), true))
            {
                processReplacement(elem, chatString, contentType);
            }
        }
    }

    /**
    * Formats the given message. Processes the messages and replaces links to
    * video/image sources with their previews or any other substitution. Spawns
    * a separate thread for replacement.
    * 
    * @param elem the element in the HTML Document.
    * @param chatString the message.
    */
    private void processReplacement(final Element elem,
                                    final String chatString,
                                    final String contentType)
    {
       final String chatFinal = chatString;

       SwingWorker worker = new SwingWorker()
       {
           public Object construct() throws Exception
           {
               String temp = "", msgStore = chatFinal;

               boolean isEnabled
                   = GuiActivator.getConfigurationService().getBoolean(
                       ReplacementProperty.REPLACEMENT_ENABLE, true);

               Map<String, ReplacementService> listSources
                   = GuiActivator.getReplacementSources();

               Iterator<Entry<String, ReplacementService>> entrySetIter
                   = listSources.entrySet().iterator();

               for (int i = 0; i < listSources.size(); i++)
               {
                   Map.Entry<String, ReplacementService> entry
                       = entrySetIter.next();

                   ReplacementService source = entry.getValue();

                   boolean isSmiley
                       = source instanceof SmiliesReplacementService;

                   if (!(GuiActivator.getConfigurationService().getBoolean(
                       ReplacementProperty.getPropertyName(source
                       .getSourceName()), true) && (isEnabled || isSmiley)))
                       continue;

                   String sourcePattern = source.getPattern();
                   Pattern p = Pattern.compile(sourcePattern,
                                   Pattern.CASE_INSENSITIVE | Pattern.DOTALL);

                   Matcher m = p.matcher(msgStore);

                   String startPlainTextTag = "";
                   String endPlainTextTag = "";

                   if (!HTML_CONTENT_TYPE.equals(contentType))
                   {
                       startPlainTextTag = START_PLAINTEXT_TAG;
                       endPlainTextTag = END_PLAINTEXT_TAG;
                   }

                   int count = 0, startPos = 0;
                   StringBuffer msgBuff = new StringBuffer();

                   while (m.find())
                   {
                       count++;
                       msgBuff.append(msgStore.substring(startPos, m.start()));
                       startPos = m.end();

                       temp = source.getReplacement(m.group());

                       if(!temp.equals(m.group(0)) || source.getSourceName()
                               .equals("DIRECTIMAGE"))
                       {
                           if(isSmiley)
                           {
                               msgBuff.append(endPlainTextTag);
                               msgBuff.append("<IMG SRC=\"");
                           }
                           else
                           {
                               msgBuff.append(
                                   "<IMG HEIGHT=\"90\" WIDTH=\"120\" SRC=\"");
                           }

                           msgBuff.append(temp);
                           msgBuff.append("\" BORDER=\"0\" ALT=\"");
                           msgBuff.append(m.group(0));
                           msgBuff.append("\"></IMG>");

                           if(isSmiley)
                               msgBuff.append(startPlainTextTag);
                       }
                       else
                       {
                           msgBuff.append(
                               msgStore.substring(m.start(), m.end()));
                       }
                   }

                   msgBuff.append(msgStore.substring(startPos));

                   /*
                    * replace the msgStore variable with the current replaced
                    * message before next iteration
                    */
                   if (!msgBuff.toString().equals(msgStore))
                   {
                       msgStore = msgBuff.toString();
                   }
               }

               if (!msgStore.equals(chatFinal))
               {
                   synchronized (scrollToBottomRunnable)
                   {
                       scrollToBottomIsPending = true;
                       document.setOuterHTML(elem, msgStore.toString()
                           .substring(msgStore.indexOf("<DIV")));
                   }
               }
               return "";
           }
       };
       worker.start();
    }

    /**
     * Ensures that the document won't become too big. When the document reaches
     * a certain size the first message in the page is removed.
     */
    private void ensureDocumentSize()
    {
        if (document.getLength() > Chat.CHAT_BUFFER_SIZE)
        {
            int msgElementCount = 0;

            Element firstMsgElement = null;

            int firstMsgIndex = 0;

            Element rootElement = this.document.getDefaultRootElement();
            // Count how many messages we have in the document.
            for (int i = 0; i < rootElement.getElementCount(); i++)
            {
                String idAttr = (String) rootElement.getElement(i)
                    .getAttributes().getAttribute("identifier");

                if(idAttr != null
                    && (idAttr.equals("message")
                        || idAttr.equals("statusMessage")
                        || idAttr.equals("systemMessage")))
                {
                    if(firstMsgElement == null)
                    {
                        firstMsgElement = rootElement.getElement(i);
                        firstMsgIndex = i;
                    }

                    msgElementCount++;
                }
            }

            // If we doesn't have any known elements in the document or if we
            // have only one long message we don't want to remove it.
            if(firstMsgElement == null || msgElementCount < 2)
                return;

            try
            {
                // Remove the header of the message if such exists.
                if(firstMsgIndex > 0)
                {
                    Element headerElement = rootElement.getElement(firstMsgIndex - 1);

                    String idAttr = (String) headerElement
                        .getAttributes().getAttribute("identifier");

                    if(idAttr != null && idAttr.equals("messageHeader"))
                    {
                        this.document.remove(headerElement.getStartOffset(),
                            headerElement.getEndOffset()
                                - headerElement.getStartOffset());
                    }
                }

                // Remove the message itself.
                this.document.remove(firstMsgElement.getStartOffset(),
                        firstMsgElement.getEndOffset()
                            - firstMsgElement.getStartOffset());
            }
            catch (BadLocationException e)
            {
                logger.error("Error removing messages from chat: ", e);
            }
        }
    }

    /**
     * Highlights keywords searched in the history.
     *
     * @param message the source message
     * @param contentType the content type
     * @param keyword the searched keyword
     * @return the formatted message
     */
    private String processKeyword(  String message,
                                    String contentType,
                                    String keyword)
    {
        String startPlainTextTag;
        String endPlainTextTag;

        if (HTML_CONTENT_TYPE.equals(contentType))
        {
            startPlainTextTag = "";
            endPlainTextTag = "";
        }
        else
        {
            startPlainTextTag = START_PLAINTEXT_TAG;
            endPlainTextTag = END_PLAINTEXT_TAG;
        }

        Matcher m
            = Pattern.compile(Pattern.quote(keyword), Pattern.CASE_INSENSITIVE)
                .matcher(message);
        StringBuffer msgBuffer = new StringBuffer();
        int prevEnd = 0;

        while (m.find())
        {
            msgBuffer.append(message.substring(prevEnd, m.start()));
            prevEnd = m.end();

            String keywordMatch = m.group().trim();

            msgBuffer.append(endPlainTextTag);
            msgBuffer.append("<b>");
            msgBuffer.append(keywordMatch);
            msgBuffer.append("</b>");
            msgBuffer.append(startPlainTextTag);
        }

        /*
         * If the keyword didn't match, let the outside world be able to
         * discover it.
         */
        if (prevEnd == 0)
            return message;

        msgBuffer.append(message.substring(prevEnd));
        return msgBuffer.toString();
    }

    /**
     * Formats the given message. Processes all smiley chars, new lines and
     * links.
     *
     * @param message the message to be formatted
     * @param contentType the content type of the message to be formatted
     * @param keyword the word to be highlighted
     * @return the formatted message
     */
    private String formatMessage(String message,
                                 String contentType,
                                 String keyword)
    {
        // If the message content type is HTML we won't process links and
        // new lines, but only the smileys.
        if (!HTML_CONTENT_TYPE.equals(contentType))
        {

            /*
             * We disallow HTML in plain-text messages. But processKeyword
             * introduces HTML. So we'll allow HTML if processKeyword has
             * introduced it in order to not break highlighting.
             */
            boolean processHTMLChars;

            if ((keyword != null) && (keyword.length() != 0))
            {
                String messageWithProcessedKeyword
                    = processKeyword(message, contentType, keyword);

                /*
                 * The same String instance will be returned if there was no
                 * keyword match. Calling #equals() is expensive so == is
                 * intentional.
                 */
                processHTMLChars = (messageWithProcessedKeyword == message);
                message = messageWithProcessedKeyword;
            }
            else
                processHTMLChars = true;

            message
                = processNewLines(
                    processLinksAndHTMLChars(message, processHTMLChars));
        }
        // If the message content is HTML, we process br and img tags.
        else
        {
            if ((keyword != null) && (keyword.length() != 0))
                message = processKeyword(message, contentType, keyword);
            message = processImgTags(processBrTags(message));
        }

        return message;
    }

    /**
     * Formats all links in a given message and optionally escapes special HTML
     * characters such as &lt;, &gt;, &amp; and &quot; in order to prevent HTML
     * injection in plain-text messages such as writing
     * <code>&lt;/PLAINTEXT&gt;</code>, HTML which is going to be rendered as
     * such and <code>&lt;PLAINTEXT&gt;</code>. The two procedures are carried
     * out in one call in order to not break URLs which contain special HTML
     * characters such as &amp;.
     * 
     * @param message The source message string.
     * @param processHTMLChars  <tt>true</tt> to escape the special HTML chars;
     * otherwise, <tt>false</tt>
     * @return The message string with properly formatted links.
     */
    private String processLinksAndHTMLChars(String message,
                                            boolean processHTMLChars)
    {
        Matcher m = URL_PATTERN.matcher(message);
        StringBuffer msgBuffer = new StringBuffer();
        int prevEnd = 0;

        while (m.find())
        {
            String fromPrevEndToStart = message.substring(prevEnd, m.start());

            if (processHTMLChars)
                fromPrevEndToStart = processHTMLChars(fromPrevEndToStart);
            msgBuffer.append(fromPrevEndToStart);
            prevEnd = m.end();

            String url = m.group().trim();

            msgBuffer.append(END_PLAINTEXT_TAG);
            msgBuffer.append("<A href=\"");
            if (url.startsWith("www"))
                msgBuffer.append("http://");
            msgBuffer.append(url);
            msgBuffer.append("\">");
            msgBuffer.append(url);
            msgBuffer.append("</A>");
            msgBuffer.append(START_PLAINTEXT_TAG);
        }

        String fromPrevEndToEnd = message.substring(prevEnd);

        if (processHTMLChars)
            fromPrevEndToEnd = processHTMLChars(fromPrevEndToEnd);
        msgBuffer.append(fromPrevEndToEnd);

        return msgBuffer.toString();
    }

    /**
     * Escapes special HTML characters such as &lt;, &gt;, &amp; and &quot; in
     * the specified message.
     *
     * @param message the message to be processed
     * @return the processed message with escaped special HTML characters
     */
    private String processHTMLChars(String message)
    {
        return
            message
                .replace("&", "&amp;")
                    .replace("<", "&lt;")
                        .replace(">", "&gt;")
                            .replace("\"", "&quot;");
    }

    /**
     * Formats message new lines.
     *
     * @param message The source message string.
     * @return The message string with properly formatted new lines.
     */
    private String processNewLines(String message)
    {

        /*
         * <br> tags are needed to visualize a new line in the html format, but
         * when copied to the clipboard they are exported to the plain text
         * format as ' ' and not as '\n'.
         *
         * See bug N4988885:
         * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4988885
         *
         * To fix this we need "&#10;" - the HTML-Code for ASCII-Character No.10
         * (Line feed).
         */
        return
            message
                .replaceAll(
                    "\n",
                    END_PLAINTEXT_TAG + "<BR/>&#10;" + START_PLAINTEXT_TAG);
    }

    /**
     * Opens a link in the default browser when clicked and shows link url in a
     * popup on mouseover.
     *
     * @param e The HyperlinkEvent.
     */
    public void hyperlinkUpdate(HyperlinkEvent e)
    {
        if (e.getEventType() == HyperlinkEvent.EventType.ENTERED)
        {
            String href = e.getDescription();

            this.currentHref = href;
        }
        else if (e.getEventType() == HyperlinkEvent.EventType.EXITED)
        {
            this.currentHref = "";
        }
    }

    /**
     * Returns the text pane of this conversation panel.
     *
     * @return The text pane of this conversation panel.
     */
    public JTextPane getChatTextPane()
    {
        return chatTextPane;
    }

    /**
     * Returns the time of the last received message.
     *
     * @return The time of the last received message.
     */
    public long getLastIncomingMsgTimestamp()
    {
        return lastIncomingMsgTimestamp;
    }

    /**
     * When a right button click is performed in the editor pane, a popup menu
     * is opened.
     * In case of the Scheme being internal, it won't open the Browser but
     * instead it will trigger the forwarded action.
     *
     * @param e The MouseEvent.
     */
    public void mouseClicked(MouseEvent e)
    {
        Point p = e.getPoint();
        SwingUtilities.convertPointToScreen(p, e.getComponent());

        if ((e.getModifiers() & InputEvent.BUTTON3_MASK) != 0
            || (e.isControlDown() && !e.isMetaDown()))
        {
            openContextMenu(p);
        }
        else if ((e.getModifiers() & InputEvent.BUTTON1_MASK) != 0
            && currentHref != null && currentHref.length() != 0)
        {
            URI uri;
            try
            {
                uri = new URI(currentHref);
            }
            catch (URISyntaxException e1)
            {
                logger.error("Invalid URL", e1);
                return;
            }
            if(uri.getScheme().equals("jitsi"))
            {
                for(ChatLinkClickedListener l:chatLinkClickedListeners)
                {
                    l.chatLinkClicked(uri);
                }
            }
            else
                GuiActivator.getBrowserLauncher().openURL(currentHref);

            // after opening the link remove the currentHref to avoid
            // clicking on the window to gain focus to open the link again
            this.currentHref = "";
        }
    }

    /**
     * Opens this panel context menu at the given point.
     *
     * @param p the point where to position the left-top cornet of the context
     *            menu
     */
    private void openContextMenu(Point p)
    {
        if (currentHref != null && currentHref.length() != 0
                && !currentHref.startsWith("jitsi://"))
        {
            rightButtonMenu.insert(openLinkItem, 0);
            rightButtonMenu.insert(copyLinkItem, 1);
            rightButtonMenu.insert(copyLinkSeparator, 2);
        }
        else
        {
            rightButtonMenu.remove(openLinkItem);
            rightButtonMenu.remove(copyLinkItem);
            rightButtonMenu.remove(copyLinkSeparator);
        }

        if (chatTextPane.getSelectedText() != null)
        {
            rightButtonMenu.enableCopy();
        }
        else
        {
            rightButtonMenu.disableCopy();
        }
        rightButtonMenu.setInvoker(chatTextPane);
        rightButtonMenu.setLocation(p.x, p.y);
        rightButtonMenu.setVisible(true);
    }

    public void mousePressed(MouseEvent e) {}

    public void mouseReleased(MouseEvent e) {}

    public void mouseEntered(MouseEvent e) {}

    public void mouseExited(MouseEvent e) {}

    public void lostOwnership(Clipboard clipboard, Transferable contents) {}

    /**
     * Returns the chat container.
     *
     * @return the chat container
     */
    public ChatConversationContainer getChatContainer()
    {
        return chatContainer;
    }

    /**
     * Copies the selected conversation panel content to the clipboard.
     */
    public void copyConversation()
    {
        this.chatTextPane.copy();
    }

    /**
     * Creates new document and all the messages that will be processed in the
     * future will be appended in it.
     */
    public void clear()
    {
        this.document = (HTMLDocument) editorKit.createDefaultDocument();
        Constants.loadSimpleStyle(
            document.getStyleSheet(), chatTextPane.getFont());
    }

    /**
     * Sets the given document to the editor pane in this panel.
     *
     * @param document the document to set
     */
    public void setContent(HTMLDocument document)
    {
        synchronized (scrollToBottomRunnable)
        {
            scrollToBottomIsPending = true;

            this.document = document;
            chatTextPane.setDocument(this.document);
        }
    }

    /**
     * Sets the default document contained in this panel, created on init or
     * when clear is invoked.
     */
    public void setDefaultContent()
    {
        setContent(document);
    }

    /**
     * Returns the document contained in this panel.
     *
     * @return the document contained in this panel
     */
    public HTMLDocument getContent()
    {
        return (HTMLDocument) this.chatTextPane.getDocument();
    }

    /**
     * Returns the right button popup menu.
     *
     * @return the right button popup menu
     */
    public ChatRightButtonMenu getRightButtonMenu()
    {
        return rightButtonMenu;
    }

    /**
     * Returns the date of the first message in the current page.
     *
     * @return the date of the first message in the current page
     */
    public Date getPageFirstMsgTimestamp()
    {
        Element rootElement = this.document.getDefaultRootElement();

        Element firstMessageElement = null;

        for(int i = 0; i < rootElement.getElementCount(); i ++)
        {
            String idAttr = (String) rootElement.getElement(i)
                    .getAttributes().getAttribute("identifier");

            if (idAttr != null && idAttr.equals("messageHeader"))
            {
                firstMessageElement = rootElement.getElement(i);
                break;
            }
        }

        if(firstMessageElement == null)
            return new Date(Long.MAX_VALUE);

        String dateObject = firstMessageElement
            .getAttributes().getAttribute("date").toString();

        return new Date(Long.parseLong(dateObject));
    }

    /**
     * Returns the date of the last message in the current page.
     *
     * @return the date of the last message in the current page
     */
    public Date getPageLastMsgTimestamp()
    {
        Element rootElement = this.document.getDefaultRootElement();

        Element lastMessageElement = null;

        for(int i = rootElement.getElementCount() - 1; i >= 0; i --)
        {
            String idAttr = (String) rootElement.getElement(i)
                .getAttributes().getAttribute("identifier");

            if (idAttr != null && idAttr.equals("messageHeader"))
            {
                lastMessageElement = rootElement.getElement(i);
                break;
            }
        }

        if(lastMessageElement == null)
            return new Date(0);

        String dateObject = lastMessageElement
            .getAttributes().getAttribute("date").toString();

        return new Date(Long.parseLong(dateObject));
    }

    /**
     * Formats HTML tags &lt;br/&gt; to &lt;br&gt; or &lt;BR/&gt; to &lt;BR&gt;.
     * The reason of this function is that the ChatPanel does not support
     * &lt;br /&gt; closing tags (XHTML syntax), thus we have to remove every
     * slash from each &lt;br /&gt; tags.
     * @param message The source message string.
     * @return The message string with properly formatted &lt;br&gt; tags.
     */
    private String processBrTags(String message)
    {
        // The resulting message after being processed by this function.
        StringBuffer processedMessage = new StringBuffer();

        // Compile the regex to match something like <br .. /> or <BR .. />.
        // This regex is case sensitive and keeps the style or other
        // attributes of the <br> tag.
        Matcher m
            = Pattern.compile("<\\s*[bB][rR](.*?)(/\\s*>)").matcher(message);
        int start = 0;

        // while we find some <br /> closing tags with a slash inside.
        while(m.find())
        {
            // First, we have to copy all the message preceding the <br> tag.
            processedMessage.append(message.substring(start, m.start()));
            // Then, we find the position of the slash inside the tag.
            int slash_index = m.group().lastIndexOf("/");
            // We copy the <br> tag till the slash exclude.
            processedMessage.append(m.group().substring(0, slash_index));
            // We copy all the end of the tag following the slash exclude.
            processedMessage.append(m.group().substring(slash_index+1));
            start = m.end();
        }
        // Finally, we have to add the end of the message following the last
        // <br> tag, or the whole message if there is no <br> tag.
        processedMessage.append(message.substring(start));

        return processedMessage.toString();
    }

    /**
     * Formats HTML tags &lt;img ... /&gt; to &lt; img ... &gt;&lt;/img&gt; or
     * &lt;IMG ... /&gt; to &lt;IMG&gt;&lt;/IMG&gt;.
     * The reason of this function is that the ChatPanel does not support
     * &lt;img /&gt; tags (XHTML syntax).
     * Thus, we remove every slash from each &lt;img /&gt; and close it with a
     * separate closing tag.
     * @param message The source message string.
     * @return The message string with properly formatted &lt;img&gt; tags.
     */
    private String processImgTags(String message)
    {
        // The resulting message after being processed by this function.
        StringBuffer processedMessage = new StringBuffer();

        // Compile the regex to match something like <img ... /> or
        // <IMG ... />. This regex is case sensitive and keeps the style,
        // src or other attributes of the <img> tag.
        Pattern p = Pattern.compile("<\\s*[iI][mM][gG](.*?)(/\\s*>)");
        Matcher m = p.matcher(message);
        int slash_index;
        int start = 0;

        // while we find some <img /> self-closing tags with a slash inside.
        while(m.find()){
            // First, we have to copy all the message preceding the <img> tag.
            processedMessage.append(message.substring(start, m.start()));
            // Then, we find the position of the slash inside the tag.
            slash_index = m.group().lastIndexOf("/");
            // We copy the <img> tag till the slash exclude.
            processedMessage.append(m.group().substring(0, slash_index));
            // We copy all the end of the tag following the slash exclude.
            processedMessage.append(m.group().substring(slash_index+1));
            // We close the tag with a separate closing tag.
            processedMessage.append("</img>");
            start = m.end();
        }
        // Finally, we have to add the end of the message following the last
        // <img> tag, or the whole message if there is no <img> tag.
        processedMessage.append(message.substring(start));

        return processedMessage.toString();
    }

    /**
     * Extend Editor pane to add URL tooltips.
     */
    private class MyTextPane
        extends JTextPane
    {
        /**
         * Returns the string to be used as the tooltip for <i>event</i>. 
         *
         * @param event the <tt>MouseEvent</tt> 
         * @return the string to be used as the tooltip for <i>event</i>.
         */
        @Override
        public String getToolTipText(MouseEvent event)
        {
            return
                ((currentHref != null) && (currentHref.length() != 0))
                    ? currentHref
                    : null;
        }
    }

    /**
     * Adds a custom component at the end of the conversation.
     * 
     * @param component the component to add at the end of the conversation.
     */
    public void addComponent(ChatConversationComponent component)
    {
        synchronized (scrollToBottomRunnable)
        {
            StyleSheet styleSheet = document.getStyleSheet();
            Style style
                = styleSheet
                    .addStyle(
                        StyleConstants.ComponentElementName,
                        styleSheet.getStyle("body"));

            // The image must first be wrapped in a style
            style
                .addAttribute(
                    AbstractDocument.ElementNameAttribute,
                    StyleConstants.ComponentElementName);

            TransparentPanel wrapPanel
                = new TransparentPanel(new BorderLayout());

            wrapPanel.add(component, BorderLayout.NORTH);

            style
                .addAttribute(StyleConstants.ComponentAttribute, wrapPanel);
            style.addAttribute("identifier", "messageHeader");
            style.addAttribute("date", component.getDate().getTime());

            scrollToBottomIsPending = true;

            // Insert the component style at the end of the text
            try
            {
                document
                    .insertString(document.getLength(), "ignored text", style);
            }
            catch (BadLocationException e)
            {
                logger.error("Insert in the HTMLDocument failed.", e);
            }
        }
    }

    /**
     * Registers a new link click listener.
     * 
     * @param listener the object that should be notified when an internal
     * link was clicked.
     */
    public void addChatLinkClickedListener(ChatLinkClickedListener listener)
    {
        if(!chatLinkClickedListeners.contains(listener))
            chatLinkClickedListeners.add(listener);
    }

    /**
     * Remove a registered link click listener.
     * 
     * @param listener a registered click listener to remove
     */
    public void removeChatLinkClickedListener(ChatLinkClickedListener listener)
    {
        chatLinkClickedListeners.remove(listener);
    }

    /**
     * Returns the date string to show for the given date.
     * 
     * @param date the date to format
     * @return the date string to show for the given date
     */
    public static String getDateString(long date)
    {
        if (GuiUtils.compareDatesOnly(date, System.currentTimeMillis()) < 0)
        {
            StringBuffer dateStrBuf = new StringBuffer();

            GuiUtils.formatDate(date, dateStrBuf);
            dateStrBuf.append(" ");
            return dateStrBuf.toString();
        }

        return "";
    }

    /**
     * Reloads images.
     */
    public void loadSkin()
    {
        openLinkItem.setIcon(
                new ImageIcon(ImageLoader.getImage(ImageLoader.BROWSER_ICON)));
        copyLinkItem.setIcon(
                new ImageIcon(ImageLoader.getImage(ImageLoader.COPY_ICON)));

        getRightButtonMenu().loadSkin();
    }

    /**
     * Highlights the string in multi user chat.
     * 
     * @param message the message to process
     * @param contentType the content type of the message
     * @param keyWord the keyword to highlight
     * @return the message string with the keyword highlighted
     */
    public String processChatRoomHighlight(String message, String contentType,
        String keyWord)
    {
        return processKeyword(message, contentType, keyWord);
    }
    
    public String processMeCommand(ChatMessage chatMessage)
    {
        String contentType = chatMessage.getContentType();
        String message = chatMessage.getMessage();

        String msgID = "message";
        String chatString = "";
        String endHeaderTag = "";

        String startDivTag = "<DIV identifier=\"" + msgID + "\">";
        String endDivTag = "</DIV>";

        String startPlainTextTag;
        String endPlainTextTag;

        if (HTML_CONTENT_TYPE.equals(contentType))
        {
            startPlainTextTag = "";
            endPlainTextTag = "";
        }
        else
        {
            startPlainTextTag = START_PLAINTEXT_TAG;
            endPlainTextTag = END_PLAINTEXT_TAG;
        }

        if (message.length() > 4 && message.substring(0, 4).equals("/me "))
        {
            chatString = startDivTag + "<B><I>";

            endHeaderTag = "</I></B>" + endDivTag;

            chatString +=

                processHTMLChars("*** " + chatMessage.getContactName() + " "
                    + message.substring(4))
                    + endHeaderTag;

            Map<String, ReplacementService> listSources =
                GuiActivator.getReplacementSources();

            Iterator<Entry<String, ReplacementService>> entrySetIter =
                listSources.entrySet().iterator();
            StringBuffer msgStore = new StringBuffer(chatString);

            for (int i = 0; i < listSources.size(); i++)
            {
                Map.Entry<String, ReplacementService> entry =
                    entrySetIter.next();

                ReplacementService source = entry.getValue();

                boolean isSmiley = source instanceof SmiliesReplacementService;
                if (isSmiley)
                {
                    String sourcePattern = source.getPattern();
                    Pattern p =
                        Pattern.compile(sourcePattern, Pattern.CASE_INSENSITIVE
                            | Pattern.DOTALL);
                    Matcher m = p.matcher(msgStore);

                    StringBuffer msgTemp = new StringBuffer(chatString);

                    while (m.find())
                    {
                        msgTemp.insert(m.start(), startPlainTextTag);
                        msgTemp.insert(m.end() + startPlainTextTag.length(),
                            endPlainTextTag);

                    }
                    if (msgTemp.length() != msgStore.length())
                        msgStore = msgTemp;
                }
            }

            return msgStore.toString();
        }
        else
            return "";
    }

    private static String createIncomingMessageTag(
            String messageID,
            String incomingMessageHeader,
            String incomingMessageParagraph)
    {
        StringBuffer messageBuff = new StringBuffer();

//        <div class="box">
//            <div class="topleft">
//                <div class="topright">
//                    <div class="messageDiv">
//                        <h3>Header</h3>
//                        <p>Text</p>
//                    </div>
//                </div>
//            </div>
//            <div class="bottomleft">
//                <div class="bottomright">
//                </div>
//            </div>
//        </div>

        messageBuff.append("<div " + createBoxStyle() + ">");
        messageBuff.append("<div " + createTopLeftStyle() + ">");
        messageBuff.append("<div " + createTopRightStyle() + ">");
        messageBuff.append("<div identifier=\"" + messageID
            + "\" " + createMessageDivStyle() + ">");
        messageBuff.append(incomingMessageHeader);
        messageBuff.append(incomingMessageParagraph);
        messageBuff.append("</div>");
        messageBuff.append("</div>");
        messageBuff.append("</div>");
        messageBuff.append("<div " + createBottomLeftStyle() + ">");
        messageBuff.append("<div " + createBottomRightStyle() + ">");
        messageBuff.append("</div>");
        messageBuff.append("</div>");
        messageBuff.append("</div>");

        return messageBuff.toString();
    }

    private static String createOutgoingMessageStyle()
    {
        StringBuffer styleBuff = new StringBuffer();

        styleBuff.append("background-image:");
        styleBuff.append("url('bundle://30.0:1/resources/images/impl/gui/lookandfeel/selectedTabMiddle.png');");
        styleBuff.append("background-repeat:");
        styleBuff.append("repeat-x;");

        return styleBuff.toString();
    }

    private static String createSmsMessageStyle()
    {
        StringBuffer styleBuff = new StringBuffer();

        styleBuff.append("background-image:");
        styleBuff.append("url('bundle://30.0:1/resources/images/impl/gui/lookandfeel/tabRight.png');");
        styleBuff.append("background-repeat:");
        styleBuff.append("repeat-x;");

        return styleBuff.toString();
    }

//  .box {
//      width: 100%;
//      margin: 0px auto;
//  }
    private static String createBoxStyle()
    {
        return "style=\"width: 100%;"
                + " margin-top: 0px;"
                + " margin-bottom: 0px;"
                + " margin-left: auto;"
                + " margin-right: auto;\"";
    }

//  .box div.topleft {
//      display: block;
//      background: url("i/box-bg.png") top left no-repeat white;
//      padding: 0em 0em 0em 1.0em;
//  }
    private static String createTopLeftStyle()
    {
        return  "style=\"display: block;"
                + " background-image: url('"+INCOMING_MESSAGE_IMAGE_PATH+"');"
                + " background-repeat: no-repeat;"
                + " background-position: top left;"
                + " background-color: #FFFFFF;"
                + " padding-top: 0em;"
                + " padding-right: 0em;"
                + " padding-bottom: 0em;"
                + " padding-left: 0em;"
                + "\"";
    }

//    .box div.topright {
//        display: block;
//        background: url("i/box-bg.png") top right no-repeat white;
//        padding: 1.0em;
//        margin: -1.0em 0 0 1.0em;
//    }
    private static String createTopRightStyle()
    {
        return  "style=\"display: block;"
                + " background-image: url('"+INCOMING_MESSAGE_IMAGE_PATH+"');"
                + " background-repeat: no-repeat;"
                + " background-position: top right;"
                + " background-color: #FFFFFF;"
                + " padding-top: 1em;"
                + " padding-right: 1em;"
                + " padding-bottom: 1em;"
                + " padding-left: 1em;"
                + " margin-top: -1.0em;"
                + " margin-right: 0em;"
                + " margin-bottom: 0em;"
                + " margin-left: 1.0em;"
                + "\"";
    }

//    .box div.bottomleft {
//        display: block;
//        height: 55px;
//        margin-top: -1.0em;
//        background: url("i/box-bg.png") bottom left no-repeat white;
//    }
    private static String createBottomLeftStyle()
    {
        return  "style=\"display: block;"
                + " height: 25px;"
                + " margin-top: -1.0em;"
                + " background-image: url('"+INCOMING_MESSAGE_IMAGE_PATH+"');"
                + " background-repeat: no-repeat;"
                + " background-position: bottom left;"
                + " background-color: #FFFFFF;"
                + "\"";
    }

//    .box div.bottomright {
//        display: block;
//        background: url("i/box-bg.png") bottom right no-repeat white;
//        height: 55px;
//        margin-left: 3.0em;
//    }
    private static String createBottomRightStyle()
    {
        return  "style=\"display: block;"
                + " height: 25px;"
                + " margin-left: 3.0em;"
                + " background-image: url('"+INCOMING_MESSAGE_IMAGE_PATH+"');"
                + " background-repeat: no-repeat;"
                + " background-position: bottom right;"
                + " background-color: #FFFFFF;"
                + "\"";
    }

//    .box div.topright div {
//        margin-right: 1.5em;
//    }
    private static String createMessageDivStyle()
    {
        return  "style=\"margin-right: 1.5em;\"";
    }

//    .box h4 {
//        margin-bottom: 0.4em;
//        background-image: none;
//        background-repeat: no-repeat;
//        margin:0;
//        padding:0;
//        text-align:center;
//        padding-bottom:15px;
//        }
}