/*
 * Copyright (c) 2013 IRCCloud, Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.irccloud.android;

import android.text.TextUtils;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.irccloud.android.data.EventsDataSource;
import com.irccloud.android.data.ServersDataSource;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;

public class CollapsedEventsList {
    public static final int TYPE_NETSPLIT = -1;
    public static final int TYPE_JOIN = 0;
    public static final int TYPE_PART = 1;
    public static final int TYPE_QUIT = 2;
    public static final int TYPE_MODE = 3;
    public static final int TYPE_POPIN = 4;
    public static final int TYPE_POPOUT = 5;
    public static final int TYPE_NICKCHANGE = 6;
    public static final int TYPE_CONNECTIONSTATUS = 7;

    public static final int MODE_OPER = 0;
    public static final int MODE_OWNER = 1;
    public static final int MODE_ADMIN = 2;
    public static final int MODE_OP = 3;
    public static final int MODE_HALFOP = 4;
    public static final int MODE_VOICE = 5;

    public static final int MODE_DEOPER = 6;
    public static final int MODE_DEOWNER = 7;
    public static final int MODE_DEADMIN = 8;
    public static final int MODE_DEOP = 9;
    public static final int MODE_DEHALFOP = 10;
    public static final int MODE_DEVOICE = 11;

    public static final int MODE_COUNT = 12;

    public boolean showChan = false;

    public class CollapsedEvent {
        long eid;
        int type;
        boolean modes[] = new boolean[MODE_COUNT];
        String nick;
        String old_nick;
        String hostmask;
        String msg;
        String from_mode;
        String from_nick;
        String target_mode;
        String chan;
        boolean netsplit;
        int count;

        public String toString() {
            return "{type: " + type + ", nick: " + nick + ", old_nick: " + old_nick + ", hostmask: " + hostmask + ", msg: " + msg + "netsplit: " + netsplit + "}";
        }

        public int modeCount() {
            int count = 0;
            for (int i = 0; i < MODE_COUNT; i++) {
                if (modes[i])
                    count++;
            }
            return count;
        }

        public boolean addMode(String mode) {
            if (mode.equalsIgnoreCase(server != null ? server.MODE_OPER : "Y")) {
                if (modes[MODE_DEOPER])
                    modes[MODE_DEOPER] = false;
                else
                    modes[MODE_OPER] = true;
            } else if (mode.equalsIgnoreCase(server != null ? server.MODE_OWNER : "q")) {
                if (modes[MODE_DEOWNER])
                    modes[MODE_DEOWNER] = false;
                else
                    modes[MODE_OWNER] = true;
            } else if (mode.equalsIgnoreCase(server != null ? server.MODE_ADMIN : "a")) {
                if (modes[MODE_DEADMIN])
                    modes[MODE_DEADMIN] = false;
                else
                    modes[MODE_ADMIN] = true;
            } else if (mode.equalsIgnoreCase(server != null ? server.MODE_OP : "o")) {
                if (modes[MODE_DEOP])
                    modes[MODE_DEOP] = false;
                else
                    modes[MODE_OP] = true;
            } else if (mode.equalsIgnoreCase(server != null ? server.MODE_HALFOP : "h")) {
                if (modes[MODE_DEHALFOP])
                    modes[MODE_DEHALFOP] = false;
                else
                    modes[MODE_HALFOP] = true;
            } else if (mode.equalsIgnoreCase(server != null ? server.MODE_VOICED : "v")) {
                if (modes[MODE_DEVOICE])
                    modes[MODE_DEVOICE] = false;
                else
                    modes[MODE_VOICE] = true;
            } else {
                return false;
            }
            if (modeCount() == 0)
                return addMode(mode);
            return true;
        }

        public boolean removeMode(String mode) {
            if (mode.equalsIgnoreCase(server != null ? server.MODE_OPER : "Y")) {
                if (modes[MODE_OPER])
                    modes[MODE_OPER] = false;
                else
                    modes[MODE_DEOPER] = true;
            } else if (mode.equalsIgnoreCase(server != null ? server.MODE_OWNER : "q")) {
                if (modes[MODE_OWNER])
                    modes[MODE_OWNER] = false;
                else
                    modes[MODE_DEOWNER] = true;
            } else if (mode.equalsIgnoreCase(server != null ? server.MODE_ADMIN : "a")) {
                if (modes[MODE_ADMIN])
                    modes[MODE_ADMIN] = false;
                else
                    modes[MODE_DEADMIN] = true;
            } else if (mode.equalsIgnoreCase(server != null ? server.MODE_OP : "o")) {
                if (modes[MODE_OP])
                    modes[MODE_OP] = false;
                else
                    modes[MODE_DEOP] = true;
            } else if (mode.equalsIgnoreCase(server != null ? server.MODE_HALFOP : "h")) {
                if (modes[MODE_HALFOP])
                    modes[MODE_HALFOP] = false;
                else
                    modes[MODE_DEHALFOP] = true;
            } else if (mode.equalsIgnoreCase(server != null ? server.MODE_VOICED : "v")) {
                if (modes[MODE_VOICE])
                    modes[MODE_VOICE] = false;
                else
                    modes[MODE_DEVOICE] = true;
            } else {
                return false;
            }
            if (modeCount() == 0)
                return removeMode(mode);
            return true;
        }

        public String getModes(boolean showSymbol) {
            final String[] mode_msgs = {
                    "promoted to oper",
                    "promoted to owner",
                    "promoted to admin",
                    "opped",
                    "halfopped",
                    "voiced",
                    "demoted from oper",
                    "demoted from owner",
                    "demoted from admin",
                    "de-opped",
                    "de-halfopped",
                    "de-voiced",
            };

            String output = null;
            if (modeCount() > 0) {
                output = "";

                for (int i = 0; i < MODE_COUNT; i++) {
                    if (modes[i]) {
                        if (output.length() > 0)
                            output += ", ";
                        output += mode_msgs[i];
                        if (showSymbol) {
                            output += " (\u0004" + mode_colors.get(mode_modes[i].substring(1)) + mode_modes[i] + "\u000f)";
                        }
                    }
                }
            }

            return output;
        }
    }

    public class comparator implements Comparator<CollapsedEvent> {
        public int compare(CollapsedEvent e1, CollapsedEvent e2) {
            if (e1.type == e2.type) {
                if (e1.eid > e2.eid)
                    return 1;
                else
                    return -1;
            } else if (e1.type > e2.type) {
                return 1;
            } else {
                return -1;
            }
        }
    }

    private ArrayList<CollapsedEvent> data = new ArrayList<CollapsedEvent>();
    private ServersDataSource.Server server;
    private HashMap<String, String> mode_colors;
    private String mode_modes[];

    public CollapsedEventsList() {
        setServer(null);
    }

    public void setServer(ServersDataSource.Server s) {
        server = s;
        if (server != null) {
            mode_colors = new HashMap<String, String>() {{
                put(server.MODE_OPER, "E02305");
                put(server.MODE_OWNER, "E7AA00");
                put(server.MODE_ADMIN, "6500A5");
                put(server.MODE_OP, "BA1719");
                put(server.MODE_HALFOP, "B55900");
                put(server.MODE_VOICED, "25B100");
            }};
            mode_modes = new String[]{
                    "+" + server.MODE_OPER,
                    "+" + server.MODE_OWNER,
                    "+" + server.MODE_ADMIN,
                    "+" + server.MODE_OP,
                    "+" + server.MODE_HALFOP,
                    "+" + server.MODE_VOICED,
                    "-" + server.MODE_OPER,
                    "-" + server.MODE_OWNER,
                    "-" + server.MODE_ADMIN,
                    "-" + server.MODE_OP,
                    "-" + server.MODE_HALFOP,
                    "-" + server.MODE_VOICED
            };
        } else {
            mode_colors = new HashMap<String, String>() {{
                put("Y", "E02305");
                put("q", "E7AA00");
                put("a", "6500A5");
                put("o", "BA1719");
                put("h", "B55900");
                put("v", "25B100");
            }};
            mode_modes = new String[]{
                    "+Y", "+q", "+a", "+o", "+h", "+v", "-Y", "-q", "-a", "-o", "-h", "-v"
            };
        }
    }

    public String toString() {
        String out = "CollapsedEventsList {\n";
        for (int i = 0; i < data.size(); i++) {
            out += "\t" + data.get(i).toString() + "\n";
        }
        out += "}";
        return out;
    }

    public boolean addEvent(EventsDataSource.Event event) {
        String type = event.type;
        if (type.startsWith("you_"))
            type = type.substring(4);

        if (type.equalsIgnoreCase("joined_channel")) {
            addEvent(event.eid, CollapsedEventsList.TYPE_JOIN, event.nick, null, event.hostmask, event.from_mode, null, event.chan);
        } else if (type.equalsIgnoreCase("parted_channel")) {
            addEvent(event.eid, CollapsedEventsList.TYPE_PART, event.nick, null, event.hostmask, event.from_mode, event.msg, event.chan);
        } else if (type.equalsIgnoreCase("quit")) {
            addEvent(event.eid, CollapsedEventsList.TYPE_QUIT, event.nick, null, event.hostmask, event.from_mode, event.msg, event.chan);
        } else if (type.equalsIgnoreCase("nickchange")) {
            addEvent(event.eid, CollapsedEventsList.TYPE_NICKCHANGE, event.nick, event.old_nick, null, event.from_mode, null, event.chan);
        } else if (type.equalsIgnoreCase("socket_closed") || type.equalsIgnoreCase("connecting_failed") || type.equalsIgnoreCase("connecting_cancelled")) {
            addEvent(event.eid, CollapsedEventsList.TYPE_CONNECTIONSTATUS, null, null, null, null, event.msg, null);
        } else if (type.equalsIgnoreCase("user_channel_mode")) {
            JsonNode ops = event.ops;
            if (ops != null) {
                CollapsedEvent e = findEvent(event.nick, event.chan);
                if (e == null) {
                    e = new CollapsedEvent();
                    e.type = TYPE_MODE;
                    e.hostmask = event.hostmask;
                    e.target_mode = event.target_mode;
                    e.nick = event.nick;
                    e.chan = event.chan;
                }
                JsonNode add = ops.get("add");
                for (int i = 0; i < add.size(); i++) {
                    JsonNode op = add.get(i);
                    if (!e.addMode(op.get("mode").asText()))
                        return false;
                    if (e.type == TYPE_MODE) {
                        if (event.from != null && event.from.length() > 0) {
                            e.from_nick = event.from;
                            e.from_mode = event.from_mode;
                        } else {
                            e.from_nick = event.server;
                            e.from_mode = "__the_server__";
                        }
                    } else {
                        e.from_mode = event.target_mode;
                    }
                }
                JsonNode remove = ops.get("remove");
                for (int i = 0; i < remove.size(); i++) {
                    JsonNode op = remove.get(i);
                    if (!e.removeMode(op.get("mode").asText()))
                        return false;
                    if (e.type == TYPE_MODE) {
                        if (event.from != null && event.from.length() > 0) {
                            e.from_nick = event.from;
                            e.from_mode = event.from_mode;
                        } else {
                            e.from_nick = event.server;
                            e.from_mode = "__the_server__";
                        }
                    } else {
                        e.from_mode = event.target_mode;
                    }
                }
                if (!data.contains(e))
                    data.add(e);
            }
        }
        return true;
    }

    public void addEvent(long eid, int type, String nick, String old_nick, String hostmask, String from_mode, String msg, String chan) {
        addEvent(eid, type, nick, old_nick, hostmask, from_mode, msg, null, chan);
    }

    public void addEvent(long eid, int type, String nick, String old_nick, String hostmask, String from_mode, String msg, String target_mode, String chan) {
        CollapsedEvent e = null;

        if (type < TYPE_NICKCHANGE) {
            if (showChan) {
                if (type == TYPE_QUIT) {
                    boolean found = false;
                    for (CollapsedEvent ev : data) {
                        if (ev.type == TYPE_JOIN) {
                            ev.type = TYPE_POPIN;
                            found = true;
                        }
                    }
                    if (found)
                        return;
                } else if (type == TYPE_JOIN) {
                    for (CollapsedEvent ev : data) {
                        if (ev.type == TYPE_QUIT) {
                            ev.type = TYPE_POPOUT;
                            return;
                        }
                    }
                }
            }

            if (old_nick != null && type != TYPE_MODE) {
                e = findEvent(old_nick, chan);
                if (e != null)
                    e.nick = nick;
            }

            if (e == null)
                e = findEvent(nick, chan);

            if (e == null) {
                e = new CollapsedEvent();
                e.eid = eid;
                e.type = type;
                e.nick = nick;
                e.old_nick = old_nick;
                e.hostmask = hostmask;
                e.from_mode = from_mode;
                e.msg = msg;
                e.target_mode = target_mode;
                e.chan = chan;
                data.add(e);
            } else {
                e.eid = eid;
                if (e.type == TYPE_MODE) {
                    e.type = type;
                    e.msg = msg;
                    e.old_nick = old_nick;
                    e.hostmask = hostmask;
                    if (from_mode != null)
                        e.from_mode = from_mode;
                    if (target_mode != null)
                        e.target_mode = target_mode;
                } else if (type != e.type && e.type == TYPE_NICKCHANGE) {
                    e.type = type;
                    e.from_mode = from_mode;
                    e.hostmask = hostmask;
                    e.nick = nick;
                    e.msg = msg;
                } else if (type == TYPE_MODE) {
                    e.from_mode = target_mode;
                } else if (type == TYPE_JOIN) {
                    if (e.type == TYPE_POPIN)
                        e.type = TYPE_JOIN;
                    else
                        e.type = TYPE_POPOUT;
                    e.from_mode = from_mode;
                    e.chan = chan;
                } else if (e.type == TYPE_POPOUT) {
                    e.type = type;
                } else if (e.type != type) {
                    e.type = TYPE_POPIN;
                }
            }
        } else {
            if (type == TYPE_NICKCHANGE) {
                for (CollapsedEvent e1 : data) {
                    if (e1.type == TYPE_NICKCHANGE && e1.nick.equalsIgnoreCase(old_nick)) {
                        if (e1.old_nick.equalsIgnoreCase(nick))
                            data.remove(e1);
                        else
                            e1.nick = nick;
                        return;
                    }
                    if ((e1.type == TYPE_JOIN || e1.type == TYPE_POPOUT) && e1.nick.equalsIgnoreCase(old_nick)) {
                        e1.old_nick = old_nick;
                        e1.nick = nick;
                        for (CollapsedEvent e2 : data) {
                            if ((e2.type == TYPE_QUIT || e2.type == TYPE_PART) && e2.nick.equalsIgnoreCase(nick)) {
                                e1.type = TYPE_POPOUT;
                                data.remove(e2);
                                break;
                            }
                        }
                        if (data.size() > 0)
                            return;
                    }
                    if ((e1.type == TYPE_QUIT || e1.type == TYPE_PART) && e1.nick.equalsIgnoreCase(nick)) {
                        e1.type = TYPE_POPOUT;
                        for (CollapsedEvent e2 : data) {
                            if (e2.type == TYPE_JOIN && e2.nick.equalsIgnoreCase(old_nick)) {
                                data.remove(e2);
                                break;
                            }
                        }
                        if (data.size() > 0)
                            return;
                    }
                }
                e = new CollapsedEvent();
                e.eid = eid;
                e.type = type;
                e.nick = nick;
                e.from_mode = from_mode;
                e.old_nick = old_nick;
                e.hostmask = hostmask;
                e.msg = msg;
                e.chan = chan;
                data.add(e);
            } else if (type == TYPE_CONNECTIONSTATUS) {
                for (CollapsedEvent e1 : data) {
                    if (e1.msg.equals(msg)) {
                        e1.count++;
                        return;
                    }
                }
                e = new CollapsedEvent();
                e.eid = eid;
                e.type = type;
                e.msg = msg;
                e.count = 1;
                data.add(e);
            } else {
                e = new CollapsedEvent();
                e.eid = eid;
                e.type = type;
                e.nick = nick;
                e.from_mode = from_mode;
                e.old_nick = old_nick;
                e.hostmask = hostmask;
                e.msg = msg;
                e.chan = chan;
                data.add(e);
            }
        }

        if (type == TYPE_QUIT && msg != null) {
            if (msg.matches("(?:[^\\s:\\/.]+\\.)+[a-z]{2,} (?:[^\\s:\\/.]+\\.)+[a-z]{2,}")) {
                String[] parts = msg.split(" ");
                if (parts.length > 1 && !parts[0].equals(parts[1])) {
                    e.netsplit = true;
                    boolean found = false;
                    for (CollapsedEvent c : data) {
                        if (c.type == TYPE_NETSPLIT && c.msg.equalsIgnoreCase(msg))
                            found = true;
                    }
                    if (!found && data.size() > 1) {
                        CollapsedEvent c = new CollapsedEvent();
                        c.eid = eid;
                        c.type = TYPE_NETSPLIT;
                        c.msg = msg;
                        data.add(c);
                    }
                }
            }
        }
    }

    public CollapsedEvent findEvent(String nick, String chan) {
        for (CollapsedEvent e : data) {
            if (e.nick != null && (e.nick.equalsIgnoreCase(nick) && (e.chan == null || e.chan.equalsIgnoreCase(chan))))
                return e;
        }
        return null;
    }

    public String formatNick(String nick, String from_mode, boolean colorize) {
        ObjectNode PREFIX = null;
        if (server != null)
            PREFIX = server.PREFIX;

        if (PREFIX == null) {
            PREFIX = new ObjectMapper().createObjectNode();
            PREFIX.put(server != null ? server.MODE_OPER : "Y", "!");
            PREFIX.put(server != null ? server.MODE_OWNER : "q", "~");
            PREFIX.put(server != null ? server.MODE_ADMIN : "a", "&");
            PREFIX.put(server != null ? server.MODE_OP : "o", "@");
            PREFIX.put(server != null ? server.MODE_HALFOP : "h", "%");
            PREFIX.put(server != null ? server.MODE_VOICED : "v", "+");
        }

        String[] colors = {"fc009a", "ff1f1a", "d20004", "fd6508", "880019", "c7009c", "804fc4", "5200b7", "123e92", "1d40ff", "108374", "2e980d", "207607", "196d61"};
        String color = null;

        if (colorize) {
            // Normalise a bit
            // typically ` and _ are used on the end alone
            String normalizedNick = nick.toLowerCase().replaceAll("[`_]+$", "");
            //remove |<anything> from the end
            normalizedNick = normalizedNick.replaceAll("|.*$", "");

            Double hash = 0.0;

            for (int i = 0; i < normalizedNick.length(); i++) {
                hash = ((int) normalizedNick.charAt(i)) + (double) ((int) (hash.longValue()) << 6) + (double) ((int) (hash.longValue()) << 16) - hash;
            }

            color = colors[(int) Math.abs(hash.longValue() % 14)];
        }

        StringBuilder output = new StringBuilder();
        boolean showSymbol = false;
        try {
            if (NetworkConnection.getInstance().getUserInfo() != null && NetworkConnection.getInstance().getUserInfo().prefs != null)
                showSymbol = NetworkConnection.getInstance().getUserInfo().prefs.getBoolean("mode-showsymbol");
        } catch (Exception e) {
        }
        String mode = "";
        if (from_mode != null && from_mode.length() > 0) {
            if (from_mode.contains(server != null ? server.MODE_OPER : "Y"))
                mode = server != null ? server.MODE_OPER : "Y";
            else if (from_mode.contains(server != null ? server.MODE_OWNER : "q"))
                mode = server != null ? server.MODE_OWNER : "q";
            else if (from_mode.contains(server != null ? server.MODE_ADMIN : "a"))
                mode = server != null ? server.MODE_ADMIN : "a";
            else if (from_mode.contains(server != null ? server.MODE_OP : "o"))
                mode = server != null ? server.MODE_OP : "o";
            else if (from_mode.contains(server != null ? server.MODE_HALFOP : "h"))
                mode = server != null ? server.MODE_HALFOP : "h";
            else if (from_mode.contains(server != null ? server.MODE_VOICED : "v"))
                mode = server != null ? server.MODE_VOICED : "v";
            else
                mode = from_mode.substring(0, 1);
        }
        if (mode != null && mode.length() > 0) {
            if (mode_colors.containsKey(mode))
                output.append("\u0004").append(mode_colors.get(mode)).append("\u0002");
            else
                output.append("\u0002");
            if (showSymbol) {
                if (PREFIX.has(mode))
                    output.append(TextUtils.htmlEncode(PREFIX.get(mode).asText()));
            } else {
                output.append("•");
            }
            output.append("\u000f ");
        }

        if (color != null)
            output.append("\u0004").append(color);
        output.append(nick);
        if (color != null)
            output.append("\u0004");
        return output.toString();
    }

    private String was(CollapsedEvent e) {
        StringBuilder was = new StringBuilder();
        String modes = e.getModes(false);

        if (e.old_nick != null && e.type != TYPE_MODE)
            was.append("was ").append(e.old_nick);
        if (modes != null && modes.length() > 0) {
            if (was.length() > 0)
                was.append("; ");
            was.append("\u00031").append(modes).append("\u000f");
        }

        if (was.length() > 0)
            was.insert(0, " (").append(")");

        return was.toString();
    }

    public String getCollapsedMessage() {
        StringBuilder message = new StringBuilder();

        if (data.size() == 0)
            return null;

        if (data.size() == 1 && data.get(0).modeCount() < ((data.get(0).type == TYPE_MODE) ? 2 : 1)) {
            CollapsedEvent e = data.get(0);
            switch (e.type) {
                case TYPE_NETSPLIT:
                    message.append(e.msg.replace(" ", " ↮ "));
                    break;
                case TYPE_MODE:
                    message.append("<b>").append(formatNick(e.nick, e.target_mode, false)).append("</b> was ").append(e.getModes(true));
                    if (e.from_nick != null) {
                        if (e.from_mode != null && e.from_mode.equalsIgnoreCase("__the_server__"))
                            message.append(" by the server <b>").append(e.from_nick).append("</b>");
                        else
                            message.append(" by ").append(formatNick(e.from_nick, e.from_mode, false));
                    }
                    break;
                case TYPE_JOIN:
                    message.append("→ <b>").append(formatNick(e.nick, e.from_mode, false)).append("</b>").append(was(e));
                    message.append(" joined");
                    if (showChan)
                        message.append(" ").append(e.chan);
                    message.append(" (").append(e.hostmask).append(")");
                    break;
                case TYPE_PART:
                    message.append("← <b>").append(formatNick(e.nick, e.from_mode, false)).append("</b>").append(was(e));
                    message.append(" left");
                    if (showChan)
                        message.append(" ").append(e.chan);
                    message.append(" (").append(e.hostmask).append(")");
                    if (e.msg != null && e.msg.length() > 0)
                        message.append(": ").append(e.msg);
                    break;
                case TYPE_QUIT:
                    message.append("⇐ <b>").append(formatNick(e.nick, e.from_mode, false)).append("</b>").append(was(e));
                    if (e.hostmask != null)
                        message.append(" quit (").append(e.hostmask).append(") ");
                    else
                        message.append(" quit: ");
                    if (e.msg != null && e.msg.length() > 0)
                        message.append(e.msg);
                    break;
                case TYPE_NICKCHANGE:
                    message.append(e.old_nick).append(" → <b>").append(formatNick(e.nick, e.from_mode, false)).append("</b>");
                    break;
                case TYPE_POPIN:
                    message.append("↔ <b>").append(formatNick(e.nick, e.from_mode, false)).append("</b>").append(was(e));
                    message.append(" popped in");
                    if (showChan)
                        message.append(" ").append(e.chan);
                    break;
                case TYPE_POPOUT:
                    message.append("↔ <b>").append(formatNick(e.nick, e.from_mode, false)).append("</b>").append(was(e));
                    message.append(" nipped out");
                    if (showChan)
                        message.append(" ").append(e.chan);
                    break;
                case TYPE_CONNECTIONSTATUS:
                    message.append(e.msg);
                    if (e.count > 1)
                        message.append(" (").append(e.count).append("x)");
                    break;
            }
        } else {
            boolean netsplit = false;
            Collections.sort(data, new comparator());
            Iterator<CollapsedEvent> i = data.iterator();
            CollapsedEvent last = null;
            CollapsedEvent next = i.next();
            CollapsedEvent e;
            int groupcount = 0;

            while (next != null) {
                e = next;

                do {
                    if (i.hasNext())
                        next = i.next();
                    else
                        next = null;
                } while (next != null && netsplit && next.netsplit);

                if (message.length() > 0 && e.type < TYPE_NICKCHANGE && ((next == null || next.type != e.type) && last != null && last.type == e.type)) {
                    if (groupcount == 1)
                        message.delete(message.length() - 2, message.length()).append(" ");
                    message.append("and ");
                }

                if (last == null || last.type != e.type) {
                    switch (e.type) {
                        case TYPE_NETSPLIT:
                            netsplit = true;
                            break;
                        case TYPE_MODE:
                            if (message.length() > 0)
                                message.append("• ");
                            message.append("\u00031mode:\u000f ");
                            break;
                        case TYPE_JOIN:
                            message.append("→ ");
                            break;
                        case TYPE_PART:
                            message.append("← ");
                            break;
                        case TYPE_QUIT:
                            message.append("⇐ ");
                            break;
                        case TYPE_NICKCHANGE:
                            if (message.length() > 0)
                                message.append("• ");
                            break;
                        case TYPE_POPIN:
                        case TYPE_POPOUT:
                            message.append("↔ ");
                            break;
                        case TYPE_CONNECTIONSTATUS:
                            break;
                    }
                }

                if (e.type == TYPE_NICKCHANGE) {
                    message.append(e.old_nick).append(" → <b>").append(formatNick(e.nick, e.from_mode, false)).append("</b>");
                    String old_nick = e.old_nick;
                    e.old_nick = null;
                    message.append(was(e));
                    e.old_nick = old_nick;
                } else if (e.type == TYPE_NETSPLIT) {
                    message.append(e.msg.replace(" ", " ↮ "));
                } else if (e.type == TYPE_CONNECTIONSTATUS) {
                    message.append(e.msg);
                    if (e.count > 1)
                        message.append(" (").append(e.count).append("x)");
                } else if (!showChan) {
                    message.append("<b>").append(formatNick(e.nick, (e.type == TYPE_MODE) ? e.target_mode : e.from_mode, false)).append("</b>").append(was(e));
                }

                if ((next == null || next.type != e.type) && !showChan) {
                    switch (e.type) {
                        case TYPE_JOIN:
                            message.append(" joined");
                            break;
                        case TYPE_PART:
                            message.append(" left");
                            break;
                        case TYPE_QUIT:
                            message.append(" quit");
                            break;
                        case TYPE_POPIN:
                            message.append(" popped in");
                            break;
                        case TYPE_POPOUT:
                            message.append(" nipped out");
                            break;
                    }
                } else if (showChan && e.type != TYPE_NETSPLIT && e.type != TYPE_CONNECTIONSTATUS) {
                    if (groupcount == 0) {
                        message.append("<b>").append(formatNick(e.nick, (e.type == TYPE_MODE) ? e.target_mode : e.from_mode, false)).append("</b>").append(was(e));
                        switch (e.type) {
                            case TYPE_JOIN:
                                message.append(" joined ");
                                break;
                            case TYPE_PART:
                                message.append(" left ");
                                break;
                            case TYPE_QUIT:
                                message.append(" quit ");
                                break;
                            case TYPE_POPIN:
                                message.append(" popped in ");
                                break;
                            case TYPE_POPOUT:
                                message.append(" nipped out ");
                                break;
                        }
                    }
                    if (e.type != TYPE_QUIT)
                        message.append(e.chan);
                }

                if (next != null && next.type == e.type) {
                    if (message.length() > 0) {
                        message.append(", ");
                        groupcount++;
                    }
                } else if (next != null) {
                    message.append(" ");
                    groupcount = 0;
                }

                last = e;
            }
        }
        return message.toString();
    }

    public void clear() {
        data.clear();
    }

    public int size() {
        return data.size();
    }
}
