var exports = module.exports = function (xs) {
    if (typeof xs !== 'object') { // of which Arrays are
        throw new TypeError('Must be an Array or an object');
    }
    
    return Object.keys(exports).reduce(function (acc, name) {
        acc[name] = exports[name].bind(null, xs);
        return acc;
    }, {});
};

exports.shuffle = function (xs) {
    if (Array.isArray(xs)) {
        // uniform shuffle
        var res = xs.slice();
        for (var i = res.length - 1; i >= 0; i--) {
            var n = Math.floor(Math.random() * i);
            var t = res[i];
            res[i] = res[n];
            res[n] = t;
        }
        return res;
    }
    else if (typeof xs === 'object') {
        // weighted shuffle
        var weights = Object.keys(xs).reduce(function (acc, key) {
            acc[key] = xs[key];
            return acc;
        }, {});
        
        var ret = [];
        
        while (Object.keys(weights).length > 0) {
            var key = exports.pick(weights);
            delete weights[key];
            ret.push(key);
        }
        
        return ret;
    }
    else {
        throw new TypeError('Must be an Array or an object');
    }
};

exports.pick = function (xs) {
    if (Array.isArray(xs)) {
        // uniform sample
        return xs[Math.floor(Math.random() * xs.length)];
    }
    else if (typeof xs === 'object') {
        // weighted sample
        var weights = exports.normalize(xs);
        if (!weights) return undefined;
        
        var n = Math.random();
        var threshold = 0;
        var keys = Object.keys(weights);
        
        for (var i = 0; i < keys.length; i++) {
            threshold += weights[keys[i]];
            if (n < threshold) return keys[i];
        }
        throw new Error('Exceeded threshold. Something is very wrong.');
    }
    else {
        throw new TypeError('Must be an Array or an object');
    }
};

exports.normalize = function (weights) {
    if (typeof weights !== 'object' || Array.isArray(weights)) {
        throw 'Not an object'
    }
    
    var keys = Object.keys(weights);
    if (keys.length === 0) return undefined;
    
    var total = keys.reduce(function (sum, key) {
        var x = weights[key];
        if (x < 0) {
            throw new Error('Negative weight encountered at key ' + key);
        }
        else if (typeof x !== 'number') {
            throw new TypeError('Number expected, got ' + typeof x);
        }
        else {
            return sum + x;
        }
    }, 0);
    
    return total === 1
        ? weights
        : keys.reduce(function (acc, key) {
            acc[key] = weights[key] / total;
            return acc;
        }, {})
    ;
};
