define([
    'jquery',
    '/bower_components/chainpad-listmap/chainpad-listmap.js',
    '/bower_components/chainpad-crypto/crypto.js',
    '/common/cryptpad-common.js',
    '/common/credential.js',
    '/bower_components/tweetnacl/nacl-fast.min.js',
    '/bower_components/scrypt-async/scrypt-async.min.js', // better load speed
], function ($, Listmap, Crypto, Cryptpad, Cred) {
    var Exports = {
        Cred: Cred,
    };

    var Nacl = window.nacl;
    var allocateBytes = function (bytes) {
        var dispense = Cred.dispenser(bytes);

        var opt = {};

        // dispense 18 bytes of entropy for your encryption key
        var encryptionSeed = dispense(18);
        // 16 bytes for a deterministic channel key
        var channelSeed = dispense(16);
        // 32 bytes for a curve key
        var curveSeed = dispense(32);

        var curvePair = Nacl.box.keyPair.fromSecretKey(new Uint8Array(curveSeed));
        opt.curvePrivate = Nacl.util.encodeBase64(curvePair.secretKey);
        opt.curvePublic = Nacl.util.encodeBase64(curvePair.publicKey);

        // 32 more for a signing key
        var edSeed = opt.edSeed = dispense(32);

        // derive a private key from the ed seed
        var signingKeypair = Nacl.sign.keyPair.fromSeed(new Uint8Array(edSeed));

        opt.edPrivate = Nacl.util.encodeBase64(signingKeypair.secretKey);
        opt.edPublic = Nacl.util.encodeBase64(signingKeypair.publicKey);

        var keys = opt.keys = Crypto.createEditCryptor(null, encryptionSeed);

        // 24 bytes of base64
        keys.editKeyStr = keys.editKeyStr.replace(/\//g, '-');

        // 32 bytes of hex
        var channelHex = opt.channelHex = Cryptpad.uint8ArrayToHex(channelSeed);

        // should never happen
        if (channelHex.length !== 32) { throw new Error('invalid channel id'); }

        opt.channel64 = Cryptpad.hexToBase64(channelHex);

        opt.userHash = '/1/edit/' + [opt.channel64, opt.keys.editKeyStr].join('/');

        return opt;
    };

    var loadUserObject = function (opt, cb) {
        var config = {
            websocketURL: Cryptpad.getWebsocketURL(),
            channel: opt.channelHex,
            data: {},
            validateKey: opt.keys.validateKey, // derived validation key
            crypto: Crypto.createEncryptor(opt.keys),
            logLevel: 1,
        };

        var rt = opt.rt = Listmap.create(config);
        rt.proxy
        .on('ready', function () {
            cb(void 0, rt);
        })
        .on('disconnect', function (info) {
            cb('E_DISCONNECT', info);
        });
    };

    var isProxyEmpty = function (proxy) {
        return Object.keys(proxy).length === 0;
    };

    Exports.loginOrRegister = function (uname, passwd, isRegister, cb) {
        if (typeof(cb) !== 'function') { return; }

        // Usernames are all lowercase. No going back on this one
        uname = uname.toLowerCase();

        // validate inputs
        if (!Cred.isValidUsername(uname)) { return void cb('INVAL_USER'); }
        if (!Cred.isValidPassword(passwd)) { return void cb('INVAL_PASS'); }

        Cred.deriveFromPassphrase(uname, passwd, 128, function (bytes) {
            // results...
            var res = {
                register: isRegister,
            };

            // run scrypt to derive the user's keys
            var opt = res.opt = allocateBytes(bytes);

            // use the derived key to generate an object
            loadUserObject(opt, function (err, rt) {
                if (err) { return void cb(err); }

                res.proxy = rt.proxy;
                res.realtime = rt.realtime;
                res.network = rt.network;

                // they're registering...
                res.userHash = opt.userHash;
                res.userName = uname;

                // export their signing key
                res.edPrivate = opt.edPrivate;
                res.edPublic = opt.edPublic;

                res.curvePrivate = opt.curvePrivate;
                res.curvePublic = opt.curvePublic;

                // they tried to just log in but there's no such user
                if (!isRegister && isProxyEmpty(rt.proxy)) {
                    rt.network.disconnect(); // clean up after yourself
                    return void cb('NO_SUCH_USER', res);
                }

                // they tried to register, but those exact credentials exist
                if (isRegister && !isProxyEmpty(rt.proxy)) {
                    rt.network.disconnect();
                    return void cb('ALREADY_REGISTERED', res);
                }

                cb(void 0, res);
            });
        });
    };

    return Exports;
});
