/**
 * The 'd3-sunburst' component visualizes tree nodes as donut sectors,
 * with the root circle in the center. The angle and area of each sector corresponds
 * to its {@link Ext.d3.hierarchy.Hierarchy#nodeValue value}. By default
 * the same value is returned for each node, meaning that siblings will span equal
 * angles and occupy equal area.
 *
 *     @example
 *     Ext.create('Ext.panel.Panel', {
 *         renderTo: Ext.getBody(),
 *         title: 'Sunburst Chart',
 *         height: 750,
 *         width: 750,
 *         layout: 'fit',
 *         items: [
 *             {
 *                 xtype: 'd3-sunburst',
 *                 padding: 20,
 *                 tooltip: {
 *                     renderer: function (component, tooltip, record) {
 *                         tooltip.setHtml(record.get('text'));
 *                     }
 *                 },
 *                 store: {
 *                     type: 'tree',
 *                     data: [
 *                         {
 *                             text: "Oscorp",
 *                             children: [
 *                                 {text: 'Norman Osborn'},
 *                                 {text: 'Harry Osborn'},
 *                                 {text: 'Arthur Stacy'}
 *                             ]
 *                         },
 *                         {
 *                             text: "SHIELD",
 *                             children: [
 *                                 {text: 'Nick Fury'},
 *                                 {text: 'Maria Hill'},
 *                                 {text: 'Tony Stark'}
 *                             ]
 *                         },
 *                         {
 *                             text: "Illuminati",
 *                             children: [
 *                                 {text: 'Namor'},
 *                                 {text: 'Tony Stark'},
 *                                 {text: 'Reed Richards'},
 *                                 {text: 'Black Bolt'},
 *                                 {text: 'Stephen Strange'},
 *                                 {text: 'Charles Xavier'}
 *                             ]
 *                         }
 *                     ]
 *                 }
 *             }
 *         ]
 *     });
 *
 */
Ext.define('Ext.d3.hierarchy.partition.Sunburst', {
    extend: 'Ext.d3.hierarchy.partition.Partition',
    xtype: 'd3-sunburst',
 
    config: {
        componentCls: 'sunburst',
 
        /**
         * The padding of a node's text inside its container.
         * @cfg {Array} textPadding 
         */
        textPadding: [5, '0.35em'],
 
        /**
         * The radius of the dot in the center of the sunburst that represents the parent node
         * of the currently visible node hierarchy and allows to zoom one level up by clicking
         * or tapping it.
         * @cfg {Number} [zoomParentDotRadius=30]
         */
        zoomParentDotRadius: 30,
 
        /**
         * The transition that happens when a node is zoomed
         * (see {@link #zoomInNode} for details):
         *
         * * `true` - for default transition
         * * `false` - no transition
         * * Object - user-defined transition is merged with default
         *
         * @cfg {Object/Boolean} [nodeZoomTransition=true]
         */
        nodeZoomTransition: true,
 
        /**
         * The transition that happens when a node is selected:
         *
         * * `true` - for default transition
         * * `false` - no transition
         * * Object - user-defined transition is merged with default
         *
         * @cfg {Object/Boolean} [nodeSelectTransition=true]
         */
        nodeSelectTransition: true
    },
 
    setupScene: function (scene) {
        this.callParent([scene]);
        this.setupScales();
        this.setupArcGenerator();
    },
 
    scaleDefaults: {
        x: {
            domain: [0, 1],
            range: [0, 2 * Math.PI]
        },
        y: {
            domain: [0, 1]
        }
    },
 
    transitionDefaults: {
        nodeZoom: {
            name: 'zoom',
            duration: 1000,
            ease: 'cubic-in-out'
        },
        nodeSelect: {
            name: 'select',
            duration: 150,
            ease: 'cubic-in-out',
            sourceScale: 1,
            targetScale: 1.07
        }
    },
 
    applyNodeZoomTransition: function (transition) {
        return this.transitionApplier(transition, 'nodeZoom');
    },
 
    applyNodeSelectTransition: function (transition) {
        return this.transitionApplier(transition, 'nodeSelect');
    },
 
    setupScales: function () {
        var d = this.scaleDefaults;
        // Node's x & dx properties will represent the angle. 
        // Node's y & dy properties will represent the area 
        // divided by π (since circle area = πr², we can treat 
        // the area as if it's been already divided by π and 
        // remove it from the right side of the equation as well, 
        // the relationship is still preserved, and we can simply 
        // take a square root of the area to get the radius). 
        this.xScale = d3.scale.linear()
            .domain(d.x.domain.slice())
            .range(d.x.range.slice());
 
        this.yScale = d3.scale.sqrt()
            .domain(d.y.domain.slice());
    },
 
    /**
     * [Arc generator](https://github.com/mbostock/d3/wiki/SVG-Shapes#arc)
     * for sunburst slices.
     * @private
     * @property {Function} arc 
     */
    arc: null,
 
    setupArcGenerator: function () {
        var me = this,
            x = me.xScale,
            y = me.yScale;
 
        me.arc = d3.svg.arc()
            .startAngle(function (node) {
                return Math.max(0, Math.min(2 * Math.PI, x(node.x)));
            })
            .endAngle(function (node) {
                return Math.max(0, Math.min(2 * Math.PI, x(node.x + node.dx)));
            })
            .innerRadius(function (node) {
                return Math.max(0, y(node.y));
            })
            .outerRadius(function (node) {
                return Math.max(0, y(node.y + node.dy));
            });
    },
 
    arcTween: function (node, index, value) {
        var me = this,
            interpolator = d3.interpolate({
                x: node.x0,
                y: node.y0,
                dx: node.dx0,
                dy: node.dy0
            }, {
                x: node.x,
                y: node.y,
                dx: node.dx,
                dy: node.dy
            });
 
        return function (t) {
            var value = interpolator(t);
            return me.arc(value);
        };
    },
 
    /**
     * @protected
     * Override parent method to neither set the layout size,
     * nor perform layout on scene resize.
     * The default layout size of 1x1 is used at all times.
     * Only the output range of scales changes.
     */
    onSceneResize: function (scene, rect) {
        var me = this,
            nodesGroup = me.nodesGroup,
            centerX = .5 * rect.width,
            centerY = .5 * rect.height,
            radius = Math.min(centerX, centerY);
 
        nodesGroup.attr('transform', 'translate(' + centerX + ',' + centerY + ')');
 
        me.setRadius(radius);
    },
 
    radius: null,
    minRadius: 1,
 
    setRadius: function (radius) {
        var me = this;
 
        radius = Math.max(me.minRadius, radius);
        me.radius = radius;
        me.yScale.range([0, radius]);
 
        me.renderScene();
    },
 
    /**
     * Zooms in the `node`, so that the sunburst only shows the node itself and its children.
     * To zoom in instantly, even when the {@link #nodeZoomTransition} config is truthy,
     * set the `transition` parameter to `true`.
     * @param {Ext.data.TreeModel} node
     * @param {Boolean} [instantly]
     */
    zoomInNode: function (node, instantly) {
        var me = this,
            scene = me.getScene(),
            transitionCfg = me.getNodeZoomTransition(),
            parentRadius = me.getZoomParentDotRadius(),
            radius = me.radius,
            xScale = me.xScale,
            yScale = me.yScale,
            arc = me.arc,
            transition,
            nodes;
 
        if (!(me.hasFirstLayout && me.hasFirstRender && me.size && node && node.isNode)) {
            return;
        }
 
        if (transitionCfg) {
            transition = scene
                .transition(transitionCfg.name)
                .duration(instantly ? 0 : transitionCfg.duration)
                .ease(transitionCfg.ease);
        } else {
            transition = scene.transition().duration(0);
        }
 
        nodes = transition.tween('scale', function () {
                // Default xScale and yScale domain is [0, 1]. 
                // Default xScale range is [0, 2π]. 
                // By reducing the xScale's domain to the span of selected slice, 
                // we make it occupy the whole pie angle. 
                // Similarly, by reducing the yScale's domain to an interval 
                // past slice's radius, we make that slice and its children 
                // occupy the whole pie radius. 
                // By making the yScale's range start with a non-zero value, 
                // we make a hole in the current subtree that now occupies 
                // the whole pie. Inside that hole the parent node (that now 
                // falls out of yScale's range) is going to be visible 
                // and available for selection to go one level up. 
                var xDomain = d3.interpolate(xScale.domain(), [node.x, node.x + node.dx]),
                    yDomain = d3.interpolate(yScale.domain(), [node.y, 1]),
                    yRange = d3.interpolate(yScale.range(), [node.y ? parentRadius : 0, radius]);
 
                return function (t) {
                    xScale.domain(xDomain(t));
                    yScale.domain(yDomain(t)).range(yRange(t));
                };
            })
            .selectAll('.' + me.defaultCls.node);
 
        nodes.selectAll('path')
            .attrTween('d', function (node) {
                return function (t) {
                    // Layout stays exactly the same, but scales 
                    // change slightly on every frame. 
                    return arc(node);
                };
            });
 
        nodes.selectAll('text')
            .call(me.positionTextFn.bind(me))
            .call(me.textVisibilityFn.bind(me));
 
        if (instantly) {
            me.xScale.domain([node.x, node.x + node.dx]);
            me.yScale.domain([node.y, 1]).range([node.y ? parentRadius : 0, radius]);
        }
    },
 
    onNodeSelect: function (node, el) {
        this.callParent(arguments);
 
        // Remove the fill given by the `colorAxis`, so that 
        // the CSS style can be used to specify the color 
        // of the selection. 
        el.select('path').style('fill', null);
        // Bring selected element to front. 
        el.each(function () {
            this.parentNode.appendChild(this);
        });
        this.nodeSelectTransitionFn(node, el);
    },
 
    /**
     * @private
     */
    nodeSelectTransitionFn: function (node, el) {
        var transitionCfg = this.getNodeSelectTransition();
 
        if (!transitionCfg) {
            return;
        }
 
        var duration = transitionCfg.duration,
            targetScale = transitionCfg.targetScale,
            sourceScale = transitionCfg.sourceScale;
 
        el
            .transition(transitionCfg.name)
            .duration(duration)
            .ease(transitionCfg.ease)
            .attr('transform', 'scale(' + targetScale +  ',' + targetScale + ')')
            .transition()
            .duration(duration)
            .attr('transform', 'scale(' + sourceScale +  ',' + sourceScale + ')');
    },
 
    onNodeDeselect: function (node, el) {
        var me = this,
            colorAxis = me.getColorAxis();
 
        me.callParent(arguments);
 
        // Restore the original color. 
        // (see 'onNodeSelect' comments). 
        el
            .select('path')
            .style('fill', function (node) {
                return colorAxis.getColor(node);
            });
    },
 
    /**
     * @private
     * Checks if a bounding box (e.g. of a text) fits inside a slice.
     * The bounding box is assumed to be centered in the middle of the slice
     * angularly, with the width of the box in the direction of the radius,
     * and left edge 'px' pixels from inner radius (r1).
     * @param {Object} bbox 
     * @param {Number} bbox.width
     * @param {Number} bbox.height
     * @param {Number} a1 Start angle in the [0, 2 * Math.PI] interval.
     * @param {Number} a2 End angle in the [0, 2 * Math.PI] interval.
     * @param {Number} r1 Inner radius.
     * @param {Number} r2 Outer radius.
     * @param {Number} px X-padding.
     * @param {Number} py Y-padding.
     * @returns {Boolean}
     */
    isBBoxInSlice: function (bbox, a1, a2, r1, r2, px, py) {
        var a = Math.abs(a2 - a1),
            width = Math.abs(r2 - r1) - px * 2,
            height = a < Math.PI
                ? 2 * (r1 + px) * Math.tan(0.5 * a) - py * 2
                : 0.5 * r2, // for very big angles text is never too tall, 
                            // so there must be some other limit 
            isWider = bbox.width > width,
            isTaller = bbox.height > height;
 
        return !(isWider || isTaller);
    },
 
    oldVal: true,
 
    /**
     * @private
     */
    textVisibilityFn: function (selection) {
        var me = this,
            x = me.xScale,
            y = me.yScale,
            textPadding = me.getTextPadding(),
            px = parseFloat(textPadding[0]),
            py = parseFloat(textPadding[1]),
            isTween = selection instanceof d3.transition,
            method = isTween ? 'attrTween' : 'attr',
            visibilityFn;
 
        function isHidden(el, node) {
            //<debug> 
            if (me.isDestroyed) {
                Ext.log.warn("Component is destroyed, shouldn't have executed this.");
            }
            //</debug> 
            var bbox = el.getBBox(), // SVG 'text' element 
                a1 = x(node.x),
                a2 = x(node.x + node.dx),
                r1 = y(node.y),
                r2 = y(node.y + node.dy),
                isBBoxInSlice = me.isBBoxInSlice(bbox, a1, a2, r1, r2, px, py),
                xDomain = x.domain(),
                yDomain = y.domain(),
                isOutOfX = xDomain[0] > node.x || xDomain[1] < (node.x + node.dx),
                isOutOfY = yDomain[0] > node.y || yDomain[1] < (node.y + node.dy);
 
            return !isBBoxInSlice || isOutOfX || isOutOfY;
        }
 
        function getVisibility(el, node) {
            return isHidden(el, node) ? 'hidden' : 'visible';
        }
 
        if (isTween) {
            visibilityFn = function (node) {
                var el = this;
                return function () {
                    return getVisibility(el, node);
                };
            };
        } else {
            visibilityFn = function (node) {
                return getVisibility(this, node);
            };
        }
 
        selection
            [method]('visibility', visibilityFn);
    },
 
    /**
     * @private
     * @param {d3.selection} selection 'text' elements.
     */
    positionTextFn: function (selection) {
        var x = this.xScale,
            y = this.yScale,
            halfPi = Math.PI / 2,
            degree = 180 / Math.PI,
            isTween = selection instanceof d3.transition,
            method = isTween ? 'attrTween' : 'attr',
            xFn, transformFn;
 
        function getX(node) {
            return y(node.y);
        }
        function getTransform(node) {
            return !node.isRoot() ? ('rotate(' + (x(node.x + node.dx / 2) - halfPi) * degree + ')') : '';
        }
 
        if (isTween) {
            xFn = function (node) {
                return function () {
                    return getX(node);
                };
            };
            transformFn = function (node) {
                return function () {
                    return getTransform(node);
                };
            };
        } else {
            xFn = function (node) {
                return getX(node);
            };
            transformFn = function (node) {
                return getTransform(node);
            };
        }
 
        selection
            [method]('x', xFn)
            [method]('transform', transformFn);
    },
 
    /**
     * @private
     * @param selection Selection of 'g' (node) elements.
     */
    saveNodeLayout: function (selection) {
        selection.each(function (node) {
            // Remember initial layout. 
            // This will be used to transition the node from old to new layout. 
            node.x0 = node.x;
            node.y0 = node.y;
            node.dx0 = node.dx;
            node.dy0 = node.dy;
        });
    },
 
    addNodes: function (selection) {
        var me = this,
            group = selection.append('g'),
            textPadding = me.getTextPadding(),
            colorAxis = me.getColorAxis(),
            nodeText = me.getNodeText();
 
        group
            .attr('class', me.defaultCls.node)
            .call(me.saveNodeLayout.bind(me))
            .call(me.onNodesAdd.bind(me));
 
        group
            .append('path')
            .attr('d', me.arc)
            .style('fill', function (node) {
                return colorAxis.getColor(node);
            });
 
        group
            .append('text')
            .attr('class', me.defaultCls.label)
            .attr('dx', textPadding[0])
            .attr('dy', textPadding[1])
            .text(function (node) {
                return nodeText(me, node);
            })
            .call(me.positionTextFn.bind(me))
            .call(me.textVisibilityFn.bind(me));
 
    },
 
    updateNodes: function (selection) {
        var me = this;
 
        selection
            .selectAll('path')
            .attr('d', me.arc);
 
        selection
            .selectAll('text')
            .call(me.positionTextFn.bind(me))
            .call(me.textVisibilityFn.bind(me));
    },
 
    updateColorAxis: function (colorAxis) {
        var me = this;
 
        if (!me.isConfiguring) {
            me.getRenderedNodes()
                .select('path')
                .style('fill', function (node) {
                    return colorAxis.getColor(node);
                });
        }
    }
 
});