/**
 * @class Ext.draw.engine.Svg
 * @extend Ext.draw.Surface
 *
 * SVG engine.
 */
Ext.define('Ext.draw.engine.Svg', {
    extend: 'Ext.draw.Surface',
    requires: ['Ext.draw.engine.SvgContext'],
    isSVG: true,
 
    config: {
        /**
         * @cfg highPrecision
         * @inheritdoc
         * Nothing needs to be done in high precision mode.
         */
        highPrecision: false
    },
 
    getElementConfig: function () {
        return {
            reference: 'element',
            style: {
                position: 'absolute'
            },
            children: [
                {
                    reference: 'bodyElement',
                    style: {
                        width: '100%',
                        height: '100%',
                        position: 'relative'
                    },
                    children: [
                        {
                            tag: 'svg',
                            reference: 'svgElement',
                            namespace: "http://www.w3.org/2000/svg",
                            width: '100%',
                            height: '100%',
                            version: 1.1
                        }
                    ]
                }
            ]
        };
    },
 
    constructor: function (config) {
        var me = this;
        me.callParent([config]);
        me.mainGroup = me.createSvgNode("g");
        me.defsElement = me.createSvgNode("defs");
        // me.svgElement is assigned in element creation of Ext.Component. 
        me.svgElement.appendChild(me.mainGroup);
        me.svgElement.appendChild(me.defsElement);
        me.ctx = new Ext.draw.engine.SvgContext(me);
    },
 
    /**
     * Creates a DOM element under the SVG namespace of the given type.
     * @param {String} type The type of the SVG DOM element.
     * @return {*} The created element.
     */
    createSvgNode: function (type) {
        var node = document.createElementNS("http://www.w3.org/2000/svg", type);
        return Ext.get(node);
    },
 
    /**
     * @private
     * Returns the SVG DOM element at the given position.
     * If it does not already exist or is a different element tag,
     * it will be created and inserted into the DOM.
     * @param {Ext.dom.Element} group The parent DOM element.
     * @param {String} tag The SVG element tag.
     * @param {Number} position The position of the element in the DOM.
     * @return {Ext.dom.Element} The SVG element.
     */
    getSvgElement: function (group, tag, position) {
        var childNodes = group.dom.childNodes,
            length = childNodes.length,
            element;
 
        if (position < length) {
            element = childNodes[position];
            if (element.tagName === tag) {
                return Ext.get(element);
            } else {
                Ext.destroy(element);
            }
        } else if (position > length) {
            Ext.raise("Invalid position.");
        }
 
        element = Ext.get(this.createSvgNode(tag));
        if (position === 0) {
            group.insertFirst(element);
        } else {
            element.insertAfter(Ext.fly(childNodes[position - 1]));
        }
        element.cache = {};
 
        return element;
    },
 
    /**
     * @private
     * Applies attributes to the given element.
     * @param {Ext.dom.Element} element The DOM element to be applied.
     * @param {Object} attributes The attributes to apply to the element.
     */
    setElementAttributes: function (element, attributes) {
        var dom = element.dom,
            cache = element.cache,
            name, value;
        for (name in attributes) {
            value = attributes[name];
            if (cache[name] !== value) {
                cache[name] = value;
                dom.setAttribute(name, value);
            }
        }
    },
 
    /**
     * @private
     * Gets the next reference element under the SVG 'defs' tag.
     * @param {String} tagName The type of reference element.
     * @return {Ext.dom.Element} The reference element.
     */
    getNextDef: function (tagName) {
        return this.getSvgElement(this.defsElement, tagName, this.defsPosition++);
    },
 
    /**
     * @method clearTransform
     * @inheritdoc
     */
    clearTransform: function () {
        var me = this;
        me.mainGroup.set({transform: me.matrix.toSvg()});
    },
 
    /**
     * @method clear
     * @inheritdoc
     */
    clear: function () {
        this.ctx.clear();
        this.removeSurplusDefs();
        this.defsPosition = 0;
    },
 
    removeSurplusDefs: function () {
        var defsElement = this.defsElement,
            defs = defsElement.dom.childNodes,
            ln = defs.length,
            i;
 
        for (= ln - 1; i > this.defsPosition; i--) {
            defsElement.removeChild(defs[i]);
        }
    },
 
    /**
     * @method renderSprite
     * @inheritdoc
     */
    renderSprite: function (sprite) {
        var me = this,
            rect = me.getRect(),
            ctx = me.ctx;
 
        // This check is simplistic, but should result in a better performance 
        // compared to !sprite.isVisible() when most surface sprites are visible. 
        if (sprite.attr.hidden || sprite.attr.globalAlpha === 0) {
            // Create an empty group for each hidden sprite, 
            // so that when these sprites do become visible, 
            // they don't need groups to be created and don't 
            // mess up the previous order of elements in the 
            // document, i.e. sprites rendered in the next 
            // frame reuse the same elements they used in the 
            // previous frame. 
            ctx.save();
            ctx.restore();
            return;
        }
 
        // Each sprite is rendered in its own group ('g' element), 
        // returned by the `ctx.save` method. 
        // Essentially, the group _is_ the sprite. 
        sprite.element = ctx.save();
        sprite.preRender(this);
        sprite.useAttributes(ctx, rect);
        if (false === sprite.render(this, ctx, [0, 0, rect[2], rect[3]])) {
            return false;
        }
        sprite.setDirty(false);
        ctx.restore();
    },
 
    /**
     * @private
     */
    toSVG: function (size, surfaces) {
        var className = Ext.getClassName(this),
            svg, surface, rect, i;
 
        svg = '<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg"' +
            ' width="' + size.width + '"' +
            ' height="' + size.height + '">';
 
        for (= 0; i < surfaces.length; i++) {
            surface = surfaces[i];
            if (Ext.getClassName(surface) !== className) {
                continue;
            }
            rect = surface.getRect();
            svg += '<g transform="translate(' + rect[0] + ',' + rect[1] + ')">';
            svg += this.serializeNode(surface.svgElement.dom);
            svg += '</g>';
        }
        svg += '</svg>';
 
        return svg;
    },
 
    b64EncodeUnicode: function (str) {
        // Since DOMStrings are 16-bit-encoded strings, in most browsers calling window.btoa 
        // on a Unicode string will cause a Character Out Of Range exception if a character 
        // exceeds the range of a 8-bit ASCII-encoded character. More information: 
        // https://developer.mozilla.org/en/docs/Web/API/WindowBase64/Base64_encoding_and_decoding 
        return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) {
            return String.fromCharCode('0x' + p1);
        }));
    },
 
    flatten: function (size, surfaces) {
        var svg = '<?xml version="1.0" standalone="yes"?>';
 
        svg += this.toSVG(size, surfaces);
 
        return {
            data: 'data:image/svg+xml;base64,' + this.b64EncodeUnicode(svg),
            type: 'svg'
        };
    },
 
    /**
     * @private
     * Serializes an SVG DOM element and its children recursively into a string.
     * @param {Object} node DOM element to serialize.
     * @return {String}
     */
    serializeNode: function (node) {
        var result = '',
            i, n, attr, child;
        if (node.nodeType === document.TEXT_NODE) {
            return node.nodeValue;
        }
        result += '<' + node.nodeName;
        if (node.attributes.length) {
            for (= 0, n = node.attributes.length; i < n; i++) {
                attr = node.attributes[i];
                result += ' ' + attr.name + '="' + Ext.String.htmlEncode(attr.value) + '"';
            }
        }
        result += '>';
        if (node.childNodes && node.childNodes.length) {
            for (= 0, n = node.childNodes.length; i < n; i++) {
                child = node.childNodes[i];
                result += this.serializeNode(child);
            }
        }
        result += '</' + node.nodeName + '>';
        return result;
    },
 
    /**
     * Destroys the Canvas element and prepares it for Garbage Collection.
     */
    destroy: function () {
        var me = this;
 
        me.ctx.destroy();
        me.mainGroup.destroy();
        me.defsElement.destroy();
 
        delete me.mainGroup;
        delete me.defsElement;
        delete me.ctx;
 
        me.callParent();
    },
 
    remove: function (sprite, destroySprite) {
        if (sprite && sprite.element) {
            // If sprite has an associated SVG element, remove it from the surface. 
            sprite.element.destroy();
            sprite.element = null;
        }
        this.callParent(arguments);
    }
});