/* global G_vmlCanvasManager */
/**
 * Provides specific methods to draw with 2D Canvas element.
 */
Ext.define('Ext.draw.engine.Canvas', {
    extend: 'Ext.draw.Surface',
    isCanvas: true,
 
    requires: [
        //<feature legacyBrowser>
        'Ext.draw.engine.excanvas',
        //</feature>
 
        'Ext.draw.Animator',
        'Ext.draw.Color'
    ],
 
    config: {
        /**
         * @cfg {Boolean} highPrecision
         * True to have the Canvas use JavaScript Number instead of single precision floating point
         * for transforms.
         *
         * For example, when using data with big numbers to plot line series, the transformation
         * matrix of the canvas will have big elements. Due to the implementation of the SVGMatrix,
         * the elements are represented by 32-bits floats, which will work incorrectly.
         * To compensate for that, we enable the canvas context to perform all the transformations
         * in JavaScript.
         *
         * Do not use this if you are not encountering 32-bit floating point errors problem,
         * since this will result in a performance penalty.
         */
        highPrecision: false
    },
 
    statics: {
        contextOverrides: {
            /**
             * @ignore
             */
            setGradientBBox: function(bbox) {
                this.bbox = bbox;
            },
 
            /**
             * Fills the subpaths of the current default path or the given path with the current
             * fill style.
             * @ignore
             */
            fill: function() {
                var fillStyle = this.fillStyle,
                    fillGradient = this.fillGradient,
                    fillOpacity = this.fillOpacity,
                    alpha = this.globalAlpha,
                    bbox = this.bbox;
 
                if (fillStyle !== Ext.util.Color.RGBA_NONE && fillOpacity !== 0) {
                    if (fillGradient && bbox) {
                        this.fillStyle = fillGradient.generateGradient(this, bbox);
                    }
 
                    if (fillOpacity !== 1) {
                        this.globalAlpha = alpha * fillOpacity;
                    }
 
                    this.$fill();
 
                    if (fillOpacity !== 1) {
                        this.globalAlpha = alpha;
                    }
 
                    if (fillGradient && bbox) {
                        this.fillStyle = fillStyle;
                    }
                }
            },
 
            /**
             * Strokes the subpaths of the current default path or the given path with the current
             * stroke style.
             * @ignore
             */
            stroke: function() {
                var strokeStyle = this.strokeStyle,
                    strokeGradient = this.strokeGradient,
                    strokeOpacity = this.strokeOpacity,
                    alpha = this.globalAlpha,
                    bbox = this.bbox;
 
                if (strokeStyle !== Ext.util.Color.RGBA_NONE && strokeOpacity !== 0) {
                    if (strokeGradient && bbox) {
                        this.strokeStyle = strokeGradient.generateGradient(this, bbox);
                    }
 
                    if (strokeOpacity !== 1) {
                        this.globalAlpha = alpha * strokeOpacity;
                    }
 
                    this.$stroke();
 
                    if (strokeOpacity !== 1) {
                        this.globalAlpha = alpha;
                    }
 
                    if (strokeGradient && bbox) {
                        this.strokeStyle = strokeStyle;
                    }
                }
            },
 
            /**
             * @ignore
             */
            fillStroke: function(attr, transformFillStroke) {
                var ctx = this,
                    fillStyle = this.fillStyle,
                    fillOpacity = this.fillOpacity,
                    strokeStyle = this.strokeStyle,
                    strokeOpacity = this.strokeOpacity,
                    shadowColor = ctx.shadowColor,
                    shadowBlur = ctx.shadowBlur,
                    none = Ext.util.Color.RGBA_NONE;
 
                if (transformFillStroke === undefined) {
                    transformFillStroke = attr.transformFillStroke;
                }
 
                if (!transformFillStroke) {
                    attr.inverseMatrix.toContext(ctx);
                }
 
                if (fillStyle !== none && fillOpacity !== 0) {
                    ctx.fill();
                    ctx.shadowColor = none;
                    ctx.shadowBlur = 0;
                }
 
                if (strokeStyle !== none && strokeOpacity !== 0) {
                    ctx.stroke();
                }
 
                ctx.shadowColor = shadowColor;
                ctx.shadowBlur = shadowBlur;
            },
 
            /**
             * 2D Canvas context in IE (up to IE10, inclusive) doesn't support
             * the setLineDash method and the lineDashOffset property.
             * @param dashList An even number of non-negative numbers specifying a dash list.
             */
            setLineDash: function(dashList) {
                if (this.$setLineDash) {
                    this.$setLineDash(dashList);
                }
            },
 
            getLineDash: function() {
                if (this.$getLineDash) {
                    return this.$getLineDash();
                }
            },
 
            /**
             * Adds points to the subpath such that the arc described by the circumference of the
             * ellipse described by the arguments, starting at the given start angle and ending at
             * the given end angle, going in the given direction (defaulting to clockwise), is added
             * to the path, connected to the previous point by a straight line.
             * @ignore
             */
            ellipse: function(cx, cy, rx, ry, rotation, start, end, anticlockwise) {
                var cos = Math.cos(rotation),
                    sin = Math.sin(rotation);
 
                this.transform(cos * rx, sin * rx, -sin * ry, cos * ry, cx, cy);
                this.arc(0, 0, 1, start, end, anticlockwise);
                this.transform(
                    cos / rx, -sin / ry,
                    sin / rx, cos / ry,
                    -(cos * cx + sin * cy) / rx, (sin * cx - cos * cy) / ry);
            },
 
            /**
             * Uses the given path commands to begin a new path on the canvas.
             * @ignore
             */
            appendPath: function(path) {
                var me = this,
                    i = 0,
                    j = 0,
                    commands = path.commands,
                    params = path.params,
                    ln = commands.length;
 
                me.beginPath();
 
                for (; i < ln; i++) {
                    switch (commands[i]) {
                        case 'M':
                            me.moveTo(params[j], params[+ 1]);
                            j += 2;
                            break;
 
                        case 'L':
                            me.lineTo(params[j], params[+ 1]);
                            j += 2;
                            break;
 
                        case 'C':
                            me.bezierCurveTo(
                                params[j], params[+ 1],
                                params[+ 2], params[+ 3],
                                params[+ 4], params[+ 5]
                            );
                            j += 6;
                            break;
 
                        case 'Z':
                            me.closePath();
                            break;
                    }
                }
            },
 
            save: function() {
                var toSave = this.toSave,
                    ln = toSave.length,
                    obj = ln && {}, // Don't allocate memory if we don't have to.
                    i = 0,
                    key;
 
                for (; i < ln; i++) {
                    key = toSave[i];
 
                    if (key in this) {
                        obj[key] = this[key];
                    }
                }
 
                this.state.push(obj);
                this.$save();
            },
 
            restore: function() {
                var obj = this.state.pop(),
                    key;
 
                if (obj) {
                    for (key in obj) {
                        this[key] = obj[key];
                    }
                }
 
                this.$restore();
            }
        }
    },
 
    splitThreshold: 3000,
 
    /**
     * @private
     * Properties to be saved/restored in the `save` and `restore` methods.
     */
    toSave: ['fillGradient', 'strokeGradient'],
 
    /**
     * @property element
     * @inheritdoc
     */
    element: {
        reference: 'element',
        children: [{
            reference: 'bodyElement',
            style: {
                width: '100%',
                height: '100%',
                position: 'relative'
            }
        }]
    },
 
    /**
     * @private
     *
     * Creates the canvas element.
     */
    createCanvas: function() {
        var canvas, overrides, ctx, name;
 
        canvas = Ext.Element.create({
            tag: 'canvas',
            cls: Ext.baseCSSPrefix + 'surface-canvas'
        });
 
        // Emulate Canvas in IE8 with VML.
        if (window.G_vmlCanvasManager) {
            G_vmlCanvasManager.initElement(canvas.dom);
            this.isVML = true;
        }
 
        overrides = Ext.draw.engine.Canvas.contextOverrides;
        ctx = canvas.dom.getContext('2d');
 
        if (ctx.ellipse) {
            delete overrides.ellipse;
        }
 
        ctx.state = [];
        ctx.toSave = this.toSave;
 
        // Saving references to the native Canvas context methods that we'll be overriding.
        for (name in overrides) {
            ctx['$' + name] = ctx[name];
        }
 
        Ext.apply(ctx, overrides);
 
        if (this.getHighPrecision()) {
            this.enablePrecisionCompensation(ctx);
        }
        else {
            this.disablePrecisionCompensation(ctx);
        }
 
        this.bodyElement.appendChild(canvas);
        this.canvases.push(canvas);
        this.contexts.push(ctx);
    },
 
    updateHighPrecision: function(highPrecision) {
        var contexts = this.contexts,
            ln = contexts.length,
            i, context;
 
        for (= 0; i < ln; i++) {
            context = contexts[i];
 
            if (highPrecision) {
                this.enablePrecisionCompensation(context);
            }
            else {
                this.disablePrecisionCompensation(context);
            }
        }
    },
 
    precisionNames: [
        'rect',
        'fillRect',
        'strokeRect',
        'clearRect',
        'moveTo',
        'lineTo',
        'arc',
        'arcTo',
        'save',
        'restore',
        'updatePrecisionCompensate',
        'setTransform',
        'transform',
        'scale',
        'translate',
        'rotate',
        'quadraticCurveTo',
        'bezierCurveTo',
        'createLinearGradient',
        'createRadialGradient',
        'fillText',
        'strokeText',
        'drawImage'
    ],
 
    /**
     * @private
     * Clears canvas of compensation for canvas' use of single precision floating point.
     * @param {CanvasRenderingContext2D} ctx The canvas context.
     */
    disablePrecisionCompensation: function(ctx) {
        var regularOverrides = Ext.draw.engine.Canvas.contextOverrides,
            precisionOverrides = this.precisionNames,
            ln = precisionOverrides.length,
            i, name;
 
        for (= 0; i < ln; i++) {
            name = precisionOverrides[i];
 
            if (!(name in regularOverrides)) {
                delete ctx[name];
            }
        }
 
        this.setDirty(true);
    },
 
    /**
     * @private
     * Compensate for canvas' use of single precision floating point.
     * @param {CanvasRenderingContext2D} ctx The canvas context.
     */
    enablePrecisionCompensation: function(ctx) {
        var surface = this,
            xx = 1,
            yy = 1,
            dx = 0,
            dy = 0,
            matrix = new Ext.draw.Matrix(),
            transStack = [],
            comp = {},
            regularOverrides = Ext.draw.engine.Canvas.contextOverrides,
            originalCtx = ctx.constructor.prototype,
 
            /**
             * @cfg {Object} precisionOverrides
             * @ignore
             */
            precisionOverrides = {
                toSave: surface.toSave,
                /**
                 * Adds a new closed subpath to the path, representing the given rectangle.
                 * @return {*} 
                 * @ignore
                 */
                rect: function(x, y, w, h) {
                    return originalCtx.rect.call(this, x * xx + dx, y * yy + dy, w * xx, h * yy);
                },
 
                /**
                 * Paints the given rectangle onto the canvas, using the current fill style.
                 * @ignore
                 */
                fillRect: function(x, y, w, h) {
                    this.updatePrecisionCompensateRect();
                    originalCtx.fillRect.call(this, x * xx + dx, y * yy + dy, w * xx, h * yy);
                    this.updatePrecisionCompensate();
                },
 
                /**
                 * Paints the box that outlines the given rectangle onto the canvas, using
                 * the current stroke style.
                 * @ignore
                 */
                strokeRect: function(x, y, w, h) {
                    this.updatePrecisionCompensateRect();
                    originalCtx.strokeRect.call(this, x * xx + dx, y * yy + dy, w * xx, h * yy);
                    this.updatePrecisionCompensate();
                },
 
                /**
                 * Clears all pixels on the canvas in the given rectangle to transparent black.
                 * @ignore
                 */
                clearRect: function(x, y, w, h) {
                    return originalCtx.clearRect.call(this, x * xx + dx, y * yy + dy, w * xx,
                                                      h * yy);
                },
 
                /**
                 * Creates a new subpath with the given point.
                 * @ignore
                 */
                moveTo: function(x, y) {
                    return originalCtx.moveTo.call(this, x * xx + dx, y * yy + dy);
                },
 
                /**
                 * Adds the given point to the current subpath, connected to the previous one
                 * by a straight line.
                 * @ignore
                 */
                lineTo: function(x, y) {
                    return originalCtx.lineTo.call(this, x * xx + dx, y * yy + dy);
                },
 
                /**
                 * Adds points to the subpath such that the arc described by the circumference
                 * of the circle described by the arguments, starting at the given start angle
                 * and ending at the given end angle, going in the given direction (defaulting
                 * to clockwise), is added to the path, connected to the previous point
                 * by a straight line.
                 * @ignore
                 */
                arc: function(x, y, radius, startAngle, endAngle, anticlockwise) {
                    this.updatePrecisionCompensateRect();
                    originalCtx.arc.call(this, x * xx + dx, y * xx + dy, radius * xx, startAngle,
                                         endAngle, anticlockwise);
                    this.updatePrecisionCompensate();
                },
 
                /**
                 * Adds an arc with the given control points and radius to the current subpath,
                 * connected to the previous point by a straight line.  If two radii are provided,
                 * the first controls the width of the arc's ellipse, and the second controls
                 * the height. If only one is provided, or if they are the same, the arc is from
                 * a circle.
                 *
                 * In the case of an ellipse, the rotation argument controls the clockwise
                 * inclination of the ellipse relative to the x-axis.
                 * @ignore
                 */
                arcTo: function(x1, y1, x2, y2, radius) {
                    this.updatePrecisionCompensateRect();
                    originalCtx.arcTo.call(this, x1 * xx + dx, y1 * yy + dy, x2 * xx + dx,
                                           y2 * yy + dy, radius * xx);
                    this.updatePrecisionCompensate();
                },
 
                /**
                 * Pushes the context state to the state stack.
                 * @ignore
                 */
                save: function() {
                    transStack.push(matrix);
                    matrix = matrix.clone();
                    regularOverrides.save.call(this);
                    originalCtx.save.call(this);
                },
 
                /**
                 * Pops the state stack and restores the state.
                 * @ignore
                 */
                restore: function() {
                    matrix = transStack.pop();
                    regularOverrides.restore.call(this);
                    originalCtx.restore.call(this);
                    this.updatePrecisionCompensate();
                },
 
                /**
                 * @ignore
                 */
                updatePrecisionCompensate: function() {
                    matrix.precisionCompensate(surface.devicePixelRatio, comp);
                    xx = comp.xx;
                    yy = comp.yy;
                    dx = comp.dx;
                    dy = comp.dy;
                    originalCtx.setTransform.call(this, surface.devicePixelRatio, comp.b, comp.c,
                                                  comp.d, 0, 0);
                },
 
                /**
                 * @ignore
                 */
                updatePrecisionCompensateRect: function() {
                    matrix.precisionCompensateRect(surface.devicePixelRatio, comp);
                    xx = comp.xx;
                    yy = comp.yy;
                    dx = comp.dx;
                    dy = comp.dy;
                    originalCtx.setTransform.call(this, surface.devicePixelRatio, comp.b, comp.c,
                                                  comp.d, 0, 0);
                },
 
                /**
                 * Changes the transformation matrix to the matrix given by the arguments
                 * as described below.
                 * @ignore
                 */
                setTransform: function(x2x, x2y, y2x, y2y, newDx, newDy) {
                    matrix.set(x2x, x2y, y2x, y2y, newDx, newDy);
                    this.updatePrecisionCompensate();
                },
 
                /**
                 * Changes the transformation matrix to apply the matrix given by the arguments
                 * as described below.
                 * @ignore
                 */
                transform: function(x2x, x2y, y2x, y2y, newDx, newDy) {
                    matrix.append(x2x, x2y, y2x, y2y, newDx, newDy);
                    this.updatePrecisionCompensate();
                },
 
                /**
                 * Scales the transformation matrix.
                 * @return {*} 
                 * @ignore
                 */
                scale: function(sx, sy) {
                    this.transform(sx, 0, 0, sy, 0, 0);
                },
 
                /**
                 * Translates the transformation matrix.
                 * @return {*} 
                 * @ignore
                 */
                translate: function(dx, dy) {
                    this.transform(1, 0, 0, 1, dx, dy);
                },
 
                /**
                 * Rotates the transformation matrix.
                 * @return {*} 
                 * @ignore
                 */
                rotate: function(radians) {
                    var cos = Math.cos(radians),
                        sin = Math.sin(radians);
 
                    this.transform(cos, sin, -sin, cos, 0, 0);
                },
 
                /**
                 * Adds the given point to the current subpath, connected to the previous one by a
                 * quadratic Bézier curve with the given control point.
                 * @return {*} 
                 * @ignore
                 */
                quadraticCurveTo: function(cx, cy, x, y) {
                    originalCtx.quadraticCurveTo.call(this, cx * xx + dx, cy * yy + dy,
                                                      x * xx + dx, y * yy + dy);
                },
 
                /**
                 * Adds the given point to the current subpath, connected to the previous one
                 * by a cubic Bézier curve with the given control points.
                 * @return {*} 
                 * @ignore
                 */
                bezierCurveTo: function(c1x, c1y, c2x, c2y, x, y) {
                    originalCtx.bezierCurveTo.call(this, c1x * xx + dx, c1y * yy + dy,
                                                   c2x * xx + dx, c2y * yy + dy,
                                                   x * xx + dx, y * yy + dy);
                },
 
                /**
                 * Returns an object that represents a linear gradient that paints along the line
                 * given by the coordinates represented by the arguments.
                 * @return {*} 
                 * @ignore
                 */
                createLinearGradient: function(x0, y0, x1, y1) {
                    var grad;
 
                    this.updatePrecisionCompensateRect();
                    grad = originalCtx.createLinearGradient.call(this, x0 * xx + dx, y0 * yy + dy,
                                                                 x1 * xx + dx, y1 * yy + dy);
 
                    this.updatePrecisionCompensate();
 
                    return grad;
                },
 
                /**
                 * Returns a CanvasGradient object that represents a radial gradient that paints
                 * along the cone given by the circles represented by the arguments. If either
                 * of the radii are negative, throws an IndexSizeError exception.
                 * @return {*} 
                 * @ignore
                 */
                createRadialGradient: function(x0, y0, r0, x1, y1, r1) {
                    var grad;
 
                    this.updatePrecisionCompensateRect();
                    grad = originalCtx.createLinearGradient.call(this, x0 * xx + dx, y0 * xx + dy,
                                                                 r0 * xx, x1 * xx + dx,
                                                                 y1 * xx + dy, r1 * xx);
 
                    this.updatePrecisionCompensate();
 
                    return grad;
                },
 
                /**
                 * Fills the given text at the given position. If a maximum width is provided,
                 * the text will be scaled to fit that width if necessary.
                 * @ignore
                 */
                fillText: function(text, x, y, maxWidth) {
                    originalCtx.setTransform.apply(this, matrix.elements);
 
                    if (typeof maxWidth === 'undefined') {
                        originalCtx.fillText.call(this, text, x, y);
                    }
                    else {
                        originalCtx.fillText.call(this, text, x, y, maxWidth);
                    }
 
                    this.updatePrecisionCompensate();
                },
 
                /**
                 * Strokes the given text at the given position. If a
                 * maximum width is provided, the text will be scaled to
                 * fit that width if necessary.
                 * @ignore
                 */
                strokeText: function(text, x, y, maxWidth) {
                    originalCtx.setTransform.apply(this, matrix.elements);
 
                    if (typeof maxWidth === 'undefined') {
                        originalCtx.strokeText.call(this, text, x, y);
                    }
                    else {
                        originalCtx.strokeText.call(this, text, x, y, maxWidth);
                    }
 
                    this.updatePrecisionCompensate();
                },
 
                /**
                 * Fills the subpaths of the current default path or the given path with the current
                 * fill style.
                 * @ignore
                 */
                fill: function() {
                    var fillGradient = this.fillGradient,
                        bbox = this.bbox;
 
                    this.updatePrecisionCompensateRect();
 
                    if (fillGradient && bbox) {
                        this.fillStyle = fillGradient.generateGradient(this, bbox);
                    }
 
                    originalCtx.fill.call(this);
                    this.updatePrecisionCompensate();
                },
 
                /**
                 * Strokes the subpaths of the current default path or the given path with the
                 * current stroke style.
                 * @ignore
                 */
                stroke: function() {
                    var strokeGradient = this.strokeGradient,
                        bbox = this.bbox;
 
                    this.updatePrecisionCompensateRect();
 
                    if (strokeGradient && bbox) {
                        this.strokeStyle = strokeGradient.generateGradient(this, bbox);
                    }
 
                    originalCtx.stroke.call(this);
                    this.updatePrecisionCompensate();
                },
 
                /**
                 * Draws the given image onto the canvas.  If the first argument isn't an img,
                 * canvas, or video element, throws a TypeMismatchError exception. If the image
                 * has no image data, throws an InvalidStateError exception. If the one of the
                 * source rectangle dimensions is zero, throws an IndexSizeError exception.
                 * If the image isn't yet fully decoded, then nothing is drawn.
                 * @return {*} 
                 * @ignore
                 */
                drawImage: function(img_elem, arg1, arg2, arg3, arg4, dst_x, dst_y, dw, dh) {
                    switch (arguments.length) {
                        case 3:
                            return originalCtx.drawImage.call(this, img_elem, arg1 * xx + dx,
                                                              arg2 * yy + dy);
 
                        case 5:
                            return originalCtx.drawImage.call(this, img_elem, arg1 * xx + dx,
                                                              arg2 * yy + dy, arg3 * xx, arg4 * yy);
 
                        case 9:
                            return originalCtx.drawImage.call(this, img_elem, arg1, arg2, arg3,
                                                              arg4, dst_x * xx + dx,
                                                              dst_y * yy * dy, dw * xx, dh * yy);
                    }
                }
            };
 
        Ext.apply(ctx, precisionOverrides);
        this.setDirty(true);
    },
 
    /**
     * Normally, a surface will have a single canvas.
     * However, on certain platforms/browsers there's a limit to how big a canvas can be.
     * 'splitThreshold' is used to determine maximum width/height of a single canvas element.
     * When a surface is wider/taller than the splitThreshold, extra canvas element(s)
     * will be created and tiled inside the surface.
     */
    updateRect: function(rect) {
        this.callParent([rect]);
 
        // eslint-disable-next-line vars-on-top
        var me = this,
            l = Math.floor(rect[0]),
            t = Math.floor(rect[1]),
            r = Math.ceil(rect[0] + rect[2]),
            b = Math.ceil(rect[1] + rect[3]),
            devicePixelRatio = me.devicePixelRatio,
            canvases = me.canvases,
            w = r - l,
            h = b - t,
            splitThreshold = Math.round(me.splitThreshold / devicePixelRatio),
            xSplits = me.xSplits = Math.ceil(/ splitThreshold),
            ySplits = me.ySplits = Math.ceil(/ splitThreshold),
            i, j, k, offsetX, offsetY,
            dom, width, height;
 
        for (= 0, offsetY = 0; j < ySplits; j++, offsetY += splitThreshold) {
 
            for (= 0, offsetX = 0; i < xSplits; i++, offsetX += splitThreshold) {
 
                k = j * xSplits + i;
 
                if (>= canvases.length) {
                    me.createCanvas();
                }
 
                dom = canvases[k].dom;
 
                dom.style.left = offsetX + 'px';
                dom.style.top = offsetY + 'px';
 
                // The Canvas doesn't automatically support hi-DPI displays.
                // We have to actually create a larger canvas (more pixels)
                // while keeping its physical size the same.
 
                height = Math.min(splitThreshold, h - offsetY);
 
                if (height * devicePixelRatio !== dom.height) {
                    dom.height = height * devicePixelRatio;
                    dom.style.height = height + 'px';
                }
 
                width = Math.min(splitThreshold, w - offsetX);
 
                if (width * devicePixelRatio !== dom.width) {
                    dom.width = width * devicePixelRatio;
                    dom.style.width = width + 'px';
                }
 
                me.applyDefaults(me.contexts[k]);
 
            }
 
        }
 
        me.activeCanvases = k = xSplits * ySplits;
 
        while (canvases.length > k) {
            canvases.pop().destroy();
        }
 
        me.clear();
    },
 
    /**
     * @method clearTransform
     * @inheritdoc
     */
    clearTransform: function() {
        var me = this,
            xSplits = me.xSplits,
            ySplits = me.ySplits,
            contexts = me.contexts,
            splitThreshold = me.splitThreshold,
            devicePixelRatio = me.devicePixelRatio,
            i, j, k, ctx;
 
        for (= 0; i < xSplits; i++) {
 
            for (= 0; j < ySplits; j++) {
 
                k = j * xSplits + i;
 
                ctx = contexts[k];
 
                ctx.translate(
                    -splitThreshold * i,
                    -splitThreshold * j
                );
                ctx.scale(
                    devicePixelRatio,
                    devicePixelRatio
                );
                me.matrix.toContext(ctx);
 
            }
 
        }
    },
 
    /**
     * @method renderSprite
     * @inheritdoc
     */
    renderSprite: function(sprite) {
        var me = this,
            rect = me.getRect(),
            surfaceMatrix = me.matrix,
            parent = sprite.getParent(),
            matrix = Ext.draw.Matrix.fly([1, 0, 0, 1, 0, 0]),
            splitThreshold = me.splitThreshold / me.devicePixelRatio,
            xSplits = me.xSplits,
            ySplits = me.ySplits,
            offsetX, offsetY,
            ctx, bbox, width, height,
            left = 0,
            right,
            top = 0,
            bottom,
            w = rect[2],
            h = rect[3],
            i, j, k;
 
        while (parent && parent.isSprite) {
            matrix.prependMatrix(parent.matrix || parent.attr && parent.attr.matrix);
            parent = parent.getParent();
        }
 
        matrix.prependMatrix(surfaceMatrix);
 
        bbox = sprite.getBBox();
 
        if (bbox) {
            bbox = matrix.transformBBox(bbox);
        }
 
        sprite.preRender(me);
 
        if (sprite.attr.hidden || sprite.attr.globalAlpha === 0) {
            sprite.setDirty(false);
 
            return;
        }
 
        // Render this sprite on all Canvas elements it spans, skipping the rest.
        for (= 0, offsetY = 0; j < ySplits; j++, offsetY += splitThreshold) {
 
            for (= 0, offsetX = 0; i < xSplits; i++, offsetX += splitThreshold) {
 
                k = j * xSplits + i;
 
                ctx = me.contexts[k];
 
                width = Math.min(splitThreshold, w - offsetX);
                height = Math.min(splitThreshold, h - offsetY);
 
                left = offsetX;
                right = left + width;
 
                top = offsetY;
                bottom = top + height;
 
                if (bbox) {
                    if (bbox.x > right ||
                        bbox.x + bbox.width < left ||
                        bbox.y > bottom ||
                        bbox.y + bbox.height < top) {
                        continue;
                    }
                }
 
                ctx.save();
                sprite.useAttributes(ctx, rect);
 
                if (false === sprite.render(me, ctx, [left, top, width, height])) {
                    return false;
                }
 
                ctx.restore();
            }
 
        }
 
        sprite.setDirty(false);
    },
 
    flatten: function(size, surfaces) {
        var targetCanvas = document.createElement('canvas'),
            className = Ext.getClassName(this),
            ratio = this.devicePixelRatio,
            ctx = targetCanvas.getContext('2d'),
            surface, canvas, rect, i, j, xy;
 
        targetCanvas.width = Math.ceil(size.width * ratio);
        targetCanvas.height = Math.ceil(size.height * ratio);
 
        for (= 0; i < surfaces.length; i++) {
            surface = surfaces[i];
 
            if (Ext.getClassName(surface) !== className) {
                continue;
            }
 
            rect = surface.getRect();
 
            for (= 0; j < surface.canvases.length; j++) {
                canvas = surface.canvases[j];
                xy = canvas.getOffsetsTo(canvas.getParent());
                ctx.drawImage(canvas.dom, (rect[0] + xy[0]) * ratio, (rect[1] + xy[1]) * ratio);
            }
        }
 
        return {
            data: targetCanvas.toDataURL(),
            type: 'png'
        };
    },
 
    applyDefaults: function(ctx) {
        var none = Ext.util.Color.RGBA_NONE;
 
        ctx.strokeStyle = none;
        ctx.fillStyle = none;
        ctx.textAlign = 'start';
        ctx.textBaseline = 'alphabetic';
        ctx.miterLimit = 1;
    },
 
    /**
     * @method clear
     * @inheritdoc
     */
    clear: function() {
        var me = this,
            activeCanvases = me.activeCanvases,
            i, canvas, ctx;
 
        for (= 0; i < activeCanvases; i++) {
            canvas = me.canvases[i].dom;
            ctx = me.contexts[i];
            ctx.setTransform(1, 0, 0, 1, 0, 0);
            ctx.clearRect(0, 0, canvas.width, canvas.height);
        }
 
        me.setDirty(true);
    },
 
    /**
     * Destroys the Canvas element and prepares it for Garbage Collection.
     */
    destroy: function() {
        var me = this,
            canvases = me.canvases,
            ln = canvases.length,
            i;
 
        for (= 0; i < ln; i++) {
            me.contexts[i] = null;
            canvases[i].destroy();
            canvases[i] = null;
        }
 
        me.contexts = me.canvases = null;
 
        me.callParent();
    },
 
    privates: {
        initElement: function() {
            var me = this;
 
            me.callParent();
            me.canvases = [];
            me.contexts = [];
            me.activeCanvases = me.xSplits = me.ySplits = 0;
        }
    }
}, function() {
    var me = this,
        proto = me.prototype,
        splitThreshold = 1e10;
 
    if (Ext.os.is.Android4 && Ext.browser.is.Chrome) {
        splitThreshold = 3000;
    }
    else if (Ext.is.iOS) {
        splitThreshold = 2200;
    }
 
    proto.splitThreshold = splitThreshold;
});