'use strict';

/*
Ox.api <f> Turns an array into a list API
    <code>Ox.api</code> takes an array and returns a function that allows you to
    run complex queries against it. See the examples below for details.
    (items, options) -> <f> List API
    items <[o]> An array of objects (key/value stores)
    options <o> Options object
        cache <b|false> If true, cache results
        enums <o> Enumerables, for example <code>{size: ['S', 'M', 'L', 'XL']}</code>
        geo <b|false> If true, return combined area with totals
        sort <[o]|[s]> Default sort, for example <code> ['+name', '-age']
        sums <[s]> List of keys to be included in totals
        unique <s|'id'> The name of the unique key
    <script>
        Ox.test.api = Ox.api([
            {id: 'foo', n: 2},
            {id: 'bar', n: 2},
            {id: 'baz', n: 1}
        ], {
            sums: ['n']
        });
        Ox.test.apiResults = {
            0: Ox.test.api(),
            1: Ox.test.api({
                keys: []
            }),
            2: Ox.test.api({
                keys: ['id'],
                sort: ['-n', '+id']
            }),
            3: Ox.test.api({
                keys: [],
                query: {
                    conditions: [
                        {key: 'id', operator: '!^', value: 'f'},
                        {key: 'n', operator: '>', value: 1}
                    ],
                    operator: '&'
                }
            }),
            4: Ox.test.api({
                keys: [],
                query: {
                    conditions: [
                        {key: 'id', operator: '=', value: 'O'},
                        {key: 'n', operator: '=', value: [1, 2]}
                    ],
                    operator: '|'
                },
                sort: ['+id']
            }),
            5: Ox.test.api({
                keys: [],
                query: {
                    conditions: [
                        {key: 'id', operator: '=', value: 'f'},
                        {
                            conditions: [
                                {key: 'id', operator: '=', value: 'a'},
                                {key: 'id', operator: '=', value: 'z'}
                            ],
                            operator: '&'
                        }
                    ],
                    operator: '|'
                },
                sort: ['+id']
            }),
            6: Ox.test.api({
                keys: [],
                range: [1, 2],
                sort: ['+id']
            }),
            7: Ox.test.api({
                positions: ['foo', 'bar'],
                sort: ['+id']
            })
        };
        Ox.test.api = Ox.api([
            {i: 0, size: 'S'},
            {i: 1, size: 'M'},
            {i: 2, size: 'L'}
        ], {
            enums: {size: ['S', 'M', 'L']},
            unique: 'i'
        });
        Ox.test.apiResults[8] = Ox.test.api({
            keys: ['size'],
            query: {
                conditions: [{key: 'size', operator: '>=', value: 'M'}],
                operator: '&'
            },
            sort: [{key: 'size', operator: '-'}]
        });
    </script>
    > Ox.test.apiResults[0].data
    {items: 3, n: 5}
    > Ox.test.apiResults[1].data
    {items: [{id: 'foo', n: 2}, {id: 'bar', n: 2}, {id: 'baz', n: 1}]}
    > Ox.test.apiResults[2].data
    {items: [{id: 'bar'}, {id: 'foo'}, {id: 'baz'}]}
    > Ox.test.apiResults[3].data
    {items: [{id: 'bar', n: 2}]}
    > Ox.test.apiResults[4].data
    {items: [{id: 'baz', n: 1}, {id: 'foo', n: 2}]}
    > Ox.test.apiResults[5].data
    {items: [{id: 'baz', n: 1}, {id: 'foo', n: 2}]}
    > Ox.test.apiResults[6].data
    {items: [{id: 'baz', n: 1}]}
    > Ox.test.apiResults[7].data
    {positions: {foo: 2, bar: 0}}
    > Ox.test.apiResults[8].data
    {items: [{i: 2, size: 'L'}, {i: 1, size: 'M'}]}
*/
Ox.api = function(items, options) {

    var api = {
            cache: options.cache,
            enums: options.enums ? parseEnums(options.enums) : {},
            geo: options.geo,
            sort: options.sort || [],
            sums: options.sums || [],
            unique: options.unique || 'id'
        },
        fn = function(options, callback) {
            var data,
                result = {data: {}, status: {code: 200, text: 'ok'}},
                sort = {};
            options = options || {};
            if (options.query) {
                // find
                options.query.conditions = parseConditions(options.query.conditions);
                result.data.items = items.filter(function(item) {
                    return testQuery(item, options.query);
                });
            } else {
                result.data.items = Ox.clone(items);
            }
            if (options.sort && result.data.items.length > 1) {
                // sort
                options.sort = parseSort(options.sort.concat(api.sort));
                options.sort.forEach(function(v) {
                    var key = v.key;
                    if (api.enums[key]) {
                        sort[key] = function(value) {
                            return api.enums[key].indexOf(value.toLowerCase());
                        };
                    } /*else if (Ox.isArray(items[0][key])) {
                        sort[key] = function(value) {
                            return value.join(', ');
                        };
                    }*/
                });
                result.data.items = Ox.sortBy(result.data.items, options.sort, sort);
            }
            if (options.positions) {
                // return positions
                data = {positions: {}};
                options.positions.forEach(function(id) {
                    data.positions[id] = Ox.indexOf(result.data.items, function(item) {
                        return item[api.unique] == id;
                    });
                });
                result.data = data;
            } else if (!options.keys) {
                // return totals
                data = {};
                api.sums.forEach(function(key) {
                    data[key] = Ox.sum(result.data.items.map(function(item) {
                        return item[key];
                    }));
                })
                data.items = result.data.items.length;
                if (api.geo) {
                    /*
                    fixme: slow, disabled
                    data.area = Ox.joinAreas(result.data.items.map(function(item) {
                        return {
                            sw: {lat: item.south, lng: item.west},
                            ne: {lat: item.north, lng: item.east}
                        };
                    }));
                    data.area = {
                        south: data.area.sw.lat,
                        west: data.area.sw.lng,
                        north: data.area.ne.lat,
                        east: data.area.ne.lng
                    };
                    */
                    data.area = {
                        south: Ox.MIN_LATITUDE,
                        west: -180,
                        north: Ox.MAX_LATITUDE,
                        east: 180
                    }
                }
                result.data = data;
            } else {
                // return items
                if (!Ox.isEmpty(options.keys)) {
                    // filter keys
                    if (options.keys.indexOf(api.unique) == -1) {
                        options.keys.push(api.unique);
                    }
                    result.data.items = result.data.items.map(function(item) {
                        var ret = {};
                        options.keys.forEach(function(key) {
                            ret[key] = item[key];
                        });
                        return ret;
                    });
                }
                if (options.range) {
                    // apply range
                    result.data.items = result.data.items.slice(
                        options.range[0], options.range[1]
                    );
                }
            }
            callback && callback(result);
            return result;
        }; 

    function parseEnums(enums) {
        // make enumerable strings lowercase
        return Ox.map(enums, function(values) {
            return values.map(function(value) {
                return value.toLowerCase();
            });
        });
    }

    function parseConditions(conditions) {
        // make string values lowercase,
        // and replace enumerable strings used with the
        // <, !<, <=, !<=, >, !>, >= or !>= operator
        // with their index
        return conditions.map(function(condition) {
            var key = condition.key,
                operator = condition.operator,
                values = Ox.makeArray(condition.value);
            if (condition.conditions) {
                condition.conditions = parseConditions(condition.conditions);
            } else {
                values = values.map(function(value) {
                    if (Ox.isString(value)) {
                        value = value.toLowerCase();
                    }
                    if (api.enums[key] && (
                        operator.indexOf('<') > -1
                        || operator.indexOf('>') > -1
                    )) {
                        value = api.enums[key].indexOf(value);
                    }
                    return value;
                });
                condition.value = Ox.isArray(condition.value)
                    ? values : values[0];
            }
            return condition;
        });
    }

    function parseSort(sort) {
        // translate 'foo' to {key: 'foo', operator: '+'}
        return sort.map(function(sort) {
            return Ox.isString(sort) ? {
                key: sort.replace(/^[\+\-]/, ''),
                operator: sort[0] == '-' ? '-' : '+'
            } : sort;
        });
    }

    function testCondition(item, condition) {
        var key = condition.key,
            operator = condition.operator.replace('!', ''),
            value = condition.value,
            not = condition.operator[0] == '!',
            itemValue = item[key],
            test = {
                '=': function(a, b) {
                    return Ox.isArray(b) ? a >= b[0] && a < b[1]
                        : Ox.isString(a) ? a.indexOf(b) > -1
                        : a === b;
                },
                '==': function(a, b) { return a === b; },
                '<': function(a, b) { return a < b; },
                '<=': function(a, b) { return a <= b; },
                '>': function(a, b) { return a > b; },
                '>=': function(a, b) { return a >= b; },
                '^': function(a, b) { return Ox.starts(a, b); },
                '$': function(a, b) { return Ox.ends(a, b); },
            };
        if (Ox.isString(itemValue)) {
            itemValue = itemValue.toLowerCase();
        }
        if (api.enums[key] && (
            operator.indexOf('<') > -1
            || operator.indexOf('>') > -1
        )) {
            itemValue = api.enums[key].indexOf(itemValue);
        }
        return test[operator](itemValue, value) == !not;
    }

    function testQuery(item, query) {
        var match = true;
        Ox.forEach(query.conditions, function(condition) {
            if (condition.conditions) {
                match = testQuery(item, condition);
            } else {
                match = testCondition(item, condition)
            }
            if (
                (query.operator == '&' && !match)
                || (query.operator == '|' && match)
            ) {
                Ox.Break();
            }
        });
        return match;
    }

    return api.cache ? Ox.cache(fn, {async: true}) : fn;

};

/*@
Ox.compact <f> Removes <code>null</code> or <code>undefined</code> values
    (array) -> <a> Array 
    > Ox.compact([null,,1,,2,,3])
    [1, 2, 3]
@*/
Ox.compact = function(array) {
    return array.filter(function(value) {
        return !Ox.isNull(value) && !Ox.isUndefined(value);
    });
};

/*@
Ox.find <f> Returns array elements that match a string
    Returns an array of case-insensitive matches: exact match first, then
    leading matches, then other matches
    > Ox.find(['Bar', 'Barfoo', 'Foo', 'Foobar'], 'foo')
    ['Foo', 'Foobar', 'Barfoo']
    > Ox.find(['Bar', 'Barfoo', 'Foo', 'Foobar'], 'foo', true)
    ['Foo', 'Foobar']
@*/
Ox.find = function(array, string, leading) {
    var ret = [[], []];
    string = string.toLowerCase();
    array.forEach(function(value) {
        var lowerCase = value.toLowerCase(),
            index = lowerCase.indexOf(string);
        index > -1 && ret[index == 0 ? 0 : 1][
            lowerCase == string ? 'unshift' : 'push'
        ](value);
    })
    return leading ? ret[0] : ret[0].concat(ret[1]);
};

/*@
Ox.flatten <f> Flattens an array
    (arr) -> <a> Array
    > Ox.flatten([1, [2, [3], 2], 1])
    [1, 2, 3, 2, 1]
@*/
Ox.flatten = function(array) {
    var ret = [];
    array.forEach(function(value) {
        if (Ox.isArray(value)) {
            Ox.flatten(value).forEach(function(value) {
                ret.push(value);
            });
        } else {
            ret.push(value);
        }
    });
    return ret;
};

/*@
Ox.getIndexById <f> Returns the first array index of an object with a given id
    > Ox.getIndexById([{id: 'foo', str: 'Foo'}, {id: 'bar', str: 'Bar'}], 'bar')
    1
    > Ox.getIndexById([{id: 'foo', str: 'Foo'}, {id: 'bar', str: 'Bar'}], 'baz')
    -1
@*/
Ox.getIndexById = function(array, id) {
    return Ox.indexOf(array, function(obj) {
        return obj.id === id;
    });
};

/*@
Ox.getObjectById <f> Returns the first object in an array with a given id
    > Ox.getObjectById([{id: 'foo', str: 'Foo'}, {id: 'bar', str: 'Bar'}], 'bar')
    {id: "bar", str: "Bar"}
    > Ox.getObjectById([{id: 'foo', str: 'Foo'}, {id: 'bar', str: 'Bar'}], 'baz')
    null
@*/
Ox.getObjectById = function(array, id) {
    var index = Ox.getIndexById(array, id);
    return index > -1 ? array[index] : null;
};

/*
Ox.indexOf = function(arr) {
    // indexOf for primitives, test for function, deep equal for others
};
*/

/*@
Ox.makeArray <f> Wraps any non-array in an array.
    > Ox.makeArray('foo')
    ['foo']
    > Ox.makeArray(['foo'])
    ['foo']
@*/
Ox.makeArray = function(value) {
    var ret, type = Ox.typeOf(value);
    if (type == 'arguments') {
        ret = Ox.toArray(value);
    } else if (type == 'array') {
        ret = value;
    } else {
        ret = [value];
    }
    return ret;
};

/*@
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</code>.
    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 array = [];
    Ox.loop.apply(null, Ox.toArray(arguments).concat(function(index) {
        array.push(index);
    }));
    return array;
};

(function() {

    function getSortValues(arr, fn) {
        var arr_ = fn ? arr.map(fn) : arr,
            len, matches = [], sort = {};
        // find numbers
        arr.forEach(function(val, i) {
            var match;
            if (Ox.isString(val)) {
                match = arr_[i].match(/\d+/g);
                if (match) {
                    matches = matches.concat(match);
                }
            }
        });
        // get length of longest number
        len = Ox.max(Ox.map(matches, function(val) {
            return val.length;
        }));
        // make lowercase, remove leading non-word characters,
        // pad numbers and move leading articles to the end
        arr.forEach(function(val, i) {
            var val_ = arr_[i];
            if (
                Ox.isEmpty(val_)
                || Ox.isNull(val_)
                || Ox.isUndefined(val_)
            ) {
                sort[val] = null;
            } else if (Ox.isString(val_)) {
                sort[val] = val_.toLowerCase()
                    .replace(/^\W+/, '')
                    .replace(/\d+/g, function(match) {
                        return Ox.pad(match, len);
                    });
                Ox.forEach(['a', 'an', 'the'], function(article) {
                    var len;
                    if (new RegExp('^' + article + ' ').test(sort[val])) {
                        len = article.length;
                        sort[val] = sort[val].slice(len + 1) + ', '
                            + sort[val].slice(0, len);
                        Ox.Break();
                    }
                });
            } else {
                sort[val] = val_;
            }
        });
        return sort;
    }

    /*@
    Ox.sort <f> Sorts an array, handling articles and digits, ignoring capitalization
        (array) -> <a> Sorted array
        (array, map) -> <a> Sorted array
        array <a> Array
        map <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(['In 80 Days Around the World', 'In 9 Minutes Around the World'])
        ['In 9 Minutes Around the World', 'In 80 Days Around the World']
        > Ox.sort(['Man', 'A Plan', 'The Canal'])
        ['The Canal', 'Man', 'A Plan']
    @*/
    Ox.sort = function(array, map) {
        var values = getSortValues(map ? array.map(map) : array);
        return array.sort(function(a, b) {
            a = map ? map(a) : a;
            b = map ? map(b) : b;
            var ret = 0;
            if (values[a] < values[b]) {
                ret = -1;
            } else if (values[a] > values[b]) {
                ret = 1;
            }
            return ret;
        });
    };

    /*@
    Ox.sortBy <f> Sorts an array of objects by given properties
        (array, by[, map]) -> <a> Sorted array
        array <[o]> Array of objects
        by <[s]> Array of object keys (asc: 'foo' or '+foo', desc: '-foo')
        map <o> Optional map functions, per key, that return the sort value
        > 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(array, by, map) {
        var values = {};
        by = Ox.makeArray(by);
        map = map || {};
        by = by.map(function(value) {
            return Ox.isString(value) ? {
                key: value.replace(/^[\+\-]/, ''),
                operator: value[0] == '-' ? '-' : '+'
            } : value;
        });
        by.map(function(value) {
            return value.key;
        }).forEach(function(key) {
            values[key] = getSortValues(array.map(function(value) {
                return value[key];
            }), map[key]);
        });
        return array.sort(function(a, b) {
            var aValue, bValue, index = 0, key, ret = 0;
            while (ret == 0 && index < by.length) {
                key = by[index].key;
                aValue = values[key][a[key]];
                bValue = values[key][b[key]];
                if ((aValue === null) != (bValue === null)) {
                    ret = aValue === null ? 1 : -1;
                } else if (aValue < bValue) {
                    ret = by[index].operator == '+' ? -1 : 1;
                } else if (aValue > bValue) {
                    ret = by[index].operator == '+' ? 1 : -1;
                } else {
                    index++;
                }
            }
            return ret;
        });
    };

}());

/*@
Ox.toArray <f> Takes an array-like object and returns a true array
    (value) -> <a> True array
    value <*> Array-like object
    > (function() { return Ox.toArray(arguments); }('foo', 'bar'))
    ['foo', 'bar']
    > Ox.toArray('foo')
    ['f', 'o', 'o']
    > Ox.toArray({0: 'f', 1: 'o', 2: 'o', length: 3})
    ['f', 'o', 'o']
@*/
Ox.toArray = function(collection) {
    return Ox.slice(collection);
};
try {
    Array.prototype.slice.call(document.getElementsByTagName('a'));
} catch (error) {
    // Handle MSIE NodeLists
    Ox.toArray = function(collection) {
        var i, length, ret = [];
        try {
            ret = Ox.slice(collection);
        } catch (error) {
            length = collection.length;
            for (i = 0; i < length; i++) {
                ret[i] = collection[i];
            }
        }
        return ret;
    };
}

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

/*@
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.toArray(arguments),
        array = [];
    args[0].forEach(function(value, index) {
        array[index] = [];
        args.forEach(function(value) {
            array[index].push(value[index]);
        });
    });
    return array;
};