/**
 * Box selection tool.
 *
 * @module echarts/component/helper/BrushController
 */

define(function (require) {

    var Eventful = require('zrender/mixin/Eventful');
    var zrUtil = require('zrender/core/util');
    var graphic = require('../../util/graphic');
    var interactionMutex = require('./interactionMutex');
    var DataDiffer = require('../../data/DataDiffer');

    var curry = zrUtil.curry;
    var each = zrUtil.each;
    var map = zrUtil.map;
    var mathMin = Math.min;
    var mathMax = Math.max;
    var mathPow = Math.pow;

    var COVER_Z = 10000;
    var UNSELECT_THRESHOLD = 6;
    var MIN_RESIZE_LINE_WIDTH = 6;
    var MUTEX_RESOURCE_KEY = 'globalPan';

    var DIRECTION_MAP = {
        w: [0, 0],
        e: [0, 1],
        n: [1, 0],
        s: [1, 1]
    };
    var CURSOR_MAP = {
        w: 'ew',
        e: 'ew',
        n: 'ns',
        s: 'ns',
        ne: 'nesw',
        sw: 'nesw',
        nw: 'nwse',
        se: 'nwse'
    };
    var DEFAULT_BRUSH_OPT = {
        brushStyle: {
            lineWidth: 2,
            stroke: 'rgba(0,0,0,0.3)',
            fill: 'rgba(0,0,0,0.1)'
        },
        transformable: true,
        brushMode: 'single',
        removeOnClick: false
    };

    var baseUID = 0;

    /**
     * @alias module:echarts/component/helper/BrushController
     * @constructor
     * @mixin {module:zrender/mixin/Eventful}
     * @event module:echarts/component/helper/BrushController#brush
     *        params:
     *            areas: Array.<Array>, coord relates to container group,
     *                                    If no container specified, to global.
     *            opt {
     *                isEnd: boolean,
     *                removeOnClick: boolean
     *            }
     *
     * @param {module:zrender/zrender~ZRender} zr
     */
    function BrushController(zr) {

        if (__DEV__) {
            zrUtil.assert(zr);
        }

        Eventful.call(this);

        /**
         * @type {module:zrender/zrender~ZRender}
         * @private
         */
        this._zr = zr;

        /**
         * @type {module:zrender/container/Group}
         * @readOnly
         */
        this.group = new graphic.Group();

        /**
         * Only for drawing (after enabledBrush).
         * @private
         * @type {string}
         */
        this._brushType;

        /**
         * Only for drawing (after enabledBrush).
         * @private
         * @type {Object}
         */
        this._brushOption;

        /**
         * @private
         * @type {Object}
         */
        this._panels;

        /**
         * @private
         * @type {Array.<nubmer>}
         */
        this._track = [];

        /**
         * @private
         * @type {boolean}
         */
        this._dragging;

        /**
         * @private
         * @type {Array}
         */
        this._covers = [];

        /**
         * @private
         * @type {moudule:zrender/container/Group}
         */
        this._creatingCover;

        /**
         * true means global panel
         * @private
         * @type {module:zrender/container/Group|boolean}
         */
        this._creatingPanel;

        /**
         * @private
         * @type {boolean}
         */
        this._enableGlobalPan;

        /**
         * @private
         * @type {boolean}
         */
        if (__DEV__) {
            this._mounted;
        }

        /**
         * @private
         * @type {string}
         */
        this._uid = 'brushController_' + baseUID++;

        /**
         * @private
         * @type {Object}
         */
        this._handlers = {};
        each(mouseHandlers, function (handler, eventName) {
            this._handlers[eventName] = zrUtil.bind(handler, this);
        }, this);
    }

    BrushController.prototype = {

        constructor: BrushController,

        /**
         * If set to null/undefined/false, select disabled.
         * @param {Object} brushOption
         * @param {string|boolean} brushOption.brushType 'line', 'rect', 'polygon' or false
         *                          If pass false/null/undefined, disable brush.
         * @param {number} [brushOption.brushMode='single'] 'single' or 'multiple'
         * @param {boolean} [brushOption.transformable=true]
         * @param {boolean} [brushOption.removeOnClick=false]
         * @param {Object} [brushOption.brushStyle]
         * @param {number} [brushOption.brushStyle.width]
         * @param {number} [brushOption.brushStyle.lineWidth]
         * @param {string} [brushOption.brushStyle.stroke]
         * @param {string} [brushOption.brushStyle.fill]
         */
        enableBrush: function (brushOption) {
            if (__DEV__) {
                zrUtil.assert(this._mounted);
            }

            this._brushType && doDisableBrush(this);
            brushOption.brushType && doEnableBrush(this, brushOption);

            return this;
        },

        /**
         * @param {Array.<Object>} panelOpts If not pass, it is global brush.
         *        Each items: {panelId, rect}
         */
        setPanels: function (panelOpts) {
            var oldPanels = this._panels || {};
            var newPanels = this._panels = panelOpts && panelOpts.length && {};
            var thisGroup = this.group;

            newPanels && each(panelOpts, function (panelOpt) {
                var panelId = panelOpt.panelId;
                var panel = oldPanels[panelId];
                if (!panel) {
                    panel = new graphic.Rect({
                        silent: true,
                        invisible: true
                    });
                    thisGroup.add(panel);
                }
                panel.attr('shape', panelOpt.rect);
                panel.__brushPanelId = panelId;
                newPanels[panelId] = panel;
                oldPanels[panelId] = null;
            });

            each(oldPanels, function (panel) {
                panel && thisGroup.remove(panel);
            });

            return this;
        },

        /**
         * @param {Object} [opt]
         * @return {boolean} [opt.enableGlobalPan=false]
         * @return {boolean} [opt.position=[0, 0]]
         * @return {boolean} [opt.rotation=0]
         * @return {boolean} [opt.scale=[1, 1]]
         */
        mount: function (opt) {
            opt = opt || {};

            if (__DEV__) {
                this._mounted = true; // should be at first.
            }

            this._enableGlobalPan = opt.enableGlobalPan;

            var thisGroup = this.group;
            this._zr.add(thisGroup);

            thisGroup.attr({
                position: opt.position || [0, 0],
                rotation: opt.rotation || 0,
                scale: opt.scale || [1, 1]
            });

            return this;
        },

        eachCover: function (cb, context) {
            each(this._covers, cb, context);
        },

        /**
         * Update covers.
         * @param {Array.<Object>} brushOptionList Like:
         *        [
         *            {id: 'xx', brushType: 'line', range: [23, 44], brushStyle, transformable},
         *            {id: 'yy', brushType: 'rect', range: [[23, 44], [23, 54]]},
         *            ...
         *        ]
         *        `brushType` is required in each cover info.
         *        `id` is not mandatory.
         *        `brushStyle`, `transformable` is not mandatory, use DEFAULT_BRUSH_OPT by default.
         *        If brushOptionList is null/undefined, all covers removed.
         */
        updateCovers: function (brushOptionList) {
            if (__DEV__) {
                zrUtil.assert(this._mounted);
            }

            brushOptionList = zrUtil.map(brushOptionList, function (brushOption) {
                return zrUtil.merge(zrUtil.clone(DEFAULT_BRUSH_OPT), brushOption, true);
            });

            var tmpIdPrefix = '\0-brush-index-';
            var oldCovers = this._covers;
            var newCovers = this._covers = [];
            var controller = this;
            var creatingCover = this._creatingCover;

            (new DataDiffer(oldCovers, brushOptionList, oldGetKey, getKey))
                .add(addOrUpdate)
                .update(addOrUpdate)
                .remove(remove)
                .execute();

            return this;

            function getKey(brushOption, index) {
                return (brushOption.id != null ? brushOption.id : tmpIdPrefix + index)
                    + '-' + brushOption.brushType;
            }

            function oldGetKey(cover, index) {
                return getKey(cover.__brushOption, index);
            }

            function addOrUpdate(newIndex, oldIndex) {
                var newBrushOption = brushOptionList[newIndex];
                // Consider setOption in event listener of brushSelect,
                // where updating cover when creating should be forbiden.
                if (oldIndex != null && oldCovers[oldIndex] === creatingCover) {
                    newCovers[newIndex] = oldCovers[oldIndex];
                }
                else {
                    var cover = newCovers[newIndex] = oldIndex != null
                        ? (
                            oldCovers[oldIndex].__brushOption = newBrushOption,
                            oldCovers[oldIndex]
                        )
                        : endCreating(controller, createCover(controller, newBrushOption));
                    updateCoverAfterCreation(controller, cover);
                }
            }

            function remove(oldIndex) {
                if (oldCovers[oldIndex] !== creatingCover) {
                    controller.group.remove(oldCovers[oldIndex]);
                }
            }
        },

        unmount: function () {
            this.enableBrush(false);

            // container may 'removeAll' outside.
            clearCovers(this);
            this._zr.remove(this.group);

            if (__DEV__) {
                this._mounted = false; // should be at last.
            }

            return this;
        },

        dispose: function () {
            this.unmount();
            this.off();
        }
    };

    zrUtil.mixin(BrushController, Eventful);


    function doEnableBrush(controller, brushOption) {
        var zr = controller._zr;

        // Consider roam, which takes globalPan too.
        if (!controller._enableGlobalPan) {
            interactionMutex.take(zr, MUTEX_RESOURCE_KEY, controller._uid);
        }

        each(controller._handlers, function (handler, eventName) {
            zr.on(eventName, handler);
        });

        controller._brushType = brushOption.brushType;
        controller._brushOption = zrUtil.merge(zrUtil.clone(DEFAULT_BRUSH_OPT), brushOption, true);
    }

    function doDisableBrush(controller) {
        var zr = controller._zr;

        interactionMutex.release(zr, MUTEX_RESOURCE_KEY, controller._uid);

        each(controller._handlers, function (handler, eventName) {
            zr.off(eventName, handler);
        });

        controller._brushType = controller._brushOption = null;
    }

    function createCover(controller, brushOption) {
        var cover = coverRenderers[brushOption.brushType].createCover(controller, brushOption);
        updateZ(cover);
        cover.__brushOption = brushOption;
        controller.group.add(cover);
        return cover;
    }

    function endCreating(controller, creatingCover) {
        var coverRenderer = getCoverRenderer(creatingCover);
        if (coverRenderer.endCreating) {
            coverRenderer.endCreating(controller, creatingCover);
            updateZ(creatingCover);
        }
        return creatingCover;
    }

    function updateCoverShape(controller, cover) {
        var brushOption = cover.__brushOption;
        getCoverRenderer(cover).updateCoverShape(
            controller, cover, brushOption.range, brushOption
        );
    }

    function updateZ(group) {
        group.traverse(function (el) {
            el.z = COVER_Z;
            el.z2 = COVER_Z; // Consider in given container.
        });
    }

    function updateCoverAfterCreation(controller, cover) {
        getCoverRenderer(cover).updateCommon(controller, cover);
        updateCoverShape(controller, cover);
    }

    function getCoverRenderer(cover) {
        return coverRenderers[cover.__brushOption.brushType];
    }

    function getPanelByPoint(controller, x, y) {
        var panels = controller._panels;
        if (!panels) {
            return true; // Global panel
        }
        var panel;
        each(panels, function (pn) {
            pn.contain(x, y) && (panel = pn);
        });
        return panel;
    }

    function getPanelByCover(controller, cover) {
        var panels = controller._panels;
        if (!panels) {
            return true; // Global panel
        }
        var panelId = cover.__brushOption.panelId;
        // User may give cover without coord sys info,
        // which is then treated as global panel.
        return panelId != null ? panels[panelId] : true;
    }

    function clearCovers(controller) {
        var covers = controller._covers;
        var originalLength = covers.length;
        each(covers, function (cover) {
            controller.group.remove(cover);
        }, controller);
        covers.length = 0;

        return !!originalLength;
    }

    function trigger(controller, opt) {
        var areas = map(controller._covers, function (cover) {
            var brushOption = cover.__brushOption;
            var range = zrUtil.clone(brushOption.range);

            return {
                brushType: brushOption.brushType,
                panelId: brushOption.panelId,
                range: range
            };
        });

        controller.trigger('brush', areas, {
            isEnd: !!opt.isEnd,
            removeOnClick: !!opt.removeOnClick
        });
    }

    function shouldShowCover(controller) {
        var track = controller._track;

        if (!track.length) {
            return false;
        }

        var p2 = track[track.length - 1];
        var p1 = track[0];
        var dx = p2[0] - p1[0];
        var dy = p2[1] - p1[1];
        var dist = mathPow(dx * dx + dy * dy, 0.5);

        return dist > UNSELECT_THRESHOLD;
    }

    function getTrackEnds(track) {
        var tail = track.length - 1;
        tail < 0 && (tail = 0);
        return [track[0], track[tail]];
    }

    function createBaseRectCover(doDrift, controller, brushOption, edgeNames) {
        var cover = new graphic.Group();

        cover.add(new graphic.Rect({
            name: 'main',
            style: makeStyle(brushOption),
            silent: true,
            draggable: true,
            cursor: 'move',
            drift: curry(doDrift, controller, cover, 'nswe'),
            ondragend: curry(trigger, controller, {isEnd: true})
        }));

        each(
            edgeNames,
            function (name) {
                cover.add(new graphic.Rect({
                    name: name,
                    style: {opacity: 0},
                    draggable: true,
                    silent: true,
                    invisible: true,
                    drift: curry(doDrift, controller, cover, name),
                    ondragend: curry(trigger, controller, {isEnd: true})
                }));
            }
        );

        return cover;
    }

    function updateBaseRect(controller, cover, localRange, brushOption) {
        var lineWidth = brushOption.brushStyle.lineWidth || 0;
        var handleSize = mathMax(lineWidth, MIN_RESIZE_LINE_WIDTH);
        var x = localRange[0][0];
        var y = localRange[1][0];
        var xa = x - lineWidth / 2;
        var ya = y - lineWidth / 2;
        var x2 = localRange[0][1];
        var y2 = localRange[1][1];
        var x2a = x2 - handleSize + lineWidth / 2;
        var y2a = y2 - handleSize + lineWidth / 2;
        var width = x2 - x;
        var height = y2 - y;
        var widtha = width + lineWidth;
        var heighta = height + lineWidth;

        updateRectShape(controller, cover, 'main', x, y, width, height);

        if (brushOption.transformable) {
            updateRectShape(controller, cover, 'w', xa, ya, handleSize, heighta);
            updateRectShape(controller, cover, 'e', x2a, ya, handleSize, heighta);
            updateRectShape(controller, cover, 'n', xa, ya, widtha, handleSize);
            updateRectShape(controller, cover, 's', xa, y2a, widtha, handleSize);

            updateRectShape(controller, cover, 'nw', xa, ya, handleSize, handleSize);
            updateRectShape(controller, cover, 'ne', x2a, ya, handleSize, handleSize);
            updateRectShape(controller, cover, 'sw', xa, y2a, handleSize, handleSize);
            updateRectShape(controller, cover, 'se', x2a, y2a, handleSize, handleSize);
        }
    }

    function updateCommon(controller, cover) {
        var brushOption = cover.__brushOption;
        var transformable = brushOption.transformable;

        var mainEl = cover.childAt(0);
        mainEl.useStyle(makeStyle(brushOption));
        mainEl.attr({
            silent: !transformable,
            cursor: transformable ? 'move' : 'default'
        });

        each(
            ['w', 'e', 'n', 's', 'se', 'sw', 'ne', 'nw'],
            function (name) {
                var el = cover.childOfName(name);
                var globalDir = getGlobalDirection(controller, name);

                el && el.attr({
                    silent: !transformable,
                    invisible: !transformable,
                    cursor: transformable ? CURSOR_MAP[globalDir] + '-resize' : null
                });
            }
        );
    }

    function updateRectShape(controller, cover, name, x, y, w, h) {
        var el = cover.childOfName(name);
        el && el.setShape(pointsToRect(
            clipByPanel(controller, cover, [[x, y], [x + w, y + h]])
        ));
    }

    function makeStyle(brushOption) {
        return zrUtil.defaults({strokeNoScale: true}, brushOption.brushStyle);
    }

    function formatRectRange(x, y, x2, y2) {
        var min = [mathMin(x, x2), mathMin(y, y2)];
        var max = [mathMax(x, x2), mathMax(y, y2)];

        return [
            [min[0], max[0]], // x range
            [min[1], max[1]] // y range
        ];
    }

    function getTransform(controller) {
        return graphic.getTransform(controller.group);
    }

    function getGlobalDirection(controller, localDirection) {
        if (localDirection.length > 1) {
            localDirection = localDirection.split('');
            var globalDir = [
                getGlobalDirection(controller, localDirection[0]),
                getGlobalDirection(controller, localDirection[1])
            ];
            (globalDir[0] === 'e' || globalDir[0] === 'w') && globalDir.reverse();
            return globalDir.join('');
        }
        else {
            var map = {w: 'left', e: 'right', n: 'top', s: 'bottom'};
            var inverseMap = {left: 'w', right: 'e', top: 'n', bottom: 's'};
            var globalDir = graphic.transformDirection(
                map[localDirection], getTransform(controller)
            );
            return inverseMap[globalDir];
        }
    }

    function driftRect(toRectRange, fromRectRange, controller, cover, name, dx, dy, e) {
        var brushOption = cover.__brushOption;
        var rectRange = toRectRange(brushOption.range);
        var localDelta = toLocalDelta(controller, dx, dy);

        each(name.split(''), function (namePart) {
            var ind = DIRECTION_MAP[namePart];
            rectRange[ind[0]][ind[1]] += localDelta[ind[0]];
        });

        brushOption.range = fromRectRange(formatRectRange(
            rectRange[0][0], rectRange[1][0], rectRange[0][1], rectRange[1][1]
        ));

        updateCoverAfterCreation(controller, cover);
        trigger(controller, {isEnd: false});
    }

    function driftPolygon(controller, cover, dx, dy, e) {
        var range = cover.__brushOption.range;
        var localDelta = toLocalDelta(controller, dx, dy);

        each(range, function (point) {
            point[0] += localDelta[0];
            point[1] += localDelta[1];
        });

        updateCoverAfterCreation(controller, cover);
        trigger(controller, {isEnd: false});
    }

    function toLocalDelta(controller, dx, dy) {
        var thisGroup = controller.group;
        var localD = thisGroup.transformCoordToLocal(dx, dy);
        var localZero = thisGroup.transformCoordToLocal(0, 0);

        return [localD[0] - localZero[0], localD[1] - localZero[1]];
    }

    function clipByPanel(controller, cover, data) {
        var panel = getPanelByCover(controller, cover);
        if (panel === true) { // Global panel
            return zrUtil.clone(data);
        }

        var panelRect = panel.getBoundingRect();

        return zrUtil.map(data, function (point) {
            var x = point[0];
            x = mathMax(x, panelRect.x);
            x = mathMin(x, panelRect.x + panelRect.width);
            var y = point[1];
            y = mathMax(y, panelRect.y);
            y = mathMin(y, panelRect.y + panelRect.height);
            return [x, y];
        });
    }

    function pointsToRect(points) {
        var xmin = mathMin(points[0][0], points[1][0]);
        var ymin = mathMin(points[0][1], points[1][1]);
        var xmax = mathMax(points[0][0], points[1][0]);
        var ymax = mathMax(points[0][1], points[1][1]);

        return {
            x: xmin,
            y: ymin,
            width: xmax - xmin,
            height: ymax - ymin
        };
    }

    function resetCursor(controller, e) {
        var x = e.offsetX;
        var y = e.offsetY;
        var zr = controller._zr;

        if (controller._brushType) { // If active
            var panels = controller._panels;
            var covers = controller._covers;
            var inCover;

            for (var i = 0; i < covers.length; i++) {
                if (coverRenderers[covers[i].__brushOption.brushType].contain(covers[i], x, y)) {
                    inCover = true;
                    break;
                }
            }

            if (!inCover) {
                if (panels) { // Brush on panels
                    each(panels, function (panel) {
                        panel.contain(x, y) && zr.setCursorStyle('crosshair');
                    });
                }
                else { // Global brush
                    zr.setCursorStyle('crosshair');
                }
            }
        }
    }

    function preventDefault(e) {
        var rawE = e.event;
        rawE.preventDefault && rawE.preventDefault();
    }

    function mainShapeContain(cover, x, y) {
        return cover.childOfName('main').contain(x, y);
    }

    function updateCoverByMouse(controller, e, isEnd) {
        var x = e.offsetX;
        var y = e.offsetY;
        var creatingCover = controller._creatingCover;
        var panel = controller._creatingPanel;
        var thisBrushOption = controller._brushOption;
        var eventParams;

        controller._track.push(controller.group.transformCoordToLocal(x, y));

        if (shouldShowCover(controller) || creatingCover) {

            if (panel && !creatingCover) {
                thisBrushOption.brushMode === 'single' && clearCovers(controller);
                var brushOption = zrUtil.clone(thisBrushOption);
                brushOption.panelId = panel === true ? null : panel.__brushPanelId;
                creatingCover = controller._creatingCover = createCover(controller, brushOption);
                controller._covers.push(creatingCover);
            }

            if (creatingCover) {
                var coverRenderer = coverRenderers[controller._brushType];
                var coverBrushOption = creatingCover.__brushOption;

                coverBrushOption.range = coverRenderer.getCreatingRange(
                    clipByPanel(controller, creatingCover, controller._track)
                );

                if (isEnd) {
                    endCreating(controller, creatingCover);
                    coverRenderer.updateCommon(controller, creatingCover);
                }

                updateCoverShape(controller, creatingCover);

                eventParams = {isEnd: isEnd};
            }
        }
        else if (
            isEnd
            && thisBrushOption.brushMode === 'single'
            && thisBrushOption.removeOnClick
        ) {
            // Help user to remove covers easily, only by a tiny drag, in 'single' mode.
            // But a single click do not clear covers, because user may have casual
            // clicks (for example, click on other component and do not expect covers
            // disappear).
            // Only some cover removed, trigger action, but not every click trigger action.
            if (getPanelByPoint(controller, x, y) && clearCovers(controller)) {
                eventParams = {isEnd: isEnd, removeOnClick: true};
            }
        }

        return eventParams;
    }

    var mouseHandlers = {

        mousedown: function (e) {
            if (this._dragging) {
                // In case some browser do not support globalOut,
                // and release mose out side the browser.
                handleDragEnd.call(this, e);
            }
            else if (!e.target || !e.target.draggable) {

                preventDefault(e);

                var x = e.offsetX;
                var y = e.offsetY;

                this._creatingCover = null;
                var panel = this._creatingPanel = getPanelByPoint(this, x, y);

                if (panel) {
                    this._dragging = true;
                    this._track = [this.group.transformCoordToLocal(x, y)];
                }
            }
        },

        mousemove: function (e) {
            // set Cursor
            resetCursor(this, e);

            if (this._dragging) {

                preventDefault(e);

                var eventParams = updateCoverByMouse(this, e, false);

                eventParams && trigger(this, eventParams);
            }
        },

        mouseup: handleDragEnd //,

        // FIXME
        // in tooltip, globalout should not be triggered.
        // globalout: handleDragEnd
    };

    function handleDragEnd(e) {
        if (this._dragging) {

            preventDefault(e);

            var eventParams = updateCoverByMouse(this, e, true);

            this._dragging = false;
            this._track = [];
            this._creatingCover = null;

            // trigger event shoule be at final, after procedure will be nested.
            eventParams && trigger(this, eventParams);
        }
    }

    /**
     * key: brushType
     * @type {Object}
     */
    var coverRenderers = {

        lineX: getLineRenderer(0),

        lineY: getLineRenderer(1),

        rect: {
            createCover: function (controller, brushOption) {
                return createBaseRectCover(
                    curry(
                        driftRect,
                        function (range) {
                            return range;
                        },
                        function (range) {
                            return range;
                        }
                    ),
                    controller,
                    brushOption,
                    ['w', 'e', 'n', 's', 'se', 'sw', 'ne', 'nw']
                );
            },
            getCreatingRange: function (localTrack) {
                var ends = getTrackEnds(localTrack);
                return formatRectRange(ends[1][0], ends[1][1], ends[0][0], ends[0][1]);
            },
            updateCoverShape: function (controller, cover, localRange, brushOption) {
                updateBaseRect(controller, cover, localRange, brushOption);
            },
            updateCommon: updateCommon,
            contain: mainShapeContain
        },

        polygon: {
            createCover: function (controller, brushOption) {
                var cover = new graphic.Group();

                // Do not use graphic.Polygon because graphic.Polyline do not close the
                // border of the shape when drawing, which is a better experience for user.
                cover.add(new graphic.Polyline({
                    name: 'main',
                    style: makeStyle(brushOption),
                    silent: true
                }));

                return cover;
            },
            getCreatingRange: function (localTrack) {
                return localTrack;
            },
            endCreating: function (controller, cover) {
                cover.remove(cover.childAt(0));
                // Use graphic.Polygon close the shape.
                cover.add(new graphic.Polygon({
                    name: 'main',
                    draggable: true,
                    drift: curry(driftPolygon, controller, cover),
                    ondragend: curry(trigger, controller, {isEnd: true})
                }));
            },
            updateCoverShape: function (controller, cover, localRange, brushOption) {
                cover.childAt(0).setShape({
                    points: clipByPanel(controller, cover, localRange)
                });
            },
            updateCommon: updateCommon,
            contain: mainShapeContain
        }
    };

    function getLineRenderer(xyIndex) {
        return {
            createCover: function (controller, brushOption) {
                return createBaseRectCover(
                    curry(
                        driftRect,
                        function (range) {
                            var rectRange = [range, [0, 100]];
                            xyIndex && rectRange.reverse();
                            return rectRange;
                        },
                        function (rectRange) {
                            return rectRange[xyIndex];
                        }
                    ),
                    controller,
                    brushOption,
                    [['w', 'e'], ['n', 's']][xyIndex]
                );
            },
            getCreatingRange: function (localTrack) {
                var ends = getTrackEnds(localTrack);
                var min = mathMin(ends[0][xyIndex], ends[1][xyIndex]);
                var max = mathMax(ends[0][xyIndex], ends[1][xyIndex]);

                return [min, max];
            },
            updateCoverShape: function (controller, cover, localRange, brushOption) {
                var brushWidth = brushOption.brushStyle.width;
                var otherExtent;
                // If brushWidth not specified, fit the panel.
                if (brushWidth == null) {
                    var panel = getPanelByCover(controller, cover);
                    var base = 0;
                    if (panel !== true) {
                        var rect = panel.getBoundingRect();
                        brushWidth = xyIndex ? rect.width : rect.height;
                        base = xyIndex ? rect.x : rect.y;
                    }
                    // FIXME
                    // do not support global panel yet.
                    otherExtent = [base, base + (brushWidth || 0)];
                }
                else {
                    otherExtent = [-brushWidth / 2, brushWidth / 2];
                }
                var rectRange = [localRange, otherExtent];
                xyIndex && rectRange.reverse();

                updateBaseRect(controller, cover, rectRange, brushOption);
            },
            updateCommon: updateCommon,
            contain: mainShapeContain
        };
    }

    return BrushController;
});