/**
 * The base class of every SVG D3 Component that can also be used standalone.
 * For example:
 *
 *     @example
 *     Ext.create({
 *         renderTo: document.body,
 *
 *         width: 300,
 *         height: 300,
 *
 *         xtype: 'd3',
 *
 *         listeners: {
 *             scenesetup: function (component, scene) {
 *                 var data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
 *                     colors = d3.scale.category20(),
 *                     twoPi = 2 * Math.PI,
 *                     gap = twoPi / data.length,
 *                     r = 100;
 *
 *                 scene.append('g')
 *                     .attr('transform', 'translate(150,150)')
 *                     .selectAll('circle')
 *                     .data(data)
 *                     .enter()
 *                     .append('circle')
 *                     .attr('fill', function (d) {
 *                         return colors(d);
 *                     })
 *                     .attr('stroke', 'black')
 *                     .attr('stroke-width', 3)
 *                     .attr('r', function (d) {
 *                         return d * 3;
 *                     })
 *                     .attr('cx', function (d, i) {
 *                         return r * Math.cos(gap * i);
 *                     })
 *                     .attr('cy', function (d, i) {
 *                         return r * Math.sin(gap * i);
 *                     });
 *             }
 *         }
 *     });
 *
 */
Ext.define('Ext.d3.svg.Svg', {
    extend: 'Ext.d3.Component',
    xtype: ['d3-svg', 'd3'],
 
    isSvg: true,
 
    config: {
        /**
         * The padding of the scene.
         * See {@link Ext.util.Format#parseBox} for syntax details,
         * if using a string for this config.
         * @cfg {Object/String/Number} [padding=0]
         * @cfg {Number} padding.top
         * @cfg {Number} padding.right
         * @cfg {Number} padding.bottom
         * @cfg {Number} padding.left
         */
        padding: {
            top: 0,
            left: 0,
            right: 0,
            bottom: 0
        },
 
        /**
         * If the scene elements that go outside the scene and into the padding area
         * should be clipped.
         * Note: stock D3 components are not designed to work with this config set to `true`.
         * @cfg {Boolean} [clipScene=false]
         */
        clipScene: false
    },
 
    /**
     * @protected
     * @property
     * Class names used by this component.
     * See {@link #onClassExtended}.
     */
    defaultCls: {
        wrapper: Ext.baseCSSPrefix + 'd3-wrapper',
        scene: Ext.baseCSSPrefix + 'd3-scene',
        hidden: Ext.baseCSSPrefix + 'd3-hidden'
    },
 
    /**
     * @private
     * See {@link #getDefs}.
     */
    defs: null,
    /**
     * @private
     * The padding and clipping is applied to the scene's wrapper element,
     * not to the scene itself. See {@link #getWrapper}.
     */
    wrapper: null,
    wrapperClipRect: null,
    wrapperClipId: 'wrapper-clip',
 
    /**
     * @private
     * See {@link #getScene}.
     */
    scene: null,
    sceneRect: null, // object with scene's position and dimensions: x, y, width, height 
 
    /**
     * @private
     * See {@link #getSvg}.
     */
    svg: null,
 
    onClassExtended: function (subClass, data) {
        Ext.apply(data.defaultCls, subClass.superclass.defaultCls);
    },
 
    applyPadding: function (padding, oldPadding) {
        var result;
 
        if (!Ext.isObject(padding)) {
            result =  Ext.util.Format.parseBox(padding);
        } else if (!oldPadding) {
            result = padding;
        } else {
            result = Ext.apply(oldPadding, padding);
        }
 
        return result;
    },
 
    getSvg: function () {
        // Spec: https://www.w3.org/TR/SVG/struct.html 
        // Note: foreignObject is not supported in IE11 and below (can't use HTML elements inside SVG). 
        return this.svg || (this.svg = d3.select(this.element.dom).append('svg').attr('version', '1.1'));
    },
 
    /**
     * @private
     * Calculates and sets scene size and position based on the given `size` object
     * and the {@link #padding} config.
     * @param {Object} size 
     * @param {Number} size.width
     * @param {Number} size.height
     */
    resizeHandler: function (size) {
        var me = this,
            svg = me.getSvg(),
            paddingCfg = me.getPadding(),
            isRtl = me.getInherited().rtl,
            wrapper = me.getWrapper(),
            wrapperClipRect = me.getWrapperClipRect(),
            scene = me.getScene(),
            width = size && size.width,
            height = size && size.height,
            rect;
 
        if (!(width && height)) {
            return;
        }
 
        svg
            .attr('width', width)
            .attr('height', height);
 
        rect = me.sceneRect || (me.sceneRect = {});
 
        rect.x = isRtl ? paddingCfg.right : paddingCfg.left;
        rect.y = paddingCfg.top;
        rect.width = width - paddingCfg.left - paddingCfg.right;
        rect.height = height - paddingCfg.top - paddingCfg.bottom;
 
        wrapper
            .attr('transform', 'translate(' + rect.x + ',' + rect.y + ')');
 
        wrapperClipRect
            .attr('width', rect.width)
            .attr('height', rect.height);
 
        me.onSceneResize(scene, rect);
        me.fireEvent('sceneresize', me, scene, rect);
    },
 
    updatePadding: function () {
        var me = this;
 
        if (!me.isConfiguring) {
            me.resizeHandler(me.getSize());
        }
    },
 
    /**
     * @event sceneresize
     * Fires after scene size has changed.
     * Notes: the scene is a 'g' element, so it cannot actually have a size.
     * The size reported is the size the drawing is supposed to fit in.
     * @param {Ext.d3.svg.Svg} component
     * @param {d3.selection} scene
     * @param {Object} size An object with `width` and `height` properties.
     */
 
    getSceneRect: function () {
        return this.sceneRect;
    },
 
    getContentRect: function () {
        // Note that `getBBox` will also measure invisible elements in the scene. 
        return this.scene && this.scene.node().getBBox();
    },
 
    getViewportRect: function () {
        return this.sceneRect;
    },
 
    alignContent: function (x, y) {
        // This method doesn't account for content scaling. 
        var me = this,
            sceneRect = me.getSceneRect(),
            contentRect = me.getContentRect(),
            tx, ty, translation;
 
        if (sceneRect && contentRect) {
            switch (x) {
                case 'center':
                    tx = sceneRect.width / 2 - (contentRect.x + contentRect.width / 2);
                    break;
                case 'left':
                    tx = -contentRect.x;
                    break;
                case 'right':
                    tx = sceneRect.width - (contentRect.x + contentRect.width);
                    break;
                default:
                    Ext.raise('Invalid value. Valid `x` values are: center, left, right.');
            }
            switch (y) {
                case 'center':
                    ty = sceneRect.height / 2 - (contentRect.y + contentRect.height / 2);
                    break;
                case 'top':
                    ty = -contentRect.y;
                    break;
                case 'bottom':
                    ty = sceneRect.height - (contentRect.y + contentRect.height);
                    break;
                default:
                    Ext.raise('Invalid value. Valid `y` values are: center, top, bottom.');
            }
        }
 
        if (Ext.isNumber(tx) && Ext.isNumber(ty)) {
            translation = [tx, ty];
            me.scene.attr('transform', 'translate(' + translation + ')');
            me.panZoom && me.panZoom.setPanZoomSilently(translation);
        }
    },
 
    /**
     * @protected
     * This method is called after the scene gets its position and size.
     * It's a good place to recalculate layout(s) and re-render the scene.
     * @param {d3.selection} scene
     * @param {Object} rect 
     * @param {Number} rect.x
     * @param {Number} rect.y
     * @param {Number} rect.width
     * @param {Number} rect.height
     */
    onSceneResize: Ext.emptyFn,
 
    /**
     * Whether or not the component got its first size.
     * Can be used in the `sceneresize` event handler to do user-defined setup on first
     * resize, for example:
     *
     *     listeners: {
     *         sceneresize: function (component, scene, rect) {
     *             if (!component.size) {
     *                 // set things up
     *             } else {
     *                 // handle resize
     *             }
     *         }
     *     }
     *
     * @cfg {Object} size 
     * @accessor
     */
 
    /**
     * Get the scene element as a D3 selection.
     * If the scene doesn't exist, it will be created.
     * @return {d3.selection}
     */
    getScene: function () {
        var me = this,
            padding = me.getWrapper(),
            scene = me.scene;
 
        if (!scene) {
            me.scene = scene = padding.append('g').classed(me.defaultCls.scene, true);
 
            me.setupScene(scene);
            me.fireEvent('scenesetup', me, scene);
        }
 
        return scene;
    },
 
    /**
     * @private
     */
    clearScene: function () {
        var me = this,
            scene = me.scene,
            defs = me.defs;
 
        if (scene) {
            scene = scene.node();
            scene.removeAttribute('transform');
            while (scene.firstChild) {
                scene.removeChild(scene.firstChild);
            }
        }
 
        if (defs) {
            defs = defs.node();
            while (defs.firstChild) {
                defs.removeChild(defs.firstChild);
            }
        }
    },
 
    showScene: function () {
        this.scene && this.scene.classed(this.defaultCls.hidden, false);
    },
 
    hideScene: function () {
        this.scene && this.scene.classed(this.defaultCls.hidden, true);
    },
 
    /**
     * @protected
     * Called once when the scene (main group) is created.
     * @param {d3.selection} scene The scene as a D3 selection.
     */
    setupScene: Ext.emptyFn,
 
    onPanZoom: function (interaction, scaling, translation) {
        // The order of transformations matters here. 
        this.scene.attr('transform',
            'translate(' + translation + ')scale(' + scaling + ')');
    },
 
    /**
     * @event scenesetup
     * Fires once after the scene has been created.
     * Note that at this time the component doesn't have a size yet.
     * @param {Ext.d3.svg.Svg} component
     * @param {d3.selection} scene
     */
 
    getWrapper: function () {
        var me = this,
            padding = me.wrapper;
 
        if (!padding) {
            padding = me.wrapper = me.getSvg().append('g').classed(me.defaultCls.wrapper, true);
        }
 
        return padding;
    },
 
    getWrapperClipRect: function () {
        var me = this,
            rect = me.wrapperClipRect;
 
        if (!rect) {
            rect = me.wrapperClipRect = me.getDefs()
                .append('clipPath').attr('id', me.wrapperClipId)
                .append('rect').attr('fill', 'white');
        }
 
        return rect;
    },
 
    updateClipScene: function (clipScene) {
        this.getWrapper().attr('clip-path', clipScene ? 'url(#' + this.wrapperClipId + ')' : '');
    },
 
    /**
     * SVG ['defs'](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/defs) element
     * as a D3 selection.
     * @return {d3.selection}
     */
    getDefs: function () {
        var defs = this.defs;
 
        if (!defs) {
            defs = this.defs = this.getSvg().append('defs');
        }
 
        return defs;
    },
 
    destroy: function () {
        this.getSvg().remove();
        this.callParent();
    }
 
});