define(function (require) {

    var layout = require('../../util/layout');
    var nest = require('../../util/array/nest');
    var zrUtil = require('zrender/core/util');

    return function (ecModel, api) {

        ecModel.eachSeriesByType('sankey', function (seriesModel) {

            var nodeWidth = seriesModel.get('nodeWidth');
            var nodeGap = seriesModel.get('nodeGap');

            var layoutInfo = getViewRect(seriesModel, api);

            seriesModel.layoutInfo = layoutInfo;

            var width = layoutInfo.width;
            var height = layoutInfo.height;

            var graph = seriesModel.getGraph();

            var nodes = graph.nodes;
            var edges = graph.edges;

            computeNodeValues(nodes);

            var filteredNodes = nodes.filter(function (node) {
                return node.getLayout().value === 0;
            });

            var iterations = filteredNodes.length !== 0
                ? 0 : seriesModel.get('layoutIterations');

            layoutSankey(nodes, edges, nodeWidth, nodeGap, width, height, iterations);
        });
    };

    /**
     * get the layout position of the whole view.
     */
    function getViewRect(seriesModel, api) {
        return layout.getLayoutRect(
            seriesModel.getBoxLayoutParams(), {
                width: api.getWidth(),
                height: api.getHeight()
            }
        );
    }

    function layoutSankey(nodes, edges, nodeWidth, nodeGap, width, height, iterations) {
        computeNodeBreadths(nodes, nodeWidth, width);
        computeNodeDepths(nodes, edges, height, nodeGap, iterations);
        computeEdgeDepths(nodes);
    }

    /**
     * compute the value of each node by summing the associated edge's value.
     * @param {module:echarts/data/Graph~Node} nodes
     */
    function computeNodeValues(nodes) {
        zrUtil.each(nodes, function (node) {
            var value1 = sum(node.outEdges, getEdgeValue);
            var value2 = sum(node.inEdges, getEdgeValue);
            var value = Math.max(value1, value2);
            node.setLayout({value: value}, true);
        });
    }

    /**
     * compute the x-position for each node.
     * @param {module:echarts/data/Graph~Node} nodes
     * @param  {number} nodeWidth
     * @param  {number} width
     */
    function computeNodeBreadths(nodes, nodeWidth, width) {
        var remainNodes = nodes;
        var nextNode = null;
        var x = 0;
        var kx = 0;

        while (remainNodes.length) {
            nextNode = [];

            for (var i = 0, len = remainNodes.length; i < len; i++) {
                var node = remainNodes[i];
                node.setLayout({x: x}, true);
                node.setLayout({dx: nodeWidth}, true);

                for (var j = 0, lenj = node.outEdges.length; j < lenj; j++) {
                    nextNode.push(node.outEdges[j].node2);
                }
            }
            remainNodes = nextNode;
            ++x;
        }

        moveSinksRight(nodes, x);
        kx = (width - nodeWidth) / (x - 1);

        scaleNodeBreadths(nodes, kx);
    }

    /**
     * all the node without outEgdes are assigned maximum breadth and
     * be aligned in the last column.
     * @param {module:echarts/data/Graph~Node} nodes
     * @param {number} x
     */
    function moveSinksRight(nodes, x) {
        zrUtil.each(nodes, function (node) {
            if(!node.outEdges.length) {
                node.setLayout({x: x-1}, true);
            }
        });
    }

    /**
     * scale node x-position to the width.
     * @param {module:echarts/data/Graph~Node} nodes
     * @param {number} kx
     */
    function scaleNodeBreadths(nodes, kx) {
        zrUtil.each(nodes, function(node) {
            var nodeX = node.getLayout().x * kx;
            node.setLayout({x: nodeX}, true);
        });
    }

    /**
     * using Gauss-Seidel iterations method to compute the node depth(y-position).
     * @param {module:echarts/data/Graph~Node} nodes
     * @param {module:echarts/data/Graph~Edge} edges
     * @param {number} height
     * @param {numbber} nodeGap
     * @param {number} iterations
     */
    function computeNodeDepths(nodes, edges, height, nodeGap, iterations) {
        var nodesByBreadth = nest()
            .key(function (d) {
                return d.getLayout().x;
            })
            .sortKeys(ascending)
            .entries(nodes)
            .map(function (d) {
                return d.values;
            });

        initializeNodeDepth(nodes, nodesByBreadth, edges, height, nodeGap);
        resolveCollisions(nodesByBreadth, nodeGap, height);

        for (var alpha = 1; iterations > 0; iterations--) {
            alpha *= 0.99;
            relaxRightToLeft(nodesByBreadth, alpha);
            resolveCollisions(nodesByBreadth, nodeGap, height);
            relaxLeftToRight(nodesByBreadth, alpha);
            resolveCollisions(nodesByBreadth, nodeGap, height);
        }
    }

    /**
     * compute the original y-position for each node.
     * @param {module:echarts/data/Graph~Node} nodes
     * @param {Array.<Array.<module:echarts/data/Graph~Node>>} nodesByBreadth
     * @param {module:echarts/data/Graph~Edge} edges
     * @param {number} height
     * @param {number} nodeGap
     */
    function initializeNodeDepth(nodes, nodesByBreadth, edges, height, nodeGap) {
        var kyArray = [];
        zrUtil.each(nodesByBreadth, function (nodes) {
            var n = nodes.length;
            var sum = 0;
            zrUtil.each(nodes, function (node) {
                sum += node.getLayout().value;
            });
            var ky = (height - (n-1) * nodeGap) / sum;
            kyArray.push(ky);
        });
        kyArray.sort(function (a, b) {
            return a - b;
        });
        var ky0 = kyArray[0];

        zrUtil.each(nodesByBreadth, function (nodes) {
            zrUtil.each(nodes, function (node, i) {
                node.setLayout({y: i}, true);
                var nodeDy = node.getLayout().value * ky0;
                node.setLayout({dy: nodeDy}, true);
            });
        });

        zrUtil.each(edges, function (edge) {
            var edgeDy = +edge.getValue() * ky0;
            edge.setLayout({dy: edgeDy}, true);
        });
    }

    /**
     * resolve the collision of initialized depth.
     * @param {Array.<Array.<module:echarts/data/Graph~Node>>} nodesByBreadth
     * @param {number} nodeGap
     * @param {number} height
     */
    function resolveCollisions(nodesByBreadth, nodeGap, height) {
        zrUtil.each(nodesByBreadth, function (nodes) {
            var node;
            var dy;
            var y0 = 0;
            var n = nodes.length;
            var i;

            nodes.sort(ascendingDepth);

            for (i = 0; i < n; i++) {
                node = nodes[i];
                dy = y0 - node.getLayout().y;
                if(dy > 0) {
                    var nodeY = node.getLayout().y + dy;
                    node.setLayout({y: nodeY}, true);
                }
                y0 = node.getLayout().y + node.getLayout().dy + nodeGap;
            }

            // if the bottommost node goes outside the biunds, push it back up
            dy = y0 - nodeGap - height;
            if (dy > 0) {
                var nodeY = node.getLayout().y -dy;
                node.setLayout({y: nodeY}, true);
                y0 = node.getLayout().y;
                for (i = n - 2; i >= 0; --i) {
                    node = nodes[i];
                    dy = node.getLayout().y + node.getLayout().dy + nodeGap - y0;
                    if (dy > 0) {
                        nodeY = node.getLayout().y - dy;
                        node.setLayout({y: nodeY}, true);
                    }
                    y0 = node.getLayout().y;
                }
            }
        });
    }

    /**
     * change the y-position of the nodes, except most the right side nodes.
     * @param {Array.<Array.<module:echarts/data/Graph~Node>>} nodesByBreadth
     * @param {number} alpha
     */
    function relaxRightToLeft(nodesByBreadth, alpha) {
        zrUtil.each(nodesByBreadth.slice().reverse(), function (nodes) {
            zrUtil.each(nodes, function (node) {
                if (node.outEdges.length) {
                    var y = sum(node.outEdges, weightedTarget) / sum(node.outEdges, getEdgeValue);
                    var nodeY = node.getLayout().y + (y - center(node)) * alpha;
                    node.setLayout({y: nodeY}, true);
                }
            });
        });
    }

    function weightedTarget(edge) {
        return center(edge.node2) * edge.getValue();
    }

    /**
     * change the y-position of the nodes, except most the left side nodes.
     * @param {Array.<Array.<module:echarts/data/Graph~Node>>} nodesByBreadth
     * @param {number} alpha
     */
    function relaxLeftToRight(nodesByBreadth, alpha) {
        zrUtil.each(nodesByBreadth, function (nodes) {
            zrUtil.each(nodes, function (node) {
                if (node.inEdges.length) {
                    var y = sum(node.inEdges, weightedSource) / sum(node.inEdges, getEdgeValue);
                    var nodeY = node.getLayout().y + (y - center(node)) * alpha;
                    node.setLayout({y: nodeY}, true);
                }
            });
        });
    }

    function weightedSource(edge) {
        return center(edge.node1) * edge.getValue();
    }

    /**
     * compute the depth(y-position) of each edge.
     * @param {module:echarts/data/Graph~Node} nodes
     */
    function computeEdgeDepths(nodes) {
        zrUtil.each(nodes, function (node) {
            node.outEdges.sort(ascendingTargetDepth);
            node.inEdges.sort(ascendingSourceDepth);
        });
        zrUtil.each(nodes, function (node) {
            var sy = 0;
            var ty = 0;
            zrUtil.each(node.outEdges, function (edge) {
                edge.setLayout({sy: sy}, true);
                sy += edge.getLayout().dy;
            });
            zrUtil.each(node.inEdges, function (edge) {
                edge.setLayout({ty: ty}, true);
                ty += edge.getLayout().dy;
            });
        });
    }

    function ascendingTargetDepth(a, b) {
        return a.node2.getLayout().y - b.node2.getLayout().y;
    }

    function ascendingSourceDepth(a, b) {
        return a.node1.getLayout().y - b.node1.getLayout().y;
    }

    function sum(array, f) {
        var s = 0;
        var n = array.length;
        var a;
        var i = -1;
        if (arguments.length === 1) {
            while (++i < n) {
                a = +array[i];
                if (!isNaN(a)) {
                    s += a;
                }
            }
        }
        else {
            while (++i < n) {
                a = +f.call(array, array[i], i);
                if(!isNaN(a)) {
                    s += a;
                }
            }
        }
        return s;
    }

    function center(node) {
        return node.getLayout().y + node.getLayout().dy / 2;
    }

    function ascendingDepth(a, b) {
        return a.getLayout().y - b.getLayout().y;
    }

    function ascending(a, b) {
        return a < b ? -1 : a > b ? 1 : a == b ? 0 : NaN;
    }

    function getEdgeValue(edge) {
        return edge.getValue();
    }

});
