
var Proteus = require("proteus"),
    async   = require("async"),
    Task    = require("sake/task"),
    options = require("sake/options"),
    log     = require("sake/logger"),
    Runner,
    ActionRunner,
    TaskRunner
;

/**
 * Base class for Task and Actions runners
 */
Runner = Proteus.Class.derive({
    init: function (callback) {
        this.callback = callback;
    },
    
    run: function () {
        this.postRun.apply(this, arguments);
    },
    
    postRun: function () {
        this.complete.apply(this, arguments);
    },
    
    complete: function (err) {
        var nil = this.callback && this.callback(err);
        this.destroy();
    },
    
    destroy: function () {
        delete this.callback;
    }
});

/**
 * Responsible for running a tasks actions, one by one.
 */
ActionRunner = Runner.derive({
    
    init: function (task, callback) {
        this.task = task;
        ActionRunner.__super__.init.call(this, callback);
    },
    
    run: function () {
        var task        = this.task,
            actions     = task.actions,
            noActions   = !actions || !actions.length,
            callback    = ActionRunner.__super__.run.bind(this)
        ;
        
        if (noActions) {
            log.debug("No actions for " + task.fqn);
            callback();
            return;
        }
        
        async.series(actions.map(this.makeAsyncCallback, this), callback);
    },
    
    destroy: function () {
        delete this.task;
        ActionRunner.__super__.destroy.call(this);
    },
    
    makeActionCallback: function (idx, action, fn, callback, abort) {
        var task        = this.task,
            isDryRun    = options.dryrun,
            dryRunSfx   = isDryRun ? " (dry-run)" : ""
        ;
        
        if (task !== this.task || action !== fn.action) {
            log.debug(task.fqn + " action callback called out of context.");
            return;
        }
        
        task.removeListener("actionDone", arguments.callee);
        task.removeListener("abort", arguments.callee);

        log.debug(
            task.fqn + " action " + idx +
            (abort ? " aborted." : " complete" + dryRunSfx + ".")
        );
        
        callback(abort);
    },
    
    makeAsyncCallback: function (action, idx) {
        var self        = this,
            task        = this.task,
            isDryRun    = options.dryrun,
            dryRunSfx   = isDryRun ? " (dry-run)" : "",
            actionCb    = this.makeActionCallback,
            asyncFn
        ;

        asyncFn = function (asyncCb) {
            var cb, nil;
            
            cb = actionCb.bind(self, idx, action, asyncFn, asyncCb);
            
            task.on("actionDone", cb);
            task.on("abort", cb);
            
            log.debug("Execute " + task.fqn + " action " + idx + dryRunSfx);
            
            nil = isDryRun ? 
                asyncFn.action = doneCb && doneCb(task) :
                action.apply(task, [task].concat(task.arguments));
        };
        
        asyncFn.action = action;
        return asyncFn;
    },
});

/**
 * Responsible for executing a task's prerequisites one by one
 */
TaskRunner = Runner.derive({
    
    init: function (task, callback) {
        this.task = task;
        TaskRunner.__super__.init.call(this, callback);
    },
    
    run: function () {
        var task        = this.task,
            running     = task.isRunning,
            prereqs     = task.prerequisites,
            hasPreqs    = prereqs && prereqs.length,
            callback    = TaskRunner.__super__.run.bind(this)
        ;
        
        if (running) {
            log.warn(
                "Possible dependency recursion: " + task.fqn +
                " is already running."
            );
            return;
        }
        
        if (!hasPreqs) {
            log.debug("No prerequisites for " + task.fqn);
            return callback();
        }
        
        task.isRunning = true;
        async.series(prereqs.map(this.makeAsyncCallback, this), callback);
    },
    
    postRun: function (err) {
        var callback = TaskRunner.__super__.postRun.bind(this);
        (this.actionRunner = new ActionRunner(this.task, callback)).run();
    },
    
    makeAsyncCallback: function (taskname) {
        var self = this,
            task = Task.get(taskname, self.task.namespace)
        ;
        
        return function (asyncCb) {
            function taskCb () {
                task.removeListener("complete", taskCb);
                asyncCb();
            }
            
            task.on("complete", taskCb);
            task.invoke();
        };
    },
    
    destroy: function () {
        delete this.task.isRunning;
        delete this.actionRunner;
        TaskRunner.__super__.destroy.call(this);
    }
    
});

module.exports = {
    run: function (task, callback) {
        (new TaskRunner(task, callback)).run();
    }
};

@@ -2148,16 +2148,34 @@
  (abort 
+&& abort !== task 
 ? %22 abor

Make sure the abort param is not our task reference.