'use strict';

/*@
Ox.cache <f> Memoize a function
    fn <f> function
    options <o>
        async <b|false> function is async, last argument must be callback
        key   <f|JSON.stringify> return key for arguments
    <script>
        Ox.test.fn = Ox.cache(function(n) { return n * Math.random(); });
    </script>
    > Ox.test.fn(10) == Ox.test.fn(10);
    true
    > Ox.test.fn(10) == Ox.test.fn.clear()(10);
    false
@*/
// TODO: add async test
Ox.cache = function(fn, options) {
    var cache = {}, ret;
    options = options || {};
    options.async = options.async || false;
    options.key = options.key || JSON.stringify;
    ret = function() {
        var args = Ox.slice(arguments), key = options.key(args);
        function callback() {
            // cache all arguments passed to callback
            cache[key] = Ox.slice(arguments);
            // call the original callback
            Ox.last(args).apply(this, arguments);
        }
        if (options.async) {
            if (!(key in cache)) {
                // call function with patched callback
                fn.apply(this, args.slice(0, -1).concat(callback));
            } else {
                // call callback with cached arguments
                setTimeout(function() {
                    callback.apply(this, cache[key]);
                });
            }
        } else {
            if (!(key in cache)) {
                cache[key] = fn.apply(this, args);
            }
            return cache[key];
        }
    };
    ret.clear = function() {
        if (arguments.length == 0) {
            cache = {};
        } else {
            Ox.makeArray(arguments).forEach(function(key) {
                delete cache[key];
            });
        }
        return ret;
    };
    return ret;
};

/*@
Ox.identity <f> Returns its first argument
    This can be used as a default iterator
    > Ox.identity(Infinity)
    Infinity
@*/
Ox.identity = function(value) {
    return value;
};

/*@
Ox.noop <f> Returns undefined and calls optional callback without arguments
    This can be used as a default iterator in an asynchronous loop, or to
    combine a synchronous and an asynchronous code path.
    > Ox.noop(1, 2, 3)
    undefined
    > Ox.noop(1, 2, 3, function() { Ox.test(arguments.length, 0); })
    undefined
@*/
Ox.noop = function() {
    var callback = Ox.last(arguments);
    Ox.isFunction(callback) && callback();
};

/*@
Ox.queue <f> Queue of asynchronous function calls with cached results
    The results are cached based on all arguments to `fn`, except the last one,
    which is the callback.
    (fn, maxThreads) -> <f> Queue function
        .cancel <f> Cancels all running function calls
        .clear <f> Clears the queue
        .reset <f> Cancels all running function calls and clears the queue
    fn <f> Queued function
    maxThreads <n|10> Number of parallel function calls
@*/
Ox.queue = function(fn, maxThreads) {
    var maxThreads = maxThreads || 10,
        processing = [],
        queued = [],
        ret = Ox.cache(function() {
            var args = Ox.slice(arguments);
            queued.push({args: args, key: getKey(args)});
            process();
        }, {async: true, key: getKey}),
        threads = 0;
    ret.cancel = function() {
        threads -= processing.length;
        processing = [];
        return ret;
    };
    ret.clear = function() {
        threads = 0;
        queued = [];
        return ret;
    };
    ret.reset = function() {
        return ret.cancel().clear();
    };
    function getKey(args) {
        return JSON.stringify(args.slice(0, -1));
    }
    function process() {
        var n = Math.min(queued.length, maxThreads - threads);
        if (n) {
            threads += n;
            processing = processing.concat(queued.splice(0, n));
            Ox.parallelForEach(
                processing,
                function(value, index, array, callback) {
                    var args = value.args, key = value.key;
                    fn.apply(this, args.slice(0, -1).concat(function(result) {
                        var index = Ox.indexOf(processing, function(value) {
                            return value.key == key;
                        });
                        if (index > -1) {
                            processing.splice(index, 1);
                            args.slice(-1)[0](result);
                            threads--;
                        }
                        callback();
                    }));
                },
                process
            );
        }
    }
    return ret;
};

/*@
Ox.time <f> Returns the time it takes to execute a given function
    (fn) -> <n> Time in milliseconds
@*/
Ox.time = function(fn) {
    var time = new Date();
    fn();
    return new Date() - time;
};