// @tag core
/**
 * Provides the ability to execute one or more arbitrary tasks in an asynchronous manner.
 *
 * Generally, you can use the singleton {@link Ext.TaskManager}.  Or you can create 
 * separate TaskRunner instances to start and stop unique tasks independent of one 
 * another.
 * 
 * Example usage:
 *
 *     @example
 *     var runner = new Ext.util.TaskRunner(),
 *         clock, updateClock, task;
 *     
 *     clock = Ext.getBody().appendChild({
 *         id: 'clock'
 *     });
 *     
 *     // Start a simple clock task that updates a div once per second
 *     updateClock = function() {
 *         clock.setHtml(Ext.Date.format(new Date(), 'g:i:s A'));
 *     };
 *     
 *     task = runner.start({
 *         run: updateClock,
 *         interval: 1000
 *     });
 *
 * The equivalent using TaskManager:
 *
 *     @example
 *     var clock, updateClock, task;
 *     
 *     clock = Ext.getBody().appendChild({
 *         id: 'clock'
 *     });
 *     
 *     // Start a simple clock task that updates a div once per second
 *     updateClock = function() {
 *         clock.setHtml(Ext.Date.format(new Date(), 'g:i:s A'));
 *     };
 *     
 *     var task = Ext.TaskManager.start({
 *         run: updateClock,
 *         interval: 1000
 *     });
 *
 * To end a running task:
 * 
 *      task.destroy();
 *
 * If a task needs to be started and stopped repeated over time, you can create a
 * {@link Ext.util.TaskRunner.Task Task} instance.
 *
 *     var runner = new Ext.util.TaskRunner(),
 *         task;
 *     
 *     task = runner.newTask({
 *         run: function() {
 *             // useful code
 *         },
 *         interval: 1000
 *     });
 *     
 *     task.start();
 *     
 *     // ...
 *     
 *     task.stop();
 *     
 *     // ...
 *     
 *     task.start();
 *
 * A re-usable, single-run task can be managed similar to the above:
 *
 *     var runner = new Ext.util.TaskRunner(),
 *         task;
 *     
 *     task = runner.newTask({
 *         run: function() {
 *             // useful code
 *         },
 *         interval: 1000,
 *         repeat: 1
 *     });
 *     
 *     task.start();
 *     
 *     // ...
 *     
 *     task.stop();
 *     
 *     // ...
 *     
 *     task.start();
 *
 * See the {@link #start} method for details about how to configure a Task.
 *
 * Also see {@link Ext.util.DelayedTask}.
 * 
 * @constructor
 * @param {Number/Object} [interval=10] The minimum precision in milliseconds supported by 
 * this TaskRunner instance. Alternatively, a config object to apply to the new instance.
 */
Ext.define('Ext.util.TaskRunner', {
// @require Ext.Function
 
    /**
     * @cfg {Boolean} fireIdleEvent
     * This may be configured `false` to inhibit firing of the {@link
     * Ext.GlobalEvents#idle idle event} after task invocation. By default the tasks
     * run in a given tick determine whether `idle` events fire.
     */
    fireIdleEvent: null,
 
    /**
     * @cfg {Number} interval
     * How often to run the task in milliseconds. Defaults to every 10ms.
     */
    interval: 10,
 
    /**
     * @property timerId
     * The id of the current timer.
     * @private
     */
    timerId: null,
 
    constructor: function(interval) {
        var me = this;
 
        if (typeof interval === 'number') {
            me.interval = interval;
        }
        else if (interval) {
            Ext.apply(me, interval);
        }
 
        me.tasks = [];
        me.timerFn = me.onTick.bind(me);
    },
 
    /**
     * Creates a new {@link Ext.util.TaskRunner.Task Task} instance. These instances can
     * be easily started and stopped.
     * @param {Object} config The config object. For details on the supported properties,
     * see {@link #start}.
     *
     * @return {Ext.util.TaskRunner.Task} 
     * Ext.util.TaskRunner.Task instance, which can be useful for method chaining.
     */
    newTask: function(config) {
        var task = new Ext.util.TaskRunner.Task(config);
 
        task.manager = this;
 
        //<debug>
        if (Ext.Timer.track) {
            task.creator = new Error().stack;
        }
        //</debug>
 
        return task;
    },
 
    /**
     * Starts a new task.
     *
     * Before each invocation, Ext injects the property `taskRunCount` into the task object
     * so that calculations based on the repeat count can be performed.
     * 
     * The returned task will contain a `destroy` method that can be used to destroy the
     * task and cancel further calls. This is equivalent to the {@link #stop} method.
     *
     * @param {Object} task A config object that supports the following properties:
     * @param {Function} task.run The function to execute each time the task is invoked. The
     * function will be called at each interval and passed the `args` argument if specified,
     * and the current invocation count if not.
     * 
     * If a particular scope (`this` reference) is required, be sure to specify it using
     * the `scope` argument.
     * 
     * @param {Function} task.onError The function to execute in case of unhandled
     * error on task.run.
     *
     * @param {Boolean} task.run.return `false` from this function to terminate the task.
     *
     * @param {Number} task.interval The frequency in milliseconds with which the task
     * should be invoked.
     *
     * @param {Object[]} [task.args] An array of arguments to be passed to the function
     * specified by `run`. If not specified, the current invocation count is passed.
     *
     * @param {Boolean} [task.addCountToArgs=false] True to add the current invocation count as 
     * one of the arguments of args. 
     * Note: This only takes effect when args is specified.
     *
     * @param {Object} [task.scope] The scope (`this` reference) in which to execute the
     * `run` function. Defaults to the task config object.
     *
     * @param {Number} [task.duration] The length of time in milliseconds to invoke the task
     * before stopping automatically (defaults to indefinite).
     *
     * @param {Number} [task.repeat] The number of times to invoke the task before stopping
     * automatically (defaults to indefinite).
     *
     * @param {Number} [task.fireIdleEvent=true] If all tasks in a TaskRunner's execution 
     * sweep are configured with `fireIdleEvent: false`, then the 
     * {@link Ext.GlobalEvents#idle idle event} is not fired when the TaskRunner's execution
     * sweep finishes.
     *
     * @param {Boolean} [task.fireOnStart=false] True to run the task immediately instead of 
     * waiting for the _interval's_ initial pass to call the _run_ function.
     */
    start: function(task) {
        var me = this,
            now = Ext.Date.now();
 
        if (!task.pending) {
            me.tasks.push(task);
            task.pending = true; // don't allow the task to be added to me.tasks again
        }
 
        task.stopped = false; // might have been previously stopped...
        task.taskStartTime = now;
        task.taskRunTime = task.fireOnStart !== false ? 0 : task.taskStartTime;
        task.taskRunCount = 0;
 
        if (!me.firing) {
            if (task.fireOnStart !== false) {
                me.startTimer(0, now);
            }
            else {
                me.startTimer(task.interval, now);
            }
        }
 
        return task;
    },
 
    /**
     * Stops an existing running task.
     * @param {Object} task The task to stop.
     * @param {Boolean} andRemove Pass `true` to also remove the task from the queue.
     * @return {Object} The task
     */
    stop: function(task, andRemove) {
        var me = this,
            tasks = me.tasks,
            pendingCount = 0,
            i;
 
        // NOTE: we don't attempt to remove the task from me.tasks at this point because
        // this could be called from inside a task which would then corrupt the state of
        // the loop in onTick
        if (!task.stopped) {
            task.stopped = true;
            task.pending = false;
 
            if (task.onStop) {
                task.onStop.call(task.scope || task, task);
            }
        }
 
        if (andRemove) {
            Ext.Array.remove(tasks, task);
        }
 
        // If there are now no pending tasks
        // we shhuld stop the timer.
        for (= 0; !pendingCount && i < tasks.length; i++) {
            if (!tasks[i].stopped) {
                pendingCount++;
            }
        }
 
        if (!pendingCount) {
            Ext.undefer(me.timerId);
            me.timerId = null;
        }
 
        return task;
    },
 
    /**
     * Stops all tasks that are currently running.
     * @param {Boolean} andRemove Pass `true` to also remove the tasks from the queue.
     */
    stopAll: function(andRemove) {
        var me = this;
 
        // onTick will take care of cleaning up the mess after this point...
        // must use reverse in case a task is removed.
        Ext.each(this.tasks, function(task) {
            me.stop(task, andRemove);
        }, null, true);
    },
 
    //-------------------------------------------------------------------------
 
    firing: false,
 
    nextExpires: 1e99,
 
    /**
     * @private
     */
    onTick: function() {
        var me = this,
            tasks = me.tasks,
            fireIdleEvent = me.fireIdleEvent, // null by default
            now = Ext.Date.now(),
            nextExpires = 1e99,
            len = tasks.length,
            expires, newTasks, i, task, rt, remove, args;
 
        //<debug>
        var timer = Ext.Timer.get(me.timerId); // eslint-disable-line vars-on-top, one-var
 
        if (timer) {
            timer.tasks = [];
        }
        //</debug>
 
        me.timerId = null;
        me.firing = true; // ensure we don't startTimer during this loop...
 
        // tasks.length can be > len if start is called during a task.run call... so we
        // first check len to avoid tasks.length reference but eventually we need to also
        // check tasks.length. we avoid repeating use of tasks.length by setting len at
        // that time (to help the next loop)
        for (= 0; i < len || i < (len = tasks.length); ++i) {
            task = tasks[i];
 
            if (!(remove = task.stopped)) {
                expires = task.taskRunTime + task.interval;
 
                if (expires <= now) {
                    rt = 1; // otherwise we have a stale "rt"
 
                    // If all tasks left specify fireIdleEvent as false, then don't do it
                    if (fireIdleEvent === null && task.fireIdleEvent !== false) {
                        fireIdleEvent = true;
                    }
                    
                    task.taskRunCount++;
 
                    if (task.args) {
                        args =
                            task.addCountToArgs ? task.args.concat([task.taskRunCount]) : task.args;
                    }
                    else {
                        args = [task.taskRunCount];
                    }
 
                    // We want the exceptions not to get caught while unit testing
                    //<debug>
                    if (timer) {
                        timer.tasks.push(task);
                    }
 
                    if (me.disableTryCatch) {
                        rt = task.run.apply(task.scope || task, args);
                    }
                    else {
                    //</debug>
                        try {
                            rt = task.run.apply(task.scope || task, args);
                        }
                        catch (taskError) {
                            try {
                                // <debug>
                                Ext.log({
                                    fn: task.run,
                                    prefix: 'Error while running task',
                                    stack: taskError.stack,
                                    msg: taskError,
                                    level: 'error'
                                });
 
                                // </debug>
                                if (task.onError) {
                                    rt = task.onError.call(task.scope || task, task, taskError);
                                }
                            }
                            catch (e) {
                                // ignore
                            }
                        }
                    //<debug>
                    }
                    //</debug>
                    
                    task.taskRunTime = now;
 
                    if (rt === false || task.taskRunCount === task.repeat) {
                        me.stop(task);
                        remove = true;
                    }
                    else {
                        remove = task.stopped; // in case stop was called by run
                        expires = now + task.interval;
                    }
                }
 
                if (!remove && task.duration && task.duration <= (now - task.taskStartTime)) {
                    me.stop(task);
                    remove = true;
                }
            }
 
            if (remove) {
                task.pending = false; // allow the task to be added to me.tasks again
 
                // once we detect that a task needs to be removed, we copy the tasks that
                // will carry forward into newTasks... this way we avoid O(N*N) to remove
                // each task from the tasks array (and ripple the array down) and also the
                // potentially wasted effort of making a new tasks[] even if all tasks are
                // going into the next wave.
                if (!newTasks) {
                    newTasks = tasks.slice(0, i);
                    // we don't set me.tasks here because callbacks can also start tasks,
                    // which get added to me.tasks... so we will visit them in this loop
                    // and account for their expirations in nextExpires...
                }
            }
            else {
                if (newTasks) {
                    newTasks.push(task); // we've cloned the tasks[], so keep this one...
                }
 
                if (nextExpires > expires) {
                    nextExpires = expires; // track the nearest expiration time
                }
            }
        }
 
        if (newTasks) {
            // only now can we copy the newTasks to me.tasks since no user callbacks can
            // take place
            me.tasks = newTasks;
        }
 
        me.firing = false; // we're done, so allow startTimer afterwards
 
        if (me.tasks.length) {
            // we create a new Date here because all the callbacks could have taken a long
            // time... we want to base the next timeout on the current time (after the
            // callback storm):
            me.startTimer(nextExpires - now, Ext.Date.now());
        }
 
        // If all tasks fired and had fireIdleEvent=false then our fireIdleEvent var
        // will still be null. This is to allow any task that does not suppress idle
        // to override those that do. The only other reason our var will be null is if
        // no tasks fired. In which case, no need for idle either.
        if (fireIdleEvent === null) {
            fireIdleEvent = false;
        }
 
        Ext._suppressIdle = !fireIdleEvent;
    },
 
    /**
    * @private
    */
    startTimer: function(timeout, now) {
        var me = this,
            expires = now + timeout,
            timerId = me.timerId;
 
        // Check to see if this request is enough in advance of the current timer. If so,
        // we reschedule the timer based on this new expiration.
        if (timerId && me.nextExpires - expires > me.interval) {
            timerId = Ext.undefer(timerId);
        }
 
        if (!timerId) {
            if (timeout < me.interval) {
                timeout = me.interval;
            }
 
            me.timerId = Ext.defer(me.timerFn, timeout);
            me.nextExpires = expires;
 
            //<debug>
            var timer = Ext.Timer.get(me.timerId); // eslint-disable-line vars-on-top
 
            if (timer) {
                timer.runner = me;
            }
            //</debug>
        }
    }
}, function() {
    var me = this,
        proto = me.prototype;
 
    /**
     * Destroys this instance, stopping all tasks that are currently running.
     * @method destroy
     */
    proto.destroy = proto.stopAll;
 
    /**
     * Instances of this class are created by {@link Ext.util.TaskRunner#newTask} method.
     * 
     * For details on config properties, see {@link Ext.util.TaskRunner#start}.
     * @class Ext.util.TaskRunner.Task
     */
    me.Task = new Ext.Class({
        isTask: true,
 
        /**
         * This flag is set to `true` by {@link #stop}.
         * @private
         */
        stopped: true, // this avoids the odd combination of !stopped && !pending
        
        fireOnStart: false,
        
        constructor: function(config) {
            Ext.apply(this, config);
        },
        
        /**
         * Restarts this task, clearing it duration, expiration and run count.
         * @param {Number} [interval] Optionally reset this task's interval.
         */
        restart: function(interval) {
            if (interval !== undefined) {
                this.interval = interval;
            }
            
            this.manager.start(this);
        },
    
        /**
         * Starts this task if it is not already started.
         * @param {Number} [interval] Optionally reset this task's interval.
         */
        start: function(interval) {
            if (this.stopped) {
                this.restart(interval);
            }
        },
        
        /**
         * Stops this task.
         */
        stop: function(andRemove) {
            this.manager.stop(this, andRemove);
        },
        
        destroy: function() {
            this.stop(true);
        }
    });
 
    proto = me.Task.prototype;
        
    /**
     * Destroys this instance, stopping this task's execution.
     * @method destroy
     * @member Ext.util.TaskRunner.Task
     */
    proto.destroy = proto.stop;
});