/**
 * @class Ext.draw.engine.SvgContext
 *
 * A class that imitates a canvas context but generates svg elements instead.
 */
Ext.define('Ext.draw.engine.SvgContext', {
 
    requires: ['Ext.draw.Color'],
 
    /**
     * @private
     * Properties to be saved/restored in the `save` and `restore` methods.
     */
    toSave: ['strokeOpacity', 'strokeStyle', 'fillOpacity', 'fillStyle', 'globalAlpha',
        'lineWidth', 'lineCap', 'lineJoin', 'lineDash', 'lineDashOffset', 'miterLimit',
        'shadowOffsetX', 'shadowOffsetY', 'shadowBlur', 'shadowColor',
        'globalCompositeOperation', 'position', 'fillGradient', 'strokeGradient'],
 
    strokeOpacity: 1,
    strokeStyle: 'none',
    fillOpacity: 1,
    fillStyle: 'none',
    lineDas: [],
    lineDashOffset: 0,
    globalAlpha: 1,
    lineWidth: 1,
    lineCap: 'butt',
    lineJoin: 'miter',
    miterLimit: 10,
    shadowOffsetX: 0,
    shadowOffsetY: 0,
    shadowBlur: 0,
    shadowColor: 'none',
    globalCompositeOperation: 'src',
 
    urlStringRe: /^url\(#([\w\-]+)\)$/,
 
    constructor: function (SvgSurface) {
        var me = this;
 
        me.surface = SvgSurface;
        // Stack of contexts. 
        me.state = [];
        me.matrix = new Ext.draw.Matrix();
        // Currently manipulated path. 
        me.path = null;
        me.clear();
    },
 
    /**
     * Clears the context.
     */
    clear: function () {
        // Current group to put paths into. 
        this.group = this.surface.mainGroup;
        // Position within the current group. 
        this.position = 0;
        this.path = null;
    },
 
    /**
     * @private
     * @param {String} tag 
     * @return {*}
     */
    getElement: function (tag) {
        return this.surface.getSvgElement(this.group, tag, this.position++);
    },
 
    /**
     * Pushes the context state to the state stack.
     */
    save: function () {
        var toSave = this.toSave,
            obj = {},
            group = this.getElement('g'),
            key, i;
 
        for (= 0; i < toSave.length; i++) {
            key = toSave[i];
            if (key in this) {
                obj[key] = this[key];
            }
        }
        this.position = 0;
        obj.matrix = this.matrix.clone();
        this.state.push(obj);
        this.group = group;
        return group;
    },
 
    /**
     * Pops the state stack and restores the state.
     */
    restore: function () {
        var toSave = this.toSave,
            obj = this.state.pop(),
            group = this.group,
            children = group.dom.childNodes,
            key, i;
 
        // Removing extra DOM elements that were not reused. 
        while (children.length > this.position) {
            group.last().destroy();
        }
        for (= 0; i < toSave.length; i++) {
            key = toSave[i];
            if (key in obj) {
                this[key] = obj[key];
            } else {
                delete this[key];
            }
        }
 
        this.setTransform.apply(this, obj.matrix.elements);
        this.group = group.getParent();
    },
 
    /**
     * Changes the transformation matrix to apply the matrix given by the arguments as described below.
     * @param {Number} xx 
     * @param {Number} yx 
     * @param {Number} xy 
     * @param {Number} yy 
     * @param {Number} dx 
     * @param {Number} dy 
     */
    transform: function (xx, yx, xy, yy, dx, dy) {
        if (this.path) {
            var inv = Ext.draw.Matrix.fly([xx, yx, xy, yy, dx, dy]).inverse();
            this.path.transform(inv);
        }
        this.matrix.append(xx, yx, xy, yy, dx, dy);
    },
 
    /**
     * Changes the transformation matrix to the matrix given by the arguments as described below.
     * @param {Number} xx 
     * @param {Number} yx 
     * @param {Number} xy 
     * @param {Number} yy 
     * @param {Number} dx 
     * @param {Number} dy 
     */
    setTransform: function (xx, yx, xy, yy, dx, dy) {
        if (this.path) {
            this.path.transform(this.matrix);
        }
        this.matrix.reset();
        this.transform(xx, yx, xy, yy, dx, dy);
    },
 
    /**
     * Scales the current context by the specified horizontal (x) and vertical (y) factors.
     * @param {Number} x The horizontal scaling factor, where 1 equals unity or 100% scale.
     * @param {Number} y The vertical scaling factor.
     */
    scale: function (x, y) {
        this.transform(x, 0, 0, y, 0, 0);
    },
 
    /**
     * Rotates the current context coordinates (that is, a transformation matrix).
     * @param {Number} angle The rotation angle, in radians.
     */
    rotate: function (angle) {
        var xx = Math.cos(angle),
            yx = Math.sin(angle),
            xy = -Math.sin(angle),
            yy = Math.cos(angle);
        this.transform(xx, yx, xy, yy, 0, 0);
    },
 
    /**
     * Specifies values to move the origin point in a canvas.
     * @param {Number} x The value to add to horizontal (or x) coordinates.
     * @param {Number} y The value to add to vertical (or y) coordinates.
     */
    translate: function (x, y) {
        this.transform(1, 0, 0, 1, x, y);
    },
 
    setGradientBBox: function (bbox) {
        this.bbox = bbox;
    },
 
    /**
     * Resets the current default path.
     */
    beginPath: function () {
        this.path = new Ext.draw.Path();
    },
 
    /**
     * Creates a new subpath with the given point.
     * @param {Number} x 
     * @param {Number} y 
     */
    moveTo: function (x, y) {
        if (!this.path) {
            this.beginPath();
        }
        this.path.moveTo(x, y);
        this.path.element = null;
    },
 
    /**
     * Adds the given point to the current subpath, connected to the previous one by a straight line.
     * @param {Number} x 
     * @param {Number} y 
     */
    lineTo: function (x, y) {
        if (!this.path) {
            this.beginPath();
        }
        this.path.lineTo(x, y);
        this.path.element = null;
    },
 
    /**
     * Adds a new closed subpath to the path, representing the given rectangle.
     * @param {Number} x 
     * @param {Number} y 
     * @param {Number} width 
     * @param {Number} height 
     */
    rect: function (x, y, width, height) {
        this.moveTo(x, y);
        this.lineTo(+ width, y);
        this.lineTo(+ width, y + height);
        this.lineTo(x, y + height);
        this.closePath();
    },
 
    /**
     * Paints the box that outlines the given rectangle onto the canvas, using the current stroke style.
     * @param {Number} x 
     * @param {Number} y 
     * @param {Number} width 
     * @param {Number} height 
     */
    strokeRect: function (x, y, width, height) {
        this.beginPath();
        this.rect(x, y, width, height);
        this.stroke();
    },
 
    /**
     * Paints the given rectangle onto the canvas, using the current fill style.
     * @param {Number} x 
     * @param {Number} y 
     * @param {Number} width 
     * @param {Number} height 
     */
    fillRect: function (x, y, width, height) {
        this.beginPath();
        this.rect(x, y, width, height);
        this.fill();
    },
 
    /**
     * Marks the current subpath as closed, and starts a new subpath with a point the same as the start and end of the newly closed subpath.
     */
    closePath: function () {
        if (!this.path) {
            this.beginPath();
        }
        this.path.closePath();
        this.path.element = null;
    },
 
    /**
     * Arc command using svg parameters.
     * @param {Number} r1 
     * @param {Number} r2 
     * @param {Number} rotation 
     * @param {Number} large 
     * @param {Number} swipe 
     * @param {Number} x2 
     * @param {Number} y2 
     */
    arcSvg: function (r1, r2, rotation, large, swipe, x2, y2) {
        if (!this.path) {
            this.beginPath();
        }
        this.path.arcSvg(r1, r2, rotation, large, swipe, x2, y2);
        this.path.element = null;
    },
 
    /**
     * 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.
     * @param {Number} x 
     * @param {Number} y 
     * @param {Number} radius 
     * @param {Number} startAngle 
     * @param {Number} endAngle 
     * @param {Number} anticlockwise 
     */
    arc: function (x, y, radius, startAngle, endAngle, anticlockwise) {
        if (!this.path) {
            this.beginPath();
        }
        this.path.arc(x, y, radius, startAngle, endAngle, anticlockwise);
        this.path.element = null;
    },
 
    /**
     * 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.
     * @param {Number} x 
     * @param {Number} y 
     * @param {Number} radiusX 
     * @param {Number} radiusY 
     * @param {Number} rotation 
     * @param {Number} startAngle 
     * @param {Number} endAngle 
     * @param {Number} anticlockwise 
     */
    ellipse: function (x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise) {
        if (!this.path) {
            this.beginPath();
        }
        this.path.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise);
        this.path.element = null;
    },
 
    /**
     * 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.
     * @param {Number} x1 
     * @param {Number} y1 
     * @param {Number} x2 
     * @param {Number} y2 
     * @param {Number} radiusX 
     * @param {Number} radiusY 
     * @param {Number} rotation 
     */
    arcTo: function (x1, y1, x2, y2, radiusX, radiusY, rotation) {
        if (!this.path) {
            this.beginPath();
        }
        this.path.arcTo(x1, y1, x2, y2, radiusX, radiusY, rotation);
        this.path.element = null;
    },
 
    /**
     * Adds the given point to the current subpath, connected to the previous one by a cubic Bézier curve with the given control points.
     * @param {Number} x1 
     * @param {Number} y1 
     * @param {Number} x2 
     * @param {Number} y2 
     * @param {Number} x3 
     * @param {Number} y3 
     */
    bezierCurveTo: function (x1, y1, x2, y2, x3, y3) {
        if (!this.path) {
            this.beginPath();
        }
        this.path.bezierCurveTo(x1, y1, x2, y2, x3, y3);
        this.path.element = null;
    },
 
    /**
     * 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.
     * @param {String} text 
     * @param {Number} x 
     * @param {Number} y 
     */
    strokeText: function (text, x, y) {
        text = String(text);
        if (this.strokeStyle) {
            var element = this.getElement('text'),
                tspan = this.surface.getSvgElement(element, 'tspan', 0);
            this.surface.setElementAttributes(element, {
                "x": x,
                "y": y,
                "transform": this.matrix.toSvg(),
                "stroke": this.strokeStyle,
                "fill": "none",
                "opacity": this.globalAlpha,
                "stroke-opacity": this.strokeOpacity,
                "style": "font: " + this.font,
                "stroke-dasharray": this.lineDash.join(','),
                "stroke-dashoffset": this.lineDashOffset
            });
            if (this.lineDash.length) {
                this.surface.setElementAttributes(element, {
                    "stroke-dasharray": this.lineDash.join(','),
                    "stroke-dashoffset": this.lineDashOffset
                });
            }
            if (tspan.dom.firstChild) {
                tspan.dom.removeChild(tspan.dom.firstChild);
            }
            this.surface.setElementAttributes(tspan, {
                "alignment-baseline": "alphabetic"
            });
            tspan.dom.appendChild(document.createTextNode(Ext.String.htmlDecode(text)));
        }
    },
 
    /**
     * 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.
     * @param {String} text 
     * @param {Number} x 
     * @param {Number} y 
     */
    fillText: function (text, x, y) {
        text = String(text);
        if (this.fillStyle) {
            var element = this.getElement('text'),
                tspan = this.surface.getSvgElement(element, 'tspan', 0);
            this.surface.setElementAttributes(element, {
                "x": x,
                "y": y,
                "transform": this.matrix.toSvg(),
                "fill": this.fillStyle,
                "opacity": this.globalAlpha,
                "fill-opacity": this.fillOpacity,
                "style": "font: " + this.font
            });
            if (tspan.dom.firstChild) {
                tspan.dom.removeChild(tspan.dom.firstChild);
            }
            this.surface.setElementAttributes(tspan, {
                "alignment-baseline": "alphabetic"
            });
            tspan.dom.appendChild(document.createTextNode(Ext.String.htmlDecode(text)));
        }
    },
 
    /**
     * 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.
     * @param {HTMLElement} image 
     * @param {Number} sx 
     * @param {Number} sy 
     * @param {Number} sw 
     * @param {Number} sh 
     * @param {Number} dx 
     * @param {Number} dy 
     * @param {Number} dw 
     * @param {Number} dh 
     */
    drawImage: function (image, sx, sy, sw, sh, dx, dy, dw, dh) {
        var me = this,
            element = me.getElement('image'),
            x = sx, y = sy,
            width = typeof sw === 'undefined' ? image.width : sw,
            height = typeof sh === 'undefined' ? image.height : sh,
            viewBox = null;
        if (typeof dh !== 'undefined') {
            viewBox = sx + " " + sy + " " + sw + " " + sh;
            x = dx;
            y = dy;
            width = dw;
            height = dh;
        }
        element.dom.setAttributeNS("http:/" + "/www.w3.org/1999/xlink", "href", image.src);
        me.surface.setElementAttributes(element, {
            viewBox: viewBox,
            x: x,
            y: y,
            width: width,
            height: height,
            opacity: me.globalAlpha,
            transform: me.matrix.toSvg()
        });
    },
 
    /**
     * Fills the subpaths of the current default path or the given path with the current fill style.
     */
    fill: function () {
        var me = this;
 
        if (!me.path) {
            return;
        }
        if (me.fillStyle) {
            var path,
                fillGradient = me.fillGradient,
                element = me.path.element,
                bbox = me.bbox,
                fill;
 
            if (!element) {
                path = me.path.toString();
                element = me.path.element = me.getElement('path');
                me.surface.setElementAttributes(element, {
                    "d": path,
                    "transform": me.matrix.toSvg()
                });
            }
            if (fillGradient && bbox) {
                // This indirectly calls ctx.createLinearGradient or ctx.createRadialGradient, 
                // depending on the type of gradient, and returns an instance of 
                // Ext.draw.engine.SvgContext.Gradient. 
                fill = fillGradient.generateGradient(me, bbox);
            } else {
                fill = me.fillStyle;
            }
            me.surface.setElementAttributes(element, {
                "fill": fill,
                "fill-opacity": me.fillOpacity * me.globalAlpha
            });
        }
    },
 
    /**
     * Strokes the subpaths of the current default path or the given path with the current stroke style.
     */
    stroke: function () {
        var me = this;
 
        if (!me.path) {
            return;
        }
        if (me.strokeStyle) {
            var path,
                strokeGradient = me.strokeGradient,
                element = me.path.element,
                bbox = me.bbox,
                stroke;
 
            if (!element || !me.path.svgString) {
                path = me.path.toString();
                if (!path) {
                    return;
                }
                element = me.path.element = me.getElement('path');
                me.surface.setElementAttributes(element, {
                    "fill": "none",
                    "d": path,
                    "transform": me.matrix.toSvg()
                });
            }
            if (strokeGradient && bbox) {
                // This indirectly calls ctx.createLinearGradient or ctx.createRadialGradient, 
                // depending on the type of gradient, and returns an instance of 
                // Ext.draw.engine.SvgContext.Gradient. 
                stroke = strokeGradient.generateGradient(me, bbox);
            } else {
                stroke = me.strokeStyle;
            }
            me.surface.setElementAttributes(element, {
                "stroke": stroke,
                "stroke-linecap": me.lineCap,
                "stroke-linejoin": me.lineJoin,
                "stroke-width": me.lineWidth,
                "stroke-opacity": me.strokeOpacity * me.globalAlpha,
                "stroke-dasharray": me.lineDash.join(','),
                "stroke-dashoffset": me.lineDashOffset
            });
            if (me.lineDash.length) {
                me.surface.setElementAttributes(element, {
                    "stroke-dasharray": me.lineDash.join(','),
                    "stroke-dashoffset": me.lineDashOffset
                });
            }
        }
    },
 
    /**
     * @protected
     *
     * Note: After the method guarantees the transform matrix will be inverted.
     * @param {Object} attr The attribute object
     * @param {Boolean} [transformFillStroke] Indicate whether to transform fill and stroke. If this is not
     *      given, then uses `attr.transformFillStroke` instead.
     */
    fillStroke: function (attr, transformFillStroke) {
        var ctx = this,
            fillStyle = ctx.fillStyle,
            strokeStyle = ctx.strokeStyle,
            fillOpacity = ctx.fillOpacity,
            strokeOpacity = ctx.strokeOpacity;
 
        if (transformFillStroke === undefined) {
            transformFillStroke = attr.transformFillStroke;
        }
 
        if (!transformFillStroke) {
            attr.inverseMatrix.toContext(ctx);
        }
 
        if (fillStyle && fillOpacity !== 0) {
            ctx.fill();
        }
 
        if (strokeStyle && strokeOpacity !== 0) {
            ctx.stroke();
        }
    },
 
    appendPath: function (path) {
        this.path = path.clone();
    },
 
    setLineDash: function (lineDash) {
        this.lineDash = lineDash;
    },
 
    getLineDash: function () {
        return this.lineDash;
    },
 
    /**
     * Returns an object that represents a linear gradient that paints along the line
     * given by the coordinates represented by the arguments.
     * @param {Number} x0 
     * @param {Number} y0 
     * @param {Number} x1 
     * @param {Number} y1 
     * @return {Ext.draw.engine.SvgContext.Gradient}
     */
    createLinearGradient: function (x0, y0, x1, y1) {
        var me = this,
            element = me.surface.getNextDef('linearGradient'),
            gradient;
 
        me.surface.setElementAttributes(element, {
            "x1": x0,
            "y1": y0,
            "x2": x1,
            "y2": y1,
            "gradientUnits": "userSpaceOnUse"
        });
        gradient = new Ext.draw.engine.SvgContext.Gradient(me, me.surface, element);
 
        return gradient;
    },
 
    /**
     * 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.
     * @param {Number} x0 
     * @param {Number} y0 
     * @param {Number} r0 
     * @param {Number} x1 
     * @param {Number} y1 
     * @param {Number} r1 
     * @return {Ext.draw.engine.SvgContext.Gradient}
     */
    createRadialGradient: function (x0, y0, r0, x1, y1, r1) {
        var me = this,
            element = me.surface.getNextDef('radialGradient'),
            gradient;
 
        me.surface.setElementAttributes(element, {
            "fx": x0,
            "fy": y0,
            "cx": x1,
            "cy": y1,
            "r": r1,
            "gradientUnits": "userSpaceOnUse"
        });
        gradient = new Ext.draw.engine.SvgContext.Gradient(me, me.surface, element, r0 / r1);
 
        return gradient;
    }
});
 
/**
 * @class Ext.draw.engine.SvgContext.Gradient.
 *
 * A class that implements native CanvasGradient interface
 * (https://developer.mozilla.org/en/docs/Web/API/CanvasGradient)
 * and a `toString` method that returns the ID of the gradient.
 */
Ext.define('Ext.draw.engine.SvgContext.Gradient', {
 
    // Gradients workflow in SVG engine: 
    // 
    // Inside the 'fill' & 'stroke' methods of the SVG Context 
    // we check if the 'ctx.fillGradient' or 'ctx.strokeGradient' 
    // objects exist. 
    // These objects are instances of Ext.draw.gradient.Gradient 
    // and are assigned to the ctx by the sprite's 'useAttributes' method, 
    // if the sprite has any gradients. 
    // 
    // Additionally, we check if the 'ctx.bbox' object exists - the bounding box 
    // for the gradients, set by the sprite's 'setGradientBBox' method. 
    // 
    // If we have both bbox and a valid instance of Ext.draw.gradient.Gradient, 
    // the 'generateGradient' method of the instance is called, 
    // which in turn calls 'ctx.createLinearGradient' or 'ctx.createRadialGradient' 
    // depending on the type of the gradient represented by the instance. 
    // These methods create a 'linearGradient' or 'radialGradient' SVG 
    // node and wrap it into a Ext.draw.engine.SvgContext.Gradient instance. 
    // 
    // The Ext.draw.engine.SvgContext.Gradient instance is then used internally 
    // by the Ext.draw.gradient.Gradient to add color 'stop' nodes 
    // to the gradient node, and by the SVG context when the 'fill' or 
    // 'stroke' attribute of a 'path' node is set to the Ext.draw.engine.SvgContext.Gradient 
    // instance, which is implicitly converted to a string - a 'url(#id)' reference 
    // to the gradient element wrapped by the instance. 
 
    isGradient: true,
 
    constructor: function (ctx, surface, element, compression) {
        var me = this;
 
        me.ctx = ctx;
        me.surface = surface;
        me.element = element;
        me.position = 0;
        me.compression = compression || 0;
    },
 
    /**
     * Adds a color stop with the given color to the gradient at the given offset. 0.0 is the offset at one end of the gradient, 1.0 is the offset at the other end.
     * @param {Number} offset 
     * @param {String} color 
     */
    addColorStop: function (offset, color) {
        var me = this,
            stop = me.surface.getSvgElement(me.element, 'stop', me.position++),
            compression = me.compression;
 
        me.surface.setElementAttributes(stop, {
            "offset": (((1 - compression) * offset + compression) * 100).toFixed(2) + '%',
            "stop-color": color,
            "stop-opacity": Ext.draw.Color.fly(color).a.toFixed(15)
        });
    },
 
    toString: function () {
        var children = this.element.dom.childNodes;
        // Removing surplus stops in case existing gradient element with more stops was reused. 
        while (children.length > this.position) {
            Ext.fly(children[children.length - 1]).destroy();
        }
        return 'url(#' + this.element.getId() + ')';
    }
 
});