/**
 * The 'd3-treemap' component uses D3's
 * [TreeMap Layout](https://github.com/d3/d3-hierarchy/#treemap)
 * to recursively subdivide area into rectangles, where the area of any node in the tree
 * corresponds to its value.
 *
 *     @example
 *     Ext.create('Ext.panel.Panel', {
 *         renderTo: Ext.getBody(),
 *         title: 'TreeMap Chart',
 *         height: 750,
 *         width: 750,
 *         layout: 'fit',
 *         items: [
 *             {
 *                 xtype: 'd3-treemap',
 *                 tooltip: {
 *                     renderer: function (component, tooltip, node) {
 *                         tooltip.setHtml(node.data.get('text'));
 *                     }
 *                 },
 *                 nodeValue: function (record) {
 *                     // The value in your data to derive the size of the tile from.
 *                     return record.get('value');
 *                 },
 *                 store: {
 *                     type: 'tree',
 *                     data: [
 *                         {  text: 'Hulk',
 *                            value : 5,
 *                            children: [
 *                                 { text: 'The Leader', value: 3 },
 *                                 { text: 'Abomination', value: 2 },
 *                                 { text: 'Sandman', value: 1 }
 *                             ]
 *                         },
 *                         {   text: 'Vision',
 *                             value : 4,
 *                             children: [
 *                                 { text: 'Kang', value: 4 },
 *                                 { text: 'Magneto', value: 3 },
 *                                 { text: 'Norman Osborn', value: 2 },
 *                                 { text: 'Anti-Vision', value: 1 }
 *                             ]
 *                         },
 *                         {   text: 'Ghost Rider',
 *                             value : 3,
 *                             children: [
 *                                 { text: 'Mephisto', value: 1 }
 *                             ]
 *                         },
 *                         {   text: 'Loki',
 *                             value : 2,
 *                             children: [
 *                                 { text: 'Captain America', value: 3 },
 *                                 { text: 'Deadpool', value: 4 },
 *                                 { text: 'Odin', value: 5 },
 *                                 { text: 'Scarlet Witch', value: 2 },
 *                                 { text: 'Silver Surfer', value: 1 }
 *                             ]
 *                         },
 *                         {   text: 'Daredevil',
 *                             value : 1,
 *                             children: [
 *                                 { text: 'Purple Man', value: 4 },
 *                                 { text: 'Kingpin', value: 3 },
 *                                 { text: 'Namor', value: 2 },
 *                                 { text: 'Sabretooth', value: 1 }
 *                             ]
 *                         }
 *                     ]
 *                 }
 *             }
 *         ]
 *     });
 *
 */
Ext.define('Ext.d3.hierarchy.TreeMap', {
    extend: 'Ext.d3.hierarchy.Hierarchy',
    xtype: 'd3-treemap',

    requires: [
        'Ext.d3.Helpers'
    ],

    config: {
        componentCls: 'treemap',

        /**
         * @cfg {Function} tiling
         * The [tiling method](https://github.com/d3/d3-hierarchy#treemap_tile) to use
         * with the `treemap` layout. For example:
         *
         *     tiling: 'd3.treemapBinary'
         *
         */
        tiling: null,

        /**
         * @cfg {Object} parentTile
         * Parent tile options.
         *
         * @cfg {Number} [parentTile.padding=4]
         * Determines the amount of extra space to reserve between
         * the parent and its children. Uniform on all sides, except the top
         * padding is calculated by the component itself depending on the height
         * of the tile's title.
         *
         * @cfg {Object} parentTile.label Parent tile label options.
         *
         * @cfg {Number} [parentTile.label.offset=[5, 2]]
         * The offset of the label from the top-left corner of the tile's rect.
         *
         * @cfg {Number[]} parentTile.label.clipSize
         * If the size of a parent node is smaller than this size, its label will be hidden.
         */
        parentTile: {
            padding: 4,
            label: {
                offset: [5, 2],
                clipSize: [110, 40]
            }
        },

        /**
         * @cfg {Object} leafTile
         * Leaf tile options.
         *
         * @cfg {Number} [leafTile.padding=0]
         * The [padding](https://github.com/d3/d3-hierarchy#treemap_paddingInner)
         * used to separate a node’s adjacent children.
         *
         * @cfg {Object} leafTile.label Child tile label options.
         *
         * @cfg {Number[]} [leafTile.label.offset] The offset of the label from the
         * top-left corner of the tile's rect. Defaults to `[5, 1]`.
         */
        leafTile: {
            padding: 0
        },

        nodeTransform: function(selection) {
            selection.attr('transform', function(node) {
                return 'translate(' + node.x0 + ',' + node.y0 + ')';
            });
        },

        /**
         * @cfg {String} busyLayoutText The text to show when the layout is in progress.
         */
        busyLayoutText: 'Layout in progress...',

        noParentValue: true,

        noSizeLayout: false,

        /**
         * @cfg {Boolean} scaleLabels
         * @since 6.5.0
         * If `true` the bigger tiles will have (more or less) proportionally bigger labels.
         */
        scaleLabels: false
    },

    /**
     * @private
     * @property
     */
    labelQuantizer: null,

    constructor: function(config) {
        this.labelQuantizer = this.createLabelQuantizer();

        this.callParent([config]);
    },

    /**
     * @private
     */
    createLabelQuantizer: function() {
        return d3.scaleQuantize().domain([8, 27]).range(['8px', '12px', '18px', '27px']);
    },

    /**
     * @private
     */
    labelSizer: function(node, element) {
        return Math.min((node.x1 - node.x0) / 4, (node.y1 - node.y0) / 2);
    },

    applyTiling: function(tiling, oldTiling) {
        return Ext.d3.Helpers.eval(tiling || oldTiling);
    },

    updateTiling: function(tiling) {
        if (tiling) {
            this.getLayout().tile(tiling);

            if (!this.isConfiguring) {
                this.performLayout();
            }
        }
    },

    updateLeafTile: function(leafTile) {
        if (leafTile) {
            this.getLayout().paddingInner(leafTile.padding || 0);
        }

        if (!this.isConfiguring) {
            this.performLayout();
        }
    },

    applyParentTile: function(parentTile) {
        var me = this,
            padding;

        if (parentTile) {
            padding = parentTile.padding;

            if (Ext.isNumber(padding)) {
                parentTile.padding = function(node) {
                    return !me.getRootVisible() && node === me.root ? 0 : padding;
                };
            }
        }

        return parentTile;
    },

    updateParentTile: function(parentTile) {
        var layout, padding;

        if (parentTile) {
            layout = this.getLayout();
            padding = parentTile.padding;

            layout.paddingRight(padding);
            layout.paddingBottom(padding);
            layout.paddingLeft(padding);
        }

        if (!this.isConfiguring) {
            this.performLayout();
        }
    },

    applyLayout: function() {
        return d3.treemap().round(true);
    },

    setLayoutSize: function(size) {
        this.callParent([size]);
    },

    deferredLayoutId: null,

    isLayoutBlocked: function(layout) {
        var me = this,
            maskText = me.getBusyLayoutText(),
            blocked = false;

        if (!me.deferredLayoutId) {
            me.showMask(maskText);
            // Let the mask render...
            // Note: small timeouts are not always enough to render the mask's DOM,
            //       100 seems to work every time everywhere.
            me.deferredLayoutId = setTimeout(me.performLayout.bind(me), 100);
            blocked = true;
        }
        else {
            clearTimeout(me.deferredLayoutId);
            me.deferredLayoutId = null;
        }

        return blocked;
    },

    setupScene: function(scene) {
        this.callParent([scene]);
        this.getScaleLabels(); // interested in side effects
    },

    onAfterRender: function() {
        this.hideMask();
    },

    /**
     * @private
     * A map of the {nodeId: Boolean} format,
     * where the Boolean value controls visibility of the label.
     */
    hiddenParentLabels: null,

    /**
     * @private
     * A map of the {nodeId: SVGRect} format,
     * where the SVGRect value is the bounding box of the label.
     */
    labelSizes: null,

    /**
     * Override superclass method here, because getting bbox of the scene won't always
     * produce the intended result: hidden text that sticks out of its container will
     * still be measured.
     */
    getContentRect: function() {
        var sceneRect = this.getSceneRect(),
            contentRect = this.contentRect || (this.contentRect = { x: 0, y: 0 });

        // The (x, y) in `contentRect` should be untranslated content position relative
        // to the scene's origin, which is expected to always be (0, 0) for TreeMap.
        // But the (x, y) in `sceneRect` are relative to component's origin:
        // (padding.left, padding.top).

        if (sceneRect) {
            contentRect.width = sceneRect.width;
            contentRect.height = sceneRect.height;
        }

        return sceneRect && contentRect;
    },

    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('rect').style('fill', null);
    },

    onNodeDeselect: function(node, el) {
        var me = this,
            colorAxis = me.getColorAxis();

        me.callParent(arguments);

        // Restore the original color.
        // (see 'onNodeSelect' comments).
        el
            .select('rect')
            .style('fill', function(node) {
                return colorAxis.getColor(node);
            });
    },

    renderNodes: function(nodeElements) {
        var me = this,
            root = me.root,
            layout = me.getLayout(),
            parentTile = me.getParentTile(),
            parentLabel = parentTile.label,
            nodeTransform = me.getNodeTransform(),
            hiddenParentLabels = me.hiddenParentLabels = {},
            labelSizes = me.labelSizes = {},
            nodes;

        // To show parent node's label, we need to make space for it during layout by adding
        // extra top padding to the node. But during the layout, the size of the label
        // is not known. We can determine label visibility based on the size of the node,
        // but that alone is not enough, as long nodes can have even longer labels.
        // Clipping labels instead of hiding them is not possible, because overflow is not
        // supported in SVG 1.1, and lack of proper nested SVGs support in IE prevents us
        // from clipping via wrapping labels with 'svg' elements.

        // So knowing the size of the label is crucial.
        // Measuring can only be done after a node is rendered, so we do it this way:

        // 1) first layout pass (in performLayout) has no padding
        // 2) render nodes (callParent down below)
        // 3) measure labels
        // 4) second layout pass (down below) calculates padding for each parent node
        // 5) adjust postion and size of nodes, and label visibility

        me.callParent([nodeElements]);

        layout.paddingTop(function(node) {
            // This will be called for parent nodes only.
            // Leaf node label visibility is determined in `textVisibilityFn`.
            var record = node.data,
                id = record.data.id,
                size = labelSizes[id],
                clipSize = parentLabel.clipSize,
                padding, dx, dy;

            if (!me.getRootVisible() && node === me.root) {
                // Hide the root node by removing its padding, so its children obscure it.
                padding = 0;
                hiddenParentLabels[id] = true;
            }
            else {
                padding = parentLabel.offset[1] * 2;
                dx = node.x1 - node.x0;
                dy = node.y1 - node.y0;

                if (size.width < dx && size.height < dy && dx > clipSize[0] && dy > clipSize[1]) {
                    padding += size.height;
                    hiddenParentLabels[id] = false;
                }
                else {
                    hiddenParentLabels[id] = true;
                }
            }

            return padding;
        });

        me.root = root = layout(root);

        nodes = me.nodes = root.descendants();

        layout.paddingTop(0);

        // Reapplying the data to the nodes that were just created in the 'callParent' above.
        nodeElements = me.getRenderedNodes().data(nodes, me.getNodeKey());
        // 'enter' and 'exit' selections are empty at this point.

        nodeElements
            .transition(me.layoutTransition)
            .call(nodeTransform);

        nodeElements
            .select('rect')
            .call(me.nodeSizeFn.bind(me));

        nodeElements
            .select('text')
            .call(me.textVisibilityFn.bind(me))
            .each(function(node) {
                if (node.data.isLeaf()) {
                    this.setAttribute('x', (node.x1 - node.x0) / 2);
                    this.setAttribute('y', (node.y1 - node.y0) / 2);
                }
                else {
                    this.setAttribute('x', parentLabel.offset[0]);
                    this.setAttribute('y', parentLabel.offset[1]);
                }
            });
    },

    addNodes: function(selection) {
        var me = this,
            labelSizes = me.labelSizes,
            colorAxis = me.getColorAxis(),
            nodeText = me.getNodeText(),
            scaleLabels = me.getScaleLabels(),
            cls = me.defaultCls;

        selection.append('rect')
            .style('fill', function(node) {
                return colorAxis.getColor(node);
            });

        selection.append('text')
            .style('font-size', function(node) {
                return scaleLabels ? me.getLabelSize(node, this) : null;
            })
            .each(function(node) {
                var text = nodeText(me, node);

                this.textContent = text == null ? '' : text;
                this.setAttribute('class', cls.label);

                Ext.d3.Helpers.fakeDominantBaseline(
                    this, node.data.isLeaf() ? 'central' : 'text-before-edge'
                );

                labelSizes[node.data.id] = this.getBBox();
            });
    },

    updateNodes: function(selection) {
        var me = this,
            nodeText = me.getNodeText(),
            scaleLabels = me.getScaleLabels(),
            labelSizes = me.labelSizes;

        selection.select('rect')
            .call(me.nodeSizeFn.bind(me));

        selection.select('text')
            .style('font-size', function(node) {
                return scaleLabels ? me.getLabelSize(node, this) : null;
            })
            .each(function(node) {
                var text = nodeText(me, node);

                this.textContent = text == null ? '' : text;
                labelSizes[node.data.id] = this.getBBox();
            });
    },

    /**
     * @private
     */
    getLabelSize: function(node, element) {
        if (node.data.isLeaf()) {
            // If too many different font sizes are used, the SVG rendering may slow down
            // significantly when scene's content is scaled (e.g. when zooming in/out
            // with the PanZoom interaction), so we quantize calculated font sizes.
            return this.labelQuantizer(this.labelSizer(node, element));
        }

        return null; // Parent node font size is set via CSS.
    },

    updateScaleLabels: function() {
        if (!this.isConfiguring) {
            this.performLayout();
        }
    },

    updateNodeValue: function(nodeValue) {
        this.callParent(arguments);

        // The parent method doesn't perform layout automatically, nor should it,
        // as for some hiearchy components merely re-rendering the scene will be
        // sufficient. For treemaps however, a layout is necessary to determine
        // label size and visibility.
        if (!this.isConfiguring) {
            this.performLayout();
        }
    },

    /**
     * @private
     */
    nodeSizeFn: function(selection) {
        selection
            .attr('width', function(node) {
                return Math.max(0, node.x1 - node.x0);
            })
            .attr('height', function(node) {
                return Math.max(0, node.y1 - node.y0);
            });
    },

    isLabelVisible: function(element, node) {
        var me = this,
            bbox = element.getBBox(),
            width = node.x1 - node.x0,
            height = node.y1 - node.y0,
            isLeaf = node.data.isLeaf(),
            parentTile = me.getParentTile(),
            hiddenParentLabels = me.hiddenParentLabels,
            parentLabelOffset = parentTile.label.offset,
            result;

        if (isLeaf) {
            // At least one pixel gap between the 'text' and 'rect' edges.
            width -= 2;
            height -= 2;
        }
        else {
            width -= parentLabelOffset[0] * 2;
            height -= parentLabelOffset[1] * 2;
        }

        if (isLeaf || !node.data.isExpanded()) {
            result = bbox.width < width && bbox.height < height;
        }
        else {
            result = !hiddenParentLabels[node.data.id];
        }

        return result;
    },

    /**
     * @private
     */
    textVisibilityFn: function(selection) {
        var me = this;

        selection.classed(me.defaultCls.invisible, function(node) {
            return !me.isLabelVisible(this, node);
        });
    },

    destroy: function() {
        var me = this,
            colorAxis = me.getColorAxis();

        if (me.deferredLayoutId) {
            clearTimeout(me.deferredLayoutId);
        }

        colorAxis.destroy();

        me.callParent();
    }
});
