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

define(function (require, exports, module) {

    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]
            }
        }
    }

})
