/**
 * High level crypto api that invokes native crypto (if available) and
 * gracefully degrades to JS crypto (if unavailable)
 */
define(function(require) {
    'use strict';

    var util = require('cryptoLib/util'),
        aes = require('cryptoLib/aes-cbc'),
        rsa = require('cryptoLib/rsa'),
        cryptoBatch = require('cryptoLib/crypto-batch'),
        pbkdf2 = require('js/crypto/pbkdf2'),
        config = require('js/app-config').config;

    var passBasedKey,
        BATCH_WORKER = '/crypto/crypto-batch-worker.js',
        PBKDF2_WORKER = '/crypto/pbkdf2-worker.js';

    var Crypto = function() {

    };

    /**
     * Initializes the crypto modules by fetching the user's
     * encrypted secret key from storage and storing it in memory.
     */
    Crypto.prototype.init = function(args, callback) {
        var self = this;

        // valdiate input
        if (!args.emailAddress || !args.keySize || !args.rsaKeySize || typeof args.password !== 'string' || !args.salt) {
            callback({
                errMsg: 'Crypto init failed. Not all args set!'
            });
            return;
        }

        self.emailAddress = args.emailAddress;
        self.keySize = args.keySize;
        self.ivSize = args.keySize;
        self.rsaKeySize = args.rsaKeySize;

        // derive PBKDF2 from password in web worker thread
        self.deriveKey(args.password, args.salt, self.keySize, function(err, derivedKey) {
            if (err) {
                callback(err);
                return;
            }

            // remember pbkdf2 for later use
            passBasedKey = derivedKey;

            // check if key exists
            if (!args.storedKeypair) {
                // generate keys, encrypt and persist if none exists
                generateKeypair(derivedKey);
            } else {
                // decrypt key
                decryptKeypair(args.storedKeypair, derivedKey);
            }

        });

        function generateKeypair(derivedKey) {
            // generate RSA keypair in web worker
            rsa.generateKeypair(self.rsaKeySize, function(err, generatedKeypair) {
                if (err) {
                    callback(err);
                    return;
                }

                // encrypt keypair
                var iv = util.random(self.ivSize);
                var encryptedPrivateKey = aes.encrypt(generatedKeypair.privkeyPem, derivedKey, iv);

                // new encrypted keypair object
                var newKeypair = {
                    publicKey: {
                        _id: generatedKeypair._id,
                        userId: self.emailAddress,
                        publicKey: generatedKeypair.pubkeyPem
                    },
                    privateKey: {
                        _id: generatedKeypair._id,
                        userId: self.emailAddress,
                        encryptedKey: encryptedPrivateKey,
                        iv: iv
                    }
                };

                // return generated keypair for storage in keychain dao
                callback(null, newKeypair);
            });
        }

        function decryptKeypair(storedKeypair, derivedKey) {
            var decryptedPrivateKey;

            // validate input
            if (!storedKeypair || !storedKeypair.privateKey || !storedKeypair.privateKey.encryptedKey || !storedKeypair.privateKey.iv) {
                callback({
                    errMsg: 'Incomplete arguments for private key decryption!'
                });
                return;
            }

            // try to decrypt with derivedKey
            try {
                var prK = storedKeypair.privateKey;
                decryptedPrivateKey = aes.decrypt(prK.encryptedKey, derivedKey, prK.iv);
            } catch (ex) {
                callback({
                    errMsg: 'Wrong password!'
                });
                return;
            }
            // set rsa keys
            rsa.init(storedKeypair.publicKey.publicKey, decryptedPrivateKey, storedKeypair.publicKey._id);

            callback();
        }
    };

    /**
     * Do PBKDF2 key derivation in a WebWorker thread
     */
    Crypto.prototype.deriveKey = function(password, salt, keySize, callback) {
        startWorker({
            script: PBKDF2_WORKER,
            args: {
                password: password,
                salt: salt,
                keySize: keySize
            },
            callback: callback,
            noWorker: function() {
                return pbkdf2.getKey(password, salt, keySize);
            }
        });
    };

    //
    // En/Decrypt a list of items with AES in a WebWorker thread
    //

    Crypto.prototype.symEncryptList = function(list, callback) {
        var self = this,
            key, envelope, envelopes = [];

        // generate single secret key shared for all list items
        key = util.random(self.keySize);

        // package objects into batchable envelope format
        list.forEach(function(i) {
            envelope = {
                id: i.id,
                plaintext: i,
                key: key,
                iv: util.random(self.ivSize)
            };
            envelopes.push(envelope);
        });

        startWorker({
            script: BATCH_WORKER,
            args: {
                type: 'symEncrypt',
                list: envelopes
            },
            callback: function(err, encryptedList) {
                // return generated secret key
                callback(err, {
                    key: key,
                    list: encryptedList
                });
            },
            noWorker: function() {
                return cryptoBatch.authEncryptList(envelopes);
            }
        });
    };

    Crypto.prototype.symDecryptList = function(list, keys, callback) {
        startWorker({
            script: BATCH_WORKER,
            args: {
                type: 'symDecrypt',
                list: list,
                keys: keys
            },
            callback: callback,
            noWorker: function() {
                return cryptoBatch.authDecryptList(list, keys);
            }
        });
    };

    //
    // En/Decrypt something speficially using the user's secret key
    //

    Crypto.prototype.encryptListForUser = function(list, receiverPubkeys, callback) {
        var self = this,
            envelope, envelopes = [];

        if (!receiverPubkeys || receiverPubkeys.length !== 1) {
            callback({
                errMsg: 'Encryption is currently implemented for only one receiver!'
            });
            return;
        }

        var keypair = rsa.exportKeys();
        var senderPrivkey = {
            _id: keypair._id,
            privateKey: keypair.privkeyPem
        };

        // package objects into batchable envelope format
        list.forEach(function(i) {
            envelope = {
                id: i.id,
                plaintext: i,
                key: util.random(self.keySize),
                iv: util.random(self.ivSize),
                receiverPk: receiverPubkeys[0]._id
            };
            envelopes.push(envelope);
        });

        startWorker({
            script: BATCH_WORKER,
            args: {
                type: 'asymEncrypt',
                list: envelopes,
                senderPrivkey: senderPrivkey,
                receiverPubkeys: receiverPubkeys
            },
            callback: callback,
            noWorker: function() {
                return cryptoBatch.encryptListForUser(envelopes, receiverPubkeys, senderPrivkey);
            }
        });
    };

    Crypto.prototype.decryptListForUser = function(list, senderPubkeys, callback) {
        if (!senderPubkeys || senderPubkeys < 1) {
            callback({
                errMsg: 'Sender public keys must be set!'
            });
            return;
        }

        var keypair = rsa.exportKeys();
        var receiverPrivkey = {
            _id: keypair._id,
            privateKey: keypair.privkeyPem
        };

        startWorker({
            script: BATCH_WORKER,
            args: {
                type: 'asymDecrypt',
                list: list,
                receiverPrivkey: receiverPrivkey,
                senderPubkeys: senderPubkeys
            },
            callback: callback,
            noWorker: function() {
                return cryptoBatch.decryptListForUser(list, senderPubkeys, receiverPrivkey);
            }
        });
    };

    //
    // Re-encrypt keys item and items seperately
    //

    Crypto.prototype.reencryptListKeysForUser = function(list, senderPubkeys, callback) {
        var keypair = rsa.exportKeys();
        var receiverPrivkey = {
            _id: keypair._id,
            privateKey: keypair.privkeyPem
        };

        startWorker({
            script: BATCH_WORKER,
            args: {
                type: 'reencrypt',
                list: list,
                receiverPrivkey: receiverPrivkey,
                senderPubkeys: senderPubkeys,
                symKey: passBasedKey
            },
            callback: callback,
            noWorker: function() {
                return cryptoBatch.reencryptListKeysForUser(list, senderPubkeys, receiverPrivkey, passBasedKey);
            }
        });
    };

    Crypto.prototype.decryptKeysAndList = function(list, callback) {
        startWorker({
            script: BATCH_WORKER,
            args: {
                type: 'decryptItems',
                list: list,
                symKey: passBasedKey
            },
            callback: callback,
            noWorker: function() {
                return cryptoBatch.decryptKeysAndList(list, passBasedKey);
            }
        });
    };

    //
    // helper functions
    //

    function startWorker(options) {
        // check for WebWorker support
        if (window.Worker) {
            // init webworker thread
            var worker = new Worker(config.workerPath + options.script);
            worker.onmessage = function(e) {
                if (e.data.err) {
                    options.callback(e.data.err);
                    return;
                }
                // return result from the worker
                options.callback(null, e.data);
            };
            worker.onerror = function(e) {
                // show error message in console
                console.error('Error handling web worker: Line ' + e.lineno + ' in ' + e.filename + ': ' + e.message);
                // return error
                options.callback({
                    errMsg: (e.message) ? e.message : e
                });
                return;
            };
            // send data to the worker
            worker.postMessage(options.args);
            return;
        }

        // no WebWorker support... do synchronous call
        var result;
        try {
            result = options.noWorker();
        } catch (e) {
            // return error
            options.callback({
                errMsg: (e.message) ? e.message : e
            });
            return;
        }
        options.callback(null, result);
    }

    return Crypto;
});