/**
 * @class Ext.fx.Manager
 * Animation Manager which keeps track of all current animations and manages them on a frame by frame basis.
 * @private
 * @singleton
 */
 
Ext.define('Ext.fx.Manager', {
 
    /* Begin Definitions */
 
    singleton: true,
 
    requires: [
        'Ext.util.MixedCollection',
        'Ext.util.TaskRunner',
        'Ext.fx.target.Element',
        'Ext.fx.target.ElementCSS',
        'Ext.fx.target.CompositeElement',
        'Ext.fx.target.CompositeElementCSS',
        'Ext.fx.target.Sprite',
        'Ext.fx.target.CompositeSprite',
        'Ext.fx.target.Component'
    ],
 
    mixins: {
        queue: 'Ext.fx.Queue'
    },
 
    /* End Definitions */
 
    /**
     * @private
     */
    constructor: function() {
        var me = this;
        me.items = new Ext.util.MixedCollection();
        me.targetArr = {};
        me.mixins.queue.constructor.call(me);
        
        // Do not use fireIdleEvent: false. Each tick of the TaskRunner needs to fire the idleEvent 
        // in case an animation callback/listener adds a listener. 
        me.taskRunner = new Ext.util.TaskRunner();
    },
 
    /**
     * @cfg {Number} interval Default interval in miliseconds to calculate each frame.  Defaults to 16ms (~60fps)
     */
    interval: 16,
 
    /**
     * @cfg {Boolean} forceJS Force the use of JavaScript-based animation instead of CSS3 animation, even when CSS3
     * animation is supported by the browser. This defaults to true currently, as CSS3 animation support is still
     * considered experimental at this time, and if used should be thouroughly tested across all targeted browsers.
     * @protected
     */
    forceJS: true,
 
    /**
     * @private
     * Target Factory
     */
    createTarget: function(target) {
        var me = this,
            useCSS3 = !me.forceJS && Ext.supports.Transitions,
            targetObj;
 
        me.useCSS3 = useCSS3;
 
        if (target) {
            // dom element, string or fly 
            if (target.tagName || Ext.isString(target) || target.isFly) {
                target = Ext.get(target);
                targetObj = new Ext.fx.target['Element' + (useCSS3 ? 'CSS' : '')](target);
            }
            // Element 
            else if (target.dom) {
                targetObj = new Ext.fx.target['Element' + (useCSS3 ? 'CSS' : '')](target);
            }
            // Element Composite 
            else if (target.isComposite) {
                targetObj = new Ext.fx.target['CompositeElement' + (useCSS3 ? 'CSS' : '')](target);
            }
            // Draw Sprite 
            else if (target.isSprite) {
                targetObj = new Ext.fx.target.Sprite(target);
            }
            // Draw Sprite Composite 
            else if (target.isCompositeSprite) {
                targetObj = new Ext.fx.target.CompositeSprite(target);
            }
            // Component 
            else if (target.isComponent) {
                targetObj = new Ext.fx.target.Component(target);
            }
            else if (target.isAnimTarget) {
                return target;
            }
            else {
                return null;
            }
            me.targets.add(targetObj);
            return targetObj;
        }
        else {
            return null;
        }
    },
 
    /**
     * Add an Anim to the manager. This is done automatically when an Anim instance is created.
     * @param {Ext.fx.Anim} anim
     */
    addAnim: function(anim) {
        var me = this,
            items = me.items,
            task = me.task;
 
        // Make sure we use the anim's id, not the anim target's id here. The anim id will be unique on 
        // each call to addAnim. `anim.target` is the DOM element being targeted, and since multiple animations 
        // can target a single DOM node concurrently, the target id cannot be assumned to be unique. 
        items.add(anim.id, anim);
        //Ext.log('+     added anim ', anim.id, ', target: ', anim.target.getId(), ', duration: ', anim.duration); 
 
        // Start the timer if not already running 
        if (!task && items.length) {
            task = me.task = {
                run: me.runner,
                interval: me.interval,
                scope: me
            };
            //Ext.log('--->> Starting task'); 
            me.taskRunner.start(task);
        }
    },
 
    /**
     * Remove an Anim from the manager. This is done automatically when an Anim ends.
     * @param {Ext.fx.Anim} anim
     */
    removeAnim: function(anim) {
        var me = this,
            items = me.items,
            task = me.task;
                
        items.removeAtKey(anim.id);
        //Ext.log('    X removed anim ', anim.id, ', target: ', anim.target.getId(), ', frames: ', anim.frameCount, ', item count: ', items.length); 
        
        // Stop the timer if there are no more managed Anims 
        if (task && !items.length) {
            //Ext.log('[]--- Stopping task'); 
            me.taskRunner.stop(task);
            delete me.task;
        }
    },
 
    /**
     * @private
     * Runner function being called each frame
     */
    runner: function() {
        var me = this,
            items = me.items.getRange(),
            i = 0,
            len = items.length,
            anim;
 
        //Ext.log('      executing anim runner task with ', len, ' items'); 
        me.targetArr = {};
 
        // Single timestamp for all animations this interval 
        me.timestamp = new Date();
        
        // Loop to start any new animations first before looping to 
        // execute running animations (which will also include all animations 
        // started in this loop). This is a subtle difference from simply 
        // iterating in one loop and starting then running each animation, 
        // but separating the loops is necessary to ensure that all new animations 
        // actually kick off prior to existing ones regardless of array order. 
        // Otherwise in edge cases when there is excess latency in overall 
        // performance, allowing existing animations to run before new ones can 
        // lead to dropped frames and subtle race conditions when they are 
        // interdependent, which is often the case with certain Element fx. 
        for (; i < len; i++) {
            anim = items[i];
            
            if (anim.isReady()) {
                //Ext.log('      starting anim ', anim.id, ', target: ', anim.target.id); 
                me.startAnim(anim);
            }
        }
        
        for (= 0; i < len; i++) {
            anim = items[i];
            
            if (anim.isRunning()) {
                //Ext.log('      running anim ', anim.target.id); 
                me.runAnim(anim);
            }
            //<debug> 
            //else if (!me.useCSS3) { 
                // When using CSS3 transitions the animations get paused since they are not 
                // needed once the transition is handed over to the browser, so we can 
                // ignore this case. However if we are doing JS animations and something is 
                // paused here it's possibly unintentional. 
                //Ext.log(' (i)  anim ', anim.id, ' is active but not running...'); 
            //} 
            //</debug> 
        }
 
        // Apply all the pending changes to their targets 
        me.applyPendingAttrs();
        
        // Avoid retaining target references after we are finished with anims 
        me.targetArr = null;
    },
 
    /**
     * @private
     * Start the individual animation (initialization)
     */
    startAnim: function(anim) {
        anim.start(this.timestamp);
    },
 
    /**
     * @private
     * Run the individual animation for this frame
     */
    runAnim: function(anim, forceEnd) {
        if (!anim) {
            return;
        }
        var me = this,
            useCSS3 = me.useCSS3 && anim.target.type === 'element',
            elapsedTime = me.timestamp - anim.startTime,
            lastFrame = (elapsedTime >= anim.duration),
            target, o;
            
        if (forceEnd) {
            elapsedTime = anim.duration;
            lastFrame = true;
        }
 
        target = this.collectTargetData(anim, elapsedTime, useCSS3, lastFrame);
        
        // For CSS3 animation, we need to immediately set the first frame's attributes without any transition 
        // to get a good initial state, then add the transition properties and set the final attributes. 
        if (useCSS3) {
            //Ext.log(' (i)  using CSS3 transitions'); 
            
            // Flush the collected attributes, without transition 
            anim.target.setAttr(target.anims[anim.id].attributes, true);
 
            // Add the end frame data 
            me.collectTargetData(anim, anim.duration, useCSS3, lastFrame);
 
            // Pause the animation so runAnim doesn't keep getting called 
            anim.paused = true;
 
            target = anim.target.target;
            // We only want to attach an event on the last element in a composite 
            if (anim.target.isComposite) {
                target = anim.target.target.last();
            }
 
            // Listen for the transitionend event 
            o = {};
            o[Ext.supports.CSS3TransitionEnd] = anim.lastFrame;
            o.scope = anim;
            o.single = true;
            target.on(o);
        }
        return target;
    },
    
    jumpToEnd: function(anim) {
        var me = this,
            target, clear;
 
        // We may not be in the middle of a tick, where targetAttr is cleared, 
        // so if we don't have it, poke it in here while we jump to the end state 
        if (!me.targetArr) {
            me.targetArr = {};
            clear = true;
        }
 
        target = me.runAnim(anim, true);
        me.applyAnimAttrs(target, target.anims[anim.id]);
        if (clear) {
            me.targetArr = null;
        }
    },
 
    /**
     * @private
     * Collect target attributes for the given Anim object at the given timestamp
     * @param {Ext.fx.Anim} anim The Anim instance
     * @param {Number} elapsedTime Time after the anim's start time
     * @param {Boolean} [useCSS3=false] True if using CSS3-based animation, else false
     * @param {Boolean} [isLastFrame=false] True if this is the last frame of animation to be run, else false
     * @return {Object} The animation target wrapper object containing the passed animation along with the
     * new attributes to set on the target's element in the next animation frame.
     */
    collectTargetData: function(anim, elapsedTime, useCSS3, isLastFrame) {
        var targetId = anim.target.getId(),
            target = this.targetArr[targetId];
        
        if (!target) {
            // Create a thin wrapper around the target so that we can create a link between the 
            // target element and its associated animations. This is important later when applying 
            // attributes to the target so that each animation can be independently run with its own 
            // duration and stopped at any point without affecting other animations for the same target. 
            target = this.targetArr[targetId] = {
                id: targetId,
                el: anim.target,
                anims: {}
            };
        }
 
        // This is a wrapper for the animation so that we can also save state along with it, 
        // including the current elapsed time and lastFrame status. Even though this method only 
        // adds a single anim object per call, each target element could have multiple animations 
        // associated with it, which is why the anim is added to the target's `anims` hash by id. 
        target.anims[anim.id] = {
            id: anim.id,
            anim: anim,
            elapsed: elapsedTime,
            isLastFrame: isLastFrame,
            // This is the object that gets applied to the target element below in applyPendingAttrs(): 
            attributes: [{
                duration: anim.duration,
                easing: (useCSS3 && anim.reverse) ? anim.easingFn.reverse().toCSS3() : anim.easing,
                // This is where the magic happens. The anim calculates what its new attributes should 
                // be based on the current frame and returns those as a hash of values. 
                attrs: anim.runAnim(elapsedTime)
            }]
        };
        
        return target;
    },
    
    // Duplicating this code for performance reasons. We only want to apply the anims 
    // to a single animation because we're hitting the end. It may be out of sequence from 
    // the runner timer. 
    applyAnimAttrs: function(target, animWrap) {
        var anim = animWrap.anim;
        if (animWrap.attributes && anim.isRunning()) {
            target.el.setAttr(animWrap.attributes, false, animWrap.isLastFrame);
                            
            // If this particular anim is at the last frame end it 
            if (animWrap.isLastFrame) {
                anim.lastFrame();
            }
        }
    },
    
    /**
     * @private
     * Apply all pending attribute changes to their targets
     */
    applyPendingAttrs: function() {
        var targetArr = this.targetArr,
            target, targetId, animWrap, anim, animId;
        
        // Loop through each target 
        for (targetId in targetArr) {
            if (targetArr.hasOwnProperty(targetId)) {
                target = targetArr[targetId];
                
                // Each target could have multiple associated animations, so iterate those 
                for (animId in target.anims) {
                    if (target.anims.hasOwnProperty(animId)) {
                        animWrap = target.anims[animId];
                        anim = animWrap.anim;
                        
                        // If the animation has valid attributes, set them on the target 
                        if (animWrap.attributes && anim.isRunning()) {
                            //Ext.log('  >   applying attributes for anim ', animWrap.id, ', target: ', target.id, ', elapsed: ', animWrap.elapsed); 
                            target.el.setAttr(animWrap.attributes, false, animWrap.isLastFrame);
                            
                            // If this particular anim is at the last frame end it 
                            if (animWrap.isLastFrame) {
                                //Ext.log('      running last frame for ', animWrap.id, ', target: ', targetId); 
                                anim.lastFrame();
                            }
                        }
                    }
                }
            }
        }
    },
    
    clear: function() {
        var me = this;
 
        if (me.taskRunner) {
            me.taskRunner.stopAll(true);
        }
        me.targetArr = {};
        me.items.clear();
        me.targets.clear();
    }
});