/**
 * @private
 * Flyweight object to process the attributes of a sprite.
 * A single instance of the AttributeDefinition is created per sprite class.
 * See `onClassCreated` and `onClassExtended` callbacks
 * of the {@link Ext.draw.sprite.Sprite} for more info.
 */
Ext.define('Ext.draw.sprite.AttributeDefinition', {
    requires: [
        'Ext.draw.sprite.AttributeParser',
        'Ext.draw.sprite.AnimationParser'
    ],

    config: {
        /**
         * @cfg {Object} defaults Defines the default values of attributes.
         */
        defaults: {
            $value: {},
            lazy: true
        },

        /**
         * @cfg {Object} aliases Defines the alternative names for attributes.
         */
        aliases: {},

        /**
         * @cfg {Object} animationProcessors Defines the process used to animate between attributes.
         * One doesn't have to define animation processors for sprite attributes that use
         * predefined {@link #processors} from the {@link Ext.draw.sprite.AttributeParser} singleton.
         * For such attributes matching animation processors from the {@link Ext.draw.sprite.AnimationParser}
         * singleton will be used automatically.
         * However, if you have a custom processor for an attribute that should support
         * animation, you must provide a corresponding animation processor for it here.
         * For more information on animation processors please see {@link Ext.draw.sprite.AnimationParser}
         * documentation.
         */
        animationProcessors: {},

        /**
         * @cfg {Object} processors Defines the preprocessing used on the attributes.
         * One can define a custom processor function here or use the name of a predefined
         * processor from the {@link Ext.draw.sprite.AttributeParser} singleton.
         */
        processors: {
            // A plus side of lazy initialization is that the 'processors' and 'defaults' will
            // only be applied for those sprite classes that are actually instantiated.
            $value: {},
            lazy: true
        },

        /**
         * @cfg {Object} dirtyTriggers
         * @deprecated Use the {@link #triggers} config instead.
         */
        dirtyTriggers: {},

        /**
         * @cfg {Object} triggers Defines which updaters have to be called when an attribute is changed.
         * For example, the config below indicates that the 'size' updater
         * of a {@link Ext.draw.sprite.Square square} sprite has to be called
         * when the 'size' attribute changes.
         *
         *     triggers: {
         *         size: 'size'   // Use comma-separated values here if multiple updaters have to be called.
         *     }                  // Note that the order is _not_ guaranteed.
         *
         * If any of the updaters to be called (triggered by the {@link Ext.draw.sprite.Sprite#setAttributes call)
         * set attributes themselves and those attributes have triggers defined for them,
         * then their updaters will be called after all current updaters finish execution.
         *
         * The updater functions themselves are defined in the {@link #updaters} config,
         * aside from the 'canvas' updater, which doesn't have to be defined and acts as a flag,
         * indicating that this attribute should be applied to a Canvas context (or whatever emulates it).
         * @since 5.1.0
         */
        triggers: {},

        /**
         * @cfg {Object} updaters Defines the postprocessing used by the attribute.
         * Inside the updater function 'this' refers to the sprite that the attributes belong to.
         * In case of an instancing sprite 'this' will refer to the instancing template.
         * The two parameters passed to the updater function are the attributes object
         * of the sprite or instance, and the names of attributes that triggered this updater call.
         *
         * The example below shows how the 'size' updater changes other attributes
         * of a {@link Ext.draw.sprite.Square square} sprite sprite when its 'size' attribute changes.
         *
         *     updaters: {
         *         size: function (attr) {
         *             var size = attr.size;
         *             this.setAttributes({   // Changes to these attributes will trigger the 'path' updater.
         *                 x: attr.x - size,
         *                 y: attr.y - size,
         *                 height: 2 * size,
         *                 width: 2 * size
         *             });
         *         }
         *     }
         */
        updaters: {}
    },

    inheritableStatics: {
        /**
         * @private
         * Processor declaration in the form of 'processorFactory(argument1,argument2,...)'.
         * E.g.: {@link Ext.draw.sprite.AttributeParser#enums enums},
         * {@link Ext.draw.sprite.AttributeParser#limited limited}.
         */
        processorFactoryRe: /^(\w+)\(([\w\-,]*)\)$/
    },

    // The sprite class for which AttributeDefinition instance is created.
    spriteClass: null,

    constructor: function (config) {
        var me = this;
        me.initConfig(config);
    },

    applyDefaults: function (defaults, oldDefaults) {
        oldDefaults = Ext.apply(oldDefaults || {}, this.normalize(defaults));
        return oldDefaults;
    },

    applyAliases: function (aliases, oldAliases) {
        return Ext.apply(oldAliases || {}, aliases);
    },

    applyProcessors: function (processors, oldProcessors) {
        this.getAnimationProcessors(); // Apply custom animation processors first.
        var result = oldProcessors || {},
            defaultProcessor = Ext.draw.sprite.AttributeParser,
            processorFactoryRe = this.self.processorFactoryRe,
            animationProcessors = {},
            anyAnimationProcessors,
            name, match, fn;

        for (name in processors) {
            fn = processors[name];
            if (typeof fn === 'string') {
                match = fn.match(processorFactoryRe);
                if (match) { // enums(... , limited(... or something of that nature.
                    fn = defaultProcessor[match[1]].apply(defaultProcessor, match[2].split(','));
                } else if (defaultProcessor[fn]) {
                    // Names of animation parsers match the names of attribute parsers.
                    animationProcessors[name] = fn;
                    anyAnimationProcessors = true;
                    fn = defaultProcessor[fn];
                }
            }
            //<debug>
            if (!Ext.isFunction(fn)) {
                Ext.raise(this.spriteClass.$className + ": processor '" + name + "' has not been found.");
            }
            //</debug>
            result[name] = fn;
        }

        if (anyAnimationProcessors) {
            this.setAnimationProcessors(animationProcessors);
        }

        return result;
    },

    applyAnimationProcessors: function (animationProcessors, oldAnimationProcessors) {
        var parser = Ext.draw.sprite.AnimationParser,
            name, item;

        if (!oldAnimationProcessors) {
            oldAnimationProcessors = {};
        }

        for (name in animationProcessors) {
            item = animationProcessors[name];
            if (item === 'none') {
                oldAnimationProcessors[name] = null;
            } else if (Ext.isString(item) && !(name in oldAnimationProcessors)) {
                if (item in parser) {
                    // The while loop is used to resolve aliases, e.g. `num: 'number'`,
                    // where `number` maps to a parser object or is an alias too.
                    while (Ext.isString(parser[item])) {
                        item = parser[item];
                    }
                    oldAnimationProcessors[name] = parser[item];
                }
            } else if (Ext.isObject(item)) {
                oldAnimationProcessors[name] = item;
            }
        }
        return oldAnimationProcessors;
    },

    updateDirtyTriggers: function (dirtyTriggers) {
        this.setTriggers(dirtyTriggers);
    },

    applyTriggers: function (triggers, oldTriggers) {
        if (!oldTriggers) {
            oldTriggers = {};
        }
        for (var name in triggers) {
            oldTriggers[name] = triggers[name].split(',');
        }
        return oldTriggers;
    },

    applyUpdaters: function (updaters, oldUpdaters) {
        return Ext.apply(oldUpdaters || {}, updaters);
    },

    batchedNormalize: function (batchedChanges, keepUnrecognized) {
        if (!batchedChanges) {
            return {};
        }
        var processors = this.getProcessors(),
            aliases = this.getAliases(),
            translation = batchedChanges.translation || batchedChanges.translate,
            normalized = {},
            i, ln, name, val,
            rotation, scaling,
            matrix, subVal, split;

        if ('rotation' in batchedChanges) {
            rotation = batchedChanges.rotation;
        } else {
            rotation = ('rotate' in batchedChanges) ? batchedChanges.rotate : undefined;
        }

        if ('scaling' in batchedChanges) {
            scaling = batchedChanges.scaling;
        } else {
            scaling = ('scale' in batchedChanges) ? batchedChanges.scale : undefined;
        }

        if (typeof scaling !== 'undefined') {
            if (Ext.isNumber(scaling)) {
                normalized.scalingX = scaling;
                normalized.scalingY = scaling;
            } else {
                if ('x' in scaling) {
                    normalized.scalingX = scaling.x;
                }
                if ('y' in scaling) {
                    normalized.scalingY = scaling.y;
                }
                if ('centerX' in scaling) {
                    normalized.scalingCenterX = scaling.centerX;
                }
                if ('centerY' in scaling) {
                    normalized.scalingCenterY = scaling.centerY;
                }
            }
        }

        if (typeof rotation !== 'undefined') {
            if (Ext.isNumber(rotation)) {
                rotation = Ext.draw.Draw.rad(rotation);
                normalized.rotationRads = rotation;
            } else {
                if ('rads' in rotation) {
                    normalized.rotationRads = rotation.rads;
                } else if ('degrees' in rotation) {
                    if (Ext.isArray(rotation.degrees)) {
                        normalized.rotationRads = Ext.Array.map(rotation.degrees, function (deg) {
                            return Ext.draw.Draw.rad(deg);
                        });
                    } else {
                        normalized.rotationRads = Ext.draw.Draw.rad(rotation.degrees);
                    }
                }
                if ('centerX' in rotation) {
                    normalized.rotationCenterX = rotation.centerX;
                }
                if ('centerY' in rotation) {
                    normalized.rotationCenterY = rotation.centerY;
                }
            }
        }
        if (typeof translation !== 'undefined') {
            if ('x' in translation) {
                normalized.translationX = translation.x;
            }
            if ('y' in translation) {
                normalized.translationY = translation.y;
            }
        }

        if ('matrix' in batchedChanges) {
            matrix = Ext.draw.Matrix.create(batchedChanges.matrix);
            split = matrix.split();

            normalized.matrix = matrix;
            normalized.rotationRads = split.rotation;
            normalized.rotationCenterX = 0;
            normalized.rotationCenterY = 0;
            normalized.scalingX = split.scaleX;
            normalized.scalingY = split.scaleY;
            normalized.scalingCenterX = 0;
            normalized.scalingCenterY = 0;
            normalized.translationX = split.translateX;
            normalized.translationY = split.translateY;
        }

        for (name in batchedChanges) {
            val = batchedChanges[name];
            if (typeof val === 'undefined') {
                continue;
            } else if (Ext.isArray(val)) {
                if (name in aliases) {
                    name = aliases[name];
                }
                if (name in processors) {
                    normalized[name] = [];
                    for (i = 0, ln = val.length; i < ln; i++) {
                        subVal = processors[name].call(this, val[i]);
                        if (typeof subVal !== 'undefined') {
                            normalized[name][i] = subVal;
                        }
                    }
                } else if (keepUnrecognized){
                    normalized[name] = val;
                }
            } else {
                if (name in aliases) {
                    name = aliases[name];
                }
                if (name in processors) {
                    val = processors[name].call(this, val);
                    if (typeof val !== 'undefined') {
                        normalized[name] = val;
                    }
                } else if (keepUnrecognized){
                    normalized[name] = val;
                }
            }
        }
        return normalized;
    },

    /**
     * Normalizes the changes given via their processors before they are applied as attributes.
     *
     * @param {Object} changes The changes given.
     * @param {Boolean} keepUnrecognized If 'true', unknown attributes will be passed through as normalized values.
     * @return {Object} The normalized values.
     */
    normalize: function (changes, keepUnrecognized) {
        if (!changes) {
            return {};
        }
        var processors = this.getProcessors(),
            aliases = this.getAliases(),
            translation = changes.translation || changes.translate,
            normalized = {},
            name, val, rotation, scaling, matrix, split;

        if ('rotation' in changes) {
            rotation = changes.rotation;
        } else {
            rotation = ('rotate' in changes) ? changes.rotate : undefined;
        }

        if ('scaling' in changes) {
            scaling = changes.scaling;
        } else {
            scaling = ('scale' in changes) ? changes.scale : undefined;
        }

        if (translation) {
            if ('x' in translation) {
                normalized.translationX = translation.x;
            }
            if ('y' in translation) {
                normalized.translationY = translation.y;
            }
        }

        if (typeof scaling !== 'undefined') {
            if (Ext.isNumber(scaling)) {
                normalized.scalingX = scaling;
                normalized.scalingY = scaling;
            } else {
                if ('x' in scaling) {
                    normalized.scalingX = scaling.x;
                }
                if ('y' in scaling) {
                    normalized.scalingY = scaling.y;
                }
                if ('centerX' in scaling) {
                    normalized.scalingCenterX = scaling.centerX;
                }
                if ('centerY' in scaling) {
                    normalized.scalingCenterY = scaling.centerY;
                }
            }
        }

        if (typeof rotation !== 'undefined') {
            if (Ext.isNumber(rotation)) {
                rotation = Ext.draw.Draw.rad(rotation);
                normalized.rotationRads = rotation;
            } else {
                if ('rads' in rotation) {
                    normalized.rotationRads = rotation.rads;
                } else if ('degrees' in rotation) {
                    normalized.rotationRads = Ext.draw.Draw.rad(rotation.degrees);
                }
                if ('centerX' in rotation) {
                    normalized.rotationCenterX = rotation.centerX;
                }
                if ('centerY' in rotation) {
                    normalized.rotationCenterY = rotation.centerY;
                }
            }
        }

        if ('matrix' in changes) {
            matrix = Ext.draw.Matrix.create(changes.matrix);
            split = matrix.split();

            // This will NOT update the transformation matrix of a sprite
            // with the given elements. It will attempt to extract the
            // individual transformation attributes from the transformation matrix
            // elements provided. Then the extracted attributes will be used by
            // the sprite's 'applyTransformations' method to calculate
            // the transformation matrix of the sprite.
            // It's not possible to recover all the information from the given
            // transformation matrix elements. Shearing and centers of rotation
            // and scaling are not recovered.
            // Ideally, this should work like sprite.transform([elements], true),
            // i.e. update the transformation matrix of a sprite directly,
            // without attempting to update sprite's transformation attributes.
            // But we are not changing the behavior (just yet) for compatibility
            // reasons.

            normalized.matrix = matrix;
            normalized.rotationRads = split.rotation;
            normalized.rotationCenterX = 0;
            normalized.rotationCenterY = 0;
            normalized.scalingX = split.scaleX;
            normalized.scalingY = split.scaleY;
            normalized.scalingCenterX = 0;
            normalized.scalingCenterY = 0;
            normalized.translationX = split.translateX;
            normalized.translationY = split.translateY;
        }

        for (name in changes) {
            val = changes[name];
            if (typeof val === 'undefined') {
                continue;
            }
            if (name in aliases) {
                name = aliases[name];
            }
            if (name in processors) {
                val = processors[name].call(this, val);
                if (typeof val !== 'undefined') {
                    normalized[name] = val;
                }
            } else if (keepUnrecognized){
                normalized[name] = val;
            }
        }
        return normalized;
    },

    setBypassingNormalization: function (attr, modifierStack, changes) {
        return modifierStack.pushDown(attr, changes);
    },

    set: function (attr, modifierStack, changes) {
        changes = this.normalize(changes);
        return this.setBypassingNormalization(attr, modifierStack, changes);
    }
});