/**
 * @class Ext.draw.engine.Svg
 * @extends Ext.draw.Surface
 * Provides specific methods to draw with SVG.
 */

Ext.define('Ext.draw.engine.Svg', {

   
/* Begin Definitions */

    extend
: 'Ext.draw.Surface',

    requires
: ['Ext.draw.Draw', 'Ext.draw.Sprite', 'Ext.draw.Matrix', 'Ext.core.Element'],

   
/* End Definitions */

    engine
: 'Svg',

    trimRe
: /^\s+|\s+$/g,
    spacesRe
: /\s+/,
    xlink
: "http:/" + "/www.w3.org/1999/xlink",

    translateAttrs
: {
        radius
: "r",
        radiusX
: "rx",
        radiusY
: "ry",
        path
: "d",
        lineWidth
: "stroke-width",
        fillOpacity
: "fill-opacity",
        strokeOpacity
: "stroke-opacity",
        strokeLinejoin
: "stroke-linejoin"
   
},

    minDefaults
: {
        circle
: {
            cx
: 0,
            cy
: 0,
            r
: 0,
            fill
: "none",
            stroke
: null,
           
"stroke-width": null,
            opacity
: null,
           
"fill-opacity": null,
           
"stroke-opacity": null
       
},
        ellipse
: {
            cx
: 0,
            cy
: 0,
            rx
: 0,
            ry
: 0,
            fill
: "none",
            stroke
: null,
           
"stroke-width": null,
            opacity
: null,
           
"fill-opacity": null,
           
"stroke-opacity": null
       
},
        rect
: {
            x
: 0,
            y
: 0,
            width
: 0,
            height
: 0,
            rx
: 0,
            ry
: 0,
            fill
: "none",
            stroke
: null,
           
"stroke-width": null,
            opacity
: null,
           
"fill-opacity": null,
           
"stroke-opacity": null
       
},
        text
: {
            x
: 0,
            y
: 0,
           
"text-anchor": "start",
           
"font-family": null,
           
"font-size": null,
           
"font-weight": null,
           
"font-style": null,
            fill
: "#000",
            stroke
: null,
           
"stroke-width": null,
            opacity
: null,
           
"fill-opacity": null,
           
"stroke-opacity": null
       
},
        path
: {
            d
: "M0,0",
            fill
: "none",
            stroke
: null,
           
"stroke-width": null,
            opacity
: null,
           
"fill-opacity": null,
           
"stroke-opacity": null
       
},
        image
: {
            x
: 0,
            y
: 0,
            width
: 0,
            height
: 0,
            preserveAspectRatio
: "none",
            opacity
: null
       
}
   
},

    createSvgElement
: function(type, attrs) {
       
var el = this.domRef.createElementNS("http:/" + "/www.w3.org/2000/svg", type),
            key
;
       
if (attrs) {
           
for (key in attrs) {
                el
.setAttribute(key, String(attrs[key]));
           
}
       
}
       
return el;
   
},

    createSpriteElement
: function(sprite) {
       
// Create svg element and append to the DOM.
       
var el = this.createSvgElement(sprite.type);
        el
.id = sprite.id;
       
if (el.style) {
            el
.style.webkitTapHighlightColor = "rgba(0,0,0,0)";
       
}
        sprite
.el = Ext.get(el);
       
this.applyZIndex(sprite); //performs the insertion
        sprite
.matrix = Ext.create('Ext.draw.Matrix');
        sprite
.bbox = {
            plain
: 0,
            transform
: 0
       
};
        sprite
.fireEvent("render", sprite);
       
return el;
   
},

    getBBox
: function (sprite, isWithoutTransform) {
       
var realPath = this["getPath" + sprite.type](sprite);
       
if (isWithoutTransform) {
            sprite
.bbox.plain = sprite.bbox.plain || Ext.draw.Draw.pathDimensions(realPath);
           
return sprite.bbox.plain;
       
}
        sprite
.bbox.transform = sprite.bbox.transform || Ext.draw.Draw.pathDimensions(Ext.draw.Draw.mapPath(realPath, sprite.matrix));
       
return sprite.bbox.transform;
   
},
   
    getBBoxText
: function (sprite) {
       
var bbox = {},
            bb
, height, width, i, ln, el;

       
if (sprite && sprite.el) {
            el
= sprite.el.dom;
           
try {
                bbox
= el.getBBox();
               
return bbox;
           
} catch(e) {
               
// Firefox 3.0.x plays badly here
           
}
            bbox
= {x: bbox.x, y: Infinity, width: 0, height: 0};
            ln
= el.getNumberOfChars();
           
for (i = 0; i < ln; i++) {
                bb
= el.getExtentOfChar(i);
                bbox
.y = Math.min(bb.y, bbox.y);
                height
= bb.y + bb.height - bbox.y;
                bbox
.height = Math.max(bbox.height, height);
                width
= bb.x + bb.width - bbox.x;
                bbox
.width = Math.max(bbox.width, width);
           
}
           
return bbox;
       
}
   
},

    hide
: function() {
       
Ext.get(this.el).hide();
   
},

    show
: function() {
       
Ext.get(this.el).show();
   
},

    hidePrim
: function(sprite) {
       
this.addCls(sprite, Ext.baseCSSPrefix + 'hide-visibility');
   
},

    showPrim
: function(sprite) {
       
this.removeCls(sprite, Ext.baseCSSPrefix + 'hide-visibility');
   
},

    getDefs
: function() {
       
return this._defs || (this._defs = this.createSvgElement("defs"));
   
},

    transform
: function(sprite) {
       
var me = this,
            matrix
= Ext.create('Ext.draw.Matrix'),
            transforms
= sprite.transformations,
            transformsLength
= transforms.length,
            i
= 0,
            transform
, type;
           
       
for (; i < transformsLength; i++) {
            transform
= transforms[i];
            type
= transform.type;
           
if (type == "translate") {
                matrix
.translate(transform.x, transform.y);
           
}
           
else if (type == "rotate") {
                matrix
.rotate(transform.degrees, transform.x, transform.y);
           
}
           
else if (type == "scale") {
                matrix
.scale(transform.x, transform.y, transform.centerX, transform.centerY);
           
}
       
}
        sprite
.matrix = matrix;
        sprite
.el.set({transform: matrix.toSvg()});
   
},

    setSize
: function(w, h) {
       
var me = this,
            el
= me.el;
       
        w
= +w || me.width;
        h
= +h || me.height;
        me
.width = w;
        me
.height = h;

        el
.setSize(w, h);
        el
.set({
            width
: w,
            height
: h
       
});
        me
.callParent([w, h]);
   
},

    /**
     * Get the region for the surface's canvas area
     * @returns {Ext.util.Region}
     */

    getRegion
: function() {
       
// Mozilla requires using the background rect because the svg element returns an
       
// incorrect region. Webkit gives no region for the rect and must use the svg element.
       
var svgXY = this.el.getXY(),
            rectXY
= this.bgRect.getXY(),
            max
= Math.max,
            x
= max(svgXY[0], rectXY[0]),
            y
= max(svgXY[1], rectXY[1]);
       
return {
            left
: x,
            top
: y,
            right
: x + this.width,
            bottom
: y + this.height
       
};
   
},

    onRemove
: function(sprite) {
       
if (sprite.el) {
            sprite
.el.remove();
           
delete sprite.el;
       
}
       
this.callParent(arguments);
   
},
   
    setViewBox
: function(x, y, width, height) {
       
if (isFinite(x) && isFinite(y) && isFinite(width) && isFinite(height)) {
           
this.callParent(arguments);
           
this.el.dom.setAttribute("viewBox", [x, y, width, height].join(" "));
       
}
   
},

    render
: function (container) {
       
var me = this;
       
if (!me.el) {
           
var width = me.width || 10,
                height
= me.height || 10,
                el
= me.createSvgElement('svg', {
                    xmlns
: "http:/" + "/www.w3.org/2000/svg",
                    version
: 1.1,
                    width
: width,
                    height
: height
               
}),
                defs
= me.getDefs(),

               
// Create a rect that is always the same size as the svg root; this serves 2 purposes:
               
// (1) It allows mouse events to be fired over empty areas in Webkit, and (2) we can
               
// use it rather than the svg element for retrieving the correct client rect of the
               
// surface in Mozilla (see https://bugzilla.mozilla.org/show_bug.cgi?id=530985)
                bgRect
= me.createSvgElement("rect", {
                    width
: "100%",
                    height
: "100%",
                    fill
: "#000",
                    stroke
: "none",
                    opacity
: 0
               
}),
                webkitRect
;
           
               
if (Ext.isSafari3) {
                   
// Rect that we will show/hide to fix old WebKit bug with rendering issues.
                    webkitRect
= me.createSvgElement("rect", {
                        x
: -10,
                        y
: -10,
                        width
: "110%",
                        height
: "110%",
                        fill
: "none",
                        stroke
: "#000"
                   
});
               
}
            el
.appendChild(defs);
           
if (Ext.isSafari3) {
                el
.appendChild(webkitRect);
           
}
            el
.appendChild(bgRect);
            container
.appendChild(el);
            me
.el = Ext.get(el);
            me
.bgRect = Ext.get(bgRect);
           
if (Ext.isSafari3) {
                me
.webkitRect = Ext.get(webkitRect);
                me
.webkitRect.hide();
           
}
            me
.el.on({
                scope
: me,
                mouseup
: me.onMouseUp,
                mousedown
: me.onMouseDown,
                mouseover
: me.onMouseOver,
                mouseout
: me.onMouseOut,
                mousemove
: me.onMouseMove,
                mouseenter
: me.onMouseEnter,
                mouseleave
: me.onMouseLeave,
                click
: me.onClick
           
});
       
}
        me
.renderAll();
   
},

   
// private
    onMouseEnter
: function(e) {
       
if (this.el.parent().getRegion().contains(e.getPoint())) {
           
this.fireEvent('mouseenter', e);
       
}
   
},

   
// private
    onMouseLeave
: function(e) {
       
if (!this.el.parent().getRegion().contains(e.getPoint())) {
           
this.fireEvent('mouseleave', e);
       
}
   
},
   
// @private - Normalize a delegated single event from the main container to each sprite and sprite group
    processEvent
: function(name, e) {
       
var target = e.getTarget(),
            surface
= this.surface,
            sprite
;

       
this.fireEvent(name, e);
       
// We wrap text types in a tspan, sprite is the parent.
       
if (target.nodeName == "tspan" && target.parentNode) {
            target
= target.parentNode;
       
}
        sprite
= this.items.get(target.id);
       
if (sprite) {
            sprite
.fireEvent(name, sprite, e);
       
}
   
},

   
/* @private - Wrap SVG text inside a tspan to allow for line wrapping.  In addition this normallizes
     * the baseline for text the vertical middle of the text to be the same as VML.
     */

    tuneText
: function (sprite, attrs) {
       
var el = sprite.el.dom,
            tspans
= [],
            height
, tspan, text, i, ln, texts, factor;

       
if (attrs.hasOwnProperty("text")) {
           tspans
= this.setText(sprite, attrs.text);
       
}
       
// Normalize baseline via a DY shift of first tspan. Shift other rows by height * line height (1.2)
       
if (tspans.length) {
            height
= this.getBBoxText(sprite).height;
           
for (i = 0, ln = tspans.length; i < ln; i++) {
               
// The text baseline for FireFox 3.0 and 3.5 is different than other SVG implementations
               
// so we are going to normalize that here
                factor
= (Ext.isFF3_0 || Ext.isFF3_5) ? 2 : 4;
                tspans
[i].setAttribute("dy", i ? height * 1.2 : height / factor);
           
}
            sprite
.dirty = true;
       
}
   
},

    setText
: function(sprite, textString) {
         
var me = this,
             el
= sprite.el.dom,
             x
= el.getAttribute("x"),
             tspans
= [],
             height
, tspan, text, i, ln, texts;
       
       
while (el.firstChild) {
            el
.removeChild(el.firstChild);
       
}
       
// Wrap each row into tspan to emulate rows
        texts
= String(textString).split("\n");
       
for (i = 0, ln = texts.length; i < ln; i++) {
            text
= texts[i];
           
if (text) {
                tspan
= me.createSvgElement("tspan");
                tspan
.appendChild(document.createTextNode(Ext.htmlDecode(text)));
                tspan
.setAttribute("x", x);
                el
.appendChild(tspan);
                tspans
[i] = tspan;
           
}
       
}
       
return tspans;
   
},

    renderAll
: function() {
       
this.items.each(this.renderItem, this);
   
},

    renderItem
: function (sprite) {
       
if (!this.el) {
           
return;
       
}
       
if (!sprite.el) {
           
this.createSpriteElement(sprite);
       
}
       
if (sprite.zIndexDirty) {
           
this.applyZIndex(sprite);
       
}
       
if (sprite.dirty) {
           
this.applyAttrs(sprite);
           
this.applyTransformations(sprite);
       
}
   
},

    redraw
: function(sprite) {
        sprite
.dirty = sprite.zIndexDirty = true;
       
this.renderItem(sprite);
   
},

    applyAttrs
: function (sprite) {
       
var me = this,
            el
= sprite.el,
           
group = sprite.group,
            sattr
= sprite.attr,
            groups
, i, ln, attrs, font, key, style, name, rect;

       
if (group) {
            groups
= [].concat(group);
            ln
= groups.length;
           
for (i = 0; i < ln; i++) {
               
group = groups[i];
                me
.getGroup(group).add(sprite);
           
}
           
delete sprite.group;
       
}
        attrs
= me.scrubAttrs(sprite) || {};

       
// if (sprite.dirtyPath) {
        sprite
.bbox.plain = 0;
        sprite
.bbox.transform = 0;
           
if (sprite.type == "circle" || sprite.type == "ellipse") {
                attrs
.cx = attrs.cx || attrs.x;
                attrs
.cy = attrs.cy || attrs.y;
           
}
           
else if (sprite.type == "rect") {
                attrs
.rx = attrs.ry = attrs.r;
           
}
           
else if (sprite.type == "path" && attrs.d) {
                attrs
.d = Ext.draw.Draw.pathToAbsolute(attrs.d);
           
}
            sprite
.dirtyPath = false;
       
// }

       
if (attrs['clip-rect']) {
            me
.setClip(sprite, attrs);
           
delete attrs['clip-rect'];
       
}
       
if (sprite.type == 'text' && attrs.font && sprite.dirtyFont) {
            el
.set({ style: "font: " + attrs.font});
            sprite
.dirtyFont = false;
       
}
       
if (sprite.type == "image") {
            el
.dom.setAttributeNS(me.xlink, "href", attrs.src);
       
}
       
Ext.applyIf(attrs, me.minDefaults[sprite.type]);

       
if (sprite.dirtyHidden) {
           
(sattr.hidden) ? me.hidePrim(sprite) : me.showPrim(sprite);
            sprite
.dirtyHidden = false;
       
}
       
for (key in attrs) {
           
if (attrs.hasOwnProperty(key) && attrs[key] != null) {
                el
.dom.setAttribute(key, String(attrs[key]));
           
}
       
}
       
if (sprite.type == 'text') {
            me
.tuneText(sprite, attrs);
       
}

       
//set styles
        style
= sattr.style;
       
if (style) {
            el
.setStyle(style);
       
}

        sprite
.dirty = false;

       
if (Ext.isSafari3) {
           
// Refreshing the view to fix bug EXTJSIV-1: rendering issue in old Safari 3
            me
.webkitRect.show();
            setTimeout
(function () {
                me
.webkitRect.hide();
           
});
       
}
   
},

    setClip
: function(sprite, params) {
       
var me = this,
            rect
= params["clip-rect"],
            clipEl
, clipPath;
       
if (rect) {
           
if (sprite.clip) {
                sprite
.clip.parentNode.parentNode.removeChild(sprite.clip.parentNode);
           
}
            clipEl
= me.createSvgElement('clipPath');
            clipPath
= me.createSvgElement('rect');
            clipEl
.id = Ext.id(null, 'ext-clip-');
            clipPath
.setAttribute("x", rect.x);
            clipPath
.setAttribute("y", rect.y);
            clipPath
.setAttribute("width", rect.width);
            clipPath
.setAttribute("height", rect.height);
            clipEl
.appendChild(clipPath);
            me
.getDefs().appendChild(clipEl);
            sprite
.el.dom.setAttribute("clip-path", "url(#" + clipEl.id + ")");
            sprite
.clip = clipPath;
       
}
       
// if (!attrs[key]) {
       
//     var clip = Ext.getDoc().dom.getElementById(sprite.el.getAttribute("clip-path").replace(/(^url\(#|\)$)/g, ""));
       
//     clip && clip.parentNode.removeChild(clip);
       
//     sprite.el.setAttribute("clip-path", "");
       
//     delete attrss.clip;
       
// }
   
},

    /**
     * Insert or move a given sprite's element to the correct place in the DOM list for its zIndex
     * @param {Ext.draw.Sprite} sprite
     */

    applyZIndex
: function(sprite) {
       
var idx = this.normalizeSpriteCollection(sprite),
            el
= sprite.el,
            prevEl
;
       
if (this.el.dom.childNodes[idx + 2] !== el.dom) { //shift by 2 to account for defs and bg rect
           
if (idx > 0) {
               
// Find the first previous sprite which has its DOM element created already
               
do {
                    prevEl
= this.items.getAt(--idx).el;
               
} while (!prevEl && idx > 0);
           
}
            el
.insertAfter(prevEl || this.bgRect);
       
}
        sprite
.zIndexDirty = false;
   
},

    createItem
: function (config) {
       
var sprite = Ext.create('Ext.draw.Sprite', config);
        sprite
.surface = this;
       
return sprite;
   
},

    addGradient
: function(gradient) {
        gradient
= Ext.draw.Draw.parseGradient(gradient);
       
var ln = gradient.stops.length,
            vector
= gradient.vector,
            gradientEl
,
            stop
,
            stopEl
,
            i
;
       
if (gradient.type == "linear") {
            gradientEl
= this.createSvgElement("linearGradient");
            gradientEl
.setAttribute("x1", vector[0]);
            gradientEl
.setAttribute("y1", vector[1]);
            gradientEl
.setAttribute("x2", vector[2]);
            gradientEl
.setAttribute("y2", vector[3]);
       
}
       
else {
            gradientEl
= this.createSvgElement("radialGradient");
            gradientEl
.setAttribute("cx", gradient.centerX);
            gradientEl
.setAttribute("cy", gradient.centerY);
            gradientEl
.setAttribute("r", gradient.radius);
           
if (Ext.isNumber(gradient.focalX) && Ext.isNumber(gradient.focalY)) {
                gradientEl
.setAttribute("fx", gradient.focalX);
                gradientEl
.setAttribute("fy", gradient.focalY);
           
}
       
}    
        gradientEl
.id = gradient.id;
       
this.getDefs().appendChild(gradientEl);

       
for (i = 0; i < ln; i++) {
            stop
= gradient.stops[i];
            stopEl
= this.createSvgElement("stop");
            stopEl
.setAttribute("offset", stop.offset + "%");
            stopEl
.setAttribute("stop-color", stop.color);
            stopEl
.setAttribute("stop-opacity",stop.opacity);
            gradientEl
.appendChild(stopEl);
       
}
   
},

    /**
     * Checks if the specified CSS class exists on this element's DOM node.
     * @param {String} className The CSS class to check for
     * @return {Boolean} True if the class exists, else false
     */

    hasCls
: function(sprite, className) {
       
return className && (' ' + (sprite.el.dom.getAttribute('class') || '') + ' ').indexOf(' ' + className + ' ') != -1;
   
},

    addCls
: function(sprite, className) {
       
var el = sprite.el,
            i
,
            len
,
            v
,
            cls
= [],
            curCls
=  el.getAttribute('class') || '';
       
// Separate case is for speed
       
if (!Ext.isArray(className)) {
           
if (typeof className == 'string' && !this.hasCls(sprite, className)) {
                el
.set({ 'class': curCls + ' ' + className });
           
}
       
}
       
else {
           
for (i = 0, len = className.length; i < len; i++) {
                v
= className[i];
               
if (typeof v == 'string' && (' ' + curCls + ' ').indexOf(' ' + v + ' ') == -1) {
                    cls
.push(v);
               
}
           
}
           
if (cls.length) {
                el
.set({ 'class': ' ' + cls.join(' ') });
           
}
       
}
   
},

    removeCls
: function(sprite, className) {
       
var me = this,
            el
= sprite.el,
            curCls
=  el.getAttribute('class') || '',
            i
, idx, len, cls, elClasses;
       
if (!Ext.isArray(className)){
            className
= [className];
       
}
       
if (curCls) {
            elClasses
= curCls.replace(me.trimRe, ' ').split(me.spacesRe);
           
for (i = 0, len = className.length; i < len; i++) {
                cls
= className[i];
               
if (typeof cls == 'string') {
                    cls
= cls.replace(me.trimRe, '');
                    idx
= Ext.Array.indexOf(elClasses, cls);
                   
if (idx != -1) {
                        elClasses
.splice(idx, 1);
                   
}
               
}
           
}
            el
.set({ 'class': elClasses.join(' ') });
       
}
   
},

    destroy
: function() {
       
var me = this;
       
        me
.callParent();
       
if (me.el) {
            me
.el.remove();
       
}
       
delete me.el;
   
}
});