/**
 * 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.scaleOrdinal(d3.schemeCategory20c),
 *                     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',
        invisible: Ext.baseCSSPrefix + 'd3-invisible'
    },

    /**
     * @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.applyIf(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;
    },

    scaleRe: /scale\((.+),(.+)\)/i,

    alignContentTransitionName: null,

    alignMap: {
        'c': 'cc',
        't': 'ct',
        'b': 'cb',
        'l': 'lc',
        'r': 'rc',

        'lc': 'lc',
        'cl': 'lc',

        'rc': 'rc',
        'cr': 'rc',

        'ct': 'ct',
        'tc': 'ct',

        'cb': 'cb',
        'bc': 'cb',

        'rt': 'rt',
        'tr': 'rt',

        'rb': 'rb',
        'br': 'rb',

        'lt': 'lt',
        'tl': 'lt',

        'lb': 'lb',
        'bl': 'lb'
    },

    /**
     * Aligns rendered content in the scene.
     * @param {String} options Align options.
     * * c - center
     * * l - left
     * * r - right
     * * t - top
     * * b - bottom
     * Align options can be combined. E.g. 'tr' - top-right.
     * If the align options string is only one character long, the other component
     * is assumed to be 'c'. E.g. 'c' - center vertically and horizontally.
     * @param {d3.transition/String} [transition]
     * The transition to use (a D3 transition instance)
     * or create (a key in the {@link #transitions} map).
     */
    alignContent: function(options, transition) {
        var me = this,
            scale = '',
            sx = 1,
            sy = 1,
            sceneRect = me.getSceneRect(),
            contentRect = me.getContentRect(),
            scene = me.scene.interrupt(me.alignContentTransitionName),
            tx, ty, translation,
            transform, scaleMatch,
            xy, x, y;

        if (sceneRect && contentRect) {

            transform = scene.attr('transform');

            if (transform) {
                scaleMatch = transform.match(me.scaleRe);

                if (scaleMatch) {
                    scale = scaleMatch[0];
                    sx = scaleMatch[1];
                    sy = scaleMatch[2];
                }
            }

            xy = me.alignMap[options];

            if (!xy) {
                Ext.raise('Invalid content align options.');
            }

            x = xy[0];
            y = xy[1];

            switch (x) {
                case 'c':
                    tx = sceneRect.width / 2 - (contentRect.x + contentRect.width / 2) * sx;
                    break;
                case 'l':
                    tx = -contentRect.x * sx;
                    break;
                case 'r':
                    tx = sceneRect.width - (contentRect.x + contentRect.width) * sx;
                    break;
            }

            switch (y) {
                case 'c':
                    ty = sceneRect.height / 2 - (contentRect.y + contentRect.height / 2) * sy;
                    break;
                case 't':
                    ty = -contentRect.y * sy;
                    break;
                case 'b':
                    ty = sceneRect.height - (contentRect.y + contentRect.height) * sy;
                    break;
            }
        }

        if (Ext.isNumber(tx) && Ext.isNumber(ty)) {
            translation = [tx, ty];

            if (transition) {
                if (typeof transition === 'string') {
                    transition = me.createTransition(transition);
                }

                me.alignContentTransitionName = transition._name;
                scene = scene.transition(transition);
            }

            scene.attr('transform', 'translate(' + translation + ')' + scale);

            if (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() {
        var scene = this.scene;

        if (scene) {
            scene.classed(this.defaultCls.invisible, false);
        }
    },

    hideScene: function() {
        var scene = this.scene;

        if (scene) {
            scene.classed(this.defaultCls.invisible, 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 + ')');
    },

    removeInteraction: function(interaction) {
        if (interaction.isPanZoom) {
            this.scene.attr('transform', null);
        }

        this.callParent([interaction]);
    },

    /**
     * @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();
    }

});
