/**
 * Lodash mixins for combinatorics
 * by: wassname & visangela 
 * url: https://gist.github.com/wassname/a882ac3981c8e18d2556/edit
 * lodash contrib issue: https://github.com/node4good/lodash-contrib/issues/47
 * lic: same as lodash
 * Inspired by python itertools: https://docs.python.org/2.7/library/itertools.html
 *
 * Usage:
 *   permutations([0,1,2],2)                 // [[0,1],[0,2],[1,0],[1,2],[2,0],[2,1]]
 *   combinations([0,1,2],2)                 // [[0,1],[0,2],[1,2]]
 *   combinations_with_replacement([0,1,2],2)// [[0,0],[0,1],[0,2],[1,1],[1,2],[2,2]]
 *   product([0,1,2],[0,1,2])                // [[0,0],[0,1],[0,2],[1,0],[1,1],[1,2],[2,0],[2,1],[2,2]]
 *
 * Multiple input types:
 *   product('me','hi')
 *   product({who:['me','you'],say:['hi','by']})
 *   product(['me','you'],['hi','by'])
 *   product(['me','hi'])
 *   combinations([0,1,2,3],2)
 *   permutations([1,2,3],2)
 *   permutations('cat',2)
 */
var _ = require('lodash')

/**
 * Generate all combination of arguments when given arrays or strings
 * e.g. [['Ben','Jade','Darren'],['Smith','Miller']] to [['Ben','Smith'],[..]]
 * e.g. 'the','cat' to [['t', 'c'],['t', 'a'], ...]
**/
function _cartesianProductOf(args) {
    if (arguments.length>1) args=_.toArray(arguments);

    // strings to arrays of letters
    args=_.map(args, opt=>typeof opt==='string'?_.toArray(opt):opt)

    return _.reduce(args, function(a, b) {
        return _.flatten(_.map(a, function(x) {
            return _.map(b, function(y) {
                return _.concat(x,[y]);
            });
        }), true);
    }, [ [] ]);
}

/** Generate all combination of arguments from objects
  *  {Object} opts    - An object or arrays with keys describing options  {firstName:['Ben','Jade','Darren'],lastName:['Smith','Miller']}
  *  {Array}        - An array of objects e.g. [{firstName:'Ben',LastName:'Smith'},{..]
  **/
function _cartesianProductObj(optObj){
    var keys = _.keys(optObj);
    var opts = _.values(optObj);
    var combs = _cartesianProductOf(opts);
    return _.map(combs,function(comb){
        return _.zipObject(keys,comb);
    });
}

/**
 * Generate the cartesian product of input objects, arrays, or strings
 *
 *
 * product('me','hi')
 * // => [["m","h"],["m","i"],["e","h"],["e","i"]]
 *
 * product([1,2,3],['a','b','c']
 * // => [[1,"a"],[1,"b"],[1,"c"],[2,"a"],[2,"b"],[2,"c"],[3,"a"],[3,"b"],[3,"c"]]
 *
 * product({who:['me','you'],say:['hi','by']})
 * // => [{"who":"me","say":"hi"},{"who":"me","say":"by"},{"who":"you","say":"hi"},{"who":"you","say":"by"}]
 *
 * // It also takes in a single array of args
 * product(['me','hi'])
 * // => [["m","h"],["m","i"],["e","h"],["e","i"]]
 */
function product(opts){
    if (arguments.length===1 && !_.isArray(opts))
        return _cartesianProductObj(opts)
    else if (arguments.length===1)
        return _cartesianProductOf(opts)
    else
        return _cartesianProductOf(arguments)
}

/**
 * Generate permutations, in all possible orderings, with no repeat values
 *
 *
 * permutations([1,2,3],2)
 * // => [[1,2],[1,3],[2,1],[2,3],[3,1],[3,2]
 *
 * permutations('cat',2)
 * // => [["c","a"],["c","t"],["a","c"],["a","t"],["t","c"],["t","a"]]
 */
function permutations(obj, n){
    if (typeof obj=='string') obj = _.toArray(obj)
    n = n?n:obj.length
    // make n copies of keys/indices
    let nInds=[];
    for (var j = 0; j < n; j++) {nInds.push(_.keys(obj)) }
    // get product of the indices, then filter to remove the same key twice
    // var arrangements = product(nInds).filter(pair=>pair[0]!==pair[1]) // this line only removes duplicates from the first two elements.
    let arrangements = product(nInds);
    let out=[]
    for (let j=0; j< arrangements.length;j++ ) {
        let outt = arrangements[j].filter((value, index, self)=> {return self.indexOf(value) === index})
        if (outt.length === arrangements[j].length) out.push(outt)
    }
    return _.map(out,indices=>_.map(indices,i=>obj[i]))
}



/**
 * Generate n combinations of an object with no repeat values in each combination.
 *
 *
 * combinations([0,1,2,3],2)
 * // => [[0,1],[0,2],[0,3],[1,2],[1,3],[2,3]]
 */
function combinations(obj,n){
    /* filter out keys out of order, e.g. [0,1] is ok but [1,0] isn't */
    function isSorted(arr) {
        return _.every(arr, function (value, index, array) {
            return index === 0 || String(array[index - 1]) <= String(value);
        });
    }
    // array with n copies of the keys of obj
    return _(permutations(_.keys(obj),n))
        .filter(isSorted)
        .map(indices=>_.map(indices,i=>obj[i]))
        .value()
}

/**
 * Generate n combinations with repeat values.
 *
 *
 * combinations_with_replacement([0,1,2,3],2)
 * // => [[0,0],[0,1],[0,2],[0,3],[1,1],[1,2],[1,3],[2,2],[2,3],[3,3]]
 */
function combinations_with_replacement(obj,n){
    if (typeof obj=='string') obj = _.toArray(obj)
    n = n?n:obj.length
    // make n copies of keys/indices
    for (var j = 0, nInds=[]; j < n; j++) {nInds.push(_.keys(obj)) }
    // get product of the indices, then filter to keep elements in order
    var arrangements = product(nInds).filter(pair=>pair[0]<=pair[1])
    return _.map(arrangements,indices=>_.map(indices,i=>obj[i]))
}

module.exports={combinations_with_replacement,combinations,product,permutations}