/**
 * A sprite is an object rendered in a drawing {@link Ext.draw.Surface}.
 * The Sprite class itself is an abstract class and is not meant to be used directly.
 * Every sprite in the Draw and Chart packages is a subclass of the Ext.draw.sprite.Sprite.
 * The standard Sprite subclasses are:
 *
 * * {@link Ext.draw.sprite.Path} - A sprite that represents a path.
 * * {@link Ext.draw.sprite.Rect} - A sprite that represents a rectangle.
 * * {@link Ext.draw.sprite.Circle} - A sprite that represents a circle.
 * * {@link Ext.draw.sprite.Sector} - A sprite representing a pie slice.
 * * {@link Ext.draw.sprite.Arc} - A sprite that represents a circular arc.
 * * {@link Ext.draw.sprite.Ellipse} - A sprite that represents an ellipse.
 * * {@link Ext.draw.sprite.EllipticalArc} - A sprite that represents an elliptical arc.
 * * {@link Ext.draw.sprite.Text} - A sprite that represents text.
 * * {@link Ext.draw.sprite.Image} -  A sprite that represents an image.
 * * {@link Ext.draw.sprite.Instancing} - A sprite that represents multiple instances based on the given template.
 * * {@link Ext.draw.sprite.Composite} - Represents a group of sprites.
 *
 * Sprites can be created with a reference to a {@link Ext.draw.Surface}
 *
 *      var drawContainer = Ext.create('Ext.draw.Container', {
 *          // ...
 *      });
 *
 *      var sprite = Ext.create('Ext.draw.sprite.Sprite', {
 *          type: 'circle',
 *          fill: '#ff0',
 *          surface: drawContainer.getSurface('main'),
 *          radius: 5
 *      });
 *
 * Sprites can also be added to the surface as a configuration object:
 *
 *      var sprite = drawContainer.getSurface('main').add({
 *          type: 'circle',
 *          fill: '#ff0',
 *          radius: 5
 *      });
 */
Ext.define('Ext.draw.sprite.Sprite', {
    alias: 'sprite.sprite',
 
    mixins: {
        observable: 'Ext.mixin.Observable'
    },
 
    requires: [
        'Ext.draw.Draw',
        'Ext.draw.gradient.Gradient',
        'Ext.draw.sprite.AttributeDefinition',
        'Ext.draw.modifier.Target',
        'Ext.draw.modifier.Animation',
        'Ext.draw.modifier.Highlight'
    ],
 
    isSprite: true,
 
    statics: {
        defaultHitTestOptions: {
            fill: true,
            stroke: true
        }
    },
 
    inheritableStatics: {
        def: {
            processors: {
                /**
                 * @cfg {String} [strokeStyle="none"] The color of the stroke (a CSS color value).
                 */
                strokeStyle: "color",
 
                /**
                 * @cfg {String} [fillStyle="none"] The color of the shape (a CSS color value).
                 */
                fillStyle: "color",
 
                /**
                 * @cfg {Number} [strokeOpacity=1] The opacity of the stroke. Limited from 0 to 1.
                 */
                strokeOpacity: "limited01",
 
                /**
                 * @cfg {Number} [fillOpacity=1] The opacity of the fill. Limited from 0 to 1.
                 */
                fillOpacity: "limited01",
 
                /**
                 * @cfg {Number} [lineWidth=1] The width of the line stroke.
                 */
                lineWidth: "number",
 
                /**
                 * @cfg {String} [lineCap="butt"] The style of the line caps.
                 */
                lineCap: "enums(butt,round,square)",
 
                /**
                 * @cfg {String} [lineJoin="miter"] The style of the line join.
                 */
                lineJoin: "enums(round,bevel,miter)",
 
                /**
                 * @cfg {Array} [lineDash=[]]
                 * An even number of non-negative numbers specifying a dash/space sequence.
                 * Note that while this is supported in IE8 (VML engine), the behavior is
                 * different from Canvas and SVG. Please refer to this document for details:
                 * http://msdn.microsoft.com/en-us/library/bb264085(v=vs.85).aspx
                 * Although IE9 and IE10 have Canvas support, the 'lineDash'
                 * attribute is not supported in those browsers.
                 */
                lineDash: "data",
 
                /**
                 * @cfg {Number} [lineDashOffset=0]
                 * A number specifying how far into the line dash sequence drawing commences.
                 */
                lineDashOffset: "number",
 
                /**
                 * @cfg {Number} [miterLimit=10]
                 * Sets the distance between the inner corner and the outer corner where two lines meet.
                 */
                miterLimit: "number",
 
                /**
                 * @cfg {String} [shadowColor="none"] The color of the shadow (a CSS color value).
                 */
                shadowColor: "color",
 
                /**
                 * @cfg {Number} [shadowOffsetX=0] The offset of the sprite's shadow on the x-axis.
                 */
                shadowOffsetX: "number",
 
                /**
                 * @cfg {Number} [shadowOffsetY=0] The offset of the sprite's shadow on the y-axis.
                 */
                shadowOffsetY: "number",
 
                /**
                 * @cfg {Number} [shadowBlur=0] The amount blur used on the shadow.
                 */
                shadowBlur: "number",
 
                /**
                 * @cfg {Number} [globalAlpha=1] The opacity of the sprite. Limited from 0 to 1.
                 */
                globalAlpha: "limited01",
 
                /**
                 * @cfg {String} [globalCompositeOperation=source-over]
                 * Indicates how source images are drawn onto a destination image.
                 * globalCompositeOperation attribute is not supported by the SVG and VML (excanvas) engines.
                 */
                globalCompositeOperation: "enums(source-over,destination-over,source-in,destination-in,source-out,destination-out,source-atop,destination-atop,lighter,xor,copy)",
 
                /**
                 * @cfg {Boolean} [hidden=false] Determines whether or not the sprite is hidden.
                 */
                hidden: "bool",
 
                /**
                 * @cfg {Boolean} [transformFillStroke=false]
                 * Determines whether the fill and stroke are affected by sprite transformations.
                 */
                transformFillStroke: "bool",
 
                /**
                 * @cfg {Number} [zIndex=0]
                 * The stacking order of the sprite.
                 */
                zIndex: "number",
 
                /**
                 * @cfg {Number} [translationX=0]
                 * The translation of the sprite on the x-axis.
                 */
                translationX: "number",
 
                /**
                 * @cfg {Number} [translationY=0]
                 * The translation of the sprite on the y-axis.
                 */
                translationY: "number",
 
                /**
                 * @cfg {Number} [rotationRads=0]
                 * The angle of rotation of the sprite in radians.
                 */
                rotationRads: "number",
 
                /**
                 * @cfg {Number} [rotationCenterX=null]
                 * The central coordinate of the sprite's scale operation on the x-axis.
                 */
                rotationCenterX: "number",
 
                /**
                 * @cfg {Number} [rotationCenterY=null]
                 * The central coordinate of the sprite's rotate operation on the y-axis.
                 */
                rotationCenterY: "number",
 
                /**
                 * @cfg {Number} [scalingX=1] The scaling of the sprite on the x-axis.
                 */
                scalingX: "number",
 
                /**
                 * @cfg {Number} [scalingY=1] The scaling of the sprite on the y-axis.
                 */
                scalingY: "number",
 
                /**
                 * @cfg {Number} [scalingCenterX=null]
                 * The central coordinate of the sprite's scale operation on the x-axis.
                 */
                scalingCenterX: "number",
 
                /**
                 * @cfg {Number} [scalingCenterY=null]
                 * The central coordinate of the sprite's scale operation on the y-axis.
                 */
                scalingCenterY: "number",
                
                constrainGradients: "bool"
            },
 
            aliases: {
                "stroke": "strokeStyle",
                "fill": "fillStyle",
                "color": "fillStyle",
                "stroke-width": "lineWidth",
                "stroke-linecap": "lineCap",
                "stroke-linejoin": "lineJoin",
                "stroke-miterlimit": "miterLimit",
                "text-anchor": "textAlign",
                "opacity": "globalAlpha",
 
                translateX: "translationX",
                translateY: "translationY",
                rotateRads: "rotationRads",
                rotateCenterX: "rotationCenterX",
                rotateCenterY: "rotationCenterY",
                scaleX: "scalingX",
                scaleY: "scalingY",
                scaleCenterX: "scalingCenterX",
                scaleCenterY: "scalingCenterY"
            },
 
            defaults: {
                hidden: false,
                zIndex: 0,
 
                strokeStyle: "none",
                fillStyle: "none",
                lineWidth: 1,
                lineDash: [],
                lineDashOffset: 0,
                lineCap: "butt",
                lineJoin: "miter",
                miterLimit: 10,
 
                shadowColor: "none",
                shadowOffsetX: 0,
                shadowOffsetY: 0,
                shadowBlur: 0,
 
                globalAlpha: 1,
                strokeOpacity: 1,
                fillOpacity: 1,
                transformFillStroke: false,
 
                translationX: 0,
                translationY: 0,
                rotationRads: 0,
                rotationCenterX: null,
                rotationCenterY: null,
                scalingX: 1,
                scalingY: 1,
                scalingCenterX: null,
                scalingCenterY: null,
                
                constrainGradients: false
            },
 
            triggers: {
                hidden: "canvas",
                zIndex: "zIndex",
 
                globalAlpha: "canvas",
                globalCompositeOperation: "canvas",
 
                transformFillStroke: "canvas",
                strokeStyle: "canvas",
                fillStyle: "canvas",
                strokeOpacity: "canvas",
                fillOpacity: "canvas",
 
                lineWidth: "canvas",
                lineCap: "canvas",
                lineJoin: "canvas",
                lineDash: "canvas",
                lineDashOffset: "canvas",
                miterLimit: "canvas",
 
                shadowColor: "canvas",
                shadowOffsetX: "canvas",
                shadowOffsetY: "canvas",
                shadowBlur: "canvas",
 
                translationX: "transform",
                translationY: "transform",
                rotationRads: "transform",
                rotationCenterX: "transform",
                rotationCenterY: "transform",
                scalingX: "transform",
                scalingY: "transform",
                scalingCenterX: "transform",
                scalingCenterY: "transform",
                
                constrainGradients: "canvas"
            },
 
            updaters: {
                bbox: function (attr) {
                    var hasRotation = attr.rotationRads !== 0,
                        hasScaling = attr.scalingX !== 1 || attr.scalingY !== 1,
                        noRotationCenter = attr.rotationCenterX === null || attr.rotationCenterY === null,
                        noScalingCenter = attr.scalingCenterX === null || attr.scalingCenterY === null;
 
                    attr.bbox.plain.dirty = true;
                    attr.bbox.transform.dirty = true;
 
                    if (hasRotation && noRotationCenter || hasScaling && noScalingCenter) {
                        this.scheduleUpdaters(attr, {transform: []});
                    }
                },
 
                zIndex: function (attr) {
                    attr.dirtyZIndex = true;
                },
 
                transform: function (attr) {
                    attr.dirtyTransform = true;
                    attr.bbox.transform.dirty = true;
                }
            }
        }
    },
 
    /**
     * @property {Object} attr 
     * The visual attributes of the sprite, e.g. strokeStyle, fillStyle, lineWidth...
     */
    attr: {},
 
    config: {
        parent: null,
        /**
         * @cfg {Ext.draw.Surface} surface
         * The surface that this sprite is rendered into.
         */
        surface: null
    },
 
    onClassExtended: function (subClass, data) {
        // The `def` here is no longer a config, but an instance 
        // of the AttributeDefinition class created with that config, 
        // which can now be retrieved from `initialConfig`. 
        var initCfg = subClass.superclass.self.def.initialConfig,
            cfg;
 
        // If sprite defines attributes of its own, merge that with those of its parent. 
        if (data.inheritableStatics && data.inheritableStatics.def) {
            cfg = Ext.merge({}, initCfg, data.inheritableStatics.def);
            subClass.def = Ext.create('Ext.draw.sprite.AttributeDefinition', cfg);
            delete data.inheritableStatics.def;
        } else {
            subClass.def = Ext.create('Ext.draw.sprite.AttributeDefinition', initCfg);
        }
    },
 
    constructor: function (config) {
        //<debug> 
        if (Ext.getClassName(this) === 'Ext.draw.sprite.Sprite') {
            throw 'Ext.draw.sprite.Sprite is an abstract class';
        }
        //</debug> 
        var me = this;
 
        config = Ext.isObject(config) ? config : {};
 
        me.id = config.id || Ext.id(null, 'ext-sprite-');
        me.attr = {};
        me.mixins.observable.constructor.apply(me, arguments);
        
        var modifiers = Ext.Array.from(config.modifiers, true);
        me.prepareModifiers(modifiers);
        me.initializeAttributes();
        me.setAttributes(me.self.def.getDefaults(), true);
        //<debug> 
        var processors = me.self.def.getProcessors();
        for (var name in config) {
            if (name in processors && me['get' + name.charAt(0).toUpperCase() + name.substr(1)]) {
                Ext.Error.raise('The ' + me.$className +
                    ' sprite has both a config and an attribute with the same name: ' + name + '.');
            }
        }
        //</debug> 
        me.setAttributes(config);
    },
 
    getDirty: function () {
        return this.attr.dirty;
    },
 
    setDirty: function (dirty) {
        if ((this.attr.dirty = dirty)) {
            if (this._parent) {
                this._parent.setDirty(true);
            }
        }
    },
 
    addModifier: function (modifier, reinitializeAttributes) {
        var me = this;
        if (!(modifier instanceof Ext.draw.modifier.Modifier)) {
            modifier = Ext.factory(modifier, null, null, 'modifier');
        }
        modifier.setSprite(this);
        if (modifier.preFx || modifier.config && modifier.config.preFx) {
            if (me.fx.getPrevious()) {
                me.fx.getPrevious().setNext(modifier);
            }
            modifier.setNext(me.fx);
        } else {
            me.topModifier.getPrevious().setNext(modifier);
            modifier.setNext(me.topModifier);
        }
        if (reinitializeAttributes) {
            me.initializeAttributes();
        }
        return modifier;
    },
 
    prepareModifiers: function (additionalModifiers) {
        // Set defaults 
        var me = this,
            i, ln;
 
        me.topModifier = new Ext.draw.modifier.Target({sprite: me});
 
        // Link modifiers 
        me.fx = new Ext.draw.modifier.Animation({sprite: me});
        me.fx.setNext(me.topModifier);
 
        for (= 0, ln = additionalModifiers.length; i < ln; i++) {
            me.addModifier(additionalModifiers[i], false);
        }
    },
 
    initializeAttributes: function () {
        var me = this;
        me.topModifier.prepareAttributes(me.attr);
    },
 
    /**
     * @private
     * Calls updaters triggered by changes to sprite attributes.
     * @param attr The attributes of a sprite or its instance.
     */
    callUpdaters: function (attr) {
        var me = this,
            pendingUpdaters = attr.pendingUpdaters,
            updaters = me.self.def.getUpdaters(),
            any = false,
            dirty = false,
            flags, updater, fn;
 
        // If updaters set sprite attributes that trigger other updaters, 
        // those updaters are not called right away, but wait until all current 
        // updaters are called (till the next do/while loop iteration). 
 
        me.callUpdaters = Ext.emptyFn; // Hide class method from the instance. 
 
        do {
            any = false;
            for (updater in pendingUpdaters) {
                any = true;
                flags = pendingUpdaters[updater];
                delete pendingUpdaters[updater];
                fn = updaters[updater];
                if (typeof fn === 'string') {
                    fn = me[fn];
                }
                if (fn) {
                    fn.call(me, attr, flags);
                }
            }
            dirty = dirty || any;
        } while (any);
 
        delete me.callUpdaters; // Restore class method. 
 
        if (dirty) {
            me.setDirty(true);
        }
    },
 
    /**
     * @private
     * Schedules specified updaters to be called.
     * Updaters are called implicitly as a result of a change to sprite attributes.
     * But sometimes it may be required to call an updater without setting an attribute,
     * and without messing up the updater call order (by calling the updater immediately).
     * For example:
     *
     *     updaters: {
     *          onDataX: function (attr) {
     *              this.processDataX();
     *              // Process data Y every time data X is processed.
     *              // Call the onDataY updater as if changes to dataY attribute itself
     *              // triggered the update.
     *              this.scheduleUpdaters(attr, {onDataY: ['dataY']});
     *              // Alternatively:
     *              // this.scheduleUpdaters(attr, ['onDataY'], ['dataY']);
     *          }
     *     }
     *
     * @param {Object} attr The attributes object (not necesseraly of a sprite, but of its instance).
     * @param {Object/String[]} updaters A map of updaters to be called to attributes that triggered the update.
     * @param {String[]} [triggers] Attributes that triggered the update. An optional parameter.
     * If used, the `updaters` parameter will be treated an array of updaters to be called.
     */
    scheduleUpdaters: function (attr, updaters, triggers) {
        var pendingUpdaters = attr.pendingUpdaters,
            updater;
 
        function schedule() {
            if (updater in pendingUpdaters) {
                if (triggers.length) {
                    pendingUpdaters[updater] = Ext.Array.merge(pendingUpdaters[updater], triggers);
                }
            } else {
                pendingUpdaters[updater] = triggers;
            }
        }
 
        if (triggers) {
            for (var i = 0, ln = updaters.length; i < ln; i++) {
                updater = updaters[i];
                schedule();
            }
        } else {
            for (updater in updaters) {
                triggers = updaters[updater];
                schedule();
            }
        }
    },
 
    /**
     * Set attributes of the sprite.
     *
     * @param {Object} changes The content of the change.
     * @param {Boolean} [bypassNormalization] `true` to avoid normalization of the given changes.
     * @param {Boolean} [avoidCopy] `true` to avoid copying the `changes` object.
     * The content of object may be destroyed.
     */
    setAttributes: function (changes, bypassNormalization, avoidCopy) {
        //if (changes && 'fillStyle' in changes) { 
        //    console.groupCollapsed('set fillStyle', this.getId(), this.attr.part); 
        //    console.trace(); 
        //    console.groupEnd(); 
        //} 
        var attr = this.attr;
        if (bypassNormalization) {
            if (avoidCopy) {
                this.topModifier.pushDown(attr, changes);
            } else {
                this.topModifier.pushDown(attr, Ext.apply({}, changes));
            }
        } else {
            this.topModifier.pushDown(attr, this.self.def.normalize(changes));
        }
    },
 
    /**
     * Set attributes of the sprite, assuming the names and values have already been
     * normalized.
     *
     * @deprecated Use setAttributes directy with bypassNormalization argument being `true`.
     * @param {Object} changes The content of the change.
     * @param {Boolean} [avoidCopy] `true` to avoid copying the `changes` object.
     * The content of object may be destroyed.
     */
    setAttributesBypassingNormalization: function (changes, avoidCopy) {
        return this.setAttributes(changes, true, avoidCopy);
    },
 
    /**
     * Returns the bounding box for the given Sprite as calculated with the Canvas engine.
     *
     * @param {Boolean} [isWithoutTransform] Whether to calculate the bounding box with the current transforms or not.
     */
    getBBox: function (isWithoutTransform) {
        var me = this,
            attr = me.attr,
            bbox = attr.bbox,
            plain = bbox.plain,
            transform = bbox.transform;
        if (plain.dirty) {
            me.updatePlainBBox(plain);
            plain.dirty = false;
        }
        if (isWithoutTransform) {
            return plain;
        } else {
            me.applyTransformations();
            if (transform.dirty) {
                me.updateTransformedBBox(transform, plain);
                transform.dirty = false;
            }
            return transform;
        }
    },
 
    /**
     * @protected
     * Subclass will fill the plain object with `x`, `y`, `width`, `height` information of the plain bounding box of
     * this sprite.
     *
     * @param {Object} plain Target object.
     */
    updatePlainBBox: Ext.emptyFn,
 
    /**
     * @protected
     * Subclass will fill the plain object with `x`, `y`, `width`, `height` information of the transformed
     * bounding box of this sprite.
     *
     * @param {Object} transform Target object.
     * @param {Object} plain Auxiliary object providing information of plain object.
     */
    updateTransformedBBox: function (transform, plain) {
        this.attr.matrix.transformBBox(plain, 0, transform);
    },
 
    /**
     * Subclass can rewrite this function to gain better performance.
     * @param {Boolean} isWithoutTransform 
     * @return {Array}
     */
    getBBoxCenter: function (isWithoutTransform) {
        var bbox = this.getBBox(isWithoutTransform);
        if (bbox) {
            return [
                bbox.x + bbox.width * 0.5,
                bbox.y + bbox.height * 0.5
            ];
        } else {
            return [0, 0];
        }
    },
 
    /**
     * Hide the sprite.
     * @return {Ext.draw.sprite.Sprite} this
     * @chainable
     */
    hide: function () {
        this.attr.hidden = true;
        this.setDirty(true);
        return this;
    },
 
    /**
     * Show the sprite.
     * @return {Ext.draw.sprite.Sprite} this
     * @chainable
     */
    show: function () {
        this.attr.hidden = false;
        this.setDirty(true);
        return this;
    },
 
    /**
     * Applies sprite's attributes to the given context.
     * @param {Object} ctx Context to apply sprite's attributes to.
     * @param {Array} rect The rect of the context to be affected by gradients.
     */
    useAttributes: function (ctx, rect) {
        this.applyTransformations();
        var attr = this.attr,
            canvasAttributes = attr.canvasAttributes,
            strokeStyle = canvasAttributes.strokeStyle,
            fillStyle = canvasAttributes.fillStyle,
            lineDash = canvasAttributes.lineDash,
            lineDashOffset = canvasAttributes.lineDashOffset,
            id;
 
        if (strokeStyle) {
            if (strokeStyle.isGradient) {
                ctx.strokeStyle = 'black';
                ctx.strokeGradient = strokeStyle;
            } else {
                ctx.strokeGradient = false;
            }
        }
 
        if (fillStyle) {
            if (fillStyle.isGradient) {
                ctx.fillStyle = 'black';
                ctx.fillGradient = fillStyle;
            } else {
                ctx.fillGradient = false;
            }
        }
 
        if (lineDash) {
            ctx.setLineDash(lineDash);
        }
 
        // Only set lineDashOffset to contexts that support the property (excludes VML). 
        if (Ext.isNumber(lineDashOffset + ctx.lineDashOffset)) {
            ctx.lineDashOffset = lineDashOffset;
        }
 
        for (id in canvasAttributes) {
            if (canvasAttributes[id] !== undefined && canvasAttributes[id] !== ctx[id]) {
                ctx[id] = canvasAttributes[id];
            }
        }
 
        this.setGradientBBox(ctx, rect);
    },
 
    setGradientBBox: function (ctx, rect) {
        var attr = this.attr;
        if (attr.constrainGradients) {
            ctx.setGradientBBox({x: rect[0], y: rect[1], width: rect[2], height: rect[3]});
        } else {
            ctx.setGradientBBox(this.getBBox(attr.transformFillStroke));
        }
    },
 
    /**
     * @private
     *
     * Calculates forward and inverse transform matrices.
     * @param {Boolean} force Forces recalculation of transform matrices even when sprite's transform attributes supposedly haven't changed.
     */
    applyTransformations: function (force) {
        if (!force && !this.attr.dirtyTransform) {
            return;
        }
        var me = this,
            attr = me.attr,
            center = me.getBBoxCenter(true),
            centerX = center[0],
            centerY = center[1],
 
            x = attr.translationX,
            y = attr.translationY,
 
            sx = attr.scalingX,
            sy = attr.scalingY === null ? attr.scalingX : attr.scalingY,
            scx = attr.scalingCenterX === null ? centerX : attr.scalingCenterX,
            scy = attr.scalingCenterY === null ? centerY : attr.scalingCenterY,
 
            rad = attr.rotationRads,
            rcx = attr.rotationCenterX === null ? centerX : attr.rotationCenterX,
            rcy = attr.rotationCenterY === null ? centerY : attr.rotationCenterY,
 
            cos = Math.cos(rad),
            sin = Math.sin(rad);
 
        if (sx === 1 && sy === 1) {
            scx = 0;
            scy = 0;
        }
 
        if (rad === 0) {
            rcx = 0;
            rcy = 0;
        }
 
        attr.matrix.elements = [
            cos * sx, sin * sy,
            -sin * sx, cos * sy,
            scx + (rcx - cos * rcx - scx + rcy * sin) * sx + x,
            scy + (rcy - cos * rcy - scy + rcx * -sin) * sy + y
        ];
        attr.matrix.inverse(attr.inverseMatrix);
        attr.dirtyTransform = false;
        attr.bbox.transform.dirty = true;
    },
 
    /**
     * Called before rendering.
     */
    preRender: Ext.emptyFn,
 
    /**
     * Render method.
     * @param {Ext.draw.Surface} surface The surface.
     * @param {Object} ctx A context object compatible with CanvasRenderingContext2D.
     * @param {Array} rect The clip rect (or called dirty rect) of the current rendering. Not to be confused
     * with `surface.getRect()`.
     *
     * @return {*} returns `false` to stop rendering in this frame.
     * All the sprites that haven't been rendered will have their dirty flag untouched.
     */
    render: Ext.emptyFn,
 
    /**
     * Performs a hit test on the sprite.
     * @param {Array} point A two-item array containing x and y coordinates of the point.
     * @param {Object} options Hit testing options.
     * @return {Object} A hit result object that contains more information about what
     * exactly was hit or null if nothing was hit.
     */
    hitTest: function (point, options) {
        // Meant to be overridden in subclasses for more precise hit testing. 
        // This version doesn't take any options and simply hit tests sprite's 
        // bounding box, if the sprite is visible. 
        if (this.isVisible()) {
            var x = point[0],
                y = point[1],
                bbox = this.getBBox(),
                isBBoxHit = bbox && x >= bbox.x && x <= (bbox.x + bbox.width) &&
                    y >= bbox.y && y <= (bbox.y + bbox.height);
            if (isBBoxHit) {
                return {
                    sprite: this
                };
            }
        }
        return null;
    },
 
    /**
     * @private
     * Checks if the sprite can be seen.
     * This includes the `hidden` attribute check, alpha/opacity checks,
     * fill/stroke color checks and surface/parent checks.
     * The method doesn't check if the sprite is off-screen.
     * @return {Boolean} Returns `true`, if the sprite can be seen.
     */
    isVisible: function () {
        var attr = this.attr,
            parent = this.getParent(),
            hasParent = parent && (parent.isSurface || parent.isVisible()),
            isSeen = hasParent && !attr.hidden && attr.globalAlpha,
            none1 = Ext.draw.Color.NONE,
            none2 = Ext.draw.Color.RGBA_NONE,
            hasFill = attr.fillOpacity && attr.fillStyle !== none1 && attr.fillStyle !== none2,
            hasStroke = attr.strokeOpacity && attr.strokeStyle !== none1 && attr.strokeStyle !== none2,
            result = isSeen && (hasFill || hasStroke);
 
        return !!result;
    },
 
    repaint: function () {
        var surface = this.getSurface();
        if (surface) {
            surface.renderFrame();
        }
    },
 
    /**
     * Removes the sprite and clears all listeners.
     */
    destroy: function () {
        var me = this, modifier = me.topModifier, curr;
        while (modifier) {
            curr = modifier;
            modifier = modifier.getPrevious();
            curr.destroy();
        }
        delete me.attr;
 
        me.destroy = Ext.emptyFn;
        if (me.fireEvent('beforedestroy', me) !== false) {
            me.fireEvent('destroy', me);
        }
        this.callParent();
    }
}, function () { // onClassCreated 
    // Create one AttributeDefinition instance per sprite class when a class is created 
    // and replace the `def` config with the instance that was created using that config. 
    // Here we only create an AttributeDefinition instance for the base Sprite class, 
    // attribute definitions for subclasses are created inside onClassExtended method. 
    this.def = Ext.create('Ext.draw.sprite.AttributeDefinition', this.def);
});