define([
    '/common/common-util.js',
    '/common/common-constants.js',
    '/customize/messages.js',
    '/bower_components/chainpad-crypto/crypto.js',
], function (Util, Constants, Messages, Crypto) {
    var Cursor = {};

    var convertToUint8 = function (obj) {
        var l = Object.keys(obj).length;
        var u = new Uint8Array(l);
        for (var i = 0; i<l; i++) {
            u[i] = obj[i];
        }
        return u;
    };

    // Send the client's cursor to their channel when we receive an update
    var sendMyCursor = function (ctx, clientId) {
        var client = ctx.clients[clientId];
        if (!client || !client.cursor) { return; }
        var chan = ctx.channels[client.channel];
        if (!chan) { return; }
        var data = {
            id: client.id,
            cursor: client.cursor
        };
        chan.sendMsg(JSON.stringify(data));
        ctx.emit('MESSAGE', data, chan.clients.filter(function (cl) {
            return cl !== clientId;
        }));
    };

    // Send all our cursors data when someone remote joins the channel
    var sendOurCursors = function (ctx, chan) {
        chan.clients.forEach(function (c) {
            var client = ctx.clients[c];
            if (!client) { return; }
            var data = {
                id: client.id,
                cursor: client.cursor
            };
            // Send our data to the other users (NOT including the other tabs of the same worker)
            chan.sendMsg(JSON.stringify(data));
        });
    };

    var initCursor = function (ctx, obj, client, cb) {
        var channel = obj.channel;
        var secret = obj.secret;
        if (secret.keys.cryptKey) {
            secret.keys.cryptKey = convertToUint8(secret.keys.cryptKey);
        }

        var padChan = secret.channel;
        var network = ctx.store.network;
        var first = true;

        var c = ctx.clients[client];
        if (!c) {
            c = ctx.clients[client] = {
                channel: channel,
                padChan: padChan,
                cursor: {}
            };
        } else {
            return void cb();
        }

        var chan = ctx.channels[channel];
        if (chan) {
            // This channel is already open in another tab

            // ==> Set the ID to our client object
            if (!c.id) { c.id = chan.wc.myID + '-' + client; }

            // ==> Send the cursor position of the other tabs
            chan.clients.forEach(function (cl) {
                var clientObj = ctx.clients[cl];
                if (!clientObj) { return; }
                ctx.emit('MESSAGE', {
                    id: clientObj.id,
                    cursor: clientObj.cursor
                }, [client]);
            });
            chan.sendMsg(JSON.stringify({join: true, id: c.id}));

            // ==> And push the new tab to the list
            chan.clients.push(client);
            return void cb();
        }

        var onOpen = function (wc) {

            ctx.channels[channel] = ctx.channels[channel] || {};

            var chan = ctx.channels[channel];
            if (!c.id) { c.id = wc.myID + '-' + client; }
            if (chan.clients) {
                // If 2 tabs from the same worker have been opened at the same time,
                // we have to fix both of them
                chan.clients.forEach(function (cl) {
                    if (ctx.clients[cl] && !ctx.clients[cl].id) {
                        ctx.clients[cl].id = wc.myID + '-' + cl;
                    }
                });
            }


            if (!chan.encryptor) { chan.encryptor = Crypto.createEncryptor(secret.keys); }

            wc.on('join', function () {
                sendOurCursors(ctx, chan);
            });
            wc.on('leave', function (peer) {
                ctx.emit('MESSAGE', {leave: true, id: peer}, chan.clients);
            });
            wc.on('message', function (cryptMsg) {
                var msg = chan.encryptor.decrypt(cryptMsg, secret.keys && secret.keys.validateKey);
                var parsed;
                try {
                    parsed = JSON.parse(msg);
                    if (parsed && parsed.join) {
                        return void sendOurCursors(ctx, chan);
                    }
                    ctx.emit('MESSAGE', parsed, chan.clients);
                } catch (e) { console.error(e); }
            });

            chan.wc = wc;
            chan.sendMsg = function (msg, cb) {
                cb = cb || function () {};
                var cmsg = chan.encryptor.encrypt(msg);
                wc.bcast(cmsg).then(function () {
                    cb();
                }, function (err) {
                    cb({error: err});
                });
            };

            if (!first) { return; }
            chan.clients = [client];
            first = false;
            cb();
        };

        network.join(channel).then(onOpen, function (err) {
            return void cb({error: err});
        });

        var onReconnect = function () {
            if (!ctx.channels[channel]) { console.log("cant reconnect", channel); return; }
            network.join(channel).then(onOpen, function (err) {
                console.error(err);
            });
        };
        ctx.channels[channel] = ctx.channels[channel] || {};
        ctx.channels[channel].onReconnect = onReconnect;
        network.on('reconnect', onReconnect);
    };

    var updateCursor = function (ctx, data, client, cb) {
        var c = ctx.clients[client];
        if (!c) { return void cb({error: 'NO_CLIENT'}); }
        data.color = Util.find(ctx.store.proxy, ['settings', 'general', 'cursor', 'color']);
        data.name = ctx.store.proxy[Constants.displayNameKey] || Messages.anonymous;
        data.avatar = Util.find(ctx.store.proxy, ['profile', 'avatar']);
        c.cursor = data;
        sendMyCursor(ctx, client);
        cb();
    };

    var leaveChannel = function (ctx, padChan) {
        // Leave channel and prevent reconnect when we leave a pad
        Object.keys(ctx.channels).some(function (cursorChan) {
            var channel = ctx.channels[cursorChan];
            if (channel.padChan !== padChan) { return; }
            if (channel.wc) { channel.wc.leave(); }
            if (channel.onReconnect) {
                var network = ctx.store.network;
                network.off('reconnect', channel.onReconnect);
            }
            delete ctx.channels[cursorChan];
            return true;
        });
    };
    // Remove the client from all its channels when a tab is closed
    var removeClient = function (ctx, clientId) {
        var filter = function (c) {
            return c !== clientId;
        };

        // Remove the client from our channels
        var chan;
        for (var k in ctx.channels) {
            chan = ctx.channels[k];
            chan.clients = chan.clients.filter(filter);
            if (chan.clients.length === 0) {
                if (chan.wc) { chan.wc.leave(); }
                if (chan.onReconnect) {
                    var network = ctx.store.network;
                    network.off('reconnect', chan.onReconnect);
                }
                delete ctx.channels[k];
            }
        }

        // Send the leave message to the channel we were in
        if (ctx.clients[clientId]) {
            var leaveMsg = {
                leave: true,
                id: ctx.clients[clientId].id
            };
            chan = ctx.channels[ctx.clients[clientId].channel];
            if (chan) {
                chan.sendMsg(JSON.stringify(leaveMsg));
                ctx.emit('MESSAGE', leaveMsg, chan.clients);
            }
        }

        delete ctx.clients[clientId];
    };

    Cursor.init = function (store, emit) {
        var cursor = {};
        var ctx = {
            store: store,
            emit: emit,
            channels: {},
            clients: {}
        };

        cursor.removeClient = function (clientId) {
            removeClient(ctx, clientId);
        };
        cursor.leavePad = function (padChan) {
            leaveChannel(ctx, padChan);
        };
        cursor.execCommand = function (clientId, obj, cb) {
            var cmd = obj.cmd;
            var data = obj.data;
            if (cmd === 'INIT_CURSOR') {
                return void initCursor(ctx, data, clientId, cb);
            }
            if (cmd === 'UPDATE') {
                return void updateCursor(ctx, data, clientId, cb);
            }
        };

        return cursor;
    };

    return Cursor;
});