/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.  See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership.  The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License.  You may obtain a copy of the License at
*
*   http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied.  See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import * as zrUtil from 'zrender/src/core/util';
import LinearGradient, { LinearGradientObject } from 'zrender/src/graphic/LinearGradient';
import * as eventTool from 'zrender/src/core/event';
import VisualMapView from './VisualMapView';
import * as graphic from '../../util/graphic';
import * as numberUtil from '../../util/number';
import sliderMove from '../helper/sliderMove';
import * as helper from './helper';
import * as modelUtil from '../../util/model';
import VisualMapModel from './VisualMapModel';
import ContinuousModel from './ContinuousModel';
import GlobalModel from '../../model/Global';
import ExtensionAPI from '../../core/ExtensionAPI';
import Element, { ElementEvent } from 'zrender/src/Element';
import { TextVerticalAlign, TextAlign } from 'zrender/src/core/types';
import { ColorString, Payload } from '../../util/types';
import { parsePercent } from 'zrender/src/contain/text';
import { setAsHighDownDispatcher } from '../../util/states';
import { createSymbol } from '../../util/symbol';
import ZRImage from 'zrender/src/graphic/Image';
import { ECData, getECData } from '../../util/innerStore';
import { createTextStyle } from '../../label/labelStyle';
import { findEventDispatcher } from '../../util/event';

const linearMap = numberUtil.linearMap;
const each = zrUtil.each;
const mathMin = Math.min;
const mathMax = Math.max;

// Arbitrary value
const HOVER_LINK_SIZE = 12;
const HOVER_LINK_OUT = 6;

type Orient = VisualMapModel['option']['orient'];

type ShapeStorage = {
    handleThumbs: graphic.Path[]
    handleLabelPoints: number[][]
    handleLabels: graphic.Text[]

    inRange: graphic.Polygon
    outOfRange: graphic.Polygon

    mainGroup: graphic.Group

    indicator: graphic.Path
    indicatorLabel: graphic.Text
    indicatorLabelPoint: number[]
};

type TargetDataIndices = ReturnType<ContinuousModel['findTargetDataIndices']>;

type BarVisual = {
    barColor: LinearGradient,
    barPoints: number[][]
    handlesColor: ColorString[]
};

type Direction = 'left' | 'right' | 'top' | 'bottom';
// Notice:
// Any "interval" should be by the order of [low, high].
// "handle0" (handleIndex === 0) maps to
// low data value: this._dataInterval[0] and has low coord.
// "handle1" (handleIndex === 1) maps to
// high data value: this._dataInterval[1] and has high coord.
// The logic of transform is implemented in this._createBarGroup.

class ContinuousView extends VisualMapView {
    static type = 'visualMap.continuous';
    type = ContinuousView.type;

    visualMapModel: ContinuousModel;

    private _shapes = {} as ShapeStorage;

    private _dataInterval: number[] = [];

    private _handleEnds: number[] = [];

    private _orient: Orient;

    private _useHandle: boolean;

    private _hoverLinkDataIndices: TargetDataIndices = [];

    private _dragging: boolean;

    private _hovering: boolean;

    private _firstShowIndicator: boolean;

    init(ecModel: GlobalModel, api: ExtensionAPI) {
        super.init(ecModel, api);

        this._hoverLinkFromSeriesMouseOver = zrUtil.bind(this._hoverLinkFromSeriesMouseOver, this);
        this._hideIndicator = zrUtil.bind(this._hideIndicator, this);
    }

    doRender(
        visualMapModel: ContinuousModel,
        ecModel: GlobalModel,
        api: ExtensionAPI,
        payload: {type: string, from: string}
    ) {
        if (!payload || payload.type !== 'selectDataRange' || payload.from !== this.uid) {
            this._buildView();
        }
    }

    private _buildView() {
        this.group.removeAll();

        const visualMapModel = this.visualMapModel;
        const thisGroup = this.group;

        this._orient = visualMapModel.get('orient');
        this._useHandle = visualMapModel.get('calculable');

        this._resetInterval();

        this._renderBar(thisGroup);

        const dataRangeText = visualMapModel.get('text');
        this._renderEndsText(thisGroup, dataRangeText, 0);
        this._renderEndsText(thisGroup, dataRangeText, 1);

        // Do this for background size calculation.
        this._updateView(true);

        // After updating view, inner shapes is built completely,
        // and then background can be rendered.
        this.renderBackground(thisGroup);

        // Real update view
        this._updateView();

        this._enableHoverLinkToSeries();
        this._enableHoverLinkFromSeries();

        this.positionGroup(thisGroup);
    }

    private _renderEndsText(group: graphic.Group, dataRangeText: string[], endsIndex?: 0 | 1) {
        if (!dataRangeText) {
            return;
        }

        // Compatible with ec2, text[0] map to high value, text[1] map low value.
        let text = dataRangeText[1 - endsIndex];
        text = text != null ? text + '' : '';

        const visualMapModel = this.visualMapModel;
        const textGap = visualMapModel.get('textGap');
        const itemSize = visualMapModel.itemSize;

        const barGroup = this._shapes.mainGroup;
        const position = this._applyTransform(
            [
                itemSize[0] / 2,
                endsIndex === 0 ? -textGap : itemSize[1] + textGap
            ],
            barGroup
        ) as number[];
        const align = this._applyTransform(
            endsIndex === 0 ? 'bottom' : 'top',
            barGroup
        );
        const orient = this._orient;
        const textStyleModel = this.visualMapModel.textStyleModel;

        this.group.add(new graphic.Text({
            style: createTextStyle(textStyleModel, {
                x: position[0],
                y: position[1],
                verticalAlign: orient === 'horizontal' ? 'middle' : align as TextVerticalAlign,
                align: orient === 'horizontal' ? align as TextAlign : 'center',
                text
            })
        }));
    }

    private _renderBar(targetGroup: graphic.Group) {
        const visualMapModel = this.visualMapModel;
        const shapes = this._shapes;
        const itemSize = visualMapModel.itemSize;
        const orient = this._orient;
        const useHandle = this._useHandle;
        const itemAlign = helper.getItemAlign(visualMapModel, this.api, itemSize);
        const mainGroup = shapes.mainGroup = this._createBarGroup(itemAlign);

        const gradientBarGroup = new graphic.Group();
        mainGroup.add(gradientBarGroup);

        // Bar
        gradientBarGroup.add(shapes.outOfRange = createPolygon());
        gradientBarGroup.add(shapes.inRange = createPolygon(
            null,
            useHandle ? getCursor(this._orient) : null,
            zrUtil.bind(this._dragHandle, this, 'all', false),
            zrUtil.bind(this._dragHandle, this, 'all', true)
        ));

        // A border radius clip.
        gradientBarGroup.setClipPath(new graphic.Rect({
            shape: {
                x: 0,
                y: 0,
                width: itemSize[0],
                height: itemSize[1],
                r: 3
            }
        }));

        const textRect = visualMapModel.textStyleModel.getTextRect('国');
        const textSize = mathMax(textRect.width, textRect.height);

        // Handle
        if (useHandle) {
            shapes.handleThumbs = [];
            shapes.handleLabels = [];
            shapes.handleLabelPoints = [];

            this._createHandle(visualMapModel, mainGroup, 0, itemSize, textSize, orient);
            this._createHandle(visualMapModel, mainGroup, 1, itemSize, textSize, orient);
        }

        this._createIndicator(visualMapModel, mainGroup, itemSize, textSize, orient);

        targetGroup.add(mainGroup);
    }

    private _createHandle(
        visualMapModel: ContinuousModel,
        mainGroup: graphic.Group,
        handleIndex: 0 | 1,
        itemSize: number[],
        textSize: number,
        orient: Orient
    ) {
        const onDrift = zrUtil.bind(this._dragHandle, this, handleIndex, false);
        const onDragEnd = zrUtil.bind(this._dragHandle, this, handleIndex, true);
        const handleSize = parsePercent(visualMapModel.get('handleSize'), itemSize[0]);
        const handleThumb = createSymbol(
            visualMapModel.get('handleIcon'),
            -handleSize / 2, -handleSize / 2, handleSize, handleSize,
            null, true
        );
        const cursor = getCursor(this._orient);
        handleThumb.attr({
            cursor: cursor,
            draggable: true,
            drift: onDrift,
            ondragend: onDragEnd,
            onmousemove(e) {
                eventTool.stop(e.event);
            }
        });
        handleThumb.x = itemSize[0] / 2;

        handleThumb.useStyle(visualMapModel.getModel('handleStyle').getItemStyle());
        (handleThumb as graphic.Path).setStyle({
            strokeNoScale: true,
            strokeFirst: true
        });
        (handleThumb as graphic.Path).style.lineWidth *= 2;

        handleThumb.ensureState('emphasis').style = visualMapModel.getModel(['emphasis', 'handleStyle']).getItemStyle();
        setAsHighDownDispatcher(handleThumb, true);

        mainGroup.add(handleThumb);

        // Text is always horizontal layout but should not be effected by
        // transform (orient/inverse). So label is built separately but not
        // use zrender/graphic/helper/RectText, and is located based on view
        // group (according to handleLabelPoint) but not barGroup.
        const textStyleModel = this.visualMapModel.textStyleModel;
        const handleLabel = new graphic.Text({
            cursor: cursor,
            draggable: true,
            drift: onDrift,
            onmousemove(e) {
                // For mobile device, prevent screen slider on the button.
                eventTool.stop(e.event);
            },
            ondragend: onDragEnd,
            style: createTextStyle(textStyleModel, {
                x: 0,
                y: 0,
                text: ''
            })
        });
        handleLabel.ensureState('blur').style = {
            opacity: 0.1
        };
        handleLabel.stateTransition = { duration: 200 };

        this.group.add(handleLabel);

        const handleLabelPoint = [handleSize, 0];

        const shapes = this._shapes;
        shapes.handleThumbs[handleIndex] = handleThumb;
        shapes.handleLabelPoints[handleIndex] = handleLabelPoint;
        shapes.handleLabels[handleIndex] = handleLabel;
    }

    private _createIndicator(
        visualMapModel: ContinuousModel,
        mainGroup: graphic.Group,
        itemSize: number[],
        textSize: number,
        orient: Orient
    ) {
        const scale = parsePercent(visualMapModel.get('indicatorSize'), itemSize[0]);
        const indicator = createSymbol(
            visualMapModel.get('indicatorIcon'),
            -scale / 2, -scale / 2, scale, scale,
            null, true
        );
        indicator.attr({
            cursor: 'move',
            invisible: true,
            silent: true,
            x: itemSize[0] / 2
        });
        const indicatorStyle = visualMapModel.getModel('indicatorStyle').getItemStyle();
        if (indicator instanceof ZRImage) {
            const pathStyle = indicator.style;
            indicator.useStyle(zrUtil.extend({
                // TODO other properties like x, y ?
                image: pathStyle.image,
                x: pathStyle.x, y: pathStyle.y,
                width: pathStyle.width, height: pathStyle.height
            }, indicatorStyle));
        }
        else {
            indicator.useStyle(indicatorStyle);
        }

        mainGroup.add(indicator);

        const textStyleModel = this.visualMapModel.textStyleModel;
        const indicatorLabel = new graphic.Text({
            silent: true,
            invisible: true,
            style: createTextStyle(textStyleModel, {
                x: 0,
                y: 0,
                text: ''
            })
        });
        this.group.add(indicatorLabel);

        const indicatorLabelPoint = [
            (orient === 'horizontal' ? textSize / 2 : HOVER_LINK_OUT) + itemSize[0] / 2,
            0
        ];

        const shapes = this._shapes;
        shapes.indicator = indicator;
        shapes.indicatorLabel = indicatorLabel;
        shapes.indicatorLabelPoint = indicatorLabelPoint;

        this._firstShowIndicator = true;
    }

    private _dragHandle(
        handleIndex: 0 | 1 | 'all',
        isEnd?: boolean,
        // dx is event from ondragend if isEnd is true. It's not used
        dx?: number | ElementEvent,
        dy?: number
    ) {
        if (!this._useHandle) {
            return;
        }

        this._dragging = !isEnd;

        if (!isEnd) {
            // Transform dx, dy to bar coordination.
            const vertex = this._applyTransform([dx as number, dy], this._shapes.mainGroup, true) as number[];
            this._updateInterval(handleIndex, vertex[1]);

            this._hideIndicator();
            // Considering realtime, update view should be executed
            // before dispatch action.
            this._updateView();
        }

        // dragEnd do not dispatch action when realtime.
        if (isEnd === !this.visualMapModel.get('realtime')) { // jshint ignore:line
            this.api.dispatchAction({
                type: 'selectDataRange',
                from: this.uid,
                visualMapId: this.visualMapModel.id,
                selected: this._dataInterval.slice()
            });
        }

        if (isEnd) {
            !this._hovering && this._clearHoverLinkToSeries();
        }
        else if (useHoverLinkOnHandle(this.visualMapModel)) {
            this._doHoverLinkToSeries(this._handleEnds[handleIndex as 0 | 1], false);
        }
    }

    private _resetInterval() {
        const visualMapModel = this.visualMapModel;

        const dataInterval = this._dataInterval = visualMapModel.getSelected();
        const dataExtent = visualMapModel.getExtent();
        const sizeExtent = [0, visualMapModel.itemSize[1]];

        this._handleEnds = [
            linearMap(dataInterval[0], dataExtent, sizeExtent, true),
            linearMap(dataInterval[1], dataExtent, sizeExtent, true)
        ];
    }

    /**
     * @private
     * @param {(number|string)} handleIndex 0 or 1 or 'all'
     * @param {number} dx
     * @param {number} dy
     */
    private _updateInterval(handleIndex: 0 | 1 | 'all', delta: number) {
        delta = delta || 0;
        const visualMapModel = this.visualMapModel;
        const handleEnds = this._handleEnds;
        const sizeExtent = [0, visualMapModel.itemSize[1]];

        sliderMove(
            delta,
            handleEnds,
            sizeExtent,
            handleIndex,
            // cross is forbidden
            0
        );

        const dataExtent = visualMapModel.getExtent();
        // Update data interval.
        this._dataInterval = [
            linearMap(handleEnds[0], sizeExtent, dataExtent, true),
            linearMap(handleEnds[1], sizeExtent, dataExtent, true)
        ];
    }

    private _updateView(forSketch?: boolean) {
        const visualMapModel = this.visualMapModel;
        const dataExtent = visualMapModel.getExtent();
        const shapes = this._shapes;

        const outOfRangeHandleEnds = [0, visualMapModel.itemSize[1]];
        const inRangeHandleEnds = forSketch ? outOfRangeHandleEnds : this._handleEnds;

        const visualInRange = this._createBarVisual(
            this._dataInterval, dataExtent, inRangeHandleEnds, 'inRange'
        );
        const visualOutOfRange = this._createBarVisual(
            dataExtent, dataExtent, outOfRangeHandleEnds, 'outOfRange'
        );

        shapes.inRange
            .setStyle({
                fill: visualInRange.barColor
                // opacity: visualInRange.opacity
            })
            .setShape('points', visualInRange.barPoints);
        shapes.outOfRange
            .setStyle({
                fill: visualOutOfRange.barColor
                // opacity: visualOutOfRange.opacity
            })
            .setShape('points', visualOutOfRange.barPoints);

        this._updateHandle(inRangeHandleEnds, visualInRange);
    }

    private _createBarVisual(
        dataInterval: number[],
        dataExtent: number[],
        handleEnds: number[],
        forceState: ContinuousModel['stateList'][number]
    ): BarVisual {
        const opts = {
            forceState: forceState,
            convertOpacityToAlpha: true
        };
        const colorStops = this._makeColorGradient(dataInterval, opts);

        const symbolSizes = [
            this.getControllerVisual(dataInterval[0], 'symbolSize', opts) as number,
            this.getControllerVisual(dataInterval[1], 'symbolSize', opts) as number
        ];
        const barPoints = this._createBarPoints(handleEnds, symbolSizes);

        return {
            barColor: new LinearGradient(0, 0, 0, 1, colorStops),
            barPoints: barPoints,
            handlesColor: [
                colorStops[0].color,
                colorStops[colorStops.length - 1].color
            ]
        };
    }

    private _makeColorGradient(
        dataInterval: number[],
        opts: {
            forceState?: ContinuousModel['stateList'][number]
            convertOpacityToAlpha?: boolean
        }
    ) {
        // Considering colorHue, which is not linear, so we have to sample
        // to calculate gradient color stops, but not only calculate head
        // and tail.
        const sampleNumber = 100; // Arbitrary value.
        const colorStops: LinearGradientObject['colorStops'] = [];
        const step = (dataInterval[1] - dataInterval[0]) / sampleNumber;

        colorStops.push({
            color: this.getControllerVisual(dataInterval[0], 'color', opts) as ColorString,
            offset: 0
        });

        for (let i = 1; i < sampleNumber; i++) {
            const currValue = dataInterval[0] + step * i;
            if (currValue > dataInterval[1]) {
                break;
            }
            colorStops.push({
                color: this.getControllerVisual(currValue, 'color', opts) as ColorString,
                offset: i / sampleNumber
            });
        }

        colorStops.push({
            color: this.getControllerVisual(dataInterval[1], 'color', opts) as ColorString,
            offset: 1
        });

        return colorStops;
    }

    private _createBarPoints(handleEnds: number[], symbolSizes: number[]) {
        const itemSize = this.visualMapModel.itemSize;

        return [
            [itemSize[0] - symbolSizes[0], handleEnds[0]],
            [itemSize[0], handleEnds[0]],
            [itemSize[0], handleEnds[1]],
            [itemSize[0] - symbolSizes[1], handleEnds[1]]
        ];
    }

    private _createBarGroup(itemAlign: helper.ItemAlign) {
        const orient = this._orient;
        const inverse = this.visualMapModel.get('inverse');

        return new graphic.Group(
            (orient === 'horizontal' && !inverse)
            ? {scaleX: itemAlign === 'bottom' ? 1 : -1, rotation: Math.PI / 2}
            : (orient === 'horizontal' && inverse)
            ? {scaleX: itemAlign === 'bottom' ? -1 : 1, rotation: -Math.PI / 2}
            : (orient === 'vertical' && !inverse)
            ? {scaleX: itemAlign === 'left' ? 1 : -1, scaleY: -1}
            : {scaleX: itemAlign === 'left' ? 1 : -1}
        );
    }

    private _updateHandle(handleEnds: number[], visualInRange: BarVisual) {
        if (!this._useHandle) {
            return;
        }

        const shapes = this._shapes;
        const visualMapModel = this.visualMapModel;
        const handleThumbs = shapes.handleThumbs;
        const handleLabels = shapes.handleLabels;
        const itemSize = visualMapModel.itemSize;
        const dataExtent = visualMapModel.getExtent();
        const align = this._applyTransform('left', shapes.mainGroup);

        each([0, 1], function (handleIndex) {
            const handleThumb = handleThumbs[handleIndex];
            handleThumb.setStyle('fill', visualInRange.handlesColor[handleIndex]);
            handleThumb.y = handleEnds[handleIndex];

            const val = linearMap(handleEnds[handleIndex], [0, itemSize[1]], dataExtent, true);
            const symbolSize = this.getControllerVisual(val, 'symbolSize') as number;

            handleThumb.scaleX = handleThumb.scaleY = symbolSize / itemSize[0];
            handleThumb.x = itemSize[0] - symbolSize / 2;

            // Update handle label position.
            const textPoint = graphic.applyTransform(
                shapes.handleLabelPoints[handleIndex],
                graphic.getTransform(handleThumb, this.group)
            );

            if (this._orient === 'horizontal') {
                // If visualMap controls symbol size, an additional offset needs to be added to labels to avoid collision at minimum size.
                // Offset reaches value of 0 at "maximum" position, so maximum position is not altered at all.
                const minimumOffset = align === 'left' || align === 'top'
                    ? (itemSize[0] - symbolSize) / 2
                    : (itemSize[0] - symbolSize) / -2;

                textPoint[1] += minimumOffset;
            }

            handleLabels[handleIndex].setStyle({
                x: textPoint[0],
                y: textPoint[1],
                text: visualMapModel.formatValueText(this._dataInterval[handleIndex]),
                verticalAlign: 'middle',
                align: this._orient === 'vertical' ? this._applyTransform(
                    'left',
                    shapes.mainGroup
                ) as TextAlign : 'center'
            });
        }, this);
    }

    private _showIndicator(
        cursorValue: number,
        textValue: number,
        rangeSymbol?: string,
        halfHoverLinkSize?: number
    ) {
        const visualMapModel = this.visualMapModel;
        const dataExtent = visualMapModel.getExtent();
        const itemSize = visualMapModel.itemSize;
        const sizeExtent = [0, itemSize[1]];

        const shapes = this._shapes;
        const indicator = shapes.indicator;
        if (!indicator) {
            return;
        }

        indicator.attr('invisible', false);

        const opts = {convertOpacityToAlpha: true};
        const color = this.getControllerVisual(cursorValue, 'color', opts) as ColorString;
        const symbolSize = this.getControllerVisual(cursorValue, 'symbolSize') as number;
        const y = linearMap(cursorValue, dataExtent, sizeExtent, true);
        const x = itemSize[0] - symbolSize / 2;

        const oldIndicatorPos = { x: indicator.x, y: indicator.y };
        // Update handle label position.
        indicator.y = y;
        indicator.x = x;
        const textPoint = graphic.applyTransform(
            shapes.indicatorLabelPoint,
            graphic.getTransform(indicator, this.group)
        );

        const indicatorLabel = shapes.indicatorLabel;
        indicatorLabel.attr('invisible', false);
        const align = this._applyTransform('left', shapes.mainGroup);
        const orient = this._orient;
        const isHorizontal = orient === 'horizontal';
        indicatorLabel.setStyle({
            text: (rangeSymbol ? rangeSymbol : '') + visualMapModel.formatValueText(textValue),
            verticalAlign: isHorizontal ? align as TextVerticalAlign : 'middle',
            align: isHorizontal ? 'center' : align as TextAlign
        });

        const indicatorNewProps = {
            x: x,
            y: y,
            style: {
                fill: color
            }
        };
        const labelNewProps = {
            style: {
                x: textPoint[0],
                y: textPoint[1]
            }
        };

        if (visualMapModel.ecModel.isAnimationEnabled() && !this._firstShowIndicator) {
            const animationCfg = {
                duration: 100,
                easing: 'cubicInOut',
                additive: true
            } as const;
            indicator.x = oldIndicatorPos.x;
            indicator.y = oldIndicatorPos.y;
            indicator.animateTo(indicatorNewProps, animationCfg);
            indicatorLabel.animateTo(labelNewProps, animationCfg);
        }
        else {
            indicator.attr(indicatorNewProps);
            indicatorLabel.attr(labelNewProps);
        }

        this._firstShowIndicator = false;

        const handleLabels = this._shapes.handleLabels;
        if (handleLabels) {
            for (let i = 0; i < handleLabels.length; i++) {
                // Fade out handle labels.
                // NOTE: Must use api enter/leave on emphasis/blur/select state. Or the global states manager will change it.
                this.api.enterBlur(handleLabels[i]);
            }
        }
    }

    private _enableHoverLinkToSeries() {
        const self = this;
        this._shapes.mainGroup

            .on('mousemove', function (e) {
                self._hovering = true;

                if (!self._dragging) {
                    const itemSize = self.visualMapModel.itemSize;
                    const pos = self._applyTransform(
                        [e.offsetX, e.offsetY], self._shapes.mainGroup, true, true
                    );
                    // For hover link show when hover handle, which might be
                    // below or upper than sizeExtent.
                    pos[1] = mathMin(mathMax(0, pos[1]), itemSize[1]);
                    self._doHoverLinkToSeries(
                        pos[1],
                        0 <= pos[0] && pos[0] <= itemSize[0]
                    );
                }
            })

            .on('mouseout', function () {
                // When mouse is out of handle, hoverLink still need
                // to be displayed when realtime is set as false.
                self._hovering = false;
                !self._dragging && self._clearHoverLinkToSeries();
            });
    }

    private _enableHoverLinkFromSeries() {
        const zr = this.api.getZr();

        if (this.visualMapModel.option.hoverLink) {
            zr.on('mouseover', this._hoverLinkFromSeriesMouseOver, this);
            zr.on('mouseout', this._hideIndicator, this);
        }
        else {
            this._clearHoverLinkFromSeries();
        }
    }

    private _doHoverLinkToSeries(cursorPos: number, hoverOnBar?: boolean) {
        const visualMapModel = this.visualMapModel;
        const itemSize = visualMapModel.itemSize;

        if (!visualMapModel.option.hoverLink) {
            return;
        }

        const sizeExtent = [0, itemSize[1]];
        const dataExtent = visualMapModel.getExtent();

        // For hover link show when hover handle, which might be below or upper than sizeExtent.
        cursorPos = mathMin(mathMax(sizeExtent[0], cursorPos), sizeExtent[1]);

        const halfHoverLinkSize = getHalfHoverLinkSize(visualMapModel, dataExtent, sizeExtent);
        const hoverRange = [cursorPos - halfHoverLinkSize, cursorPos + halfHoverLinkSize];
        const cursorValue = linearMap(cursorPos, sizeExtent, dataExtent, true);
        const valueRange = [
            linearMap(hoverRange[0], sizeExtent, dataExtent, true),
            linearMap(hoverRange[1], sizeExtent, dataExtent, true)
        ];
        // Consider data range is out of visualMap range, see test/visualMap-continuous.html,
        // where china and india has very large population.
        hoverRange[0] < sizeExtent[0] && (valueRange[0] = -Infinity);
        hoverRange[1] > sizeExtent[1] && (valueRange[1] = Infinity);

        // Do not show indicator when mouse is over handle,
        // otherwise labels overlap, especially when dragging.
        if (hoverOnBar) {
            if (valueRange[0] === -Infinity) {
                this._showIndicator(cursorValue, valueRange[1], '< ', halfHoverLinkSize);
            }
            else if (valueRange[1] === Infinity) {
                this._showIndicator(cursorValue, valueRange[0], '> ', halfHoverLinkSize);
            }
            else {
                this._showIndicator(cursorValue, cursorValue, '≈ ', halfHoverLinkSize);
            }
        }

        // When realtime is set as false, handles, which are in barGroup,
        // also trigger hoverLink, which help user to realize where they
        // focus on when dragging. (see test/heatmap-large.html)
        // When realtime is set as true, highlight will not show when hover
        // handle, because the label on handle, which displays a exact value
        // but not range, might mislead users.
        const oldBatch = this._hoverLinkDataIndices;
        let newBatch: TargetDataIndices = [];
        if (hoverOnBar || useHoverLinkOnHandle(visualMapModel)) {
            newBatch = this._hoverLinkDataIndices = visualMapModel.findTargetDataIndices(valueRange);
        }

        const resultBatches = modelUtil.compressBatches(oldBatch, newBatch);

        this._dispatchHighDown('downplay', helper.makeHighDownBatch(resultBatches[0], visualMapModel));
        this._dispatchHighDown('highlight', helper.makeHighDownBatch(resultBatches[1], visualMapModel));
    }

    private _hoverLinkFromSeriesMouseOver(e: ElementEvent) {
        let ecData: ECData;

        findEventDispatcher(e.target, target => {
            const currECData = getECData(target);
            if (currECData.dataIndex != null) {
                ecData = currECData;
                return true;
            }
        }, true);

        if (!ecData) {
            return;
        }

        const dataModel = this.ecModel.getSeriesByIndex(ecData.seriesIndex);

        const visualMapModel = this.visualMapModel;
        if (!visualMapModel.isTargetSeries(dataModel)) {
            return;
        }

        const data = dataModel.getData(ecData.dataType);
        const value = data.getStore().get(visualMapModel.getDataDimensionIndex(data), ecData.dataIndex) as number;

        if (!isNaN(value)) {
            this._showIndicator(value, value);
        }
    }

    private _hideIndicator() {
        const shapes = this._shapes;
        shapes.indicator && shapes.indicator.attr('invisible', true);
        shapes.indicatorLabel && shapes.indicatorLabel.attr('invisible', true);

        const handleLabels = this._shapes.handleLabels;
        if (handleLabels) {
            for (let i = 0; i < handleLabels.length; i++) {
                // Fade out handle labels.
                // NOTE: Must use api enter/leave on emphasis/blur/select state. Or the global states manager will change it.
                this.api.leaveBlur(handleLabels[i]);
            }
        }
    }

    private _clearHoverLinkToSeries() {
        this._hideIndicator();

        const indices = this._hoverLinkDataIndices;
        this._dispatchHighDown('downplay', helper.makeHighDownBatch(indices, this.visualMapModel));

        indices.length = 0;
    }

    private _clearHoverLinkFromSeries() {
        this._hideIndicator();

        const zr = this.api.getZr();

        zr.off('mouseover', this._hoverLinkFromSeriesMouseOver);
        zr.off('mouseout', this._hideIndicator);
    }
    private _applyTransform(vertex: number[], element: Element, inverse?: boolean, global?: boolean): number[]
    private _applyTransform(vertex: Direction, element: Element, inverse?: boolean, global?: boolean): Direction
    private _applyTransform(
        vertex: number[] | Direction,
        element: Element,
        inverse?: boolean,
        global?: boolean
    ) {
        const transform = graphic.getTransform(element, global ? null : this.group);

        return zrUtil.isArray(vertex)
            ? graphic.applyTransform(vertex, transform, inverse)
            : graphic.transformDirection(vertex, transform, inverse);
    }

    // TODO: TYPE more specified payload types.
    private _dispatchHighDown(type: 'highlight' | 'downplay', batch: Payload['batch']) {
        batch && batch.length && this.api.dispatchAction({
            type: type,
            batch: batch
        });
    }

    /**
     * @override
     */
    dispose() {
        this._clearHoverLinkFromSeries();
        this._clearHoverLinkToSeries();
    }

}

function createPolygon(
    points?: number[][],
    cursor?: string,
    onDrift?: (x: number, y: number) => void,
    onDragEnd?: () => void
) {
    return new graphic.Polygon({
        shape: {points: points},
        draggable: !!onDrift,
        cursor: cursor,
        drift: onDrift,
        onmousemove(e) {
            // For mobile device, prevent screen slider on the button.
            eventTool.stop(e.event);
        },
        ondragend: onDragEnd
    });
}

function getHalfHoverLinkSize(visualMapModel: ContinuousModel, dataExtent: number[], sizeExtent: number[]) {
    let halfHoverLinkSize = HOVER_LINK_SIZE / 2;
    const hoverLinkDataSize = visualMapModel.get('hoverLinkDataSize');
    if (hoverLinkDataSize) {
        halfHoverLinkSize = linearMap(hoverLinkDataSize, dataExtent, sizeExtent, true) / 2;
    }
    return halfHoverLinkSize;
}

function useHoverLinkOnHandle(visualMapModel: ContinuousModel) {
    const hoverLinkOnHandle = visualMapModel.get('hoverLinkOnHandle');
    return !!(hoverLinkOnHandle == null ? visualMapModel.get('realtime') : hoverLinkOnHandle);
}

function getCursor(orient: Orient) {
    return orient === 'vertical' ? 'ns-resize' : 'ew-resize';
}

export default ContinuousView;
