@Override
        public void actionPerformed(ActionEvent e) {
            int indexOfChannel = tabbedPane.getSelectedIndex();
            if (indexOfChannel < tabbedPane.getTabCount() - 1) tabbedPane.setSelectedIndex(indexOfChannel + 1);
        }
    }





    /** Action for changing the currently selected tab */
    static class PreviousTabAction extends AbstractAction {
        private static PreviousTabAction ref = null;

        private PreviousTabAction(String text, ImageIcon icon, String desc, Integer mnemonic) {
            super(text, icon);
            putValue(SHORT_DESCRIPTION, desc);
            putValue(MNEMONIC_KEY, mnemonic);
        }

        public static PreviousTabAction getInstance() {
            if (ref == null) ref = new PreviousTabAction("PreviousTab", prevTabIcon, null, KeyEvent.VK_P);
            return ref;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            int indexOfChannel = tabbedPane.getSelectedIndex();
            if (indexOfChannel > 0) tabbedPane.setSelectedIndex(indexOfChannel - 1);
        }
    }




    /** A small dialog allowing quick connection to a server */
    static class QuickConnectAction extends AbstractAction {
        private static QuickConnectAction ref = null;

        private QuickConnectAction(String text, ImageIcon icon, String desc, Integer mnemonic) {
            super(text, icon);
            putValue(SHORT_DESCRIPTION, desc);
            putValue(MNEMONIC_KEY, mnemonic);
        }

        public static QuickConnectAction getInstance() {
            if (ref == null) ref = new QuickConnectAction("Quick Connect", quickConnectIcon, null, KeyEvent.VK_C);
            return ref;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            SpringLayout layout = new SpringLayout();

            final JDialog dialog = new JDialog(frame, "Quick Connect", true);
            dialog.getContentPane().setLayout(layout);
            dialog.setPreferredSize(new Dimension(200, 340));
            final JButton connectButton = new JButton("Connect");
            JButton cancelButton = new JButton("Cancel");
            connectButton.setPreferredSize(new Dimension(80, 30));
            cancelButton.setPreferredSize(new Dimension(70, 30));

            JLabel networkNameLabel = new JLabel("Network Name");
            JLabel serverLabel = new JLabel("Server");
            JLabel portLabel = new JLabel("Port");
            JLabel nickLabel = new JLabel("Nick");
            JLabel passLabel = new JLabel("Password");

            final JTextField networkNameField = new JTextField();
            networkNameField.setPreferredSize(new Dimension(175, 25));
            final JTextField serverField = new JTextField();
            serverField.setPreferredSize(new Dimension(175, 25));
            final JTextField portField = new JTextField();
            portField.setPreferredSize(new Dimension(175, 25));
            final JTextField nickField = new JTextField();
            nickField.setPreferredSize(new Dimension(175, 25));
            final JTextField passField = new JTextField();
            passField.setPreferredSize(new Dimension(175, 25));

            layout.putConstraint(SpringLayout.WEST, networkNameLabel, 5, SpringLayout.WEST, dialog);
            layout.putConstraint(SpringLayout.NORTH, networkNameLabel, 5, SpringLayout.NORTH, dialog);

            layout.putConstraint(SpringLayout.WEST, networkNameField, 5, SpringLayout.WEST, dialog);
            layout.putConstraint(SpringLayout.NORTH, networkNameField, 5, SpringLayout.SOUTH, networkNameLabel);

            layout.putConstraint(SpringLayout.WEST, serverLabel, 5, SpringLayout.WEST, dialog);
            layout.putConstraint(SpringLayout.NORTH, serverLabel, 5, SpringLayout.SOUTH, networkNameField);

            layout.putConstraint(SpringLayout.WEST, serverField, 5, SpringLayout.WEST, dialog);
            layout.putConstraint(SpringLayout.NORTH, serverField, 5, SpringLayout.SOUTH, serverLabel);

            layout.putConstraint(SpringLayout.WEST, portLabel, 5, SpringLayout.WEST, dialog);
            layout.putConstraint(SpringLayout.NORTH, portLabel, 5, SpringLayout.SOUTH, serverField);

            layout.putConstraint(SpringLayout.WEST, portField, 5, SpringLayout.WEST, dialog);
            layout.putConstraint(SpringLayout.NORTH, portField, 5, SpringLayout.SOUTH, portLabel);

            layout.putConstraint(SpringLayout.WEST, nickLabel, 5, SpringLayout.WEST, dialog);
            layout.putConstraint(SpringLayout.NORTH, nickLabel, 5, SpringLayout.SOUTH, portField);

            layout.putConstraint(SpringLayout.WEST, nickField, 5, SpringLayout.WEST, dialog);
            layout.putConstraint(SpringLayout.NORTH, nickField, 5, SpringLayout.SOUTH, nickLabel);

            layout.putConstraint(SpringLayout.WEST, passLabel, 5, SpringLayout.WEST, dialog);
            layout.putConstraint(SpringLayout.NORTH, passLabel, 5, SpringLayout.SOUTH, nickField);

            layout.putConstraint(SpringLayout.WEST, passField, 5, SpringLayout.WEST, dialog);
            layout.putConstraint(SpringLayout.NORTH, passField, 5, SpringLayout.SOUTH, passLabel);

            layout.putConstraint(SpringLayout.WEST, cancelButton, 15, SpringLayout.WEST, dialog);
            layout.putConstraint(SpringLayout.SOUTH, cancelButton, -55, SpringLayout.SOUTH, dialog);

            layout.putConstraint(SpringLayout.WEST, connectButton, 10, SpringLayout.EAST, cancelButton);
            layout.putConstraint(SpringLayout.SOUTH, connectButton, -55, SpringLayout.SOUTH, dialog);

            dialog.add(networkNameLabel);
            dialog.add(networkNameField);
            dialog.add(serverLabel);
            dialog.add(serverField);
            dialog.add(portLabel);
            dialog.add(portField);
            dialog.add(nickLabel);
            dialog.add(nickField);
            dialog.add(passLabel);
            dialog.add(passField);
            dialog.add(connectButton);
            dialog.add(cancelButton);


            connectButton.addKeyListener(new KeyAdapter() {
                @Override
                public void keyPressed(KeyEvent evt) {
                    if (evt.getKeyCode() == KeyEvent.VK_ENTER) {
                        connectButton.getActionListeners()[0].actionPerformed(null);
                    }
                    if (evt.getKeyCode() == KeyEvent.VK_ENTER) {
                        dialog.dispose();
                    }
                }
            });

            connectButton.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    String networkName = networkNameField.getText().trim();
                    String server = serverField.getText().trim();
                    String port = portField.getText().trim();
                    String nick = nickField.getText().trim();
                    String pass = passField.getText().trim();

                    if (networkName.isEmpty() || server.isEmpty() || port.isEmpty() || nick.isEmpty()) {
                        JOptionPane.showMessageDialog(dialog, "Network name, server, port and nick are required");
                    } else {
                        dialog.dispose();
                        Connection.nicks[0] = nick;

                        for (int i = 0; i < tabbedPane.getTabCount(); i++) {
                            ChannelPanel channel = (ChannelPanel) tabbedPane.getComponentAt(i);
                            if (networkName.equals(channel.title)) {
                                tabbedPane.setSelectedComponent(channel);
                                String[] msg = {null, "You are already connected to " + networkName + "."};
                                try {
                                    channel.insertString(msg, errorStyle, false);
                                } catch (BadLocationException | IOException ex) {
                                    Logger.getLogger(GUI.class.getName()).log(Level.SEVERE, null, ex);
                                }
                                return;
                            }
                        }

                        Connection c = new Connection(networkName, server, Integer.valueOf(port), false);
                        if (!pass.isEmpty()) c.password = pass;
                    }
                }

            });
            cancelButton.addKeyListener(new KeyAdapter() {
                @Override
                public void keyPressed(KeyEvent e) {
                    if (e.getKeyCode() == KeyEvent.VK_ENTER || e.getKeyCode() == KeyEvent.VK_ESCAPE) {
                        dialog.dispose();
                    }
                }
            });
            cancelButton.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    dialog.dispose();
                }
            });

            cancelButton.getRootPane().setDefaultButton(cancelButton);
            cancelButton.requestFocus();
            dialog.pack();
            dialog.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
            dialog.setLocationRelativeTo(frame);
            dialog.setVisible(true);
        }
    }




    /** Menu item for closing the client */
    static class QuitProgramAction extends AbstractAction {
        private static QuitProgramAction ref = null;

        private QuitProgramAction(String text, ImageIcon icon, String desc, Integer mnemonic) {
            super(text, icon);
            putValue(SHORT_DESCRIPTION, desc);
            putValue(MNEMONIC_KEY, mnemonic);
        }

        public static QuitProgramAction getInstance() {
            if (ref == null) ref = new QuitProgramAction("Quit", quitProgramIcon, null, KeyEvent.VK_Q);
            return ref;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            System.exit(0);
        }
    }




    /** Closes and reopens server connections */
    static class ReconnectAction extends AbstractAction {
        private static ReconnectAction ref = null;

        private ReconnectAction(String text, ImageIcon icon, String desc, Integer mnemonic) {
            super(text, icon);
            putValue(SHORT_DESCRIPTION, desc);
            putValue(MNEMONIC_KEY, mnemonic);
        }

        public static ReconnectAction getInstance() {
            if (ref == null) ref = new ReconnectAction("Reconnect", reconnectIcon, null, KeyEvent.VK_R);
            return ref;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            ChannelPanel selected = (ChannelPanel) tabbedPane.getSelectedComponent();
            if (selected == null) return;
            if (selected.connection.isConnected) {
                selected.connection.disconnect();
            }

            //sleep ffor one second to avoid Error: reconnecting too fast from server
            try {
                selected.connection.thread.sleep(1000);
            } catch (InterruptedException e1) {
                System.out.println("reconnect sleep interrupted");
            }

            for (int j = 0; j < tabbedPane.getTabCount(); j++) {
                ChannelPanel otherChannel = (ChannelPanel) tabbedPane.getComponentAt(j);
                if (selected.connection == otherChannel.connection) {
                    selected.connection.thread = new Thread(selected.connection);
                    selected.connection.thread.start();
                    break;
                }
            }
            selected.connection.autoconnect = false;

            //because we can't rejoin channels until the connection is established
            //ths should check for the connection and not just blindly wait

            int tries = 0;
            while (tries != 3) {
                if (!selected.connection.isConnected) {
                    //try to reconnect every 1.5 seconds up to 4 times
                    try {
                        selected.connection.thread.sleep(1500);
                        tries++;
                    } catch (InterruptedException e1) {
                        System.out.println("interrupted sleep");
                    }
                }
                else break;
            }

            if (selected.connection.isConnected) {
                for (int i = 0; i < tabbedPane.getTabCount(); i++) {
                    ChannelPanel channel = (ChannelPanel) tabbedPane.getComponentAt(i);
                    if (channel.connection == selected.connection) {
                        if (channel.title.startsWith("#")) {
                            try {
                                selected.connection.send("JOIN " + channel.title);
                            } catch (IOException ex) {
                                System.out.println("io");
                            } catch (BadLocationException ex) {
                                Logger.getLogger(GUI.class.getName()).log(Level.SEVERE, null, ex);
                            }
                        }
                    }
                }
            }
        }
    }





    /** Displays the list of saved connections and channels to auto-connect to*/
    static class ServerListAction extends AbstractAction {
        private static ServerListAction ref = null;

        private ServerListAction(String text, ImageIcon icon, String desc, Integer mnemonic) {
            super(text, icon);
            putValue(SHORT_DESCRIPTION, desc);
            putValue(MNEMONIC_KEY, mnemonic);
        }

        public static ServerListAction getInstance() {
            if (ref == null) ref = new ServerListAction("Server List", serverListIcon, null, KeyEvent.VK_L);
            return ref;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            final JDialog dialog = new JDialog(frame, "Server List", true);
            Container contentpane = dialog.getContentPane();
            SpringLayout layout = new SpringLayout();
            contentpane.setLayout(layout);
            contentpane.setPreferredSize(new Dimension(550, 200));
            final BeanTableModel<SavedConnection> model = new BeanTableModel<>(SavedConnection.class);
            model.sortColumnNames();
            for (SavedConnection savedConnection : savedConnections) model.addRow(savedConnection);


            final JTable table = new JTable(model) {
                @Override
                public boolean isCellEditable(int row, int column) {
                    return false;
                }
            };

            table.setShowGrid(false);
            table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
            table.getColumnModel().getColumn(0).setPreferredWidth(150);
            table.getColumnModel().getColumn(1).setPreferredWidth(120);
            table.getColumnModel().getColumn(2).setPreferredWidth(272);

            JScrollPane scrollPane = new JScrollPane(table);
            scrollPane.setPreferredSize(new Dimension(540, 150));
            scrollPane.setViewportView(table);
            scrollPane.getViewport().setBackground(Color.white);

            final JButton add = new JButton("Add");
            JButton edit = new JButton("Edit");
            JButton remove = new JButton("Remove");
            JButton connect = new JButton("Connect");
            add.setPreferredSize(new Dimension(70, 30));
            edit.setPreferredSize(new Dimension(70, 30));
            remove.setPreferredSize(new Dimension(80, 30));
            connect.setPreferredSize(new Dimension(80, 30));

            scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
            scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
            dialog.setResizable(false);
            dialog.setTitle("Server List");
            contentpane.add(scrollPane);
            contentpane.add(add);
            contentpane.add(edit);
            contentpane.add(remove);
            contentpane.add(connect);
            layout.putConstraint(SpringLayout.WEST, scrollPane, 5, SpringLayout.WEST, contentpane);
            layout.putConstraint(SpringLayout.SOUTH, add, -10, SpringLayout.SOUTH, contentpane);
            layout.putConstraint(SpringLayout.WEST, add, 10, SpringLayout.WEST, contentpane);
            layout.putConstraint(SpringLayout.SOUTH, edit, -10, SpringLayout.SOUTH, contentpane);
            layout.putConstraint(SpringLayout.WEST, edit, 10, SpringLayout.EAST, add);
            layout.putConstraint(SpringLayout.SOUTH, remove, -10, SpringLayout.SOUTH, contentpane);
            layout.putConstraint(SpringLayout.WEST, remove, 10, SpringLayout.EAST, edit);
            layout.putConstraint(SpringLayout.SOUTH, connect, -10, SpringLayout.SOUTH, contentpane);
            layout.putConstraint(SpringLayout.EAST, connect, -15, SpringLayout.EAST, contentpane);

            add.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    JTextField name = new JTextField();
                    JTextField server = new JTextField();
                    JTextField port = new JTextField();
                    JTextField password = new JTextField();
                    JTextField channels = new JTextField();
                    JCheckBox autoconnect = new JCheckBox();

                    Object[] fields = {
                            "Name", name,
                            "Server", server,
                            "Port", port,
                            "Password", password,
                            "Auto Join Channels (#c1 #c2)", channels,
                            "Connect on startup", autoconnect,
                    };
                    int x = JOptionPane.showConfirmDialog(dialog, fields, "Add Server", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
                    if (x == JOptionPane.CLOSED_OPTION || x == JOptionPane.CANCEL_OPTION) return;

                    String[] s = channels.getText().split(" ");
                    ArrayList<String> channelList = new ArrayList<>(Arrays.asList(s));
                    if (!StringUtils.isNumeric(port.getText())) {
                        JOptionPane.showMessageDialog(dialog, "Port must be an integer.");
                        return;
                    }
                    SavedConnection connection = new SavedConnection(name.getText(), server.getText(), password.getText(), channelList, autoconnect.isSelected(), Integer.valueOf(port.getText().trim()));
                    model.addRow(connection);
                    savedConnections.add(connection);
                    serializeSavedConnections();
                }
            });

            add.addKeyListener(new KeyAdapter() {
                @Override
                public void keyPressed(KeyEvent evt) {
                    if (evt.getKeyCode() == KeyEvent.VK_ENTER) {
                        add.getActionListeners()[0].actionPerformed(null);
                    }
                }
            });
            edit.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    int rowIndex = table.getSelectedRow();
                    if (rowIndex == -1) return;
                    int modelIndex = table.convertRowIndexToModel(rowIndex);
                    SavedConnection conn = model.modelData.get(modelIndex);

                    JTextField ename = new JTextField(conn.retrieveName());
                    JTextField eserver = new JTextField(conn.retrieveServer());
                    JTextField eport = new JTextField(Integer.toString(conn.retrievePort()));
                    JTextField epassword = new JTextField(conn.retrievePassword());
                    String c = conn.retrieveChannels().toString();
                    JTextField echannels = new JTextField(c.substring(1, c.length() - 1));
                    JCheckBox eautoconnect = new JCheckBox();
                    if (conn.retrieveAutoConnect()) eautoconnect.setSelected(true);
                    else eautoconnect.setSelected(false);

                    Object[] efields = {
                            "Name", ename,
                            "Server", eserver,
                            "Port", eport,
                            "Password", epassword,
                            "Auto Join Channels (#c1 #c2)", echannels,
                            "Connect on startup", eautoconnect,
                    };
                    int ex = JOptionPane.showConfirmDialog(dialog, efields, "Edit Server", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
                    if (ex == JOptionPane.CLOSED_OPTION || ex == JOptionPane.CANCEL_OPTION) return;

                    String[] es = echannels.getText().replace(",", "").split(" ");
                    ArrayList<String> echannelList = new ArrayList<>(Arrays.asList(es));
                    if (!StringUtils.isNumeric(eport.getText())) {
                        JOptionPane.showMessageDialog(dialog, "Port must be an integer.");
                        return;
                    }
                    model.removeRowRange(modelIndex, modelIndex);
                    savedConnections.remove(conn);
                    SavedConnection connection = new SavedConnection(ename.getText(), eserver.getText(), epassword.getText(), echannelList, eautoconnect.isSelected(), Integer.valueOf(eport.getText().trim()));
                    model.addRow(connection);
                    savedConnections.add(connection);
                    serializeSavedConnections();
                }
            });

            remove.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    int rowIndex = table.getSelectedRow();
                    if (rowIndex == -1) return;
                    int modelIndex = table.convertRowIndexToModel(rowIndex);
                    SavedConnection conn = model.modelData.get(modelIndex);
                    savedConnections.remove(conn);
                    serializeSavedConnections();
                    model.removeRowRange(modelIndex, modelIndex);
                }
            });

            connect.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    int rowIndex = table.getSelectedRow();
                    if (rowIndex == -1) return;
                    SavedConnection conn = model.modelData.get(table.convertRowIndexToModel(rowIndex));

                    String network = conn.getNetwork(), server = conn.getServer();
                    int port = conn.retrievePort();
                    for (int i = 0; i < tabbedPane.getTabCount(); i++) {
                        ChannelPanel channel = (ChannelPanel) tabbedPane.getComponentAt(i);
                        if (network.equals(channel.title)) {
                            tabbedPane.setSelectedComponent(channel);
                            String[] msg = {null, "You are already connected to " + network + "."};
                            try {
                                channel.insertString(msg, errorStyle, false);
                            } catch (BadLocationException | IOException ex) {
                                Logger.getLogger(GUI.class.getName()).log(Level.SEVERE, null, ex);
                            }
                            dialog.dispose();
                            return;
                        }
                    }
                    new Connection(network, server, port);
                    dialog.dispose();
                }
            });
            dialog.pack();
            dialog.setLocationRelativeTo(frame);
            dialog.setVisible(true);
        }
    }



    /** Toggles visibility of the user list of a given channel */
    static class ShowNickListAction extends AbstractAction {
        private static ShowNickListAction ref = null;

        private ShowNickListAction(String text, ImageIcon icon, String desc, Integer mnemonic) {
            super(text, icon);
            putValue(SHORT_DESCRIPTION, desc);
            putValue(MNEMONIC_KEY, mnemonic);
        }

        public static ShowNickListAction getInstance() {
            if (ref == null) ref = new ShowNickListAction("Show/Hide Nicklist", showNicklistIcon, null, KeyEvent.VK_N);
            return ref;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            for (int i = 0; i < tabbedPane.getTabCount(); i++) {
                ChannelPanel channel = (ChannelPanel) tabbedPane.getComponentAt(i);
                if (channel.title.equals(channel.name)) {
                    if (channel.getRightComponent() != null) {
                        channel.setRightComponent(null);
                        channel.setDividerSize(0);
                        channel.setDividerLocation(0);
                    } else {
                        channel.setRightComponent(channel.userListScrollPane);
                        channel.setDividerSize(5);
                        channel.setDividerLocation(GUI.frame.getWidth() - 160);
                    }
                }
            }
        }
    }
    static class ColorHashMap<K,V> extends HashMap<K,V> {
        protected V defaultValue;
        public ColorHashMap(V defaultValue) {
            this.defaultValue = defaultValue;
        }
        @Override
        public V get(Object k) {
            return containsKey(k) ? super.get(k) : defaultValue;
        }