/**
 * @class Ext.draw.modifier.Modifier
 *
 * Each sprite has a stack of modifiers. The resulting attributes of sprite is
 * the content of the stack top. When setting attributes to a sprite,
 * changes will be pushed-down though the stack of modifiers and pop-back the
 * additive changes; When modifier is triggered to change the attribute of a
 * sprite, it will pop-up the changes to the top.
 */
Ext.define('Ext.draw.modifier.Modifier', {
 
    isModifier: true,
 
    mixins: {
        observable: 'Ext.mixin.Observable'
    },
 
    config: {
        /**
         * @private
         * @cfg {Ext.draw.modifier.Modifier} lower Modifier that receives the push-down changes.
         */
        lower: null,
 
        /**
         * @private
         * @cfg {Ext.draw.modifier.Modifier} upper Modifier that receives the pop-up changes.
         */
        upper: null,
 
        /**
         * @cfg {Ext.draw.sprite.Sprite} sprite The sprite to which the modifier belongs.
         */
        sprite: null
    },
 
    constructor: function (config) {
        this.mixins.observable.constructor.call(this, config);
    },
 
    updateUpper: function (upper) {
        if (upper) {
            upper.setLower(this);
        }
    },
 
    updateLower: function (lower) {
        if (lower) {
            lower.setUpper(this);
        }
    },
 
    /**
     * @private
     * Validate attribute set before use.
     *
     * @param {Object} attr The attribute to be validated. Note that it may be already initialized, so do
     * not override properties that have already been used.
     */
    prepareAttributes: function (attr) {
        if (this._lower) {
            this._lower.prepareAttributes(attr);
        }
    },
 
    /**
     * @private
     * Invoked when changes need to be popped up to the top.
     * @param {Object} attr The source attributes.
     * @param {Object} changes The changes to be popped up.
     */
    popUp: function (attr, changes) {
        if (this._upper) {
            this._upper.popUp(attr, changes);
        } else {
            Ext.apply(attr, changes);
        }
    },
 
    /**
     * @private
     *
     * This method will filter out the properties from the `changes` object, if they
     * have the same values as in the `attr` object (sprite's attributes).
     *
     * If the `receiver` object is provided, the attributes with the new values will be
     * copied from the `changes` object to the `receiver` object, and the `changes`
     * object will be left unchanged.
     *
     * The method returns the `receiver` object, if it was provided, or the `changes`
     * object otherwise.
     *
     * The method also handles a special case when a sprite attribute that is meant to be
     * animated was set to a certain value (e.g. 5), that is different from the original
     * value (e.g. 3) of the attribute, and immediately set to another value again, that
     * is the same as the original value (3). In this case, the attribute's current
     * value is still the original value, because the attribute hasn't started animating
     * yet, so a comparison against the current value is not appropriate, and the target
     * value (value at the end of animation, 5) should be used for comparison instead, so
     * that 3 won't be filtered out.
     */
    filterChanges: function (attr, changes, receiver) {
        var targets = attr.targets,
            name, value;
 
        if (receiver) {
            for (name in changes) {
                value = changes[name];
                if (value !== attr[name] || (targets && value !== targets[name])) {
                    receiver[name] = value;
                }
            }
        } else {
            for (name in changes) {
                value = changes[name];
                if (value === attr[name] && (!targets || value === targets[name])) {
                    delete changes[name];
                }
            }
        }
 
        return receiver || changes;
    },
 
    /**
     * @private
     * Invoked when changes need to be pushed down to the sprite.
     * @param {Object} attr The source attributes.
     * @param {Object} changes The changes to make. This object might be changed unexpectedly inside the method.
     * @return {Mixed}
     */
    pushDown: function (attr, changes) {
        return this._lower
            ? this._lower.pushDown(attr, changes)
            : this.filterChanges(attr, changes);
    }
});