import Future from 'fibers/future'
import Fiber from 'fibers'

const SYNC_COMMANDS = ['domain', '_events', '_maxListeners', 'setMaxListeners', 'emit',
    'addListener', 'on', 'once', 'removeListener', 'removeAllListeners', 'listeners']
const NOOP = function () {}

let commandIsRunning = false

/**
 * helper method to execute a row of hooks with certain parameters
 * @param  {Function|Function[]} hooks  list of hooks
 * @param  {Object[]} args  list of parameter for hook functions
 * @return {Promise}  promise that gets resolved once all hooks finished running
 */
let executeHooksWithArgs = (hooks, args) => {
    /**
     * make sure hooks are an array of functions
     */
    if (typeof hooks === 'function') {
        hooks = [hooks]
    }

    /**
     * make sure args is an array since we are calling apply
     */
    if (!Array.isArray(args)) {
        args = [args]
    }

    hooks = hooks.map((hook) => new Promise((resolve) => {
        try {
            /**
             * after command hooks require additional Fiber environment
             */
            return Fiber(() => {
                resolve(hook.apply(null, args))
            }).run()
        } catch (e) {
            // here could be your error message
        }

        resolve()
    }))

    return Promise.all(hooks)
}

/**
 * wraps a function into a Fiber ready context to enable sync execution and hooks
 * @param  {Function}   fn             function to be executed
 * @param  {String}     commandName    name of that function
 * @param  {Function[]} beforeCommand  method to be executed before calling the actual function
 * @param  {Function[]} afterCommand   method to be executed after calling the actual function
 * @return {Function}   actual wrapped function
 */
let wrapCommand = function (fn, commandName, beforeCommand, afterCommand) {
    return function (...commandArgs) {
        let future = new Future()

        /**
         * don't execute [before/after]Command hook if a command was executed
         * in these hooks
         */
        if (commandIsRunning) {
            fn.apply(this, commandArgs).then(future.return.bind(future), future.throw.bind(future))
            return future.wait()
        }

        commandIsRunning = true
        let commandResult, commandError
        new Promise((r) => r(beforeCommand(commandName, commandArgs)))
            .then(() => {
                return fn.apply(this, commandArgs)
            })
            .then(
                (result) => {
                    commandResult = result
                    return executeHooksWithArgs(afterCommand, [commandName, commandArgs, result])
                },
                (e) => {
                    commandError = e
                    return executeHooksWithArgs(afterCommand, [commandName, commandArgs, null, e])
                }
            )
            .then(() => {
                commandIsRunning = false

                if (commandError) {
                    return future.throw(commandError)
                }
                return future.return(commandResult)
            })

        return future.wait()
    }
}

/**
 * wraps all WebdriverIO commands
 * @param  {Object}     instance       WebdriverIO client instance (browser)
 * @param  {Function[]} beforeCommand  before command hook
 * @param  {Function[]} afterCommand   after command hook
 */
let wrapCommands = function (instance, beforeCommand, afterCommand) {
    Object.keys(Object.getPrototypeOf(instance)).forEach((commandName) => {
        if (SYNC_COMMANDS.indexOf(commandName) > -1) {
            return
        }

        let origFn = instance[commandName]
        instance[commandName] = wrapCommand(origFn, commandName, beforeCommand, afterCommand)
    })

    /**
     * Adding a command within fiber context doesn't require a special routine
     * since everything runs sync. There is no need to promisify the command.
     */
    instance.addCommand = function (fnName, fn, forceOverwrite) {
        let commandGroup = instance
        let namespace

        if (typeof fn === 'string') {
            namespace = arguments[0]
            fnName = arguments[1]
            fn = arguments[2]
            forceOverwrite = arguments[3]

            switch (typeof commandGroup[namespace]) {
            case 'function':
                throw new Error(`Command namespace "${namespace}" is used internally, and can't be overwritten!`)
            case 'undefined':
                commandGroup[namespace] = {}
                break
            }

            commandGroup = commandGroup[namespace]
        }

        if (commandGroup[fnName] && !forceOverwrite) {
            throw new Error(`Command ${fnName} is already defined!`)
        }

        commandGroup[fnName] = wrapCommand(fn, fnName, beforeCommand, afterCommand)
    }
}

let runInFiberContext = function (testInterface, ui, before, after, fnName) {
    let origFn = global[fnName]
    let testInterfaceFnName = testInterface[ui][2]

    let runSpec = function (specTitle, specFn) {
        return origFn(specTitle, function (done) {
            Fiber(() => {
                specFn.call(this)
                done()
            }).run()
        })
    }

    let runHook = function (specTitle, specFn) {
        return origFn(specTitle, function (done) {
            Fiber(() => {
                before()
                specFn.call(this)
                after()
                done()
            }).run()
        })
    }

    global[fnName] = function (...specArguments) {
        let specFn = typeof specArguments[0] === 'function' ? specArguments.shift() : specArguments.pop()
        let specTitle = specArguments[0]

        /**
         * if specFn is undefined we are dealing with a pending function
         */
        if (fnName === testInterfaceFnName && arguments.length === 1) {
            return origFn(specTitle)
        }

        if (fnName === testInterfaceFnName) {
            return runSpec(specTitle, specFn)
        }

        return runHook(specTitle, specFn)
    }

    if (fnName === testInterfaceFnName) {
        global[fnName].skip = origFn.skip
        global[fnName].only = origFn.only
    }
}

let runHook = function (hookFn, cb = NOOP) {
    return new Promise((resolve, reject) => {
        Fiber(() => {
            try {
                hookFn()
                cb()
                resolve()
            } catch (e) {
                reject(e)
            }
        }).run()
    })
}

/**
 * wraps a function that returns a promise to be used within Fiber
 * @param  {Function} origFn  actual function to be executed (needs to return a Promise)
 * @return {Object}           Fiber wait object
 */
let wrapFn = function (origFn) {
    return function (...commandArgs) {
        let future = new Future()
        let result = origFn.apply(this, commandArgs)

        result.then(future.return.bind(future), future.throw.bind(future))
        return future.wait()
    }
}

export { wrapCommand, wrapCommands, runInFiberContext, runHook, wrapFn }

@@ -934,32 +934,81 @@
 ((resolve) =%3E %7B%0A
+        let _commandIsRunning = commandIsRunning%0A
         try %7B%0A  
@@ -997,32 +997,32 @@
 g%0A        try %7B%0A
-
             /**%0A
@@ -1134,32 +1134,72 @@
 n Fiber(() =%3E %7B%0A
+                commandIsRunning = true%0A
                 
@@ -1230,16 +1230,69 @@
  args))%0A
+                commandIsRunning = _commandIsRunning%0A
         

make sure we don't run command hooks within before hook