/**
 * @class Ext.draw.Animator
 *
 * Singleton class that manages the animation pool.
 */
Ext.define('Ext.draw.Animator', {
    uses: ['Ext.draw.Draw'],
    singleton: true,
 
    frameCallbacks: {},
    frameCallbackId: 0,
    scheduled: 0,
    frameStartTimeOffset: Ext.now(),
    animations: [],
    running: false,
 
    /**
     *  Cross platform `animationTime` implementation.
     *  @return {Number} 
     */
    animationTime: function() {
        return Ext.AnimationQueue.frameStartTime - this.frameStartTimeOffset;
    },
 
    /**
     * Adds an animated object to the animation pool.
     *
     * @param {Object} animation The animation descriptor to add to the pool.
     */
    add: function(animation) {
        var me = this;
 
        if (!me.contains(animation)) {
            me.animations.push(animation);
            me.ignite();
 
            if ('fireEvent' in animation) {
                animation.fireEvent('animationstart', animation);
            }
        }
    },
 
    /**
     * Removes an animation from the pool.
     * TODO: This is broken when called within `step` method.
     * @param {Object} animation The animation to remove from the pool.
     */
    remove: function(animation) {
        var me = this,
            animations = me.animations,
            i = 0,
            l = animations.length;
 
        for (; i < l; ++i) {
            if (animations[i] === animation) {
                animations.splice(i, 1);
 
                if ('fireEvent' in animation) {
                    animation.fireEvent('animationend', animation);
                }
 
                return;
            }
        }
    },
 
    /**
     * Returns `true` or `false` whether it contains the given animation or not.
     *
     * @param {Object} animation The animation to check for.
     * @return {Boolean} 
     */
    contains: function(animation) {
        return Ext.Array.indexOf(this.animations, animation) > -1;
    },
 
    /**
     * Returns `true` or `false` whether the pool is empty or not.
     * @return {Boolean} 
     */
    empty: function() {
        return this.animations.length === 0;
    },
 
    idle: function() {
        return this.scheduled === 0 && this.animations.length === 0;
    },
 
    /**
     * Given a frame time it will filter out finished animations from the pool.
     *
     * @param {Number} frameTime The frame's start time, in milliseconds.
     */
    step: function(frameTime) {
        var me = this,
            animations = me.animations,
            animation,
            i = 0,
            ln = animations.length;
 
        for (; i < ln; i++) {
            animation = animations[i];
            animation.step(frameTime);
 
            if (!animation.animating) {
                animations.splice(i, 1);
                i--;
                ln--;
 
                if (animation.fireEvent) {
                    animation.fireEvent('animationend', animation);
                }
            }
        }
    },
 
    /**
     * Register a one-time callback that will be called at the next frame.
     * @param {Function/String} callback
     * @param {Object} scope 
     * @return {String} The ID of the scheduled callback.
     */
    schedule: function(callback, scope) {
        var id = 'frameCallback' + (this.frameCallbackId++);
 
        scope = scope || this;
 
        if (Ext.isString(callback)) {
            callback = scope[callback];
        }
 
        Ext.draw.Animator.frameCallbacks[id] = { fn: callback, scope: scope, once: true };
        this.scheduled++;
 
        Ext.draw.Animator.ignite();
 
        return id;
    },
 
    /**
     * Register a one-time callback that will be called at the next frame,
     * if that callback (with a matching function and scope) isn't already scheduled.
     * @param {Function/String} callback
     * @param {Object} scope 
     * @return {String/null} The ID of the scheduled callback or null, if that callback
     * has already been scheduled.
     */
    scheduleIf: function(callback, scope) {
        var frameCallbacks = Ext.draw.Animator.frameCallbacks,
            cb, id;
 
        scope = scope || this;
 
        if (Ext.isString(callback)) {
            callback = scope[callback];
        }
 
        for (id in frameCallbacks) {
            cb = frameCallbacks[id];
 
            if (cb.once && cb.fn === callback && cb.scope === scope) {
                return null;
            }
        }
 
        return this.schedule(callback, scope);
    },
 
    /**
     * Cancel a registered one-time callback
     * @param {String} id 
     */
    cancel: function(id) {
        if (Ext.draw.Animator.frameCallbacks[id] && Ext.draw.Animator.frameCallbacks[id].once) {
            this.scheduled = Math.max(--this.scheduled, 0);
            delete Ext.draw.Animator.frameCallbacks[id];
            Ext.draw.Draw.endUpdateIOS();
        }
 
        if (this.idle()) {
            this.extinguish();
        }
    },
 
    clear: function() {
        this.animations.length = 0;
        Ext.draw.Animator.frameCallbacks = {};
        this.extinguish();
    },
 
    /**
     * Register a recursive callback that will be called at every frame.
     *
     * @param {Function} callback 
     * @param {Object} scope 
     * @return {String} 
     */
    addFrameCallback: function(callback, scope) {
        var id = 'frameCallback' + (this.frameCallbackId++);
 
        scope = scope || this;
 
        if (Ext.isString(callback)) {
            callback = scope[callback];
        }
 
        Ext.draw.Animator.frameCallbacks[id] = { fn: callback, scope: scope };
 
        return id;
    },
 
    /**
     * Unregister a recursive callback.
     * @param {String} id 
     */
    removeFrameCallback: function(id) {
        delete Ext.draw.Animator.frameCallbacks[id];
 
        if (this.idle()) {
            this.extinguish();
        }
    },
 
    /**
     * @private
     */
    fireFrameCallbacks: function() {
        var callbacks = this.frameCallbacks,
            id, fn, cb;
 
        for (id in callbacks) {
            cb = callbacks[id];
            fn = cb.fn;
 
            if (Ext.isString(fn)) {
                fn = cb.scope[fn];
            }
 
            fn.call(cb.scope);
 
            if (callbacks[id] && cb.once) {
                this.scheduled = Math.max(--this.scheduled, 0);
                delete callbacks[id];
            }
        }
    },
 
    handleFrame: function() {
        var me = this;
 
        me.step(me.animationTime());
        me.fireFrameCallbacks();
 
        if (me.idle()) {
            me.extinguish();
        }
    },
 
    ignite: function() {
        if (!this.running) {
            this.running = true;
            Ext.AnimationQueue.start(this.handleFrame, this);
            Ext.draw.Draw.beginUpdateIOS();
        }
    },
 
    extinguish: function() {
        this.running = false;
        Ext.AnimationQueue.stop(this.handleFrame, this);
        Ext.draw.Draw.endUpdateIOS();
    }
});