define([
    'jquery',
    '/bower_components/chainpad-crypto/crypto.js',
    '/common/curve.js',
    '/common/common-hash.js',

    '/bower_components/marked/marked.min.js',
    '/common/common-realtime.js',

    // displayAvatar
    // whenRealtimeSyncs
    // getRealtime -> removeFromFriendList
    /*  UI
            Messages
            confirm
            fixHTML
            displayAvatar
            clearOwnedChannel
            alert


        pushMsg
            removeFromFriendList

        onDirectMessage
            getNetwork
            getProxy
            pushMsg

        Init
            getNetwork
            getProxy
            onDirectMessage
            removeFromFriendList
            notify
            onMessage

    */

], function ($, Crypto, Curve, Hash, Marked, Realtime) {
    var Msg = {
        inputs: [],
    };


    var Types = {
        message: 'MSG',
        update: 'UPDATE',
        unfriend: 'UNFRIEND',
        mapId: 'MAP_ID',
        mapIdAck: 'MAP_ID_ACK'
    };

    // TODO
    // - mute a channel (hide notifications or don't open it?)

    var ready = [];
    var pending = {};
    var pendingRequests = [];

    var parseMessage = function (content) {
        return Marked(content);
    };

    var createData = Msg.createData = function (proxy, hash) {
        return {
            channel: hash || Hash.createChannelId(),
            displayName: proxy['cryptpad.username'],
            profile: proxy.profile && proxy.profile.view,
            edPublic: proxy.edPublic,
            curvePublic: proxy.curvePublic,
            avatar: proxy.profile && proxy.profile.avatar
        };
    };

    var getFriend = function (proxy, pubkey) {
        if (pubkey === proxy.curvePublic) {
            var data = createData(proxy);
            delete data.channel;
            return data;
        }
        return proxy.friends ? proxy.friends[pubkey] : undefined;
    };

    var removeFromFriendList = function (proxy, realtime, curvePublic, cb) {
        if (!proxy.friends) { return; }
        var friends = proxy.friends;
        delete friends[curvePublic];
        Realtime.whenRealtimeSyncs(realtime, cb);
    };

    var getFriendList = Msg.getFriendList = function (proxy) {
        if (!proxy.friends) { proxy.friends = {}; }
        return proxy.friends;
    };

    var eachFriend = function (friends, cb) {
        Object.keys(friends).forEach(function (id) {
            if (id === 'me') { return; }
            cb(friends[id], id, friends);
        });
    };


    Msg.getFriendChannelsList = function (proxy) {
        var list = [];
        eachFriend(proxy, function (friend) {
            list.push(friend.channel);
        });
        return list;
    };

    // Messaging tools
    var avatars = {};

    // TODO make this internal to the messenger
    var channels = Msg.channels  = window.channels = {};

    var UI = Msg.UI = {};

    UI.init = function (common, $listContainer, $msgContainer) {
        var ui = {
            containers: {
                friendList: $listContainer,
                messages: $msgContainer,
            },
        };

        ui.addToFriendList = function (data, display, remove) {
            var $block = ui.containers.friendBlock;

            var $friend = $('<div>', {'class': 'friend avatar'}).appendTo($block);
            $friend.data('key', data.curvePublic);
            var $rightCol = $('<span>', {'class': 'right-col'});
            $('<span>', {'class': 'name'}).text(data.displayName).appendTo($rightCol);
            var $remove = $('<span>', {'class': 'remove fa fa-user-times'}).appendTo($rightCol);
            $remove.attr('title', common.Messages.contacts_remove);
            $friend.dblclick(function () {
                if (data.profile) {
                    window.open('/profile/#' + data.profile);
                }
            });
            $friend.click(function () {
                display(data.curvePublic);
            });
            $remove.click(function (e) {
                e.stopPropagation();
                common.confirm(common.Messages._getKey('contacts_confirmRemove', [
                    common.fixHTML(data.displayName)
                ]), function (yes) {
                    if (!yes) { return; }
                    remove(data.curvePublic);
                }, null, true);
            });
            if (data.avatar && avatars[data.avatar]) {
                $friend.append(avatars[data.avatar]);
                $friend.append($rightCol);
            } else {
                common.displayAvatar($friend, data.avatar, data.displayName, function ($img) {
                    if (data.avatar && $img) {
                        avatars[data.avatar] = $img[0].outerHTML;
                    }
                    $friend.append($rightCol);
                });
            }
            $('<span>', {'class': 'status'}).appendTo($friend);
        };

        ui.createFriendList = function (friends, display, remove) {
            var $block = ui.containers.friendBlock = $('<div>');
            eachFriend(friends, function (friend) {
                ui.addToFriendList(friend, display, remove);
            });
            $block.appendTo($listContainer);
        };

        ui.notify = function (curvePublic) {
            var $friend = $listContainer.find('.friend').filter(function (idx, el) {
                return $(el).data('key') === curvePublic;
            });
            $friend.addClass('notify');
        };

        ui.unnotify = function (curvePublic) {
            var $friend = $listContainer.find('.friend').filter(function (idx, el) {
                return $(el).data('key') === curvePublic;
            });
            $friend.removeClass('notify');
        };

        ui.update = function (curvePublic, types) {
            var proxy = common.getProxy();
            var data = getFriend(proxy, curvePublic);
            var chan = channels[data.channel];
            if (!chan.ready) {
                chan.updateOnReady = (chan.updateOnReady || []).concat(types);
                return;
            }
            var $friend = $listContainer.find('.friend').filter(function (idx, el) {
                return $(el).data('key') === curvePublic;
            });
            if (types.indexOf('displayName') >= 0) {
                $friend.find('.name').text(data.displayName);
            }
            if (types.indexOf('avatar') >= 0) {
                $friend.find('.default').remove();
                $friend.find('media-tag').remove();
                if (data.avatar && avatars[data.avatar]) {
                    $friend.prepend(avatars[data.avatar]);
                } else {
                    common.displayAvatar($friend, data.avatar, data.displayName, function ($img) {
                        if (data.avatar && $img) {
                            avatars[data.avatar] = $img[0].outerHTML;
                        }
                    });
                }
            }
        };

        ui.updateStatus = function (curvePublic, online) {
            ui.getFriend(curvePublic).find('.status')
                .attr('class', 'status ' + (online? 'online' : 'offline'));
        };

        ui.getChannel = function (curvePublic) {
            var $chat = $msgContainer.find('.chat').filter(function (idx, el) {
                return $(el).data('key') === curvePublic;
            });
            return $chat.length? $chat: null;
        };

        ui.hideInfo = function () {
            $msgContainer.find('.info').hide();
        };

        ui.showInfo = function () {
            $msgContainer.find('.info').show();
        };

        ui.createChat = function (curvePublic) {
            return $('<div>', {'class':'chat'})
                .data('key', curvePublic).appendTo($msgContainer);
        };

        ui.hideChat = function () {
            $msgContainer.find('.chat').hide();
        };

        ui.getFriend = function (curvePublic) {
            return $listContainer.find('.friend').filter(function (idx, el) {
                return $(el).data('key') === curvePublic;
            });
        };

        ui.remove = function (curvePublic) {
            var $friend = ui.getFriend(curvePublic);
            var $chat = ui.getChannel(curvePublic);
            $friend.remove();
            $chat.remove();
            ui.showInfo();
        };

        ui.createMessage = function (msg, name) {
            var $msg = $('<div>', {'class': 'message'})
                .attr('title', msg.time ? new Date(msg.time).toLocaleString(): '?');

            if (name) {
                $('<div>', {'class':'sender'}).text(name).appendTo($msg);
            }

            $('<div>', {'class':'content'}).html(parseMessage(msg.text)).appendTo($msg);
            return $msg;
        };

        ui.setEditable = function (bool) {
            bool = !bool;
            var input = ui.input;
            if (!input) { return; }

            if (bool) {
                input.setAttribute('disabled', bool);
            } else {
                input.removeAttribute('disabled');
            }

            if (common.Messages) {
                // set placeholder
                var placeholder = bool?
                    common.Messages.disconnected:
                    common.Messages.contacts_typeHere;
                input.setAttribute('placeholder', placeholder);
            }
        };

        ui.createChatBox = function (proxy, $container, curvePublic) {
            var data = getFriend(proxy, curvePublic);

            // Input
            var channel = channels[data.channel];

            var $header = $('<div>', {
                'class': 'header',
            }).appendTo($container);

            var $avatar = $('<div>', {'class': 'avatar'}).appendTo($header);

            // more history...
            $('<span>', {
                'class': 'more-history',
            })
            .text('get more history')
            .click(function () {
                console.log("GETTING HISTORY");
                channel.getPreviousMessages();
            })
            .appendTo($header);

            var $removeHistory = $('<span>', {
                'class': 'remove-history fa fa-eraser',
                title: common.Messages.contacts_removeHistoryTitle
            })
            .click(function () {
                common.confirm(common.Messages.contacts_confirmRemoveHistory, function (yes) {
                    if (!yes) { return; }
                    common.clearOwnedChannel(data.channel, function (e) {
                        if (e) {
                            console.error(e);
                            common.alert(common.Messages.contacts_removeHistoryServerError);
                            return;
                        }
                    });
                });
            });
            $removeHistory.appendTo($header);

            $('<div>', {'class': 'messages'}).appendTo($container);
            var $inputBlock = $('<div>', {'class': 'input'}).appendTo($container);

            var $input = $('<textarea>').appendTo($inputBlock);
            $input.attr('placeholder', common.Messages.contacts_typeHere);
            ui.input = $input[0];

            var send = function () {
                // TODO implement sending queue
                // TODO separate message logic from UI
                var channel = channels[data.channel];
                if (channel.sending) {
                    console.error("still sending");
                    return;
                }
                if (!$input.val()) {
                    console.error("nothing to send");
                    return;
                }
                if ($input.attr('disabled')) {
                    console.error("input is disabled");
                    return;
                }

                var payload = $input.val();
                // Send the message
                channel.sending = true;
                channel.send(payload, function (e) {
                    if (e) {
                        channel.sending = false;
                        console.error(e);
                        return;
                    }
                    $input.val('');
                    channel.refresh();
                    channel.sending = false;
                });
            };
            $('<button>', {
                'class': 'btn btn-primary fa fa-paper-plane',
                title: common.Messages.contacts_send,
            }).appendTo($inputBlock).click(send);

            var onKeyDown = function (e) {
                if (e.keyCode === 13) {
                    if (e.ctrlKey || e.shiftKey) {
                        var val = this.value;
                        if (typeof this.selectionStart === "number" && typeof this.selectionEnd === "number") {
                            var start = this.selectionStart;
                            this.value = val.slice(0, start) + "\n" + val.slice(this.selectionEnd);
                            this.selectionStart = this.selectionEnd = start + 1;
                        } else if (document.selection && document.selection.createRange) {
                            this.focus();
                            var range = document.selection.createRange();
                            range.text = "\r\n";
                            range.collapse(false);
                            range.select();
                        }
                        return false;
                    }
                    send();
                    return false;
                }
            };
            $input.on('keydown', onKeyDown);

            // Header
            var $rightCol = $('<span>', {'class': 'right-col'});
            $('<span>', {'class': 'name'}).text(data.displayName).appendTo($rightCol);
            if (data.avatar && avatars[data.avatar]) {
                $avatar.append(avatars[data.avatar]);
                $avatar.append($rightCol);
            } else {
                common.displayAvatar($avatar, data.avatar, data.displayName, function ($img) {
                    if (data.avatar && $img) {
                        avatars[data.avatar] = $img[0].outerHTML;
                    }
                    $avatar.append($rightCol);
                });
            }
        };

        return ui;
    };

    var msgAlreadyKnown = function (channel, sig) {
        return channel.messages.some(function (message) {
            return message[0] === sig;
        });
    };

    // TODO remove dependency on common
    var pushMsg = function (realtime, proxy, common, channel, cryptMsg) {
        var msg = channel.encryptor.decrypt(cryptMsg);

        var sig = cryptMsg.slice(0, 64);
        if (msgAlreadyKnown(channel, sig)) { return; }

        var parsedMsg = JSON.parse(msg);
        if (parsedMsg[0] === Types.message) {
            // TODO validate messages here
            var res = {
                type: parsedMsg[0],
                sig: sig,
                channel: parsedMsg[1],
                time: parsedMsg[2],
                text: parsedMsg[3],
            };

            channel.messages.push(res);
            return true;
        }
        if (parsedMsg[0] === Types.update) {
            if (parsedMsg[1] === proxy.curvePublic) { return; }
            var newdata = parsedMsg[3];
            var data = getFriend(proxy, parsedMsg[1]);
            var types = [];
            Object.keys(newdata).forEach(function (k) {
                if (data[k] !== newdata[k]) {
                    types.push(k);
                    data[k] = newdata[k];
                }
            });
            channel.updateUI(types);
            return;
        }
        if (parsedMsg[0] === Types.unfriend) {
            removeFromFriendList(proxy, realtime, channel.friendEd, function () {
                channel.wc.leave(Types.unfriend);
                channel.removeUI();
            });
            return;
        }
    };

    /*  Broadcast a display name, profile, or avatar change to all contacts
    */
    var updateMyData = function (proxy) {
        var friends = getFriendList(proxy);
        var mySyncData = friends.me;
        var myData = createData(proxy);
        if (!mySyncData || mySyncData.displayName !== myData.displayName
             || mySyncData.profile !== myData.profile
             || mySyncData.avatar !== myData.avatar) {
            delete myData.channel;
            Object.keys(channels).forEach(function (chan) {
                var channel = channels[chan];
                var msg = [Types.update, myData.curvePublic, +new Date(), myData];
                var msgStr = JSON.stringify(msg);
                var cryptMsg = channel.encryptor.encrypt(msgStr);
                channel.wc.bcast(cryptMsg).then(function () {
                    channel.refresh();
                }, function (err) {
                    console.error(err);
                });
            });
            friends.me = myData;
        }
    };

    var onChannelReady = function (proxy, chanId) {
        if (ready.indexOf(chanId) !== -1) { return; }
        ready.push(chanId);
        channels[chanId].updateStatus(); // c'est quoi?
        var friends = getFriendList(proxy);
        if (ready.length === Object.keys(friends).length) {
            // All channels are ready
            updateMyData(proxy);
        }
        return ready.length;
    };

    // Id message allows us to map a netfluxId with a public curve key
    var onIdMessage = function (proxy, network, msg, sender) {
        var channel;
        var isId = Object.keys(channels).some(function (chanId) {
            if (channels[chanId].userList.indexOf(sender) !== -1) {
                channel = channels[chanId];
                return true;
            }
        });

        if (!isId) { return; }

        var decryptedMsg = channel.encryptor.decrypt(msg);

        if (decryptedMsg === null) {
            // console.error('unable to decrypt message');
            // console.error('potentially meant for yourself');

            // message failed to parse, meaning somebody sent it to you but
            // encrypted it with the wrong key, or you're sending a message to
            // yourself in a different tab.
            return;
        }

        if (!decryptedMsg) {
            console.error('decrypted message was falsey but not null');
            return;
        }

        var parsed;
        try {
            parsed = JSON.parse(decryptedMsg);
        } catch (e) {
            console.error(decryptedMsg);
            return;
        }
        if (parsed[0] !== Types.mapId && parsed[0] !== Types.mapIdAck) { return; }

        // check that the responding peer's encrypted netflux id matches
        // the sender field. This is to prevent replay attacks.
        if (parsed[2] !== sender || !parsed[1]) { return; }
        channel.mapId[sender] = parsed[1];

        channel.updateStatus();

        if (parsed[0] !== Types.mapId) { return; } // Don't send your key if it's already an ACK
        // Answer with your own key
        var rMsg = [Types.mapIdAck, proxy.curvePublic, channel.wc.myID];
        var rMsgStr = JSON.stringify(rMsg);
        var cryptMsg = channel.encryptor.encrypt(rMsgStr);
        network.sendto(sender, cryptMsg);
    };

    // HERE
    var onDirectMessage = function (common, msg, sender) {
        var proxy = common.getProxy();
        var network = common.getNetwork();
        var realtime = common.getRealtime();

        if (sender !== Msg.hk) { return void onIdMessage(proxy, network, msg, sender); }
        var parsed = JSON.parse(msg);
        if ((parsed.validateKey || parsed.owners) && parsed.channel) {
            return;
        }
        if (parsed.state && parsed.state === 1 && parsed.channel) {
            if (channels[parsed.channel]) {
                // parsed.channel is Ready
                // TODO: call a function that shows that the channel is ready? (remove a spinner, ...)
                // channel[parsed.channel].ready();
                channels[parsed.channel].ready = true;
                onChannelReady(proxy, parsed.channel);
                var updateTypes = channels[parsed.channel].updateOnReady;
                if (updateTypes) {
                    channels[parsed.channel].updateUI(updateTypes);
                }
            }
            return;
        }
        var chan = parsed[3];
        if (!chan || !channels[chan]) { return; }
        pushMsg(realtime, proxy, common, channels[chan], parsed[4]);
        channels[chan].refresh();
    };
    var onMessage = function (common, msg, sender, chan) {
        if (!channels[chan.id]) { return; }

        var realtime = common.getRealtime();
        var proxy = common.getProxy();

        var isMessage = pushMsg(realtime, proxy, common, channels[chan.id], msg);
        if (isMessage) {
            // Don't notify for your own messages
            if (channels[chan.id].wc.myID !== sender) {
                channels[chan.id].notify();
            }
            channels[chan.id].refresh();
        }
    };

    Msg.getLatestMessages = function () {
        Object.keys(channels).forEach(function (id) {
            if (id === 'me') { return; }
            var friend = channels[id];
            friend.getMessagesSinceDisconnect();
            friend.refresh();
        });
    };

    var getMoreHistory = function (network, chan, hash, count) {
        var msg = [ 'GET_HISTORY_RANGE', chan.id, {
                from: hash,
                count: count,
            }
        ];

        network.sendto(network.historyKeeper, JSON.stringify(msg)).then(function () {
        }, function (err) {
            throw new Error(err);
        });
    };

    var getChannelMessagesSince = function (network, proxy, chan, data, keys) {
        var cfg = {
            validateKey: keys.validateKey,
            owners: [proxy.edPublic, data.edPublic],
            lastKnownHash: data.lastKnownHash
        };
        var msg = ['GET_HISTORY', chan.id, cfg];
        network.sendto(network.historyKeeper, JSON.stringify(msg))
          .then($.noop, function (err) {
            throw new Error(err);
        });
    };

    /*  TODO remove dependency on common
    */
    Msg.init = function (common, ui) {
        // declare common variables
        var network = common.getNetwork();
        var proxy = common.getProxy();
        var realtime = common.getRealtime();
        Msg.hk = network.historyKeeper;
        var friends = getFriendList(proxy);

        // listen for messages...
        network.on('message', function(msg, sender) {
            onDirectMessage(common, msg, sender);
        });

        // declare messenger and common methods
        var messenger = {
            ui: ui,
        };

        messenger.setActive = function (id) {
            // TODO validate id
            messenger.active = id;
        };

        // Refresh the active channel
        // TODO extract into UI method
        var refresh = function (curvePublic) {
            if (messenger.active !== curvePublic) { return; }
            var data = friends[curvePublic];
            if (!data) { return; }
            var channel = channels[data.channel];
            if (!channel) { return; }

            var $chat = ui.getChannel(curvePublic);

            if (!$chat) { return; }

            // Add new messages
            var messages = channel.messages;
            var $messages = $chat.find('.messages');
            var msg, name;
            var last = typeof(channel.lastDisplayed) === 'number'? channel.lastDisplayed: -1;
            for (var i = last + 1; i<messages.length; i++) {
                msg = messages[i];
                name = (msg.channel !== channel.lastSender)?
                    getFriend(proxy, msg.channel).displayName: undefined;

                ui.createMessage(msg, name).appendTo($messages);
                channel.lastSender = msg.channel;
            }
            $messages.scrollTop($messages[0].scrollHeight);
            channel.lastDisplayed = i-1;
            channel.unnotify();

            // return void channel.notify();
            if (messages.length > 10) {
                var lastKnownMsg = messages[messages.length - 11];
                channel.setLastMessageRead(lastKnownMsg.sig);
            }
        };
        // Display a new channel
        // TODO extract into UI method
        var display = function (curvePublic) {
            ui.hideInfo();
            var $chat = ui.getChannel(curvePublic);
            if (!$chat) {
                $chat = ui.createChat(curvePublic);
                ui.createChatBox(proxy, $chat, curvePublic);
            }
            // Show the correct div
            ui.hideChat();
            $chat.show();

            // TODO set this attr per-messenger
            messenger.setActive(curvePublic);
            // TODO don't mark messages as read unless you have displayed them

            refresh(curvePublic);
        };

        // TODO take a callback
        var remove = function (curvePublic) {
            var data = getFriend(proxy, curvePublic);
            var channel = channels[data.channel];
            var msg = [Types.unfriend, proxy.curvePublic, +new Date()];
            var msgStr = JSON.stringify(msg);
            var cryptMsg = channel.encryptor.encrypt(msgStr);
            channel.wc.bcast(cryptMsg).then(function () {
                removeFromFriendList(common, curvePublic, function () {
                    channel.wc.leave(Types.unfriend);
                    channel.removeUI();
                });
            }, function (err) {
                console.error(err);
            });
        };

        // Display friend list
        ui.createFriendList(friends, display, remove);

        // Open the channels

        // TODO extract this into an external function
        var openFriendChannel = function (data, f) {
            var keys = Curve.deriveKeys(data.curvePublic, proxy.curvePrivate);
            var encryptor = Curve.createEncryptor(keys);
            network.join(data.channel).then(function (chan) {
                var channel = channels[data.channel] = {
                    sending: false,
                    friendEd: f,
                    keys: keys,
                    encryptor: encryptor,
                    messages: [],
                    refresh: function () { refresh(data.curvePublic); },
                    notify: function () {
                        ui.notify(data.curvePublic);
                        common.notify(); // HERE
                    },
                    unnotify: function () { ui.unnotify(data.curvePublic); },
                    removeUI: function () { ui.remove(data.curvePublic); },
                    updateUI: function (types) { ui.update(data.curvePublic, types); },
                    updateStatus: function () {
                        ui.updateStatus(data.curvePublic,
                            channel.getStatus(data.curvePublic));
                    },
                    setLastMessageRead: function (hash) {
                        data.lastKnownHash = hash;
                    },
                    getLastMessageRead: function () {
                        return data.lastKnownHash;
                    },
                    isActive: function () {
                        return data.curvePublic === messenger.active;
                    },
                    getMessagesSinceDisconnect: function () {
                        getChannelMessagesSince(network, proxy, chan, data, keys);
                    },
                    wc: chan,
                    userList: [],
                    mapId: {},
                    getStatus: function (curvePublic) {
                        return channel.userList.some(function (nId) {
                            return channel.mapId[nId] === curvePublic;
                        });
                    },
                    getPreviousMessages: function () {
                        var history = channel.messages;
                        if (!history || !history.length) {
                            // TODO ask for default history?
                            return;
                        }

                        var oldestMessage = history[0];
                        if (!oldestMessage) {
                            return; // nothing to fetch
                        }

                        var messageHash = oldestMessage[0];
                        getMoreHistory(network, chan, messageHash, 10);
                    },
                    send: function (payload, cb) {
                        if (!network.webChannels.some(function (wc) {
                            if (wc.id === channel.wc.id) { return true; }
                        })) {
                            return void cb('NO_SUCH_CHANNEL');
                        }

                        var msg = [Types.message, proxy.curvePublic, +new Date(), payload];
                        var msgStr = JSON.stringify(msg);
                        var cryptMsg = channel.encryptor.encrypt(msgStr);

                        channel.wc.bcast(cryptMsg).then(function () {
                            pushMsg(realtime, proxy, common, channel, cryptMsg);
                            cb();
                        }, function (err) {
                            cb(err);
                        });
                    },
                };
                chan.on('message', function (msg, sender) {
                    onMessage(common, msg, sender, chan);
                });

                var onJoining = function (peer) {
                    if (peer === Msg.hk) { return; }
                    if (channel.userList.indexOf(peer) !== -1) { return; }
                    channel.userList.push(peer);
                    var msg = [Types.mapId, proxy.curvePublic, chan.myID];
                    var msgStr = JSON.stringify(msg);
                    var cryptMsg = channel.encryptor.encrypt(msgStr);
                    network.sendto(peer, cryptMsg);
                    channel.updateStatus();
                };
                chan.members.forEach(function (peer) {
                    if (peer === Msg.hk) { return; }
                    if (channel.userList.indexOf(peer) !== -1) { return; }
                    channel.userList.push(peer);
                });
                chan.on('join', onJoining);
                chan.on('leave', function (peer) {
                    var i = channel.userList.indexOf(peer);
                    while (i !== -1) {
                        channel.userList.splice(i, 1);
                        i = channel.userList.indexOf(peer);
                    }
                    channel.updateStatus();
                });

                getChannelMessagesSince(network, proxy, chan, data, keys);
            }, function (err) {
                console.error(err);
            });
        };

        messenger.cleanFriendChannels = function () {
            Object.keys(channels).forEach(function (id) {
                delete channels[id];
            });
        };

        var openFriendChannels = messenger.openFriendChannels = function () {
            eachFriend(friends, openFriendChannel);
        };

        messenger.setEditable = ui.setEditable;

        openFriendChannels();

        // TODO split loop innards into ui methods
        var checkNewFriends = function () {
            eachFriend(friends, function (friend, id) {
                var $friend = ui.getFriend(id);
                if (!$friend.length) {
                    openFriendChannel(friend, id);
                    ui.addToFriendList(friend, display, remove);
                }
            });
        };

        common.onDisplayNameChanged(function () {
            checkNewFriends();
            updateMyData(proxy);
        });

        return messenger;
    };

    // Invitation
    // FIXME there are too many functions with this name
    var addToFriendList = Msg.addToFriendList = function (common, data, cb) {
        var proxy = common.getProxy();
        var friends = getFriendList(proxy);
        var pubKey = data.curvePublic;

        if (pubKey === proxy.curvePublic) { return void cb("E_MYKEY"); }

        friends[pubKey] = data;

        Realtime.whenRealtimeSyncs(common.getRealtime(), function () {
            cb();
            common.pinPads([data.channel]);
        });
        common.changeDisplayName(proxy[common.displayNameKey]);
    };

    /*  Used to accept friend requests within apps other than /contacts/ */
    Msg.addDirectMessageHandler = function (common) {
        var network = common.getNetwork();
        var proxy = common.getProxy();
        if (!network) { return void console.error('Network not ready'); }
        network.on('message', function (message, sender) {
            var msg;
            if (sender === network.historyKeeper) { return; }
            try {
                var parsed = common.parsePadUrl(window.location.href);
                if (!parsed.hashData) { return; }
                var chan = parsed.hashData.channel;
                // Decrypt
                var keyStr = parsed.hashData.key;
                var cryptor = Crypto.createEditCryptor(keyStr);
                var key = cryptor.cryptKey;
                var decryptMsg;
                try {
                    decryptMsg = Crypto.decrypt(message, key);
                } catch (e) {
                    // If we can't decrypt, it means it is not a friend request message
                }
                if (!decryptMsg) { return; }
                // Parse
                msg = JSON.parse(decryptMsg);
                if (msg[1] !== parsed.hashData.channel) { return; }
                var msgData = msg[2];
                var msgStr;
                if (msg[0] === "FRIEND_REQ") {
                    msg = ["FRIEND_REQ_NOK", chan];
                    var todo = function (yes) {
                        if (yes) {
                            pending[sender] = msgData;
                            msg = ["FRIEND_REQ_OK", chan, createData(common, msgData.channel)];
                        }
                        msgStr = Crypto.encrypt(JSON.stringify(msg), key);
                        network.sendto(sender, msgStr);
                    };
                    var existing = getFriend(proxy, msgData.curvePublic);
                    if (existing) {
                        todo(true);
                        return;
                    }
                    var confirmMsg = common.Messages._getKey('contacts_request', [
                        common.fixHTML(msgData.displayName)
                    ]);
                    common.confirm(confirmMsg, todo, null, true);
                    return;
                }
                if (msg[0] === "FRIEND_REQ_OK") {
                    var idx = pendingRequests.indexOf(sender);
                    if (idx !== -1) { pendingRequests.splice(idx, 1); }

                    // FIXME clarify this function's name
                    addToFriendList(common, msgData, function (err) {
                        if (err) {
                            return void common.log(common.Messages.contacts_addError);
                        }
                        common.log(common.Messages.contacts_added);
                        var msg = ["FRIEND_REQ_ACK", chan];
                        var msgStr = Crypto.encrypt(JSON.stringify(msg), key);
                        network.sendto(sender, msgStr);
                    });
                    return;
                }
                if (msg[0] === "FRIEND_REQ_NOK") {
                    var i = pendingRequests.indexOf(sender);
                    if (i !== -1) { pendingRequests.splice(i, 1); }
                    common.log(common.Messages.contacts_rejected);
                    common.changeDisplayName(proxy[common.displayNameKey]);
                    return;
                }
                if (msg[0] === "FRIEND_REQ_ACK") {
                    var data = pending[sender];
                    if (!data) { return; }
                    addToFriendList(common, data, function (err) {
                        if (err) {
                            return void common.log(common.Messages.contacts_addError);
                        }
                        common.log(common.Messages.contacts_added);
                    });
                    return;
                }
                // TODO: timeout ACK: warn the user
            } catch (e) {
                console.error("Cannot parse direct message", msg || message, "from", sender, e);
            }
        });
    };

    Msg.getPending = function () {
        return pendingRequests;
    };

    Msg.inviteFromUserlist = function (common, netfluxId) {
        var network = common.getNetwork();
        var parsed = common.parsePadUrl(window.location.href);
        if (!parsed.hashData) { return; }
        // Message
        var chan = parsed.hashData.channel;
        var myData = createData(common);
        var msg = ["FRIEND_REQ", chan, myData];
        // Encryption
        var keyStr = parsed.hashData.key;
        var cryptor = Crypto.createEditCryptor(keyStr);
        var key = cryptor.cryptKey;
        var msgStr = Crypto.encrypt(JSON.stringify(msg), key);
        // Send encrypted message
        if (pendingRequests.indexOf(netfluxId) === -1) {
            pendingRequests.push(netfluxId);
            var proxy = common.getProxy();
            // this redraws the userlist after a change has occurred
            // TODO rename this function to reflect its purpose
            common.changeDisplayName(proxy[common.displayNameKey]);
        }
        network.sendto(netfluxId, msgStr);
    };

    return Msg;
});