/**
 * The Animation modifier.
 *
 * Sencha Charts allow users to use transitional animation on sprites. Simply set the duration
 * and easing in the animation modifier, then all the changes to the sprites will be animated.
 * 
 *     @example
 *     var drawCt = Ext.create({
 *         xtype: 'draw',
 *         renderTo: document.body,
 *         width: 400,
 *         height: 400,
 *         sprites: [{
 *             type: 'rect',
 *             x: 50,
 *             y: 50,
 *             width: 100,
 *             height: 100,
 *             fillStyle: '#1F6D91'
 *         }]
 *     });
 *     
 *     var rect = drawCt.getSurface().getItems()[0];
 *     
 *     rect.setAnimation({
 *         duration: 1000,
 *         easing: 'elasticOut'
 *     });
 *     
 *     Ext.defer(function () {
 *         rect.setAttributes({
 *             width: 250
 *         });
 *     }, 500);
 *
 * Also, you can use different durations and easing functions on different attributes by using
 * {@link #customDurations} and {@link #customEasings}.
 *
 * By default, an animation modifier will be created during the initialization of a sprite.
 * You can get the animation modifier of a sprite via its 
 * {@link Ext.draw.sprite.Sprite#method-getAnimation getAnimation} method.
 */
Ext.define('Ext.draw.modifier.Animation', {
    extend: 'Ext.draw.modifier.Modifier',
    alias: 'modifier.animation',
 
    requires: [
        'Ext.draw.TimingFunctions',
        'Ext.draw.Animator'
    ],
 
    config: {
        /**
         * @cfg {Function} easing
         * Default easing function.
         */
        easing: Ext.identityFn,
 
        /**
         * @cfg {Number} duration
         * Default duration time (ms).
         */
        duration: 0,
 
        /**
         * @cfg {Object} customEasings Overrides the default easing function for defined attributes.
         * E.g.:
         *
         *     // Assuming the sprite the modifier is applied to is a 'circle'.
         *     customEasings: {
         *         r: 'easeOut',
         *         'fillStyle,strokeStyle': 'linear',
         *         'cx,cy': function (p, n) {
         *             p = 1 - p;
         *             n = n || 1.616;
         *             return 1 - p * p * ((n + 1) * p - n);
         *         }
         *     }
         */
        customEasings: {},
 
        /**
         * @cfg {Object} customDurations Overrides the default duration for defined attributes.
         * E.g.:
         *
         *     // Assuming the sprite the modifier is applied to is a 'circle'.
         *     customDurations: {
         *         r: 1000,
         *         'fillStyle,strokeStyle': 2000,
         *         'cx,cy': 1000
         *     }
         */
        customDurations: {}
    },
 
    constructor: function(config) {
        var me = this;
 
        me.anyAnimation = me.anySpecialAnimations = false;
        me.animating = 0;
        me.animatingPool = [];
        me.callParent([config]);
    },
 
    prepareAttributes: function(attr) {
        if (!attr.hasOwnProperty('timers')) {
            attr.animating = false;
            attr.timers = {};
            // The 'targets' object is used to hold the target values for the
            // attributes while they are being animated from source to target values.
            // The 'targets' is pushed down to the lower level modifiers,
            // instead of the actual attr object, to hide the fact that the
            // attributes are being animated.
            attr.targets = Ext.Object.chain(attr);
            attr.targets.prototype = attr;
        }
 
        if (this._lower) {
            this._lower.prepareAttributes(attr.targets);
        }
    },
 
    updateSprite: function(sprite) {
        this.setConfig(sprite.config.animation);
    },
 
    updateDuration: function(duration) {
        this.anyAnimation = duration > 0;
    },
 
    applyEasing: function(easing) {
        if (typeof easing === 'string') {
            easing = Ext.draw.TimingFunctions.easingMap[easing];
        }
 
        return easing;
    },
 
    applyCustomEasings: function(newEasings, oldEasings) {
        var any, key, attrs, easing, i, ln;
 
        oldEasings = oldEasings || {};
        
        for (key in newEasings) {
            any = true;
            easing = newEasings[key];
            attrs = key.split(',');
 
            if (typeof easing === 'string') {
                easing = Ext.draw.TimingFunctions.easingMap[easing];
            }
 
            for (= 0, ln = attrs.length; i < ln; i++) {
                oldEasings[attrs[i]] = easing;
            }
        }
 
        if (any) {
            this.anySpecialAnimations = any;
        }
 
        return oldEasings;
    },
 
    /**
     * Set special easings on the given attributes. E.g.:
     *
     *     circleSprite.getAnimation().setEasingOn('r', 'elasticIn');
     *
     * @param {String/Array} attrs The source attribute(s).
     * @param {String} easing The special easings.
     */
    setEasingOn: function(attrs, easing) {
        var customEasings = {},
            i, ln;
 
        attrs = Ext.Array.from(attrs).slice();
        
        for (= 0, ln = attrs.length; i < ln; i++) {
            customEasings[attrs[i]] = easing;
        }
 
        this.setCustomEasings(customEasings);
    },
 
    /**
     * Remove special easings on the given attributes.
     * @param {String/Array} attrs The source attribute(s).
     */
    clearEasingOn: function(attrs) {
        var i, ln;
        
        attrs = Ext.Array.from(attrs, true);
 
        for (= 0, ln = attrs.length; i < ln; i++) {
            delete this._customEasings[attrs[i]];
        }
    },
 
    applyCustomDurations: function(newDurations, oldDurations) {
        var any, key, duration, attrs, i, ln;
 
        oldDurations = oldDurations || {};
        
        for (key in newDurations) {
            any = true;
            duration = newDurations[key];
            attrs = key.split(',');
 
            for (= 0, ln = attrs.length; i < ln; i++) {
                oldDurations[attrs[i]] = duration;
            }
        }
 
        if (any) {
            this.anySpecialAnimations = any;
        }
 
        return oldDurations;
    },
 
    /**
     * Set special duration on the given attributes. E.g.:
     *
     *     rectSprite.getAnimation().setDurationOn('height', 2000);
     *
     * @param {String/Array} attrs The source attributes.
     * @param {Number} duration The special duration.
     */
    setDurationOn: function(attrs, duration) {
        var customDurations = {},
            i, ln;
 
        attrs = Ext.Array.from(attrs).slice();
 
        for (= 0, ln = attrs.length; i < ln; i++) {
            customDurations[attrs[i]] = duration;
        }
 
        this.setCustomDurations(customDurations);
    },
 
    /**
     * Remove special easings on the given attributes.
     * @param {Object} attrs The source attributes.
     */
    clearDurationOn: function(attrs) {
        var i, ln;
        
        attrs = Ext.Array.from(attrs, true);
 
        for (= 0, ln = attrs.length; i < ln; i++) {
            delete this._customDurations[attrs[i]];
        }
    },
 
    /**
     * @private
     * Initializes Animator for the animation.
     * @param {Object} attr The source attributes.
     * @param {Boolean} animating The animating flag.
     */
    setAnimating: function(attr, animating) {
        var me = this,
            pool = me.animatingPool,
            i;
 
        if (attr.animating !== animating) {
            attr.animating = animating;
 
            if (animating) {
                pool.push(attr);
 
                if (me.animating === 0) {
                    Ext.draw.Animator.add(me);
                }
 
                me.animating++;
            }
            else {
                for (= pool.length; i--;) {
                    if (pool[i] === attr) {
                        pool.splice(i, 1);
                    }
                }
 
                me.animating = pool.length;
            }
        }
    },
 
    /**
     * @private
     * Set the attr with given easing and duration.
     * @param {Object} attr The attributes collection.
     * @param {Object} changes The changes that popped up from lower modifier.
     * @return {Object} The changes to pop up.
     */
    setAttrs: function(attr, changes) {
        var me = this,
            timers = attr.timers,
            parsers = me._sprite.self.def._animationProcessors,
            defaultEasing = me._easing,
            defaultDuration = me._duration,
            customDurations = me._customDurations,
            customEasings = me._customEasings,
            anySpecial = me.anySpecialAnimations,
            any = me.anyAnimation || anySpecial,
            targets = attr.targets,
            ignite = false,
            timer, name, newValue, startValue, parser, easing, duration, initial;
 
        if (!any) { // If there is no animation enabled.
            // When applying changes to attributes, simply stop current animation
            // and set the value.
            for (name in changes) {
                if (attr[name] === changes[name]) {
                    delete changes[name];
                }
                else {
                    attr[name] = changes[name];
                }
 
                delete targets[name];
                delete timers[name];
            }
 
            return changes;
        }
        else { // If any animation.
            for (name in changes) {
                newValue = changes[name];
                startValue = attr[name];
 
                if (newValue !== startValue && startValue !== undefined && startValue !== null &&
                    (parser = parsers[name])) {
                    // If this property is animating.
 
                    // Figure out the desired duration and easing.
                    easing = defaultEasing;
                    duration = defaultDuration;
 
                    if (anySpecial) {
                        // Deducing the easing function and duration
                        if (name in customEasings) {
                            easing = customEasings[name];
                        }
 
                        if (name in customDurations) {
                            duration = customDurations[name];
                        }
                    }
 
                    // Transitions betweens color and gradient or between gradients
                    // are not supported.
                    if (startValue && startValue.isGradient || newValue && newValue.isGradient) {
                        duration = 0;
                    }
 
                    // If the property is animating
                    if (duration) {
                        if (!timers[name]) {
                            timers[name] = {};
                        }
 
                        timer = timers[name];
                        timer.start = 0;
                        timer.easing = easing;
                        timer.duration = duration;
                        timer.compute = parser.compute;
                        timer.serve = parser.serve || Ext.identityFn;
                        timer.remove = changes.removeFromInstance &&
                                       changes.removeFromInstance[name];
 
                        if (parser.parseInitial) {
                            initial = parser.parseInitial(startValue, newValue);
 
                            timer.source = initial[0];
                            timer.target = initial[1];
                        }
                        else if (parser.parse) {
                            timer.source = parser.parse(startValue);
                            timer.target = parser.parse(newValue);
                        }
                        else {
                            timer.source = startValue;
                            timer.target = newValue;
                        }
 
                        // The animation started. Change to originalVal.
                        targets[name] = newValue;
                        delete changes[name];
                        ignite = true;
                        
                        continue;
                    }
                    else {
                        delete targets[name];
                    }
                }
                else {
                    delete targets[name];
                }
 
                // If the property is not animating.
                delete timers[name];
            }
        }
 
        if (ignite && !attr.animating) {
            me.setAnimating(attr, true);
        }
 
        return changes;
    },
 
    /**
     * @private
     *
     * Update attributes to current value according to current animation time.
     * This method will not affect the values of lower layers, but may delete a
     * value from it.
     * @param {Object} attr The source attributes.
     * @return {Object} The changes to pop up or null.
     */
    updateAttributes: function(attr) {
        if (!attr.animating) {
            return {};
        }
        
        // eslint-disable-next-line vars-on-top
        var changes = {},
            any = false,
            timers = attr.timers,
            targets = attr.targets,
            now = Ext.draw.Animator.animationTime(),
            name, timer, delta;
 
        // If updated in the same frame, return.
        if (attr.lastUpdate === now) {
            return null;
        }
 
        for (name in timers) {
            timer = timers[name];
 
            if (!timer.start) {
                timer.start = now;
                delta = 0;
            }
            else {
                delta = (now - timer.start) / timer.duration;
            }
 
            if (delta >= 1) {
                changes[name] = targets[name];
                delete targets[name];
 
                if (timers[name].remove) {
                    changes.removeFromInstance = changes.removeFromInstance || {};
                    changes.removeFromInstance[name] = true;
                }
 
                delete timers[name];
            }
            else {
                changes[name] = timer.serve(
                    timer.compute(timer.source, timer.target, timer.easing(delta), attr[name])
                );
                
                any = true;
            }
        }
 
        attr.lastUpdate = now;
        this.setAnimating(attr, any);
 
        return changes;
    },
 
    pushDown: function(attr, changes) {
        changes = this.callParent([attr.targets, changes]);
 
        return this.setAttrs(attr, changes);
    },
 
    popUp: function(attr, changes) {
        attr = attr.prototype;
        changes = this.setAttrs(attr, changes);
 
        if (this._upper) {
            return this._upper.popUp(attr, changes);
        }
        else {
            return Ext.apply(attr, changes);
        }
    },
 
    /**
     * @private
     * This is called as an animated object in `Ext.draw.Animator`.
     */
    step: function(frameTime) {
        var me = this,
            pool = me.animatingPool.slice(),
            ln = pool.length,
            i = 0,
            attr, changes;
 
        for (; i < ln; i++) {
            attr = pool[i];
            changes = me.updateAttributes(attr);
 
            if (changes && me._upper) {
                me._upper.popUp(attr, changes);
            }
        }
    },
 
    /**
     * Stop all animations affected by this modifier.
     */
    stop: function() {
        var me = this,
            pool = me.animatingPool,
            i, ln;
 
        this.step();
 
        for (= 0, ln = pool.length; i < ln; i++) {
            pool[i].animating = false;
        }
 
        me.animatingPool.length = 0;
        me.animating = 0;
        Ext.draw.Animator.remove(me);
    },
 
    destroy: function() {
        Ext.draw.Animator.remove(this);
        this.callParent();
    }
});