# Long event name to avoid conflicts.
updateSafe = 'gulp-browserify-watchify-glob:bundle-done'
baseOptions = ->
    cache: {}
    packageCache: {}
globOptions =
    absolute: yes

factory = (gulp, browserify, glob, _) ->
    accumulateEntriesSync = (accumulator, pattern) ->
        accumulator.concat glob.sync pattern, globOptions

    expandGlobsSync = (patterns) ->
        _.reduce patterns, accumulateEntriesSync, []

    expandGlobs = (patterns, done, accumulator = []) ->
        pattern = _.head patterns
        return process.nextTick done, null, accumulator unless pattern
        glob pattern, globOptions, (error, paths) ->
            return done error if error
            expandGlobs _.tail(patterns), done, accumulator.concat paths

    difference = (left, right) ->
        diff = new Set()
        left.forEach (item) -> right.has(item) or diff.add item
        diff

    updateEntries = (entriesGlob, entries, bundler, done) ->
        expandGlobs entriesGlob, (error, paths) ->
            return done error if error
            # The following logic relies on browserify implementation details.
            oldEntries = new Set entries
            newEntries = new Set paths
            added = difference newEntries, oldEntries
            removed = difference oldEntries, newEntries
            recorded = bundler._recorded
            bundler.reset()
            pipeline = bundler.pipeline
            added.forEach (path) -> bundler.add path
            recorded.forEach (row) ->
                return if removed.has row.file
                if row.entry then row.order = bundler._entryOrder++
                pipeline.write row
            entries.splice 0
            entries.splice 0, 0, paths...
            done error, entries

    globbedBrowserify = (options) ->
        entriesGlob = options.entries
        throw TypeError 'globbedBrowserify needs entries' unless entriesGlob
        entriesGlob = [entriesGlob] unless _.isArray entriesGlob
        entries = expandGlobsSync entriesGlob
        bundler = new browserify _.defaults {entries}, options, baseOptions()
        bundling = off
        entriesChanged = no

        beforeBundling = (done) ->
            bundling = on
            if entriesChanged
                entriesChanged = no
                updateEntries entriesGlob, entries, bundler, done
            else
                process.nextTick done

        beforeBundling.displayName = 'beforeBundling'

        afterBundling = (done) ->
            bundling = off
            bundler.emit updateSafe
            process.nextTick done

        afterBundling.displayName = 'afterBundling'

        triggerBundle = (done) ->
            entriesChanged = yes
            if bundling
                bundler.once updateSafe, -> emitUpdate done
            else
                emitUpdate done

        emitUpdate = (done) ->
            bundler.emit 'update'
            process.nextTick done

        wrapTask = (task) -> gulp.series beforeBundling, task, afterBundling

        watch = (task, kickoffTask = task) ->
            updater = _.debounce wrapTask(task), 100
            bundler.plugin 'watchify'
            bundler.on 'update', updater
            watchOptions =
                events: ['add', 'unlink']
                cwd: process.cwd()
            watcher = gulp.watch entriesGlob, watchOptions, triggerBundle
            # Function that must be invoked twice to take effect. See
            # github:paulmillr/chokidar/issues/612#issuecomment-378026034
            closeWatcher = -> closeWatcher = -> watcher.close()
            watcher.once 'ready', -> closeWatcher()
            originalClose = bundler.close
            bundler.close = ->
                closeWatcher()
                originalClose.call bundler
                bundler.removeListener 'update', updater
                bundler.close = originalClose
            wrapTask kickoffTask

        bundler.watch = watch
        bundler.wrap = wrapTask
        bundler

    _.extend globbedBrowserify, {
        default: globbedBrowserify
        expandGlobsSync
        expandGlobs
        difference
        updateEntries
        factory
    }

gulp = require 'gulp'
browserify = require 'browserify'
glob = require 'glob'
_ = require 'underscore'

module.exports = factory gulp, browserify, glob, _
