/*!
 * async.js
 * Copyright(c) 2010 Fabian Jakobs <fabian.jakobs@web.de>
 * MIT Licensed
 */


var STOP = exports.STOP = {}

exports.Generator = function(source) {
    if (typeof source == "function")
        this.source = {
            next: source
        }
    else
        this.source = source
}

;(function() {
    this.next = function(callback) {
        this.source.next(callback)
    }

    this.map = function(mapper) {
        if (!mapper)
            return this
            
        mapper = makeAsync(1, mapper)
        
        var source = this.source
        this.next = function(callback) {
            source.next(function(err, value) {
                if (err)
                    callback(err)
                else {
                    mapper(value, function(err, value) {
                        if (err)
                            callback(err)
                        else
                            callback(null, value)
                    })
                }
            })
        }
        return new this.constructor(this)
    }
    
    this.filter = function(filter) {
        if (!filter)
            return this
            
        filter = makeAsync(1, filter)
        
        var source = this.source
        this.next = function(callback) {
            source.next(function handler(err, value) {
                if (err)
                    callback(err)
                else {
                    filter(value, function(err, takeIt) {
                        if (err)
                            callback(err)
                        else if (takeIt)
                            callback(null, value)
                        else
                            source.next(handler)
                    })
                }
            })
        }
        return new this.constructor(this)
    }

    this.slice = function(begin, end) {
        var count = -1
        if (!end || end < 0)
            var end = Infinity
        
        var source = this.source
        this.next = function(callback) {
            source.next(function handler(err, value) {
                count++
                if (err)
                    callback(err)
                else if (count >= begin && count < end)
                    callback(null, value)
                else if (count >= end)
                    callback(STOP)
                else
                    source.next(handler)
            })
        }
        return new this.constructor(this)
    }
    
    this.reduce = function(reduce, initialValue) {
        reduce = makeAsync(3, reduce)

        var index = 0
        var done = false
        var previousValue = initialValue
        
        var source = this.source
        this.next = function(callback) {
            if (done)
                return callback(STOP)

            if (initialValue === undefined) {
                source.next(function(err, currentValue) {
                    if (err)
                        return callback(err, previousValue)
                    
                    previousValue = currentValue
                    reduceAll()
                })
            }
            else
                reduceAll()

            function reduceAll() {
                source.next(function handler(err, currentValue) {                    
                    if (err) {
                        done = true
                        if (err == STOP)                            
                            return callback(null, previousValue)
                        else
                            return(err)
                    }
                    reduce(previousValue, currentValue, index++, function(err, value) {
                        previousValue = value
                        source.next(handler)
                    })
                })
            }            
        }
        return new this.constructor(this)
    }
    
    this.forEach =
    this.each = function(fn) {
        fn = makeAsync(1, fn)
            
        var source = this.source
        this.next = function(callback) {
            source.next(function handler(err, value) {
                if (err) 
                    callback(err)
                else {
                    fn(value, function(err) {
                        callback(err, value)
                    })
                }
            })
        }
        return new this.constructor(this)
    }
    
    this.some = function(condition) {
        condition = makeAsync(1, condition)
        
        var source = this.source
        var done = false
        this.next = function(callback) {
            if (done)
                return callback(STOP)
            
            source.next(function handler(err, value) {
                if (err)
                    return callback(err)
                    
                condition(value, function(err, result) {
                    if (err) {
                        done = true
                        if (err == STOP)
                            callback(null, false)
                        else
                            callback(err)
                    }                        
                    else if (result) {
                        done = true
                        callback(null, true)
                    }
                    else 
                        source.next(handler)
                })
            })
        }
        return new this.constructor(this)
    }
    
    this.every = function(condition) {
        condition = makeAsync(1, condition)
        
        var source = this.source
        var done = false
        this.next = function(callback) {
            if (done)
                return callback(STOP)
            
            source.next(function handler(err, value) {
                if (err)
                    return callback(err)
                    
                condition(value, function(err, result) {
                    if (err) {
                        done = true
                        if (err == STOP)
                            callback(null, true)
                        else
                            callback(err)
                    }                        
                    else if (!result) {
                        done = true
                        callback(null, false)
                    }
                    else 
                        source.next(handler)
                })
            })
        }
        return new this.constructor(this)
    }
    
    this.call = function(context) {
        var source = this.source
        return this.map(function(fn, next) {
            fn = makeAsync(0, fn, context)
            fn.call(context, function(err, value) {
                next(err, value)
            })
        })
    }
    
    this.concat = function(generator) {
        var generators = [this]
        generators.push.apply(generators, arguments)
        var index = 0
        var source = generators[index++]
        
        return new this.constructor(function(callback) {            
            source.next(function handler(err, value) {
                if (err) {
                    if (err == STOP) {
                        source = generators[index++]
                        if (!source)
                            return callback(STOP)
                        else
                            return source.next(handler)
                    }
                    else
                        return callback(err)
                }
                else
                    return callback(null, value)
            })
        })
    }
    
    this.zip = function(generator) {
        var generators = [this]
        generators.push.apply(generators, arguments)
        
        return new this.constructor(function(callback) {
            exports.list(generators)
                .map(function(gen, next) {                    
                    gen.next(next)
                })
                .toArray(callback)
        })
    }
    
    this.expand = function(inserter, constructor) {
       if (!inserter)
            return this
            
        var inserter = makeAsync(1, inserter)
        var constructor = constructor || this.constructor
        var source = this.source;
        var spliced = null;
        
        return new constructor(function next(callback) {
            if (!spliced) {
                source.next(function(err, value) {
                    if (err)
                        return callback(err)
                        
                    inserter(value, function(err, toInsert) {
                        if (err)
                            return callback(err)
                            
                        spliced = toInsert                        
                        next(callback)
                    })

                })
            } 
            else {
                spliced.next(function(err, value) {
                    if (err == STOP) {
                        spliced = null
                        return next(callback)
                    }
                    else if (err)
                        return callback(err)
                    
                    callback(err, value)
                })
            }
        })
    }

    this.sort = function(compare) {
        var self = this
        var arrGen
        this.next = function(callback) {
            if (arrGen)
                return arrGen.next(callback)

            self.toArray(function(err, arr) {
                if (err)
                    callback(err)
                else {
                    arrGen = exports.list(arr.sort(compare))
                    arrGen.next(callback)
                }
            })            
        }
        return new this.constructor(this)
    }

    this.join = function(separator) {
        return this.$arrayOp(Array.prototype.join, separator !== undefined ? [separator] : null)
    }
    
    this.reverse = function() {
        return this.$arrayOp(Array.prototype.reverse)
    }
    
    this.$arrayOp = function(arrayMethod, args) {
        var self = this
        var i = 0
        this.next = function(callback) {
            if (i++ > 0)
                return callback(STOP)
                
            self.toArray(function(err, arr) {
                if (err)
                    callback(err, "")
                else {
                    if (args)
                        callback(null, arrayMethod.apply(arr, args))
                    else
                        callback(null, arrayMethod.call(arr))
                }
            })
        }
        return new this.constructor(this)
        
    }
    
    this.end = function(breakOnError, callback) {
        if (!callback) {
            callback = arguments[0]
            breakOnError = true
        }

        var source = this.source
        var last
        var lastError
        source.next(function handler(err, value) {
            if (err) {
                if (err == STOP)
                    callback && callback(lastError, last)
                else if (!breakOnError) {
                    lastError = err
                    source.next(handler)
                }
                else
                    callback && callback(err, value)
            }
            else  {
                last = value
                source.next(handler)
            }
        })
    }

    this.toArray = function(breakOnError, callback) {
        if (!callback) {
            callback = arguments[0]
            breakOnError = true
        }
        
        var values = []
        var errors = []
        var source = this.source
        
        source.next(function handler(err, value) {
            if (err) {
                if (err == STOP) {
                    if (breakOnError)
                        return callback(null, values)
                    else {
                        errors.length = values.length
                        return callback(errors, values)
                    }
                }
                else {
                    if (breakOnError)
                        return callback(err)
                    else
                        errors[values.length] = err
                }
            }

            values.push(value)
            source.next(handler)
        })
    }

}).call(exports.Generator.prototype)

var makeAsync = exports.makeAsync = function(args, fn, context) {
    if (fn.length > args) 
        return fn
    else {
        return function() {
            var value
            var next = arguments[args]
            try {
                value = fn.apply(context || this, arguments)
            } catch(e) {
                return next(e)
            }
            next(null, value)
        }
    }
}

exports.list = function(arr, construct) {
    var construct = construct || exports.Generator
    var i = 0
    var len = arr.length
    
    return new construct(function(callback) {
        if (i < len)
            callback(null, arr[i++])
        else
            callback(STOP)
    })
}

exports.values = function(map, construct) {
    var values = []
    for (var key in map) 
        values.push(map[key])
        
    return exports.list(values, construct)
}

exports.keys = function(map, construct) {
    var keys = []
    for (var key in map) 
        keys.push(key)
        
    return exports.list(keys, construct)
}

/*
 * range([start,] stop[, step]) -> generator of integers
 *
 * Return a generator containing an arithmetic progression of integers.
 * range(i, j) returns [i, i+1, i+2, ..., j-1] start (!) defaults to 0.
 * When step is given, it specifies the increment (or decrement).
 */ 
exports.range = function(start, stop, step, construct) {
    var construct = construct || exports.Generator
    start = start || 0
    step = step || 1
    
    if (stop === undefined || stop === null)
        stop = step > 0 ? Infinity : -Infinity
        
    var value = start
    
    return new construct(function(callback) {
        if (step > 0 && value >= stop || step < 0 && value <= stop)
            callback(STOP)
        else {
            var current = value
            value += step
            callback(null, current)
        }
    })
}

exports.concat = function(first, varargs) {
    if (arguments.length > 1)
        return first.concat.apply(first, Array.prototype.slice.call(arguments, 1))
    else
        return first
}

exports.zip = function(first, varargs) {
    if (arguments.length > 1)
        return first.zip.apply(first, Array.prototype.slice.call(arguments, 1))
    else
        return first.map(function(item, next) {
            next(null, [item])
        })
}


exports.plugin = function(members, constructors) {
    if (members) {
        for (var key in members) {
            exports.Generator.prototype[key] = members[key]
        }
    }

    if (constructors) {
        for (var key in constructors) {
            exports[key] = constructors[key]
        }
    }    
}
