/*
* 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 {curry, each, map, bind, merge, clone, defaults, assert} from 'zrender/src/core/util';
import Eventful from 'zrender/src/core/Eventful';
import * as graphic from '../../util/graphic';
import * as interactionMutex from './interactionMutex';
import DataDiffer from '../../data/DataDiffer';
import { Dictionary } from '../../util/types';
import { ZRenderType } from 'zrender/src/zrender';
import { ElementEvent } from 'zrender/src/Element';
import * as matrix from 'zrender/src/core/matrix';
import Displayable from 'zrender/src/graphic/Displayable';
import { PathStyleProps } from 'zrender/src/graphic/Path';
import tokens from '../../visual/tokens';


/**
 * BrushController is not only used in "brush component",
 * but is also used in "tooltip DataZoom", and other possible
 * further brush behavior related scenarios.
 * So `BrushController` should not depend on "brush component model".
 */


export type BrushType = 'polygon' | 'rect' | 'lineX' | 'lineY';
/**
 * Only for drawing (after enabledBrush).
 * 'line', 'rect', 'polygon' or false
 * If passing false/null/undefined, disable brush.
 * If passing 'auto', determined by panel.defaultBrushType
 */
export type BrushTypeUncertain = BrushType | false | 'auto';

export type BrushMode = 'single' | 'multiple';
// MinMax: Range of linear brush.
// MinMax[]: Range of multi-dimension like rect/polygon, which is a MinMax
//     list for each dimension of the coord sys. For example:
//     cartesian: [[xMin, xMax], [yMin, yMax]]
//     geo: [[lngMin, lngMin], [latMin, latMax]]
export type BrushDimensionMinMax = number[];
export type BrushAreaRange = BrushDimensionMinMax | BrushDimensionMinMax[];

export interface BrushCoverConfig {
    // Mandatory. determine how to convert to/from coord('rect' or 'polygon' or 'lineX/Y')
    brushType: BrushType;
    // Can be specified by user to map covers in `updateCovers`
    // in `dispatchAction({type: 'brush', areas: [{id: ...}, ...]})`
    id?: string;
    // Range in global coordinate (pixel).
    range?: BrushAreaRange;
    // When create a new area by `updateCovers`, panelId should be specified.
    // If not null/undefined, means global panel.
    // Also see `BrushAreaParam['panelId']`.
    panelId?: string;

    brushMode?: BrushMode;
    // `brushStyle`, `transformable` is not mandatory, use DEFAULT_BRUSH_OPT by default.
    brushStyle?: Pick<PathStyleProps, BrushStyleKey>;
    transformable?: boolean;
    removeOnClick?: boolean;
    z?: number;
}

/**
 * `BrushAreaCreatorOption` input to brushModel via `setBrushOption`,
 * merge and convert to `BrushCoverCreatorConfig`.
 */
export interface BrushCoverCreatorConfig extends Pick<
    BrushCoverConfig,
    'brushMode' | 'transformable' | 'removeOnClick' | 'brushStyle' | 'z'
> {
    brushType: BrushTypeUncertain;
}

type BrushStyleKey =
    'fill'
    | 'stroke'
    | 'lineWidth'
    | 'opacity'
    | 'shadowBlur'
    | 'shadowOffsetX'
    | 'shadowOffsetY'
    | 'shadowColor';


const BRUSH_PANEL_GLOBAL = true as const;

export interface BrushPanelConfig {
    // mandatory.
    panelId: string;
    // mandatory.
    clipPath(localPoints: number[][], transform: matrix.MatrixArray): number[][];
    // mandatory.
    isTargetByCursor(e: ElementEvent, localCursorPoint: number[], transform: matrix.MatrixArray): boolean;
    // optional, only used when brushType is 'auto'.
    defaultBrushType?: BrushType;
    // optional.
    getLinearBrushOtherExtent?(xyIndex: number): number[];
}
// `true` represents global panel;
type BrushPanelConfigOrGlobal = BrushPanelConfig | typeof BRUSH_PANEL_GLOBAL;


interface BrushCover extends graphic.Group {
    __brushOption: BrushCoverConfig;
}

type Point = number[];

const mathMin = Math.min;
const mathMax = Math.max;
const mathPow = Math.pow;

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

type DirectionName = 'w' | 'e' | 'n' | 's';
type DirectionNameSequence = DirectionName[];

const DIRECTION_MAP = {
    w: [0, 0],
    e: [0, 1],
    n: [1, 0],
    s: [1, 1]
} as const;
const CURSOR_MAP = {
    w: 'ew',
    e: 'ew',
    n: 'ns',
    s: 'ns',
    ne: 'nesw',
    sw: 'nesw',
    nw: 'nwse',
    se: 'nwse'
} as const;
const DEFAULT_BRUSH_OPT = {
    brushStyle: {
        lineWidth: 2,
        stroke: tokens.color.backgroundTint,
        fill: tokens.color.borderTint
    },
    transformable: true,
    brushMode: 'single',
    removeOnClick: false
};

let baseUID = 0;

export interface BrushControllerEvents {
    brush: {
        areas: {
            brushType: BrushType;
            panelId: string;
            range: BrushAreaRange;
        }[];
        isEnd: boolean;
        removeOnClick: boolean;
    }
}

/**
 * params:
 *     areas: Array.<Array>, coord relates to container group,
 *                             If no container specified, to global.
 *     opt {
 *         isEnd: boolean,
 *         removeOnClick: boolean
 *     }
 */
class BrushController extends Eventful<{
    [key in keyof BrushControllerEvents]: (params: BrushControllerEvents[key]) => void | undefined
}> {

    readonly group: graphic.Group;

    /**
     * @internal
     */
    _zr: ZRenderType;

    /**
     * @internal
     */
    _brushType: BrushTypeUncertain;

    /**
     * @internal
     * Only for drawing (after enabledBrush).
     */
    _brushOption: BrushCoverCreatorConfig;

    /**
     * @internal
     * Key: panelId
     */
    _panels: Dictionary<BrushPanelConfig>;

    /**
     * @internal
     */
    _track: number[][] = [];

    /**
     * @internal
     */
    _dragging: boolean;

    /**
     * @internal
     */
    _covers: BrushCover[] = [];

    /**
     * @internal
     */
    _creatingCover: BrushCover;

    /**
     * @internal
     */
    _creatingPanel: BrushPanelConfigOrGlobal;

    private _enableGlobalPan: boolean;

    private _mounted: boolean;

    /**
     * @internal
     */
    _transform: matrix.MatrixArray;

    private _uid: string;

    private _handlers: {
        [eventName: string]: (this: BrushController, e: ElementEvent) => void
    } = {};


    constructor(zr: ZRenderType) {
        super();

        if (__DEV__) {
            assert(zr);
        }

        this._zr = zr;

        this.group = new graphic.Group();

        this._uid = 'brushController_' + baseUID++;

        each(pointerHandlers, function (this: BrushController, handler, eventName) {
            this._handlers[eventName] = bind(handler, this);
        }, this);
    }

    /**
     * If set to `false`, select disabled.
     */
    enableBrush(brushOption: Partial<BrushCoverCreatorConfig> | false): BrushController {
        if (__DEV__) {
            assert(this._mounted);
        }

        this._brushType && this._doDisableBrush();
        (brushOption as Partial<BrushCoverCreatorConfig>).brushType && this._doEnableBrush(
            brushOption as Partial<BrushCoverCreatorConfig>
        );

        return this;
    }

    private _doEnableBrush(brushOption: Partial<BrushCoverCreatorConfig>): void {
        const zr = this._zr;

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

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

        this._brushType = brushOption.brushType;
        this._brushOption = merge(
            clone(DEFAULT_BRUSH_OPT), brushOption, true
        ) as BrushCoverCreatorConfig;
    }

    private _doDisableBrush(): void {
        const zr = this._zr;

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

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

        this._brushType = this._brushOption = null;
    }

    /**
     * @param panelOpts If not pass, it is global brush.
     */
    setPanels(panelOpts?: BrushPanelConfig[]): BrushController {
        if (panelOpts && panelOpts.length) {
            const panels = this._panels = {} as Dictionary<BrushPanelConfig>;
            each(panelOpts, function (panelOpts) {
                panels[panelOpts.panelId] = clone(panelOpts);
            });
        }
        else {
            this._panels = null;
        }
        return this;
    }

    mount(opt?: {
        enableGlobalPan?: boolean;
        x?: number;
        y?: number;
        rotation?: number;
        scaleX?: number;
        scaleY?: number
    }): BrushController {
        opt = opt || {};

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

        this._enableGlobalPan = opt.enableGlobalPan;

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

        thisGroup.attr({
            x: opt.x || 0,
            y: opt.y || 0,
            rotation: opt.rotation || 0,
            scaleX: opt.scaleX || 1,
            scaleY: opt.scaleY || 1
        });
        this._transform = thisGroup.getLocalTransform();

        return this;
    }

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

    /**
     * Update covers.
     * @param coverConfigList
     *        If coverConfigList is null/undefined, all covers removed.
     */
    updateCovers(coverConfigList: BrushCoverConfig[]) {
        if (__DEV__) {
            assert(this._mounted);
        }

        coverConfigList = map(coverConfigList, function (coverConfig) {
            return merge(clone(DEFAULT_BRUSH_OPT), coverConfig, true);
        }) as BrushCoverConfig[];

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

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

        return this;

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

        function oldGetKey(cover: BrushCover, index: number): string {
            return getKey(cover.__brushOption, index);
        }

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

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

    unmount() {
        if (__DEV__) {
            if (!this._mounted) {
                return;
            }
        }

        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() {
        this.unmount();
        this.off();
    }
}


function createCover(controller: BrushController, brushOption: BrushCoverConfig): BrushCover {
    const cover = coverRenderers[brushOption.brushType].createCover(controller, brushOption);
    cover.__brushOption = brushOption;
    updateZ(cover, brushOption);
    controller.group.add(cover);
    return cover;
}

function endCreating(controller: BrushController, creatingCover: BrushCover): BrushCover {
    const coverRenderer = getCoverRenderer(creatingCover);
    if (coverRenderer.endCreating) {
        coverRenderer.endCreating(controller, creatingCover);
        updateZ(creatingCover, creatingCover.__brushOption);
    }
    return creatingCover;
}

function updateCoverShape(controller: BrushController, cover: BrushCover): void {
    const brushOption = cover.__brushOption;
    getCoverRenderer(cover).updateCoverShape(
        controller, cover, brushOption.range, brushOption
    );
}

function updateZ(cover: BrushCover, brushOption: BrushCoverConfig): void {
    let z = brushOption.z;
    z == null && (z = COVER_Z);
    cover.traverse(function (el: Displayable) {
        el.z = z;
        el.z2 = z; // Consider in given container.
    });
}

function updateCoverAfterCreation(controller: BrushController, cover: BrushCover): void {
    getCoverRenderer(cover).updateCommon(controller, cover);
    updateCoverShape(controller, cover);
}

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

// return target panel or `true` (means global panel)
function getPanelByPoint(
    controller: BrushController,
    e: ElementEvent,
    localCursorPoint: Point
): BrushPanelConfigOrGlobal {
    const panels = controller._panels;
    if (!panels) {
        return BRUSH_PANEL_GLOBAL; // Global panel
    }
    let panel;
    const transform = controller._transform;
    each(panels, function (pn) {
        pn.isTargetByCursor(e, localCursorPoint, transform) && (panel = pn);
    });
    return panel;
}

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

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

    return !!originalLength;
}

function trigger(
    controller: BrushController,
    opt: {isEnd?: boolean, removeOnClick?: boolean}
): void {
    const areas = map(controller._covers, function (cover) {
        const brushOption = cover.__brushOption;
        const range = clone(brushOption.range);
        return {
            brushType: brushOption.brushType,
            panelId: brushOption.panelId,
            range: range
        };
    });

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

function shouldShowCover(controller: BrushController): boolean {
    const track = controller._track;

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

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

    return dist > UNSELECT_THRESHOLD;
}

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

interface RectRangeConverter {
    toRectRange: (range: BrushAreaRange) => BrushDimensionMinMax[];
    fromRectRange: (areaRange: BrushDimensionMinMax[]) => BrushAreaRange;
};
function createBaseRectCover(
    rectRangeConverter: RectRangeConverter,
    controller: BrushController,
    brushOption: BrushCoverConfig,
    edgeNameSequences: DirectionNameSequence[]
): BrushCover {
    const cover = new graphic.Group() as BrushCover;

    cover.add(new graphic.Rect({
        name: 'main',
        style: makeStyle(brushOption),
        silent: true,
        draggable: true,
        cursor: 'move',
        drift: curry(driftRect, rectRangeConverter, controller, cover, ['n', 's', 'w', 'e']),
        ondragend: curry(trigger, controller, {isEnd: true})
    }));

    each(
        edgeNameSequences,
        function (nameSequence) {
            cover.add(new graphic.Rect({
                name: nameSequence.join(''),
                style: {opacity: 0},
                draggable: true,
                silent: true,
                invisible: true,
                drift: curry(driftRect, rectRangeConverter, controller, cover, nameSequence),
                ondragend: curry(trigger, controller, {isEnd: true})
            }));
        }
    );

    return cover;
}

function updateBaseRect(
    controller: BrushController,
    cover: BrushCover,
    localRange: BrushDimensionMinMax[],
    brushOption: BrushCoverConfig
): void {
    const lineWidth = brushOption.brushStyle.lineWidth || 0;
    const handleSize = mathMax(lineWidth, MIN_RESIZE_LINE_WIDTH);
    const x = localRange[0][0];
    const y = localRange[1][0];
    const xa = x - lineWidth / 2;
    const ya = y - lineWidth / 2;
    const x2 = localRange[0][1];
    const y2 = localRange[1][1];
    const x2a = x2 - handleSize + lineWidth / 2;
    const y2a = y2 - handleSize + lineWidth / 2;
    const width = x2 - x;
    const height = y2 - y;
    const widtha = width + lineWidth;
    const 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: BrushController, cover: BrushCover): void {
    const brushOption = cover.__brushOption;
    const transformable = brushOption.transformable;

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

    each(
        [['w'], ['e'], ['n'], ['s'], ['s', 'e'], ['s', 'w'], ['n', 'e'], ['n', 'w']],
        function (nameSequence: DirectionNameSequence) {
            const el = cover.childOfName(nameSequence.join('')) as Displayable;
            const globalDir = nameSequence.length === 1
                ? getGlobalDirection1(controller, nameSequence[0])
                : getGlobalDirection2(controller, nameSequence);

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

function updateRectShape(
    controller: BrushController,
    cover: BrushCover,
    name: string,
    x: number, y: number, w: number, h: number
): void {
    const el = cover.childOfName(name) as graphic.Rect;
    el && el.setShape(pointsToRect(
        clipByPanel(controller, cover, [[x, y], [x + w, y + h]])
    ));
}

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

function formatRectRange(x: number, y: number, x2: number, y2: number): BrushDimensionMinMax[] {
    const min = [mathMin(x, x2), mathMin(y, y2)];
    const max = [mathMax(x, x2), mathMax(y, y2)];

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

function getTransform(controller: BrushController): matrix.MatrixArray {
    return graphic.getTransform(controller.group);
}

function getGlobalDirection1(
    controller: BrushController, localDirName: DirectionName
): keyof typeof CURSOR_MAP {
    const map = {w: 'left', e: 'right', n: 'top', s: 'bottom'} as const;
    const inverseMap = {left: 'w', right: 'e', top: 'n', bottom: 's'} as const;
    const dir = graphic.transformDirection(
        map[localDirName], getTransform(controller)
    );
    return inverseMap[dir];
}
function getGlobalDirection2(
    controller: BrushController, localDirNameSeq: DirectionNameSequence
): keyof typeof CURSOR_MAP {
    const globalDir = [
        getGlobalDirection1(controller, localDirNameSeq[0]),
        getGlobalDirection1(controller, localDirNameSeq[1])
    ];
    (globalDir[0] === 'e' || globalDir[0] === 'w') && globalDir.reverse();
    return globalDir.join('') as keyof typeof CURSOR_MAP;
}

function driftRect(
    rectRangeConverter: RectRangeConverter,
    controller: BrushController,
    cover: BrushCover,
    dirNameSequence: DirectionNameSequence,
    dx: number,
    dy: number
): void {
    const brushOption = cover.__brushOption;
    const rectRange = rectRangeConverter.toRectRange(brushOption.range);
    const localDelta = toLocalDelta(controller, dx, dy);

    each(dirNameSequence, function (dirName) {
        const ind = DIRECTION_MAP[dirName];
        rectRange[ind[0]][ind[1]] += localDelta[ind[0]];
    });

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

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

function driftPolygon(
    controller: BrushController,
    cover: BrushCover,
    dx: number,
    dy: number
): void {
    const range = cover.__brushOption.range as BrushDimensionMinMax[];
    const 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: BrushController, dx: number, dy: number
): BrushDimensionMinMax {
    const thisGroup = controller.group;
    const localD = thisGroup.transformCoordToLocal(dx, dy);
    const localZero = thisGroup.transformCoordToLocal(0, 0);

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

function clipByPanel(controller: BrushController, cover: BrushCover, data: Point[]): Point[] {
    const panel = getPanelByCover(controller, cover);

    return (panel && panel !== BRUSH_PANEL_GLOBAL)
        ? panel.clipPath(data, controller._transform)
        : clone(data);
}

function pointsToRect(points: Point[]): graphic.Rect['shape'] {
    const xmin = mathMin(points[0][0], points[1][0]);
    const ymin = mathMin(points[0][1], points[1][1]);
    const xmax = mathMax(points[0][0], points[1][0]);
    const ymax = mathMax(points[0][1], points[1][1]);

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

function resetCursor(
    controller: BrushController, e: ElementEvent, localCursorPoint: Point
): void {
    if (
        // Check active
        !controller._brushType
        // resetCursor should be always called when mouse is in zr area,
        // but not called when mouse is out of zr area to avoid bad influence
        // if `mousemove`, `mouseup` are triggered from `document` event.
        || isOutsideZrArea(controller, e.offsetX, e.offsetY)
    ) {
        return;
    }

    const zr = controller._zr;
    const covers = controller._covers;
    const currPanel = getPanelByPoint(controller, e, localCursorPoint);

    // Check whether in covers.
    if (!controller._dragging) {
        for (let i = 0; i < covers.length; i++) {
            const brushOption = covers[i].__brushOption;
            if (currPanel
                && (currPanel === BRUSH_PANEL_GLOBAL || brushOption.panelId === currPanel.panelId)
                && coverRenderers[brushOption.brushType].contain(
                    covers[i], localCursorPoint[0], localCursorPoint[1]
                )
            ) {
                // Use cursor style set on cover.
                return;
            }
        }
    }

    currPanel && zr.setCursorStyle('crosshair');
}

function preventDefault(e: ElementEvent): void {
    const rawE = e.event;
    rawE.preventDefault && rawE.preventDefault();
}

function mainShapeContain(cover: BrushCover, x: number, y: number): boolean {
    return (cover.childOfName('main') as Displayable).contain(x, y);
}

function updateCoverByMouse(
    controller: BrushController,
    e: ElementEvent,
    localCursorPoint: Point,
    isEnd: boolean
): {
    isEnd: boolean,
    removeOnClick?: boolean
} {
    let creatingCover = controller._creatingCover;
    const panel = controller._creatingPanel;
    const thisBrushOption = controller._brushOption;
    let eventParams;

    controller._track.push(localCursorPoint.slice());

    if (shouldShowCover(controller) || creatingCover) {

        if (panel && !creatingCover) {
            thisBrushOption.brushMode === 'single' && clearCovers(controller);
            const brushOption = clone(thisBrushOption) as BrushCoverConfig;
            brushOption.brushType = determineBrushType(brushOption.brushType, panel as BrushPanelConfig);
            brushOption.panelId = panel === BRUSH_PANEL_GLOBAL ? null : panel.panelId;
            creatingCover = controller._creatingCover = createCover(controller, brushOption);
            controller._covers.push(creatingCover);
        }

        if (creatingCover) {
            const coverRenderer = coverRenderers[
                determineBrushType(controller._brushType, panel as BrushPanelConfig)
            ];
            const 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, e, localCursorPoint) && clearCovers(controller)) {
            eventParams = {isEnd: isEnd, removeOnClick: true};
        }
    }

    return eventParams;
}

function determineBrushType(brushType: BrushTypeUncertain, panel: BrushPanelConfig): BrushType {
    if (brushType === 'auto') {
        if (__DEV__) {
            assert(
                panel && panel.defaultBrushType,
                'MUST have defaultBrushType when brushType is "atuo"'
            );
        }
        return panel.defaultBrushType;
    }
    return brushType as BrushType;
}

const pointerHandlers: Dictionary<(this: BrushController, e: ElementEvent) => void> = {

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

            preventDefault(e);

            const localCursorPoint = this.group.transformCoordToLocal(e.offsetX, e.offsetY);

            this._creatingCover = null;
            const panel = this._creatingPanel = getPanelByPoint(this, e, localCursorPoint);

            if (panel) {
                this._dragging = true;
                this._track = [localCursorPoint.slice()];
            }
        }
    },

    mousemove: function (e) {
        const x = e.offsetX;
        const y = e.offsetY;

        const localCursorPoint = this.group.transformCoordToLocal(x, y);

        resetCursor(this, e, localCursorPoint);

        if (this._dragging) {
            preventDefault(e);
            const eventParams = updateCoverByMouse(this, e, localCursorPoint, false);
            eventParams && trigger(this, eventParams);
        }
    },

    mouseup: function (e) {
        handleDragEnd(this, e);
    }
};


function handleDragEnd(controller: BrushController, e: ElementEvent) {
    if (controller._dragging) {
        preventDefault(e);

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

        const localCursorPoint = controller.group.transformCoordToLocal(x, y);
        const eventParams = updateCoverByMouse(controller, e, localCursorPoint, true);

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

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

function isOutsideZrArea(controller: BrushController, x: number, y: number): boolean {
    const zr = controller._zr;
    return x < 0 || x > zr.getWidth() || y < 0 || y > zr.getHeight();
}


interface CoverRenderer {
    createCover(controller: BrushController, brushOption: BrushCoverConfig): BrushCover;
    getCreatingRange(localTrack: Point[]): BrushAreaRange;
    updateCoverShape(
        controller: BrushController, cover: BrushCover, localRange: BrushAreaRange, brushOption: BrushCoverConfig
    ): void;
    updateCommon(controller: BrushController, cover: BrushCover): void;
    contain(cover: BrushCover, x: number, y: number): boolean;
    endCreating?(controller: BrushController, creatingCover: BrushCover): void;
}

/**
 * key: brushType
 */
const coverRenderers: Record<BrushType, CoverRenderer> = {

    lineX: getLineRenderer(0),

    lineY: getLineRenderer(1),

    rect: {
        createCover: function (controller, brushOption) {
            function returnInput(range: BrushDimensionMinMax[]): BrushDimensionMinMax[] {
                return range;
            }
            return createBaseRectCover(
                {
                    toRectRange: returnInput,
                    fromRectRange: returnInput
                },
                controller,
                brushOption,
                [['w'], ['e'], ['n'], ['s'], ['s', 'e'], ['s', 'w'], ['n', 'e'], ['n', 'w']]
            );
        },
        getCreatingRange: function (localTrack) {
            const ends = getTrackEnds(localTrack);
            return formatRectRange(ends[1][0], ends[1][1], ends[0][0], ends[0][1]);
        },
        updateCoverShape: function (controller, cover, localRange: BrushDimensionMinMax[], brushOption) {
            updateBaseRect(controller, cover, localRange, brushOption);
        },
        updateCommon: updateCommon,
        contain: mainShapeContain
    },

    polygon: {
        createCover: function (controller, brushOption) {
            const 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 as BrushCover;
        },
        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: BrushDimensionMinMax[], brushOption) {
            (cover.childAt(0) as graphic.Polygon).setShape({
                points: clipByPanel(controller, cover, localRange)
            });
        },
        updateCommon: updateCommon,
        contain: mainShapeContain
    }
};

function getLineRenderer(xyIndex: 0 | 1) {
    return {
        createCover: function (controller: BrushController, brushOption: BrushCoverConfig): BrushCover {
            return createBaseRectCover(
                {
                    toRectRange: function (range: BrushDimensionMinMax): BrushDimensionMinMax[] {
                        const rectRange = [range, [0, 100]];
                        xyIndex && rectRange.reverse();
                        return rectRange;
                    },
                    fromRectRange: function (rectRange: BrushDimensionMinMax[]): BrushDimensionMinMax {
                        return rectRange[xyIndex];
                    }
                },
                controller,
                brushOption,
                ([[['w'], ['e']], [['n'], ['s']]] as DirectionNameSequence[][])[xyIndex]
            );
        },
        getCreatingRange: function (localTrack: Point[]): BrushDimensionMinMax {
            const ends = getTrackEnds(localTrack);
            const min = mathMin(ends[0][xyIndex], ends[1][xyIndex]);
            const max = mathMax(ends[0][xyIndex], ends[1][xyIndex]);

            return [min, max];
        },
        updateCoverShape: function (
            controller: BrushController,
            cover: BrushCover,
            localRange: BrushDimensionMinMax,
            brushOption: BrushCoverConfig
        ): void {
            let otherExtent;
            // If brushWidth not specified, fit the panel.
            const panel = getPanelByCover(controller, cover);
            if (panel !== BRUSH_PANEL_GLOBAL && panel.getLinearBrushOtherExtent) {
                otherExtent = panel.getLinearBrushOtherExtent(xyIndex);
            }
            else {
                const zr = controller._zr;
                otherExtent = [0, [zr.getWidth(), zr.getHeight()][1 - xyIndex]];
            }
            const rectRange = [localRange, otherExtent];
            xyIndex && rectRange.reverse();

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

export default BrushController;
