 define(function(require) {

    var zrUtil = require('zrender/core/util');
    var graphic = require('../../util/graphic');
    var DataDiffer = require('../../data/DataDiffer');
    var helper = require('./helper');
    var Breadcrumb = require('./Breadcrumb');
    var RoamController = require('../../component/helper/RoamController');
    var BoundingRect = require('zrender/core/BoundingRect');
    var matrix = require('zrender/core/matrix');
    var animationUtil = require('../../util/animation');
    var bind = zrUtil.bind;
    var Group = graphic.Group;
    var Rect = graphic.Rect;
    var each = zrUtil.each;

    var DRAG_THRESHOLD = 3;
    var PATH_LABEL_NORMAL = ['label', 'normal'];
    var PATH_LABEL_EMPHASIS = ['label', 'emphasis'];
    var Z_BASE = 10; // Should bigger than every z.
    var Z_BG = 1;
    var Z_CONTENT = 2;

    return require('../../echarts').extendChartView({

        type: 'treemap',

        /**
         * @override
         */
        init: function (o, api) {

            /**
             * @private
             * @type {module:zrender/container/Group}
             */
            this._containerGroup;

            /**
             * @private
             * @type {Object.<string, Array.<module:zrender/container/Group>>}
             */
            this._storage = createStorage();

            /**
             * @private
             * @type {module:echarts/data/Tree}
             */
            this._oldTree;

            /**
             * @private
             * @type {module:echarts/chart/treemap/Breadcrumb}
             */
            this._breadcrumb;

            /**
             * @private
             * @type {module:echarts/component/helper/RoamController}
             */
            this._controller;

            /**
             * 'ready', 'animating'
             * @private
             */
            this._state = 'ready';

            /**
             * @private
             * @type {boolean}
             */
            this._mayClick;
        },

        /**
         * @override
         */
        render: function (seriesModel, ecModel, api, payload) {

            var models = ecModel.findComponents({
                mainType: 'series', subType: 'treemap', query: payload
            });
            if (zrUtil.indexOf(models, seriesModel) < 0) {
                return;
            }

            this.seriesModel = seriesModel;
            this.api = api;
            this.ecModel = ecModel;

            var targetInfo = helper.retrieveTargetInfo(payload, seriesModel);
            var payloadType = payload && payload.type;
            var layoutInfo = seriesModel.layoutInfo;
            var isInit = !this._oldTree;
            var thisStorage = this._storage;

            // Mark new root when action is treemapRootToNode.
            var reRoot = (payloadType === 'treemapRootToNode' && targetInfo && thisStorage)
                ? {
                    rootNodeGroup: thisStorage.nodeGroup[targetInfo.node.getRawIndex()],
                    direction: payload.direction
                }
                : null;

            var containerGroup = this._giveContainerGroup(layoutInfo);

            var renderResult = this._doRender(containerGroup, seriesModel, reRoot);
            (
                !isInit && (
                    !payloadType
                    || payloadType === 'treemapZoomToNode'
                    || payloadType === 'treemapRootToNode'
                )
            )
                ? this._doAnimation(containerGroup, renderResult, seriesModel, reRoot)
                : renderResult.renderFinally();

            this._resetController(api);

            this._renderBreadcrumb(seriesModel, api, targetInfo);
        },

        /**
         * @private
         */
        _giveContainerGroup: function (layoutInfo) {
            var containerGroup = this._containerGroup;
            if (!containerGroup) {
                // FIXME
                // 加一层containerGroup是为了clip，但是现在clip功能并没有实现。
                containerGroup = this._containerGroup = new Group();
                this._initEvents(containerGroup);
                this.group.add(containerGroup);
            }
            containerGroup.position = [layoutInfo.x, layoutInfo.y];

            return containerGroup;
        },

        /**
         * @private
         */
        _doRender: function (containerGroup, seriesModel, reRoot) {
            var thisTree = seriesModel.getData().tree;
            var oldTree = this._oldTree;

            // Clear last shape records.
            var lastsForAnimation = createStorage();
            var thisStorage = createStorage();
            var oldStorage = this._storage;
            var willInvisibleEls = [];
            var doRenderNode = zrUtil.curry(
                renderNode, seriesModel,
                thisStorage, oldStorage, reRoot,
                lastsForAnimation, willInvisibleEls
            );

            // Notice: when thisTree and oldTree are the same tree (see list.cloneShadow),
            // the oldTree is actually losted, so we can not find all of the old graphic
            // elements from tree. So we use this stragegy: make element storage, move
            // from old storage to new storage, clear old storage.

            dualTravel(
                thisTree.root ? [thisTree.root] : [],
                (oldTree && oldTree.root) ? [oldTree.root] : [],
                containerGroup,
                thisTree === oldTree || !oldTree,
                0
            );

            // Process all removing.
            var willDeleteEls = clearStorage(oldStorage);

            this._oldTree = thisTree;
            this._storage = thisStorage;

            return {
                lastsForAnimation: lastsForAnimation,
                willDeleteEls: willDeleteEls,
                renderFinally: renderFinally
            };

            function dualTravel(thisViewChildren, oldViewChildren, parentGroup, sameTree, depth) {
                // When 'render' is triggered by action,
                // 'this' and 'old' may be the same tree,
                // we use rawIndex in that case.
                if (sameTree) {
                    oldViewChildren = thisViewChildren;
                    each(thisViewChildren, function (child, index) {
                        !child.isRemoved() && processNode(index, index);
                    });
                }
                // Diff hierarchically (diff only in each subtree, but not whole).
                // because, consistency of view is important.
                else {
                    (new DataDiffer(oldViewChildren, thisViewChildren, getKey, getKey))
                        .add(processNode)
                        .update(processNode)
                        .remove(zrUtil.curry(processNode, null))
                        .execute();
                }

                function getKey(node) {
                    // Identify by name or raw index.
                    return node.getId();
                }

                function processNode(newIndex, oldIndex) {
                    var thisNode = newIndex != null ? thisViewChildren[newIndex] : null;
                    var oldNode = oldIndex != null ? oldViewChildren[oldIndex] : null;

                    var group = doRenderNode(thisNode, oldNode, parentGroup, depth);

                    group && dualTravel(
                        thisNode && thisNode.viewChildren || [],
                        oldNode && oldNode.viewChildren || [],
                        group,
                        sameTree,
                        depth + 1
                    );
                }
            }

            function clearStorage(storage) {
                var willDeleteEls = createStorage();
                storage && each(storage, function (store, storageName) {
                    var delEls = willDeleteEls[storageName];
                    each(store, function (el) {
                        el && (delEls.push(el), el.__tmWillDelete = 1);
                    });
                });
                return willDeleteEls;
            }

            function renderFinally() {
                each(willDeleteEls, function (els) {
                    each(els, function (el) {
                        el.parent && el.parent.remove(el);
                    });
                });
                each(willInvisibleEls, function (el) {
                    el.invisible = true;
                    // Setting invisible is for optimizing, so no need to set dirty,
                    // just mark as invisible.
                    el.dirty();
                });
            }
        },

        /**
         * @private
         */
        _doAnimation: function (containerGroup, renderResult, seriesModel, reRoot) {
            if (!seriesModel.get('animation')) {
                return;
            }

            var duration = seriesModel.get('animationDurationUpdate');
            var easing = seriesModel.get('animationEasing');
            var animationWrap = animationUtil.createWrap();

            // Make delete animations.
            each(renderResult.willDeleteEls, function (store, storageName) {
                each(store, function (el, rawIndex) {
                    if (el.invisible) {
                        return;
                    }

                    var parent = el.parent; // Always has parent, and parent is nodeGroup.
                    var target;

                    if (reRoot && reRoot.direction === 'drillDown') {
                        target = parent === reRoot.rootNodeGroup
                            // This is the content element of view root.
                            // Only `content` will enter this branch, because
                            // `background` and `nodeGroup` will not be deleted.
                            ? {
                                shape: {
                                    x: 0,
                                    y: 0,
                                    width: parent.__tmNodeWidth,
                                    height: parent.__tmNodeHeight
                                },
                                style: {
                                    opacity: 0
                                }
                            }
                            // Others.
                            : {style: {opacity: 0}};
                    }
                    else {
                        var targetX = 0;
                        var targetY = 0;

                        if (!parent.__tmWillDelete) {
                            // Let node animate to right-bottom corner, cooperating with fadeout,
                            // which is appropriate for user understanding.
                            // Divided by 2 for reRoot rolling up effect.
                            targetX = parent.__tmNodeWidth / 2;
                            targetY = parent.__tmNodeHeight / 2;
                        }

                        target = storageName === 'nodeGroup'
                            ? {position: [targetX, targetY], style: {opacity: 0}}
                            : {
                                shape: {x: targetX, y: targetY, width: 0, height: 0},
                                style: {opacity: 0}
                            };
                    }

                    target && animationWrap.add(el, target, duration, easing);
                });
            });

            // Make other animations
            each(this._storage, function (store, storageName) {
                each(store, function (el, rawIndex) {
                    var last = renderResult.lastsForAnimation[storageName][rawIndex];
                    var target = {};

                    if (!last) {
                        return;
                    }

                    if (storageName === 'nodeGroup') {
                        if (last.old) {
                            target.position = el.position.slice();
                            el.position = last.old;
                        }
                    }
                    else {
                        if (last.old) {
                            target.shape = zrUtil.extend({}, el.shape);
                            el.setShape(last.old);
                        }

                        if (last.fadein) {
                            el.setStyle('opacity', 0);
                            target.style = {opacity: 1};
                        }
                        // When animation is stopped for succedent animation starting,
                        // el.style.opacity might not be 1
                        else if (el.style.opacity !== 1) {
                            target.style = {opacity: 1};
                        }
                    }

                    animationWrap.add(el, target, duration, easing);
                });
            }, this);

            this._state = 'animating';

            animationWrap
                .done(bind(function () {
                    this._state = 'ready';
                    renderResult.renderFinally();
                }, this))
                .start();
        },

        /**
         * @private
         */
        _resetController: function (api) {
            var controller = this._controller;

            // Init controller.
            if (!controller) {
                controller = this._controller = new RoamController(api.getZr());
                controller.enable(this.seriesModel.get('roam'));
                controller.on('pan', bind(this._onPan, this));
                controller.on('zoom', bind(this._onZoom, this));
            }

            var rect = new BoundingRect(0, 0, api.getWidth(), api.getHeight());
            controller.rectProvider = function () {
                return rect;
            };
        },

        /**
         * @private
         */
        _clearController: function () {
            var controller = this._controller;
            if (controller) {
                controller.off('pan').off('zoom');
                controller = null;
            }
        },

        /**
         * @private
         */
        _onPan: function (dx, dy) {
            this._mayClick = false;

            if (this._state !== 'animating'
                && (Math.abs(dx) > DRAG_THRESHOLD || Math.abs(dy) > DRAG_THRESHOLD)
            ) {
                // These param must not be cached.
                var root = this.seriesModel.getData().tree.root;

                if (!root) {
                    return;
                }

                var rootLayout = root.getLayout();

                if (!rootLayout) {
                    return;
                }

                this.api.dispatchAction({
                    type: 'treemapMove',
                    from: this.uid,
                    seriesId: this.seriesModel.id,
                    rootRect: {
                        x: rootLayout.x + dx, y: rootLayout.y + dy,
                        width: rootLayout.width, height: rootLayout.height
                    }
                });
            }
        },

        /**
         * @private
         */
        _onZoom: function (scale, mouseX, mouseY) {
            this._mayClick = false;

            if (this._state !== 'animating') {
                // These param must not be cached.
                var root = this.seriesModel.getData().tree.root;

                if (!root) {
                    return;
                }

                var rootLayout = root.getLayout();

                if (!rootLayout) {
                    return;
                }

                var rect = new BoundingRect(
                    rootLayout.x, rootLayout.y, rootLayout.width, rootLayout.height
                );
                var layoutInfo = this.seriesModel.layoutInfo;

                // Transform mouse coord from global to containerGroup.
                mouseX -= layoutInfo.x;
                mouseY -= layoutInfo.y;

                // Scale root bounding rect.
                var m = matrix.create();
                matrix.translate(m, m, [-mouseX, -mouseY]);
                matrix.scale(m, m, [scale, scale]);
                matrix.translate(m, m, [mouseX, mouseY]);

                rect.applyTransform(m);

                this.api.dispatchAction({
                    type: 'treemapRender',
                    from: this.uid,
                    seriesId: this.seriesModel.id,
                    rootRect: {
                        x: rect.x, y: rect.y,
                        width: rect.width, height: rect.height
                    }
                });
            }
        },

        /**
         * @private
         */
        _initEvents: function (containerGroup) {
            // FIXME
            // 不用click以及silent的原因是，animate时视图设置silent true来避免click生效，
            // 但是animate中，按下鼠标，animate结束后（silent设回为false）松开鼠标，
            // 还是会触发click，期望是不触发。

            // Mousedown occurs when drag start, and mouseup occurs when drag end,
            // click event should not be triggered in that case.

            containerGroup.on('mousedown', function (e) {
                this._state === 'ready' && (this._mayClick = true);
            }, this);
            containerGroup.on('mouseup', function (e) {
                if (this._mayClick) {
                    this._mayClick = false;
                    this._state === 'ready' && onClick.call(this, e);
                }
            }, this);

            function onClick(e) {
                var nodeClick = this.seriesModel.get('nodeClick', true);

                if (!nodeClick) {
                    return;
                }

                var targetInfo = this.findTarget(e.offsetX, e.offsetY);

                if (!targetInfo) {
                    return;
                }

                var node = targetInfo.node;
                if (node.getLayout().isLeafRoot) {
                    this._rootToNode(targetInfo);
                }
                else {
                    if (nodeClick === 'zoomToNode') {
                        this._zoomToNode(targetInfo);
                    }
                    else if (nodeClick === 'link') {
                        var itemModel = node.hostTree.data.getItemModel(node.dataIndex);
                        var link = itemModel.get('link', true);
                        var linkTarget = itemModel.get('target', true) || 'blank';
                        link && window.open(link, linkTarget);
                    }
                }
            }
        },

        /**
         * @private
         */
        _renderBreadcrumb: function (seriesModel, api, targetInfo) {
            if (!targetInfo) {
                // Find breadcrumb tail on center of containerGroup.
                targetInfo = this.findTarget(api.getWidth() / 2, api.getHeight() / 2);

                if (!targetInfo) {
                    targetInfo = {node: seriesModel.getData().tree.root};
                }
            }

            (this._breadcrumb || (this._breadcrumb = new Breadcrumb(this.group, bind(onSelect, this))))
                .render(seriesModel, api, targetInfo.node);

            function onSelect(node) {
                if (this._state !== 'animating') {
                    helper.aboveViewRoot(seriesModel.getViewRoot(), node)
                        ? this._rootToNode({node: node})
                        : this._zoomToNode({node: node});
                }
            }
        },

        /**
         * @override
         */
        remove: function () {
            this._clearController();
            this._containerGroup && this._containerGroup.removeAll();
            this._storage = createStorage();
            this._state = 'ready';
            this._breadcrumb && this._breadcrumb.remove();
        },

        dispose: function () {
            this._clearController();
        },

        /**
         * @private
         */
        _zoomToNode: function (targetInfo) {
            this.api.dispatchAction({
                type: 'treemapZoomToNode',
                from: this.uid,
                seriesId: this.seriesModel.id,
                targetNode: targetInfo.node
            });
        },

        /**
         * @private
         */
        _rootToNode: function (targetInfo) {
            this.api.dispatchAction({
                type: 'treemapRootToNode',
                from: this.uid,
                seriesId: this.seriesModel.id,
                targetNode: targetInfo.node
            });
        },

        /**
         * @public
         * @param {number} x Global coord x.
         * @param {number} y Global coord y.
         * @return {Object} info If not found, return undefined;
         * @return {number} info.node Target node.
         * @return {number} info.offsetX x refer to target node.
         * @return {number} info.offsetY y refer to target node.
         */
        findTarget: function (x, y) {
            var targetInfo;
            var viewRoot = this.seriesModel.getViewRoot();

            viewRoot.eachNode({attr: 'viewChildren', order: 'preorder'}, function (node) {
                var bgEl = this._storage.background[node.getRawIndex()];
                // If invisible, there might be no element.
                if (bgEl) {
                    var point = bgEl.transformCoordToLocal(x, y);
                    var shape = bgEl.shape;

                    // For performance consideration, dont use 'getBoundingRect'.
                    if (shape.x <= point[0]
                        && point[0] <= shape.x + shape.width
                        && shape.y <= point[1]
                        && point[1] <= shape.y + shape.height
                    ) {
                        targetInfo = {node: node, offsetX: point[0], offsetY: point[1]};
                    }
                    else {
                        return false; // Suppress visit subtree.
                    }
                }
            }, this);

            return targetInfo;
        }

    });

    /**
     * @inner
     */
    function createStorage() {
        return {nodeGroup: [], background: [], content: []};
    }

    /**
     * @inner
     * @return Return undefined means do not travel further.
     */
    function renderNode(
        seriesModel, thisStorage, oldStorage, reRoot,
        lastsForAnimation, willInvisibleEls,
        thisNode, oldNode, parentGroup, depth
    ) {
        // Whether under viewRoot.
        if (!thisNode) {
            // Deleting nodes will be performed finally. This method just find
            // element from old storage, or create new element, set them to new
            // storage, and set styles.
            return;
        }

        var thisLayout = thisNode.getLayout();

        if (!thisLayout || !thisLayout.isInView) {
            return;
        }

        var thisWidth = thisLayout.width;
        var thisHeight = thisLayout.height;
        var thisInvisible = thisLayout.invisible;

        var thisRawIndex = thisNode.getRawIndex();
        var oldRawIndex = oldNode && oldNode.getRawIndex();

        // Node group
        var group = giveGraphic('nodeGroup', Group);

        if (!group) {
            return;
        }

        parentGroup.add(group);
        // x,y are not set when el is above view root.
        group.position = [thisLayout.x || 0, thisLayout.y || 0];
        group.__tmNodeWidth = thisWidth;
        group.__tmNodeHeight = thisHeight;

        if (thisLayout.isAboveViewRoot) {
            return group;
        }

        // Background
        var bg = giveGraphic('background', Rect, depth, Z_BG);
        if (bg) {
            bg.setShape({x: 0, y: 0, width: thisWidth, height: thisHeight});
            updateStyle(bg, function () {
                bg.setStyle('fill', thisNode.getVisual('borderColor', true));
            });
            group.add(bg);
        }

        var thisViewChildren = thisNode.viewChildren;

        // No children, render content.
        if (!thisViewChildren || !thisViewChildren.length) {
            var content = giveGraphic('content', Rect, depth, Z_CONTENT);
            content && renderContent(group);
        }

        return group;

        // ----------------------------
        // | Procedures in renderNode |
        // ----------------------------

        function renderContent(group) {
            // For tooltip.
            content.dataIndex = thisNode.dataIndex;
            content.seriesIndex = seriesModel.seriesIndex;

            var borderWidth = thisLayout.borderWidth;
            var contentWidth = Math.max(thisWidth - 2 * borderWidth, 0);
            var contentHeight = Math.max(thisHeight - 2 * borderWidth, 0);

            content.culling = true;
            content.setShape({
                x: borderWidth,
                y: borderWidth,
                width: contentWidth,
                height: contentHeight
            });

            var visualColor = thisNode.getVisual('color', true);
            updateStyle(content, function () {
                var normalStyle = {fill: visualColor};
                var emphasisStyle = thisNode.getModel('itemStyle.emphasis').getItemStyle();

                prepareText(normalStyle, emphasisStyle, visualColor, contentWidth, contentHeight);

                content.setStyle(normalStyle);
                graphic.setHoverStyle(content, emphasisStyle);
            });

            group.add(content);
        }

        function updateStyle(element, cb) {
            if (!thisInvisible) {
                // If invisible, do not set visual, otherwise the element will
                // change immediately before animation. We think it is OK to
                // remain its origin color when moving out of the view window.
                cb();

                if (!element.__tmWillVisible) {
                    element.invisible = false;
                }
            }
            else {
                // Delay invisible setting utill animation finished,
                // avoid element vanish suddenly before animation.
                !element.invisible && willInvisibleEls.push(element);
            }
        }

        function prepareText(normalStyle, emphasisStyle, visualColor, contentWidth, contentHeight) {
            var nodeModel = thisNode.getModel();
            var text = nodeModel.get('name');
            if (thisLayout.isLeafRoot) {
                var iconChar = seriesModel.get('drillDownIcon', true);
                text += iconChar ? '  ' + iconChar : '';
            }

            setText(
                text, normalStyle, nodeModel, PATH_LABEL_NORMAL,
                visualColor, contentWidth, contentHeight
            );
            setText(
                text, emphasisStyle, nodeModel, PATH_LABEL_EMPHASIS,
                visualColor, contentWidth, contentHeight
            );
        }

        function setText(text, style, nodeModel, labelPath, visualColor, contentWidth, contentHeight) {
            var labelModel = nodeModel.getModel(labelPath);
            var labelTextStyleModel = labelModel.getModel('textStyle');

            graphic.setText(style, labelModel, visualColor);

            // text.align and text.baseline is not included by graphic.setText,
            // because in most cases the two attributes are not exposed to user,
            // except in treemap.
            style.textAlign = labelTextStyleModel.get('align');
            style.textVerticalAlign = labelTextStyleModel.get('baseline');

            var textRect = labelTextStyleModel.getTextRect(text);
            if (!labelModel.getShallow('show') || textRect.height > contentHeight) {
                style.text = '';
            }
            else if (textRect.width > contentWidth) {
                style.text = labelTextStyleModel.get('ellipsis')
                    ? labelTextStyleModel.ellipsis(text, contentWidth) : '';
            }
            else {
                style.text = text;
            }
        }

        function giveGraphic(storageName, Ctor, depth, z) {
            var element = oldRawIndex != null && oldStorage[storageName][oldRawIndex];
            var lasts = lastsForAnimation[storageName];

            if (element) {
                // Remove from oldStorage
                oldStorage[storageName][oldRawIndex] = null;
                prepareAnimationWhenHasOld(lasts, element, storageName);
            }
            // If invisible and no old element, do not create new element (for optimizing).
            else if (!thisInvisible) {
                element = new Ctor({z: calculateZ(depth, z)});
                element.__tmDepth = depth;
                element.__tmStorageName = storageName;
                prepareAnimationWhenNoOld(lasts, element, storageName);
            }

            // Set to thisStorage
            return (thisStorage[storageName][thisRawIndex] = element);
        }

        function prepareAnimationWhenHasOld(lasts, element, storageName) {
            var lastCfg = lasts[thisRawIndex] = {};
            lastCfg.old = storageName === 'nodeGroup'
                ? element.position.slice()
                : zrUtil.extend({}, element.shape);
        }

        // If a element is new, we need to find the animation start point carefully,
        // otherwise it will looks strange when 'zoomToNode'.
        function prepareAnimationWhenNoOld(lasts, element, storageName) {
            var lastCfg = lasts[thisRawIndex] = {};
            var parentNode = thisNode.parentNode;

            if (parentNode && (!reRoot || reRoot.direction === 'drillDown')) {
                var parentOldX = 0;
                var parentOldY = 0;

                // New nodes appear from right-bottom corner in 'zoomToNode' animation.
                // For convenience, get old bounding rect from background.
                var parentOldBg = lastsForAnimation.background[parentNode.getRawIndex()];
                if (!reRoot && parentOldBg && parentOldBg.old) {
                    parentOldX = parentOldBg.old.width;
                    parentOldY = parentOldBg.old.height;
                }

                // When no parent old shape found, its parent is new too,
                // so we can just use {x:0, y:0}.
                lastCfg.old = storageName === 'nodeGroup'
                    ? [0, parentOldY]
                    : {x: parentOldX, y: parentOldY, width: 0, height: 0};
            }

            // Fade in, user can be aware that these nodes are new.
            lastCfg.fadein = storageName !== 'nodeGroup';
        }
    }

    // We can not set all backgroud with the same z, Because the behaviour of
    // drill down and roll up differ background creation sequence from tree
    // hierarchy sequence, which cause that lowser background element overlap
    // upper ones. So we calculate z based on depth.
    // Moreover, we try to shrink down z interval to [0, 1] to avoid that
    // treemap with large z overlaps other components.
    function calculateZ(depth, zInLevel) {
        var zb = depth * Z_BASE + zInLevel;
        return (zb - 1) / zb;
    }

});