'use strict';

/*@
Ox.compact <f> Returns an array w/o <code>undefined</code> values
    > Ox.compact([null,,1,,2,,3])
    [1, 2, 3]
@*/
Ox.compact = function(arr) {
    return arr.filter(function(val) {
        return !Ox.isNull(val) && !Ox.isUndefined(val);
    });
};

/*@
Ox.flatten <f> Flattens an array
    > Ox.flatten([1, [2, [3], 2], 1])
    [1, 2, 3, 2, 1]
@*/
Ox.flatten = function(arr) {
    // fixme: can this work for objects too?
    var ret = [];
    arr.forEach(function(val) {
        if (Ox.isArray(val)) {
            Ox.flatten(val).forEach(function(val) {
                ret.push(val);
            });
        } else {
            ret.push(val);
        }
    });
    return ret;
};

/*@ 
Ox.merge <f> Merges an array with one or more other arrays
    > Ox.merge([1], [2, 3, 2], [1])
    [1, 2, 3, 2, 1]
    > Ox.merge(1, [2, 3, 2], 1)
    [1, 2, 3, 2, 1]
@*/
Ox.merge = function(arr) {
    arr = Ox.toArray(arr);
    Ox.forEach(Array.prototype.slice.call(arguments, 1), function(arg) {
        Ox.forEach(Ox.toArray(arg), function(val) {
            arr.push(val);
        });
    });
    return arr;
};

/*@
Ox.range <f> Python-style range
    (stop) -> <[n]> range
        Returns an array of integers from <code>0</code> (inclusive) to
        <code>stop</code> (exclusive).
    (start, stop) -> <[n]> range
        Returns an array of integers from <code>start</code> (inclusive) to
        <code>stop</code> (exclusive).
    (start, stop, step) -> <[n]> range
        Returns an array of numbers from <code>start</code> (inclusive) to
        <code>stop</code> (exclusive), incrementing by <code>step</step>.
    start <n> Start value
    stop <n> Stop value
    step <n> Step value
    > Ox.range(3)
    [0, 1, 2]
    > Ox.range(1, 4)
    [1, 2, 3]
    > Ox.range(3, 0)
    [3, 2, 1]
    > Ox.range(1, 2, 0.5)
    [1, 1.5]
    > Ox.range(-1, -2, -0.5)
    [-1, -1.5]
@*/
Ox.range = function() {
    var args = Ox.makeArray(arguments),
        arr = [];
    args.push(function(i) {
        arr.push(i);
    });
    Ox.loop.apply(null, args);
    return arr;
};

(function() {

    function getSortValues(arr) {
        var len, matches = {}, sort = {};
        // find leading numbers
        arr.forEach(function(val) {
            var match = /^\d+/.exec(val);
            matches[val] = match ? match[0] : '';
        });
        // get length of longest leading number
        len = Ox.max(Ox.map(matches, function(val) {
            return val.length;
        }));
        // pad leading numbers and make lowercase
        arr.forEach(function(val) {
            sort[val] = (
                matches[val]
                ? Ox.pad(matches[val], len)
                    + val.toString().substr(matches[val].length)
                : val
            ).toLowerCase().replace(/^\W+/, '');
        });
        return sort;
    }

    /*@
    Ox.sort <f> Sorts an array, handling leading digits and ignoring capitalization
        (arr) -> <a> Sorted array
        (arr, fn) -> Sorted array
        arr <a> Array
        fn <f|u> Optional map function that returns the value for the array element
        > Ox.sort(['"z"', '10', '9', 'B', 'a'])
        ['9', '10', 'a', 'B', '"z"']
        > Ox.sort([{id: 0, name: '80 Days'}, {id: 1, name: '8 Women'}], function(v) {return v.name})
        [{id: 1, name: '8 Women'}, {id: 0, name: '80 Days'}]
    @*/
    Ox.sort = function(arr, fn) {
        var sort = getSortValues(fn ? arr.map(fn) : arr);
        return arr.sort(function(a, b) {
            a = fn ? fn(a) : a;
            b = fn ? fn(b) : b;
            var ret = 0;
            if (sort[a] < sort[b]) {
                ret = -1;
            } else if (sort[a] > sort[b]) {
                ret = 1;
            }
            return ret;
        });
    };

    /*@
    Ox.sortBy <f> Sorts an array of objects by given properties
        (arr, by) -> <a> Sorted array
        arr <[o]> Array of objects
        by <[s]> Array of object properties (asc: 'foo' or '+foo', desc: '-foo')
        > Ox.sortBy([{x: 1, y: 1}, {x: 1, y: 2}, {x: 2, y: 2}], ['+x', '-y'])
        [{x: 1, y: 2}, {x: 1, y: 1}, {x: 2, y: 2}]
        > Ox.sortBy([{id: 0, name: '80 Days'}, {id: 1, name: '8 Women'}], ['name'])
        [{id: 1, name: '8 Women'}, {id: 0, name: '80 Days'}]
    @*/
    Ox.sortBy = function(arr, by) {
        var length = by.length, values = {};
        by = by.map(function(v) {
            return {
                key: v.replace(/^[\+\-]/g, ''),
                operator: v[0] == '-' ? '-' : '+'
            };
        });
        by.map(function(v) {
            return v.key;
        }).forEach(function(key) {
            values[key] = getSortValues(arr.map(function(v) {
                return v[key];
            }));
        });
        return arr.sort(function(a, b) {
            var aValue, bValue, index = 0, key, ret = 0;
            while (ret == 0 && index < length) {
                key = by[index].key;
                aValue = values[key][a[key]];
                bValue = values[key][b[key]];
                if (aValue < bValue) {
                    ret = by[index].operator == '+' ? -1 : 1;
                } else if (aValue > bValue) {
                    ret = by[index].operator == '+' ? 1 : -1;
                } else {
                    index++;
                }
            }
            return ret;
        });
    };

}());

/*@
Ox.unique <f> Returns an array without duplicate values
    > Ox.unique([1, 2, 3, 2, 1])
    [1, 2, 3]
    > Ox.unique([NaN, NaN])
    []
@*/
Ox.unique = function(arr) {
    return Ox.filter(arr, function(val, i) {
        return arr.indexOf(val) == i;
    });
};

/*@
Ox.zip <f> Zips an array of arrays
    > Ox.zip([[0, 1], [2, 3], [4, 5]])
    [[0, 2, 4], [1, 3, 5]]
    > Ox.zip([0, 1, 2], [3, 4, 5])
    [[0, 3], [1, 4], [2, 5]]
@*/
Ox.zip = function() {
    var args = arguments.length == 1 ? arguments[0] : Ox.makeArray(arguments),
        arr = [];
    args[0].forEach(function(v, i) {
        arr[i] = [];
        args.forEach(function(v) {
            arr[i].push(v[i]);
        });
    });
    return arr;
};