define(function (require) {

    var zrUtil = require('zrender/core/util');
    var List = require('../../data/List');
    var formatUtil = require('../../util/format');
    var modelUtil = require('../../util/model');
    var numberUtil = require('../../util/number');

    var addCommas = formatUtil.addCommas;
    var encodeHTML = formatUtil.encodeHTML;

    var markerHelper = require('./markerHelper');

    var LineDraw = require('../../chart/helper/LineDraw');

    var markLineTransform = function (seriesModel, coordSys, mlModel, item) {
        var data = seriesModel.getData();
        // Special type markLine like 'min', 'max', 'average'
        var mlType = item.type;

        if (!zrUtil.isArray(item)
            && (
                mlType === 'min' || mlType === 'max' || mlType === 'average'
                // In case
                // data: [{
                //   yAxis: 10
                // }]
                || (item.xAxis != null || item.yAxis != null)
            )
        ) {
            var valueAxis;
            var valueDataDim;
            var value;

            if (item.yAxis != null || item.xAxis != null) {
                valueDataDim = item.yAxis != null ? 'y' : 'x';
                valueAxis = coordSys.getAxis(valueDataDim);

                value = zrUtil.retrieve(item.yAxis, item.xAxis);
            }
            else {
                var axisInfo = markerHelper.getAxisInfo(item, data, coordSys, seriesModel);
                valueDataDim = axisInfo.valueDataDim;
                valueAxis = axisInfo.valueAxis;
                value = markerHelper.numCalculate(data, valueDataDim, mlType);
            }
            var valueIndex = valueDataDim === 'x' ? 0 : 1;
            var baseIndex = 1 - valueIndex;

            var mlFrom = zrUtil.clone(item);
            var mlTo = {};

            mlFrom.type = null;

            mlFrom.coord = [];
            mlTo.coord = [];
            mlFrom.coord[baseIndex] = -Infinity;
            mlTo.coord[baseIndex] = Infinity;

            var precision = mlModel.get('precision');
            if (precision >= 0) {
                value = +value.toFixed(precision);
            }

            mlFrom.coord[valueIndex] = mlTo.coord[valueIndex] = value;

            item = [mlFrom, mlTo, { // Extra option for tooltip and label
                type: mlType,
                valueIndex: item.valueIndex,
                // Force to use the value of calculated value.
                value: value
            }];
        }

        item = [
            markerHelper.dataTransform(seriesModel, item[0]),
            markerHelper.dataTransform(seriesModel, item[1]),
            zrUtil.extend({}, item[2])
        ];

        // Avoid line data type is extended by from(to) data type
        item[2].type = item[2].type || '';

        // Merge from option and to option into line option
        zrUtil.merge(item[2], item[0]);
        zrUtil.merge(item[2], item[1]);

        return item;
    };

    function isInifinity(val) {
        return !isNaN(val) && !isFinite(val);
    }

    // If a markLine has one dim
    function ifMarkLineHasOnlyDim(dimIndex, fromCoord, toCoord, coordSys) {
        var otherDimIndex = 1 - dimIndex;
        var dimName = coordSys.dimensions[dimIndex];
        return isInifinity(fromCoord[otherDimIndex]) && isInifinity(toCoord[otherDimIndex])
            && fromCoord[dimIndex] === toCoord[dimIndex] && coordSys.getAxis(dimName).containData(fromCoord[dimIndex]);
    }

    function markLineFilter(coordSys, item) {
        if (coordSys.type === 'cartesian2d') {
            var fromCoord = item[0].coord;
            var toCoord = item[1].coord;
            // In case
            // {
            //  markLine: {
            //    data: [{ yAxis: 2 }]
            //  }
            // }
            if (
                fromCoord && toCoord &&
                (ifMarkLineHasOnlyDim(1, fromCoord, toCoord, coordSys)
                || ifMarkLineHasOnlyDim(0, fromCoord, toCoord, coordSys))
            ) {
                return true;
            }
        }
        return markerHelper.dataFilter(coordSys, item[0])
            && markerHelper.dataFilter(coordSys, item[1]);
    }

    function updateSingleMarkerEndLayout(
        data, idx, isFrom, mlType, valueIndex, seriesModel, api
    ) {
        var coordSys = seriesModel.coordinateSystem;
        var itemModel = data.getItemModel(idx);

        var point;
        var xPx = itemModel.get('x');
        var yPx = itemModel.get('y');
        if (xPx != null && yPx != null) {
            point = [
                numberUtil.parsePercent(xPx, api.getWidth()),
                numberUtil.parsePercent(yPx, api.getHeight())
            ];
        }
        else {
            // Chart like bar may have there own marker positioning logic
            if (seriesModel.getMarkerPosition) {
                // Use the getMarkerPoisition
                point = seriesModel.getMarkerPosition(
                    data.getValues(data.dimensions, idx)
                );
            }
            else {
                var dims = coordSys.dimensions;
                var x = data.get(dims[0], idx);
                var y = data.get(dims[1], idx);
                point = coordSys.dataToPoint([x, y]);
            }
            // Expand line to the edge of grid if value on one axis is Inifnity
            // In case
            //  markLine: {
            //    data: [{
            //      yAxis: 2
            //      // or
            //      type: 'average'
            //    }]
            //  }
            if (coordSys.type === 'cartesian2d') {
                var xAxis = coordSys.getAxis('x');
                var yAxis = coordSys.getAxis('y');
                var dims = coordSys.dimensions;
                if (isInifinity(data.get(dims[0], idx))) {
                    point[0] = xAxis.toGlobalCoord(xAxis.getExtent()[isFrom ? 0 : 1]);
                }
                else if (isInifinity(data.get(dims[1], idx))) {
                    point[1] = yAxis.toGlobalCoord(yAxis.getExtent()[isFrom ? 0 : 1]);
                }
            }
        }

        data.setItemLayout(idx, point);
    }

    var markLineFormatMixin = {
        formatTooltip: function (dataIndex) {
            var data = this._data;
            var value = this.getRawValue(dataIndex);
            var formattedValue = zrUtil.isArray(value)
                ? zrUtil.map(value, addCommas).join(', ') : addCommas(value);
            var name = data.getName(dataIndex);
            return this.name + '<br />'
                + ((name ? encodeHTML(name) + ' : ' : '') + formattedValue);
        },

        getData: function () {
            return this._data;
        },

        setData: function (data) {
            this._data = data;
        }
    };

    zrUtil.defaults(markLineFormatMixin, modelUtil.dataFormatMixin);

    require('../../echarts').extendComponentView({

        type: 'markLine',

        init: function () {
            /**
             * Markline grouped by series
             * @private
             * @type {Object}
             */
            this._markLineMap = {};
        },

        render: function (markLineModel, ecModel, api) {
            var lineDrawMap = this._markLineMap;
            for (var name in lineDrawMap) {
                lineDrawMap[name].__keep = false;
            }

            ecModel.eachSeries(function (seriesModel) {
                var mlModel = seriesModel.markLineModel;
                mlModel && this._renderSeriesML(seriesModel, mlModel, ecModel, api);
            }, this);

            for (var name in lineDrawMap) {
                if (!lineDrawMap[name].__keep) {
                    this.group.remove(lineDrawMap[name].group);
                }
            }
        },

        updateLayout: function (markLineModel, ecModel, api) {
            ecModel.eachSeries(function (seriesModel) {
                var mlModel = seriesModel.markLineModel;
                if (mlModel) {
                    var mlData = mlModel.getData();
                    var fromData = mlModel.__from;
                    var toData = mlModel.__to;
                    // Update visual and layout of from symbol and to symbol
                    fromData.each(function (idx) {
                        var lineModel = mlData.getItemModel(idx);
                        var mlType = lineModel.get('type');
                        var valueIndex = lineModel.get('valueIndex');
                        updateSingleMarkerEndLayout(fromData, idx, true, mlType, valueIndex, seriesModel, api);
                        updateSingleMarkerEndLayout(toData, idx, false, mlType, valueIndex, seriesModel, api);
                    });
                    // Update layout of line
                    mlData.each(function (idx) {
                        mlData.setItemLayout(idx, [
                            fromData.getItemLayout(idx),
                            toData.getItemLayout(idx)
                        ]);
                    });

                    this._markLineMap[seriesModel.name].updateLayout();
                }
            }, this);
        },

        _renderSeriesML: function (seriesModel, mlModel, ecModel, api) {
            var coordSys = seriesModel.coordinateSystem;
            var seriesName = seriesModel.name;
            var seriesData = seriesModel.getData();

            var lineDrawMap = this._markLineMap;
            var lineDraw = lineDrawMap[seriesName];
            if (!lineDraw) {
                lineDraw = lineDrawMap[seriesName] = new LineDraw();
            }
            this.group.add(lineDraw.group);

            var mlData = createList(coordSys, seriesModel, mlModel);

            var fromData = mlData.from;
            var toData = mlData.to;
            var lineData = mlData.line;

            mlModel.__from = fromData;
            mlModel.__to = toData;
            // Line data for tooltip and formatter
            zrUtil.extend(mlModel, markLineFormatMixin);
            mlModel.setData(lineData);

            var symbolType = mlModel.get('symbol');
            var symbolSize = mlModel.get('symbolSize');
            if (!zrUtil.isArray(symbolType)) {
                symbolType = [symbolType, symbolType];
            }
            if (typeof symbolSize === 'number') {
                symbolSize = [symbolSize, symbolSize];
            }

            // Update visual and layout of from symbol and to symbol
            mlData.from.each(function (idx) {
                var lineModel = lineData.getItemModel(idx);
                var mlType = lineModel.get('type');
                var valueIndex = lineModel.get('valueIndex');
                updateDataVisualAndLayout(fromData, idx, true, mlType, valueIndex);
                updateDataVisualAndLayout(toData, idx, false, mlType, valueIndex);
            });

            // Update visual and layout of line
            lineData.each(function (idx) {
                var lineColor = lineData.getItemModel(idx).get('lineStyle.normal.color');
                lineData.setItemVisual(idx, {
                    color: lineColor || fromData.getItemVisual(idx, 'color')
                });
                lineData.setItemLayout(idx, [
                    fromData.getItemLayout(idx),
                    toData.getItemLayout(idx)
                ]);

                lineData.setItemVisual(idx, {
                    'fromSymbolSize': fromData.getItemVisual(idx, 'symbolSize'),
                    'fromSymbol': fromData.getItemVisual(idx, 'symbol'),
                    'toSymbolSize': toData.getItemVisual(idx, 'symbolSize'),
                    'toSymbol': toData.getItemVisual(idx, 'symbol')
                });
            });

            lineDraw.updateData(lineData);

            // Set host model for tooltip
            // FIXME
            mlData.line.eachItemGraphicEl(function (el, idx) {
                el.traverse(function (child) {
                    child.dataModel = mlModel;
                });
            });

            function updateDataVisualAndLayout(data, idx, isFrom, mlType, valueIndex) {
                var itemModel = data.getItemModel(idx);

                updateSingleMarkerEndLayout(
                    data, idx, isFrom, mlType, valueIndex, seriesModel, api
                );

                data.setItemVisual(idx, {
                    symbolSize: itemModel.get('symbolSize') || symbolSize[isFrom ? 0 : 1],
                    symbol: itemModel.get('symbol', true) || symbolType[isFrom ? 0 : 1],
                    color: itemModel.get('itemStyle.normal.color') || seriesData.getVisual('color')
                });
            }

            lineDraw.__keep = true;
        }
    });

    /**
     * @inner
     * @param {module:echarts/coord/*} coordSys
     * @param {module:echarts/model/Series} seriesModel
     * @param {module:echarts/model/Model} mpModel
     */
    function createList(coordSys, seriesModel, mlModel) {

        var coordDimsInfos;
        if (coordSys) {
            coordDimsInfos = zrUtil.map(coordSys && coordSys.dimensions, function (coordDim) {
                var info = seriesModel.getData().getDimensionInfo(
                    seriesModel.coordDimToDataDim(coordDim)[0]
                ) || {}; // In map series data don't have lng and lat dimension. Fallback to same with coordSys
                info.name = coordDim;
                return info;
            });
        }
        else {
            coordDimsInfos =[{
                name: 'value',
                type: 'float'
            }];
        }

        var fromData = new List(coordDimsInfos, mlModel);
        var toData = new List(coordDimsInfos, mlModel);
        // No dimensions
        var lineData = new List([], mlModel);

        var optData = zrUtil.map(mlModel.get('data'), zrUtil.curry(
            markLineTransform, seriesModel, coordSys, mlModel
        ));
        if (coordSys) {
            optData = zrUtil.filter(
                optData, zrUtil.curry(markLineFilter, coordSys)
            );
        }
        var dimValueGetter = coordSys ? markerHelper.dimValueGetter : function (item) {
            return item.value;
        };
        fromData.initData(
            zrUtil.map(optData, function (item) { return item[0]; }),
            null, dimValueGetter
        );
        toData.initData(
            zrUtil.map(optData, function (item) { return item[1]; }),
            null, dimValueGetter
        );
        lineData.initData(
            zrUtil.map(optData, function (item) { return item[2]; })
        );
        return {
            from: fromData,
            to: toData,
            line: lineData
        };
    }
});