/*
 * Copyright 2010-2015 Institut Pasteur.
 * 
 * This file is part of Icy.
 * 
 * Icy is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Icy is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
 */
package icy.gui.inspector;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.util.ArrayList;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JTextField;
import javax.swing.JTextPane;
import javax.swing.ListSelectionModel;
import javax.swing.SwingConstants;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;

import org.schwering.irc.lib.IRCModeParser;
import org.schwering.irc.lib.IRCUser;

import icy.gui.component.CloseableTabbedPane;
import icy.gui.component.CloseableTabbedPane.CloseableTabbedPaneListener;
import icy.gui.component.ExternalizablePanel;
import icy.gui.component.IcyTextField;
import icy.gui.component.button.IcyButton;
import icy.gui.component.button.IcyToggleButton;
import icy.gui.frame.progress.AnnounceFrame;
import icy.gui.frame.progress.ToolTipFrame;
import icy.gui.main.IcyDesktopPane;
import icy.gui.main.IcyDesktopPane.AbstractDesktopOverlay;
import icy.gui.main.MainFrame;
import icy.gui.preferences.ChatPreferencePanel;
import icy.gui.preferences.PreferenceFrame;
import icy.gui.util.ComponentUtil;
import icy.gui.util.FontUtil;
import icy.gui.util.GuiUtil;
import icy.main.Icy;
import icy.network.IRCClient;
import icy.network.IRCEventListenerImpl;
import icy.network.IRCUtil;
import icy.network.NetworkUtil;
import icy.network.NetworkUtil.InternetAccessListener;
import icy.preferences.ChatPreferences;
import icy.resource.ResourceUtil;
import icy.resource.icon.IcyIcon;
import icy.system.IcyExceptionHandler;
import icy.system.thread.ThreadUtil;
import icy.type.collection.CollectionUtil;
import icy.util.DateUtil;
import icy.util.GraphicsUtil;
import icy.util.StringUtil;

public class ChatPanel extends ExternalizablePanel implements InternetAccessListener
{
    /**
     * 
     */
    private static final long serialVersionUID = -3449422097073285247L;

    private static final int MAX_SIZE = 1 * 1024 * 1024; // 1 MB

    private static final String DEFAULT_CHANNEL = "#icy";

    private class CustomIRCClient extends IRCClient
    {
        public CustomIRCClient(String host, int port, String pass, String nickName, String userName, String realName)
        {
            super(host, port, pass, nickName, userName, realName);

            setName("Chat IRC listener");
        }
    }

    private class DesktopOverlay extends AbstractDesktopOverlay
    {
        final static int FG_ALPHA = 0xC0;
        final static int BG_ALPHA = 0xA0;

        final Color defaultBgColor;
        final Color defaultFgColor;
        Color bgColor;
        Color fgColor;

        public DesktopOverlay()
        {
            super();

            // default text colors
            defaultFgColor = getFgColor(Color.black);
            defaultBgColor = getBgColor(Color.lightGray);
            fgColor = defaultFgColor;
            bgColor = defaultBgColor;
        }

        private Color getFgColor(Color c)
        {
            return new Color(c.getRed(), c.getGreen(), c.getBlue(), FG_ALPHA);
        }

        private Color getBgColor(Color c)
        {
            return new Color(c.getRed(), c.getGreen(), c.getBlue(), BG_ALPHA);
        }

        private int setAttribute(Graphics2D g2, CharSequence text, int index)
        {
            final int len = text.length();

            // no more text
            if (index >= len)
                return len;

            int result = index + 1;
            int end;
            Font f;

            switch (text.charAt(index))
            {
                case IRCUtil.CHAR_RESET:
                    // reset to normal
                    g2.setFont(FontUtil.setStyle(g2.getFont(), Font.PLAIN));
                    fgColor = defaultFgColor;
                    bgColor = defaultBgColor;
                    break;

                case IRCUtil.CHAR_BOLD:
                    // switch bold
                    f = g2.getFont();
                    g2.setFont(FontUtil.setStyle(f, f.getStyle() ^ Font.BOLD));
                    break;

                case IRCUtil.CHAR_ITALIC:
                    // switch italic
                    f = g2.getFont();
                    g2.setFont(FontUtil.setStyle(f, f.getStyle() ^ Font.ITALIC));
                    break;

                case IRCUtil.CHAR_COLOR:
                    end = StringUtil.getNextNonDigitCharIndex(text, result);
                    // no more than 2 digits to encode color
                    if ((end == -1) || (end > (result + 2)))
                        end = Math.min(text.length(), result + 2);

                    // no color info --> restore default
                    if (end == result)
                    {
                        fgColor = defaultFgColor;
                        bgColor = defaultBgColor;
                    }
                    else
                    {
                        // get foreground color
                        fgColor = getFgColor(
                                IRCUtil.getIRCColor(Integer.parseInt(text.subSequence(result, end).toString())));

                        // update position
                        result = end;

                        // search if we have background color
                        if ((result < len) && (text.charAt(result) == ','))
                        {
                            result++;

                            end = StringUtil.getNextNonDigitCharIndex(text, result);
                            // no more than 2 digits to encode color
                            if ((end == -1) || (end > (result + 2)))
                                end = Math.min(text.length(), result + 2);

                            // get background color
                            if (end != result)
                            {
                                // we don't want to support background color...
                                // bgColor =
                                // getBgColor(IRCUtil.getIRCColor(Integer.parseInt(text.subSequence(result,
                                // end)
                                // .toString())));

                                // update position
                                result = end;
                            }
                        }
                    }
                    break;

                default:
                    // System.out.println("code " + Integer.toString(text.charAt(index)));
                    break;
            }

            return result;
        }

        private int firstPreviousIndexOf(char c, int from)
        {
            int ind = from;
            if (ind >= content.length())
                return -1;

            while (ind >= 0)
            {
                if (content.charAt(ind) == c)
                    return ind;

                ind--;
            }

            return ind;
        }

        private int firstNextIndexOf(char c, int from)
        {
            final int len = content.length();
            int ind = from;

            while (ind < len)
            {
                if (content.charAt(ind) == c)
                    return ind;

                ind++;
            }

            return -1;
        }

        private boolean isChannelVisible(String channel)
        {
            // no channel name --> assume visible
            if (StringUtil.isEmpty(channel))
                return true;
            // private message --> assume visible
            if (channel.charAt(0) != '#')
                return true;

            for (String chan : ChatPreferences.getDesktopChannels().split(";"))
                if (channel.equalsIgnoreCase(fixChannelName(chan)))
                    return true;

            return false;
        }

        @Override
        public void paint(Graphics g, int width, int height)
        {
            final Graphics2D g2 = (Graphics2D) g.create();

            // modify to Monospaced font for easy space calculation
            g2.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));

            // fixed size font, every character has the same bounds
            final Rectangle2D charRect = GraphicsUtil.getStringBounds(g2, "M");
            final float charHeight = (float) charRect.getHeight();
            final float charWidth = (float) charRect.getWidth();
            // keep 20 pixels margins
            final int charByLine = (int) ((width - 20) / charRect.getWidth());

            // no enough space to draw text
            if (charByLine <= 0)
                return;

            // start y position
            float y = height;
            // start at last character
            int position = content.length() - 1;

            while ((position > 0) && (y > 0))
            {
                // get paragraph offsets
                final int start = firstPreviousIndexOf('\n', position - 1) + 1;
                final int end = position;
                final int len = end - start;

                // update position
                position = start - 1;

                // get channel name end position
                final int chanEnd = firstNextIndexOf(':', start);
                // display only if this channel is visible on desktop
                if ((chanEnd != -1) && isChannelVisible(content.substring(start, chanEnd)))
                {

                    // calculate number of lines taken by the paragraph
                    int numLineParagraph = len / charByLine;
                    if ((len % charByLine) != 0)
                        numLineParagraph++;
                    // get paragraph height
                    final float paragraphHeight = numLineParagraph * charHeight;

                    // position to end of paragraph
                    y -= paragraphHeight;

                    // set default attributes
                    g2.setFont(FontUtil.setStyle(g2.getFont(), Font.PLAIN));
                    fgColor = defaultFgColor;
                    bgColor = defaultBgColor;

                    // process paragraph
                    int index = start;
                    while (index < end)
                    {
                        final int lineEnd = Math.min(index + charByLine, end);
                        float x = 10;

                        // process line
                        while (index < lineEnd)
                        {
                            int ctrlIndex = StringUtil.getNextCtrlCharIndex(content, index);
                            // end of line
                            if ((ctrlIndex == -1) || (ctrlIndex > lineEnd))
                                ctrlIndex = lineEnd;

                            // something to draw ?
                            if (index != ctrlIndex)
                            {
                                // get String to draw
                                final String str = content.substring(index, ctrlIndex);

                                // draw string
                                g2.setColor(bgColor);
                                g2.drawString(str, x - 1, y + 1);
                                g2.setColor(fgColor);
                                g2.drawString(str, x, y);

                                // set new X position
                                x += charWidth * str.length();
                            }

                            if (ctrlIndex < lineEnd)
                                index = setAttribute(g2, content, ctrlIndex);
                            else
                                index = lineEnd;
                        }

                        // pass to next line
                        y += charHeight;
                    }

                    // set position back to end of paragraph
                    y -= paragraphHeight;
                }
            }

            g2.dispose();
        }
    }

    private class CustomIRCClientListener extends IRCEventListenerImpl
    {
        public CustomIRCClientListener()
        {
            super();
        }

        @Override
        public void onConnected()
        {
            super.onConnected();

            // join default channel
            client.doJoin(DEFAULT_CHANNEL);
            // join extras channels
            for (String extraChannel : ChatPreferences.getExtraChannels().split(";"))
                if (!StringUtil.isEmpty(extraChannel))
                    client.doJoin(fixChannelName(extraChannel));
            // authentication for registered user
            final String pass = ChatPreferences.getUserPassword();
            if (!StringUtil.isEmpty(pass))
                client.doPrivmsg("NickServ", "identify " + ChatPreferences.getNickname() + " " + pass);

            ThreadUtil.invokeLater(new Runnable()
            {
                @Override
                public void run()
                {
                    connectButton.setEnabled(true);
                }
            });

            refreshGUI();
        }

        @Override
        public void onDisconnected()
        {
            super.onDisconnected();

            ThreadUtil.invokeLater(new Runnable()
            {
                @Override
                public void run()
                {
                    connectButton.setEnabled(true);
                }
            });

            refreshGUI();
            refreshUsers();
        }

        @Override
        public void onError(int num, String msg)
        {
            super.onError(num, msg);

            switch (num)
            {
                case 432:
                case 433:
                    // Erroneous Nickname
                    if (!connectButton.isEnabled())
                    {
                        // if we were connecting, we disconnect
                        disconnect("Incorrect nickname");
                        if (num == 432)
                            onReceive(null, null, "Your nickname contains invalid caracters.");
                    }
                    refreshGUI();
                    break;

                case 437:
                    client.doNick(client.getNick() + "_");
                    break;
            }
        }

        @Override
        public void onJoin(String chan, IRCUser u)
        {
            if (isCurrentUser(u))
            {
                // add the channel pane if needed
                addChannelPane(chan);
                if (getShowStatusMessages())
                    onReceive(null, chan, "Welcome to " + IRCUtil.getBoldString(chan.substring(1)) + ".");
            }
            else
            {
                if (getShowStatusMessages())
                    onReceive(null, chan, u.getNick() + " joined.");
            }

            // refresh user list
            refreshUsers();
        }

        @Override
        public void onKick(String chan, IRCUser u, String nickPass, String msg)
        {
            super.onKick(chan, u, nickPass, msg);

            // refresh user list
            refreshUsers();
        }

        @Override
        public void onLeave(String chan, IRCUser u, String msg)
        {
            if (getShowStatusMessages())
            {
                if (StringUtil.isEmpty(msg))
                    onReceive(null, chan, u.getNick() + " left.");
                else
                    onReceive(null, chan, u.getNick() + " left" + " (" + msg + ").");
            }

            // remove the channel pane if needed
            if (isCurrentUser(u))
                removeChannelPane(chan);

            // refresh user list
            refreshUsers();
        }

        @Override
        public void onMode(IRCUser u, String nickPass, String mode)
        {
            // ignore mode set message
            // super.onMode(u, nickPass, mode);
        }

        @Override
        public void onMode(String chan, IRCUser u, IRCModeParser mp)
        {
            // ignore mode set message
            // super.onMode(chan, u, mp);
        }

        @Override
        public void onNick(IRCUser u, String nickNew)
        {
            super.onNick(u, nickNew);

            // update nickname
            if (isCurrentUser(u))
                ChatPreferences.setNickname(nickNew);
            refreshGUI();
            refreshUsers();
        }

        @Override
        public void onNotice(String target, IRCUser u, String msg)
        {
            if (msg != null)
            {
                if (getShowStatusMessages())
                {
                    if (msg.indexOf("Looking up your hostname") != -1)
                        onReceive(null, null, "Connecting...");
                }

                // ignore all others notices...

                // else
                // {
                // if (msg.indexOf("Checking Ident") != -1)
                // return;
                // if (msg.indexOf("your hostname") != -1)
                // return;
                // if (msg.indexOf("No Ident response") != -1)
                // return;
                //
                // super.onNotice(target, u, msg);
                // }
            }
        }

        @Override
        public void onQuit(IRCUser u, String msg)
        {
            super.onQuit(u, msg);

            refreshUsers();
        }

        @Override
        public void unknown(String a, String b, String c, String d)
        {
            onReceive(null, null, d);
        }

        @Override
        public void onReply(int num, String value, String msg)
        {
            switch (num)
            {
                case 353:
                    // add users to tmp list
                    tmpUserList.addAll(CollectionUtil.asList(msg.split(" ")));
                    break;

                case 366:
                    // end of user list
                    userList.setListData(tmpUserList.toArray(new String[tmpUserList.size()]));
                    tmpUserList.clear();
                    break;

                case 1:
                case 2:
                case 3:
                case 4:
                case 5:
                case 250:
                case 251:
                case 252:
                case 253:
                case 254:
                case 255:
                case 256:
                case 257:
                case 258:
                case 259:
                case 261:
                case 262:
                case 263:
                case 265:
                case 266:
                case 372:
                case 375:
                case 376:
                    // ignore
                    break;

                default:
                    onReceive(null, null, msg);
            }
        }

        @Override
        public void onReceive(String nick, String target, String msg)
        {
            final String n;
            final String t;

            // ignore close link
            if (msg.startsWith("Error: Closing Link:"))
                return;

            // ignore CTCP version request (can come from hacker)
            if (msg.equals("\001VERSION\001"))
                return;

            // target is current user --> incoming private message
            if (isCurrentUser(target))
            {
                // get the source name of private message
                if (StringUtil.isEmpty(nick))
                    n = "serv";
                else
                    n = nick;

                // incoming private message --> use source as target name
                t = n;

                // add private channel
                addChannelPane(n);
            }
            else
            {
                n = nick;
                t = target;
            }

            // show message
            addMessage(n, t, msg);
        }

        @Override
        protected boolean getShowStatusMessages()
        {
            return ChatPreferences.getShowStatusMessages();
        }
    }

    private class ChannelPanel
    {
        final String name;

        final JScrollPane scrollPane;
        final JTextPane editor;
        final StyledDocument doc;

        public ChannelPanel(String name)
        {
            super();

            this.name = name;

            editor = new JTextPane();
            editor.setEditable(false);
            doc = editor.getStyledDocument();

            scrollPane = new JScrollPane(editor);
        }
    }

    /**
     * GUI
     */
    JPanel panelBottom;
    CloseableTabbedPane tabPane;
    JScrollPane usersScrollPane;
    JList userList;
    IcyTextField sendEditor;
    IcyTextField txtNickName;
    IcyToggleButton connectButton;
    IcyToggleButton showUserPaneButton;
    IcyToggleButton desktopOverlayButton;
    IcyButton advancedButton;

    /**
     * Desktop GUI
     */
    final JPanel desktopPanel;
    final IcyTextField sendEditorDesktop;
    final IcyButton hideDesktopChatButton;
    final DesktopOverlay desktopOverlay;

    /**
     * IRC client
     */
    CustomIRCClient client;

    /**
     * internals
     */
    final ArrayList<ChannelPanel> channelPanes;
    final ArrayList<String> tmpUserList;
    final SimpleAttributeSet attributes;
    final CustomIRCClientListener ircListener;
    final StringBuilder content;
    String lastCmd;

    public ChatPanel()
    {
        super("Chat room", "chatPanel");

        channelPanes = new ArrayList<ChannelPanel>();
        tmpUserList = new ArrayList<String>();
        attributes = new SimpleAttributeSet();
        content = new StringBuilder();
        lastCmd = "";
        client = null;

        // build GUI
        initialize();

        // add default channel panel (need to be done when base GUI is done)
        addChannelPane(DEFAULT_CHANNEL);

        // build desktop GUI
        sendEditorDesktop = new IcyTextField();
        sendEditorDesktop.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                sendEditContent((IcyTextField) e.getSource());
            }
        });
        sendEditorDesktop.addKeyListener(new KeyAdapter()
        {
            @Override
            public void keyPressed(KeyEvent e)
            {
                if (e.getKeyCode() == KeyEvent.VK_UP)
                    sendEditorDesktop.setText(lastCmd);
            }
        });

        hideDesktopChatButton = new IcyButton(new IcyIcon(ResourceUtil.ICON_SQUARE_DOWN, 20));
        hideDesktopChatButton.setFlat(true);
        ComponentUtil.setFixedWidth(hideDesktopChatButton, 20);
        hideDesktopChatButton.setToolTipText("Disable desktop chat overlay");
        hideDesktopChatButton.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                ChatPreferences.setDesktopOverlay(false);
                refreshDesktopOverlayState();
                new ToolTipFrame(
                        "<b>Desktop chat overlay</b><br><br>" + "You just disabled the desktop chat overlay<br>"
                                + "but you can always access and enable it<br>" + "from the inspector \"Chat\" tab.",
                        "chat.overlay");
            }
        });

        // desktop bottom panel
        desktopPanel = GuiUtil.createLineBoxPanel(sendEditorDesktop, Box.createHorizontalStrut(2),
                Box.createHorizontalGlue(), hideDesktopChatButton);

        desktopOverlay = new DesktopOverlay();

        StyleConstants.setFontFamily(attributes, "arial");
        StyleConstants.setFontSize(attributes, 11);
        StyleConstants.setForeground(attributes, Color.black);

        sendEditor.setFont(new Font("arial", 0, 11));
        sendEditor.addKeyListener(new KeyAdapter()
        {
            @Override
            public void keyPressed(KeyEvent e)
            {
                if (e.getKeyCode() == KeyEvent.VK_UP)
                    sendEditor.setText(lastCmd);
            }
        });

        ircListener = new CustomIRCClientListener();

        refreshGUI();
        refreshDesktopOverlayState();

        addStateListener(new StateListener()
        {
            @Override
            public void stateChanged(ExternalizablePanel source, boolean externalized)
            {
                refreshGUI();
            }
        });

        // call default internet connection callback to process auto connect
        if (NetworkUtil.hasInternetAccess())
            internetUp();

        NetworkUtil.addInternetAccessListener(this);
    }

    public boolean isConnected()
    {
        return (client != null) && client.isConnected();
    }

    /**
     * Do IRC connection
     */
    public void connect()
    {
        if (!isConnected())
        {
            ThreadUtil.invokeLater(new Runnable()
            {
                @Override
                public void run()
                {
                    // connecting
                    connectButton.setEnabled(false);
                    connectButton.setToolTipText("connecting...");
                }
            });

            final String nickName = txtNickName.getText();
            // apply nickname
            ChatPreferences.setNickname(nickName);
            String userName = nickName;
            String realName = ChatPreferences.getRealname();

            if (StringUtil.isEmpty(userName))
                userName = nickName;
            if (StringUtil.isEmpty(realName))
                realName = nickName;

            // remove listener from previous client
            if (client != null)
                client.removeListener(ircListener);

            // we need to recreate the client
            client = new CustomIRCClient(ChatPreferences.getServer(), ChatPreferences.getPort(),
                    ChatPreferences.getServerPassword(), nickName, userName, realName);
            client.addListener(ircListener);

            // process connection in a separate thread as it can take sometime
            new Thread(new Runnable()
            {
                @Override
                public void run()
                {
                    try
                    {
                        client.connect();
                    }
                    catch (IOException e)
                    {
                        // error while connecting
                        IcyExceptionHandler.showErrorMessage(e, false, false);
                        System.out.println("Cannot connect to chat.");
                        System.out.println("If you use a proxy, verify you have valid SOCKS settings.");

                        // update GUI
                        ThreadUtil.invokeLater(new Runnable()
                        {
                            @Override
                            public void run()
                            {
                                connectButton.setEnabled(true);
                                connectButton.setToolTipText("Not connected - Click to connect");
                            }
                        });
                    }
                }
            }, "IRC client connection").start();
        }
    }

    public void disconnect(String message)
    {
        if (isConnected())
        {
            ThreadUtil.invokeLater(new Runnable()
            {
                @Override
                public void run()
                {
                    // closing connection
                    connectButton.setEnabled(false);
                    connectButton.setToolTipText("closing connexion...");
                }
            });

            client.doQuit(message);
        }
    }

    protected void sendEditContent(JTextField txtField)
    {
        final String text = txtField.getText();

        if (isConnected() && !StringUtil.isEmpty(text))
        {
            // send from desktop editor ?
            if (txtField == sendEditorDesktop)
                // send text to main default channel
                client.send(DEFAULT_CHANNEL, text);
            else
                // send text to current target
                client.send(getCurrentChannel(), text);
        }

        txtField.setText("");
        lastCmd = text;
    }

    /**
     * Build the panel
     */
    private void initialize()
    {
        setLayout(new BorderLayout(0, 0));

        tabPane = new CloseableTabbedPane(SwingConstants.TOP, JTabbedPane.SCROLL_TAB_LAYOUT);
        tabPane.addCloseableTabbedPaneListener(new CloseableTabbedPaneListener()
        {
            @Override
            public void tabClosed(int index, String title)
            {
                // it was a channel tab, leave channel
                if (isConnected() && title.startsWith("#"))
                    client.doPart(title);
                else
                    // directly remove the channel pane
                    removeChannelPane(title);
            }

            @Override
            public boolean tabClosing(int index, String title)
            {
                return true;
            }
        });
        tabPane.addChangeListener(new ChangeListener()
        {
            @Override
            public void stateChanged(ChangeEvent e)
            {
                // set back default tab color
                tabPane.setBackgroundAt(tabPane.getSelectedIndex(), tabPane.getBackground());
                refreshUsers();
            }
        });
        add(tabPane, BorderLayout.CENTER);

        usersScrollPane = new JScrollPane();
        usersScrollPane.setPreferredSize(new Dimension(130, 200));

        JLabel lblUtilisateur = new JLabel("Users");
        lblUtilisateur.setFont(new Font("Tahoma", Font.BOLD, 11));
        lblUtilisateur.setBorder(new LineBorder(Color.GRAY, 1, true));
        lblUtilisateur.setHorizontalAlignment(SwingConstants.CENTER);
        usersScrollPane.setColumnHeaderView(lblUtilisateur);

        userList = new JList();
        userList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        userList.addMouseListener(new MouseAdapter()
        {
            @Override
            public void mouseClicked(MouseEvent e)
            {
                if (!isConnected())
                    return;

                // double click
                if (e.getClickCount() == 2)
                {
                    final int index = userList.locationToIndex(e.getPoint());

                    if (index != -1)
                    {
                        final String nick = (String) userList.getSelectedValue();

                        // add private chat nick pane
                        if (!isCurrentUser(nick) && !StringUtil.isEmpty(nick))
                            addChannelPane(nick);
                    }
                }
            }
        });
        userList.setToolTipText("Double click on an username to send private message");

        usersScrollPane.setViewportView(userList);

        panelBottom = new JPanel();
        add(panelBottom, BorderLayout.SOUTH);
        panelBottom.setLayout(new BorderLayout(0, 0));

        sendEditor = new IcyTextField();
        sendEditor.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                sendEditContent((IcyTextField) e.getSource());
            }
        });
        sendEditor.setColumns(10);
        panelBottom.add(sendEditor, BorderLayout.SOUTH);

        JPanel topPanel = new JPanel();
        topPanel.setBorder(new EmptyBorder(0, 0, 2, 0));
        add(topPanel, BorderLayout.NORTH);

        topPanel.setLayout(new BoxLayout(topPanel, BoxLayout.LINE_AXIS));

        txtNickName = new IcyTextField();
        txtNickName.setMaximumSize(new Dimension(2147483647, 24));
        txtNickName.setText(ChatPreferences.getNickname());
        txtNickName.setToolTipText("Nick name");
        txtNickName.setColumns(10);
        topPanel.add(txtNickName);

        Component horizontalStrut_3 = Box.createHorizontalStrut(4);
        topPanel.add(horizontalStrut_3);

        JButton btnSetNickName = new JButton("Set");
        btnSetNickName.setFocusPainted(false);
        btnSetNickName.setPreferredSize(new Dimension(40, 23));
        btnSetNickName.setMaximumSize(new Dimension(40, 23));
        btnSetNickName.setMinimumSize(new Dimension(40, 23));
        btnSetNickName.setMargin(new Insets(2, 8, 2, 8));
        btnSetNickName.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                final String nickName = txtNickName.getText();

                if (isConnected())
                    client.doNick(nickName);
                else
                    ChatPreferences.setNickname(nickName);
            }
        });
        btnSetNickName.setToolTipText("Set nick name");
        topPanel.add(btnSetNickName);

        Component horizontalGlue = Box.createHorizontalGlue();
        topPanel.add(horizontalGlue);

        Component horizontalStrut_2 = Box.createHorizontalStrut(10);
        topPanel.add(horizontalStrut_2);

        connectButton = new IcyToggleButton(new IcyIcon(ResourceUtil.ICON_ON_OFF, 20));
        connectButton.setFocusPainted(false);
        connectButton.setMaximumSize(new Dimension(32, 32));
        connectButton.setMinimumSize(new Dimension(24, 24));
        connectButton.setPreferredSize(new Dimension(24, 24));
        connectButton.setToolTipText("Connect");
        connectButton.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                if (connectButton.isSelected())
                {
                    if (!NetworkUtil.hasInternetAccess())
                    {
                        new AnnounceFrame("You need internet connection to connect to the chat.", 10);
                        connectButton.setSelected(false);
                    }
                    else
                        connect();
                }
                else
                    disconnect("Manual disconnect");
            }
        });
        topPanel.add(connectButton);

        Component horizontalStrut_1 = Box.createHorizontalStrut(2);
        topPanel.add(horizontalStrut_1);

        showUserPaneButton = new IcyToggleButton(new IcyIcon("user", 20));
        showUserPaneButton.setFocusPainted(false);
        showUserPaneButton.setMaximumSize(new Dimension(32, 32));
        showUserPaneButton.setSelected(ChatPreferences.getShowUsersPanel());
        showUserPaneButton.setMinimumSize(new Dimension(24, 24));
        showUserPaneButton.setPreferredSize(new Dimension(24, 24));
        showUserPaneButton.setToolTipText("Show connected users");
        showUserPaneButton.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                final boolean visible = showUserPaneButton.isSelected();

                ChatPreferences.setShowUsersPanel(visible);
                refreshGUI();
            }
        });
        topPanel.add(showUserPaneButton);

        Component horizontalStrut_5 = Box.createHorizontalStrut(2);
        topPanel.add(horizontalStrut_5);

        desktopOverlayButton = new IcyToggleButton(new IcyIcon(ResourceUtil.ICON_CHAT, 20));
        desktopOverlayButton.setFocusPainted(false);
        desktopOverlayButton.setMaximumSize(new Dimension(32, 32));
        desktopOverlayButton.setSelected(ChatPreferences.getDesktopOverlay());
        desktopOverlayButton.setMinimumSize(new Dimension(24, 24));
        desktopOverlayButton.setPreferredSize(new Dimension(24, 24));
        desktopOverlayButton.setToolTipText("Enabled chat on desktop");
        desktopOverlayButton.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                final boolean visible = desktopOverlayButton.isSelected();

                ChatPreferences.setDesktopOverlay(visible);
                refreshDesktopOverlayState();
            }
        });
        topPanel.add(desktopOverlayButton);

        Component horizontalStrut_6 = Box.createHorizontalStrut(2);
        topPanel.add(horizontalStrut_6);

        advancedButton = new IcyButton(new IcyIcon(ResourceUtil.ICON_COG, 20));
        advancedButton.setFocusPainted(false);
        advancedButton.setMaximumSize(new Dimension(32, 32));
        advancedButton.setMinimumSize(new Dimension(24, 24));
        advancedButton.setPreferredSize(new Dimension(24, 24));
        advancedButton.setToolTipText("Advanced settings");
        advancedButton.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                new PreferenceFrame(ChatPreferencePanel.NODE_NAME);
            }
        });
        topPanel.add(advancedButton);
    }

    protected String fixChannelName(String channel)
    {
        if (!StringUtil.isEmpty(channel) && (channel.charAt(0) != '#'))
            return "#" + channel;

        return channel;
    }

    public void addChannelPane(final String channel)
    {
        // already exists...
        if (getChannelPaneIndex(channel) != -1)
            return;

        ThreadUtil.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                final ChannelPanel channelPane = new ChannelPanel(channel);

                // add to list
                channelPanes.add(channelPane);

                // and add to gui
                int index = tabPane.indexOfTab(channel);
                // add only if not already present (should alway be the case)
                if (index == -1)
                {
                    tabPane.addTab(channel, channelPane.scrollPane);
                    // get index
                    index = tabPane.indexOfTab(channel);
                    // default channel cannot be closed
                    if (channel.equals(DEFAULT_CHANNEL))
                        tabPane.setTabClosable(index, false);
                    tabPane.setSelectedIndex(index);
                }
            }
        });
    }

    public void removeChannelPane(final String channel)
    {
        final int ind = getChannelPaneIndex(channel);

        // channel exists --> remove it
        if (ind != -1)
        {
            ThreadUtil.invokeLater(new Runnable()
            {
                @Override
                public void run()
                {
                    channelPanes.remove(ind);

                    // remove from tabbed pane if needed
                    final int indTab = tabPane.indexOfTab(channel);
                    if (indTab != -1)
                        tabPane.removeTabAt(indTab);
                }
            });
        }
    }

    public int getChannelPaneIndex(String channel)
    {
        final String c;

        // empty channel means default channel
        if (StringUtil.isEmpty(channel))
            c = DEFAULT_CHANNEL;
        else
            c = channel;

        for (int i = 0; i < channelPanes.size(); i++)
        {
            final ChannelPanel cp = channelPanes.get(i);

            if (cp.name.equalsIgnoreCase(c))
                return i;
        }

        return -1;
    }

    public ChannelPanel getChannelPane(String channel)
    {
        final int ind = getChannelPaneIndex(channel);

        if (ind != -1)
            return channelPanes.get(ind);

        return null;
    }

    /**
     * Returns the current visible channel (tab channel visible).
     */
    public String getCurrentChannel()
    {
        final int ind = tabPane.getSelectedIndex();

        if (ind != -1)
            return tabPane.getTitleAt(ind);

        return null;
    }

    /**
     * Mark channel pane with blue color.
     */
    protected void markChannelPane(String channel)
    {
        final int index = getChannelPaneIndex(channel);

        if ((index != -1) && (tabPane.getSelectedIndex() != index))
        {
            ThreadUtil.invokeLater(new Runnable()
            {
                @Override
                public void run()
                {
                    // change output console tab color when new data
                    if (index < tabPane.getTabCount())
                        tabPane.setBackgroundAt(index, Color.blue);
                }
            });
        }
    }

    public JTextPane getChannelEditor(String channel)
    {
        final ChannelPanel cp = getChannelPane(channel);

        if (cp != null)
            return cp.editor;

        return null;
    }

    public StyledDocument getChannelDocument(String channel)
    {
        final ChannelPanel cp = getChannelPane(channel);

        if (cp != null)
            return cp.doc;

        return null;
    }

    protected boolean isCurrentUser(String nick)
    {
        return StringUtil.equals(nick, ChatPreferences.getNickname());
    }

    protected boolean isCurrentUser(IRCUser u)
    {
        if (u != null)
            return isCurrentUser(u.getNick());

        return false;
    }

    // int getUsersScrollPaneWidth()
    // {
    // int result = usersScrollPane.getSize().width;
    // if (result == 0)
    // return usersScrollPane.getPreferredSize().width;
    // return result;
    // }
    //
    // int getCurrentWidth()
    // {
    // int result = getSize().width;
    // if (result == 0)
    // return getPreferredSize().width;
    // return result;
    // }

    void addMessage(final String nick, final String target, final String msg)
    {
        ThreadUtil.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                final String channel;
                final String nickStr;

                if (StringUtil.isEmpty(target))
                    channel = DEFAULT_CHANNEL;
                else
                    channel = target;

                if (StringUtil.isEmpty(nick))
                    nickStr = "";
                else
                    nickStr = "<" + nick + "> ";

                final String timeStr = DateUtil.now("[HH:mm] ");

                synchronized (content)
                {
                    content.append(channel + ": " + timeStr + nickStr + msg + "\n");

                    // limit to maximum size
                    if (content.length() > MAX_SIZE)
                        content.delete(0, content.length() - MAX_SIZE);

                    refreshDesktopOverlay();
                }

                try
                {
                    final JTextPane editor = getChannelEditor(channel);
                    final StyledDocument doc = getChannelDocument(channel);

                    if ((editor != null) && (doc != null))
                    {
                        synchronized (editor)
                        {
                            IRCUtil.insertString(timeStr + nickStr + msg + "\n", doc, attributes);

                            // limit to maximum size
                            if (doc.getLength() > MAX_SIZE)
                                doc.remove(0, doc.getLength() - MAX_SIZE);

                            editor.setCaretPosition(doc.getLength());
                        }
                    }
                }
                catch (BadLocationException e)
                {
                    e.printStackTrace();
                }

                // mark channel pane color
                markChannelPane(channel);
            }
        });
    }

    /**
     * Refresh GUI state
     */
    void refreshGUI()
    {
        ThreadUtil.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                if (isConnected())
                {
                    final String nick = client.getNick();

                    if (!StringUtil.equals(txtNickName.getText(), nick))
                        txtNickName.setText(nick);

                    sendEditor.setEditable(true);
                    sendEditorDesktop.setEditable(true);
                    connectButton.setSelected(true);
                    connectButton.setToolTipText("Connected - Click to disconnect");
                }
                else
                {
                    sendEditor.setEditable(false);
                    sendEditorDesktop.setEditable(false);
                    connectButton.setSelected(false);
                    connectButton.setToolTipText("Not connected - Click to connect");
                }

                // user panel visible
                remove(usersScrollPane);
                panelBottom.remove(usersScrollPane);
                if (ChatPreferences.getShowUsersPanel())
                {
                    if ((getWidth() * 1.5) > getHeight())
                        add(usersScrollPane, BorderLayout.EAST);
                    else
                        panelBottom.add(usersScrollPane, BorderLayout.CENTER);
                }

                validate();
            }
        });
    }

    /**
     * Refresh users list
     */
    void refreshUsers()
    {
        if (isConnected())
        {
            final String channel = getCurrentChannel();

            if (!StringUtil.isEmpty(channel))
            {
                // private channel ?
                if (channel.charAt(0) != '#')
                    // display current user and channel name as users
                    userList.setListData(new String[] {ChatPreferences.getNickname(), channel});
                else
                    client.doNames(channel);
            }
        }
        else
            userList.setListData(new String[0]);
    }

    /**
     * Refresh desktop overlay state
     */
    public void refreshDesktopOverlayState()
    {
        final MainFrame mainFrame = Icy.getMainInterface().getMainFrame();
        final IcyDesktopPane desktopPane = Icy.getMainInterface().getDesktopPane();

        if ((mainFrame != null) && (desktopPane != null))
        {
            final JPanel centerPanel = mainFrame.getCenterPanel();

            // desktop overlay enable ?
            if (ChatPreferences.getDesktopOverlay())
            {
                // add desktop overlay
                desktopPane.addOverlay(desktopOverlay);
                centerPanel.add(desktopPanel, BorderLayout.SOUTH);
                centerPanel.revalidate();
            }
            else
            {
                // remove desktop overlay
                desktopPane.removeOverlay(desktopOverlay);
                centerPanel.remove(desktopPanel);
                centerPanel.revalidate();
            }

            // refresh desktop
            desktopPane.repaint();
        }

        desktopOverlayButton.setSelected(ChatPreferences.getDesktopOverlay());
    }

    /**
     * Refresh desktop overlay
     */
    void refreshDesktopOverlay()
    {
        final IcyDesktopPane desktopPane = Icy.getMainInterface().getDesktopPane();

        // refresh desktop overlay
        if ((desktopPane != null) && ChatPreferences.getDesktopOverlay())
            desktopPane.repaint();
    }

    @Override
    public void internetUp()
    {
        if (!isConnected() && ChatPreferences.getAutoConnect())
            connect();
    }

    @Override
    public void internetDown()
    {
        // disconnect("Connection lost");
    }
}