define([
], function () {
    var OO = {};

    var getHistory = function (ctx, client, cb) {
        var c = ctx.clients[client];
        if (!c) { return void cb({error: 'ENOENT'}); }
        var chan = ctx.channels[c.channel];
        if (!chan) { return void cb({error: 'ENOCHAN'}); }
        cb();
        chan.history.forEach(function (msg) {
            ctx.emit('MESSAGE', {
                msg: msg,
                validateKey: chan.validateKey
            }, [client]);
        });
        ctx.emit('HISTORY_SYNCED', {}, [client]);
    };

    var openChannel = function (ctx, obj, client, cb) {
        var channel = obj.channel;
        var padChan = obj.padChan;
        var network = ctx.store.network;
        var first = true;

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

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

            // ==> Use our netflux ID to create our client ID
            if (!c.id) { c.id = chan.wc.myID + '-' + client; }

            getHistory(ctx, client, function () {
                ctx.emit('READY', '', [client]);
            });

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

        var txid = Math.floor(Math.random() * 1000000);
        var onOpen = function (wc) {

            ctx.channels[channel] = ctx.channels[channel] || {
                history: [],
                validateKey: obj.validateKey
            };

            chan = ctx.channels[channel];
            chan.padChan = padChan;

            // Create our client ID using the netflux ID
            if (!c.id) { c.id = wc.myID + '-' + client; }

            // If this is a reconnect, we have a new netflux ID so we're going to fix
            // all our client IDs.
            if (chan.clients) {
                chan.clients.forEach(function (cl) {
                    if (ctx.clients[cl]) {
                        ctx.clients[cl].id = wc.myID + '-' + cl;
                    }
                });
            }

            wc.on('join', function () {
            });
            wc.on('leave', function () {
            });
            wc.on('message', function (msg) {
                chan.history.push(msg);
                ctx.emit('MESSAGE', {
                    msg: msg,
                    validateKey: chan.validateKey
                }, chan.clients);
            });

            chan.wc = wc;
            chan.sendMsg = function (msg, cb) {
                cb = cb || function () {};
                wc.bcast(msg).then(function () {
                    chan.history.push(msg);
                    cb();
                }, function (err) {
                    cb({error: err});
                });
            };

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

            var hk = network.historyKeeper;
            var cfg = {
                txid: txid,
                lastKnownHash: chan.lastKnownHash || chan.lastCpHash,
                metadata: {
                    validateKey: obj.validateKey,
                    owners: obj.owners,
                    expire: obj.expire
                }
            };
            var msg = ['GET_HISTORY', wc.id, cfg];
            // Add the validateKey if we are the channel creator and we have a validateKey
            if (hk) {
                network.sendto(hk, JSON.stringify(msg)).then(function () {
                }, function (err) {
                    console.error(err);
                });
            }

        };

        network.on('message', function (msg, sender) {
            if (!ctx.channels[channel]) { return; }
            var hk = network.historyKeeper;
            if (sender !== hk) { return; }

            // Parse the message
            var parsed;
            try {
                parsed = JSON.parse(msg);
            } catch (e) {}
            if (!parsed) { return; }

            // If there is a txid, make sure it's ours or abort
            if (parsed.txid && parsed.txid !== txid) { return; }

            // Keep only metadata messages for the current channel
            if (parsed.channel && parsed.channel !== channel) { return; }
            // Ignore the metadata message
            if (parsed.validateKey && parsed.channel) {
                if (!chan.validateKey) {
                    chan.validateKey = parsed.validateKey;
                }
                return;
            }
            // End of history: emit READY
            if (parsed.state && parsed.state === 1 && parsed.channel) {
                ctx.emit('READY', '', chan.clients);
                return;
            }
            if (parsed.error && parsed.channel) { return; }

            // If there is a txid, make sure it's ours or abort
            if (Array.isArray(parsed) && parsed[0] && parsed[0] !== txid) {
                return;
            }

            msg = parsed[4];

            // Keep only the history for our channel
            if (parsed[3] !== channel) { return; }

            var hash = msg.slice(0,64);
            if (hash === chan.lastKnownHash || hash === chan.lastCpHash) { return; }

            chan.lastKnownHash = hash;
            ctx.emit('MESSAGE', {
                msg: msg,
            }, chan.clients);
            chan.history.push(msg);
        });

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

        network.on('reconnect', function () {
            if (!ctx.channels[channel]) { return; }
            network.join(channel).then(onOpen, function (err) {
                console.error(err);
            });
        });
    };

    var updateHash = function (ctx, data, clientId, cb) {
        var c = ctx.clients[clientId];
        if (!c) { return void cb({ error: 'NOT_IN_CHANNEL' }); }
        var chan = ctx.channels[c.channel];
        if (!chan) { return void cb({ error: 'INVALID_CHANNEL' }); }
        var hash = data;
        var index = -1;
        chan.history.some(function (msg, idx) {
            if (msg.slice(0,64) === hash) {
                index = idx + 1;
                return true;
            }
        });
        if (index !== -1) {
            chan.history = chan.history.slice(index);
        }
        cb();
    };

    var sendMessage = function (ctx, data, clientId, cb) {
        var c = ctx.clients[clientId];
        if (!c) { return void cb({ error: 'NOT_IN_CHANNEL' }); }
        var chan = ctx.channels[c.channel];
        if (!chan) { return void cb({ error: 'INVALID_CHANNEL' }); }
        // Prepare the callback: broadcast the message to the other local tabs
        // if the message is sent
        var _cb = function (obj) {
            if (obj && obj.error) { return void cb(obj); }
            ctx.emit('MESSAGE', {
                msg: data.msg
            }, chan.clients.filter(function (cl) {
                return cl !== clientId;
            }));
            cb();
        };
        // Send the message
        if (data.isCp) {
            return void chan.sendMsg(data.isCp, _cb);
        }
        chan.sendMsg(data.msg, _cb);
    };

    var reencrypt = function (ctx, data, cId, cb) {
        var channel = data.channel;
        var network = ctx.store.network;

        var onOpen = function (wc) {
            var hk = network.historyKeeper;
            var cfg = {
                metadata: data.metadata
            };
            var msg = ['GET_HISTORY', wc.id, cfg];
            network.sendto(hk, JSON.stringify(msg));
            data.msgs.forEach(function (msg) {
                wc.bcast(msg);
            });
            wc.leave();
            cb();
        };

        ctx.store.anon_rpc.send("IS_NEW_CHANNEL", channel, function (e, response) {
            if (e) { return void cb({error: e}); }
            var isNew;
            if (response && response.length && typeof(response[0]) === 'boolean') {
                isNew = response[0];
            } else {
                cb({error: 'INVALID_RESPONSE'});
            }
            if (!isNew) { return void cb({error: 'EEXISTS'}); }

            // Channel is new: we can push our reencrypted history
            network.join(channel).then(onOpen, function (err) {
                return void cb({error: err});
            });
        });
    };

    var leaveChannel = function (ctx, padChan) {
        // Leave channel and prevent reconnect when we leave a pad
        Object.keys(ctx.channels).some(function (ooChan) {
            var channel = ctx.channels[ooChan];
            if (channel.padChan !== padChan) { return; }
            if (channel.wc) { channel.wc.leave(); }
            delete ctx.channels[ooChan];
            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(); }
                delete ctx.channels[k];
            }
        }

        if (ctx.clients[clientId]) {
            var oldChannel = ctx.clients[clientId].channel;
            var oldChan = ctx.channels[oldChannel];
            if (oldChan) {
                ctx.emit('LEAVE', {id: clientId}, [oldChan.clients[0]]);
            }
            delete ctx.clients[clientId];
        }
    };



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

        oo.removeClient = function (clientId) {
            removeClient(ctx, clientId);
        };
        oo.leavePad = function (padChan) {
            leaveChannel(ctx, padChan);
        };
        oo.execCommand = function (clientId, obj, cb) {
            var cmd = obj.cmd;
            var data = obj.data;
            if (cmd === 'SEND_MESSAGE') {
                return void sendMessage(ctx, data, clientId, cb);
            }
            if (cmd === 'UPDATE_HASH') {
                return void updateHash(ctx, data, clientId, cb);
            }
            if (cmd === 'OPEN_CHANNEL') {
                return void openChannel(ctx, data, clientId, cb);
            }
            if (cmd === 'GET_HISTORY') {
                return void getHistory(ctx, clientId, cb);
            }
            if (cmd === 'REENCRYPT') {
                return void reencrypt(ctx, data, clientId, cb);
            }
        };

        return oo;
    };

    return OO;
});