/*
* 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 Path, {PathProps} from 'zrender/src/graphic/Path';
import Group from 'zrender/src/graphic/Group';
import {extend, each, map} from 'zrender/src/core/util';
import {BuiltinTextPosition} from 'zrender/src/core/types';
import {SectorProps} from 'zrender/src/graphic/shape/Sector';
import {RectProps} from 'zrender/src/graphic/shape/Rect';
import {
    Rect,
    Sector,
    updateProps,
    initProps,
    removeElementWithFadeOut,
    traverseElements
} from '../../util/graphic';
import { getECData } from '../../util/innerStore';
import { setStatesStylesFromModel, toggleHoverEmphasis } from '../../util/states';
import { setLabelStyle, getLabelStatesModels, setLabelValueAnimation, labelInner } from '../../label/labelStyle';
import {throttle} from '../../util/throttle';
import {createClipPath} from '../helper/createClipPathFromCoordSys';
import Sausage from '../../util/shape/sausage';
import ChartView from '../../view/Chart';
import SeriesData, {DefaultDataVisual} from '../../data/SeriesData';
import GlobalModel from '../../model/Global';
import ExtensionAPI from '../../core/ExtensionAPI';
import {
    StageHandlerProgressParams,
    ZRElementEvent,
    ColorString,
    OrdinalSortInfo,
    Payload,
    OrdinalNumber,
    ParsedValue,
    ECElement
} from '../../util/types';
import BarSeriesModel, {BarDataItemOption, PolarBarLabelPosition} from './BarSeries';
import type Axis2D from '../../coord/cartesian/Axis2D';
import type Cartesian2D from '../../coord/cartesian/Cartesian2D';
import type Polar from '../../coord/polar/Polar';
import type Model from '../../model/Model';
import { isCoordinateSystemType } from '../../coord/CoordinateSystem';
import { getDefaultLabel, getDefaultInterpolatedLabel } from '../helper/labelHelper';
import OrdinalScale from '../../scale/Ordinal';
import SeriesModel from '../../model/Series';
import {AngleAxisModel, RadiusAxisModel} from '../../coord/polar/AxisModel';
import CartesianAxisModel from '../../coord/cartesian/AxisModel';
import {LayoutRect} from '../../util/layout';
import {EventCallback} from 'zrender/src/core/Eventful';
import { warn } from '../../util/log';
import {createSectorCalculateTextPosition, SectorTextPosition, setSectorTextRotation} from '../../label/sectorLabel';
import { saveOldStyle } from '../../animation/basicTransition';
import Element from 'zrender/src/Element';
import { getSectorCornerRadius } from '../helper/sectorHelper';

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

type CoordSysOfBar = BarSeriesModel['coordinateSystem'];
type RectShape = Rect['shape'];
type SectorShape = Sector['shape'];

type SectorLayout = SectorShape;
type RectLayout = RectShape;

type BarPossiblePath = Sector | Rect | Sausage;

type CartesianCoordArea = ReturnType<Cartesian2D['getArea']>;
type PolarCoordArea = ReturnType<Polar['getArea']>;

type RealtimeSortConfig = {
    baseAxis: Axis2D;
    otherAxis: Axis2D;
};
// Return a number, based on which the ordinal sorted.
type OrderMapping = (dataIndex: number) => number;

function getClipArea(coord: CoordSysOfBar, data: SeriesData) {
    const coordSysClipArea = coord.getArea && coord.getArea();
    if (isCoordinateSystemType<Cartesian2D>(coord, 'cartesian2d')) {
        const baseAxis = coord.getBaseAxis();
        // When boundaryGap is false or using time axis. bar may exceed the grid.
        // We should not clip this part.
        // See test/bar2.html
        if (baseAxis.type !== 'category' || !baseAxis.onBand) {
            const expandWidth = data.getLayout('bandWidth');
            if (baseAxis.isHorizontal()) {
                (coordSysClipArea as CartesianCoordArea).x -= expandWidth;
                (coordSysClipArea as CartesianCoordArea).width += expandWidth * 2;
            }
            else {
                (coordSysClipArea as CartesianCoordArea).y -= expandWidth;
                (coordSysClipArea as CartesianCoordArea).height += expandWidth * 2;
            }
        }
    }

    return coordSysClipArea as PolarCoordArea | CartesianCoordArea;
}

class BarView extends ChartView {
    static type = 'bar' as const;
    type = BarView.type;

    private _data: SeriesData;

    private _isLargeDraw: boolean;

    private _isFirstFrame: boolean; // First frame after series added
    private _onRendered: EventCallback;

    private _backgroundGroup: Group;

    private _backgroundEls: (Rect | Sector)[];

    private _model: BarSeriesModel;

    private _progressiveEls: Element[];

    constructor() {
        super();
        this._isFirstFrame = true;
    }

    render(seriesModel: BarSeriesModel, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload) {
        this._model = seriesModel;

        this._removeOnRenderedListener(api);

        this._updateDrawMode(seriesModel);

        const coordinateSystemType = seriesModel.get('coordinateSystem');

        if (coordinateSystemType === 'cartesian2d'
            || coordinateSystemType === 'polar'
        ) {
            // Clear previously rendered progressive elements.
            this._progressiveEls = null;

            this._isLargeDraw
                ? this._renderLarge(seriesModel, ecModel, api)
                : this._renderNormal(seriesModel, ecModel, api, payload);
        }
        else if (__DEV__) {
            warn('Only cartesian2d and polar supported for bar.');
        }
    }

    incrementalPrepareRender(seriesModel: BarSeriesModel): void {
        this._clear();
        this._updateDrawMode(seriesModel);
        // incremental also need to clip, otherwise might be overlow.
        // But must not set clip in each frame, otherwise all of the children will be marked redraw.
        this._updateLargeClip(seriesModel);
    }

    incrementalRender(params: StageHandlerProgressParams, seriesModel: BarSeriesModel): void {
        // Reset
        this._progressiveEls = [];
        // Do not support progressive in normal mode.
        this._incrementalRenderLarge(params, seriesModel);
    }

    eachRendered(cb: (el: Element) => boolean | void) {
        traverseElements(this._progressiveEls || this.group, cb);
    }

    private _updateDrawMode(seriesModel: BarSeriesModel): void {
        const isLargeDraw = seriesModel.pipelineContext.large;
        if (this._isLargeDraw == null || isLargeDraw !== this._isLargeDraw) {
            this._isLargeDraw = isLargeDraw;
            this._clear();
        }
    }

    private _renderNormal(
        seriesModel: BarSeriesModel,
        ecModel: GlobalModel,
        api: ExtensionAPI,
        payload: Payload
    ): void {
        const group = this.group;
        const data = seriesModel.getData();
        const oldData = this._data;

        const coord = seriesModel.coordinateSystem;
        const baseAxis = coord.getBaseAxis();
        let isHorizontalOrRadial: boolean;

        if (coord.type === 'cartesian2d') {
            isHorizontalOrRadial = (baseAxis as Axis2D).isHorizontal();
        }
        else if (coord.type === 'polar') {
            isHorizontalOrRadial = baseAxis.dim === 'angle';
        }

        const animationModel = seriesModel.isAnimationEnabled() ? seriesModel : null;

        const realtimeSortCfg = shouldRealtimeSort(seriesModel, coord);

        if (realtimeSortCfg) {
            this._enableRealtimeSort(realtimeSortCfg, data, api);
        }

        const needsClip = seriesModel.get('clip', true) || realtimeSortCfg;
        const coordSysClipArea = getClipArea(coord, data);
        // If there is clipPath created in large mode. Remove it.
        group.removeClipPath();
        // We don't use clipPath in normal mode because we needs a perfect animation
        // And don't want the label are clipped.

        const roundCap = seriesModel.get('roundCap', true);

        const drawBackground = seriesModel.get('showBackground', true);
        const backgroundModel = seriesModel.getModel('backgroundStyle');
        const barBorderRadius = backgroundModel.get('borderRadius') || 0;

        const bgEls: BarView['_backgroundEls'] = [];
        const oldBgEls = this._backgroundEls;

        const isInitSort = payload && payload.isInitSort;
        const isChangeOrder = payload && payload.type === 'changeAxisOrder';

        function createBackground(dataIndex: number) {
            const bgLayout = getLayout[coord.type](data, dataIndex);
            const bgEl = createBackgroundEl(coord, isHorizontalOrRadial, bgLayout);
            bgEl.useStyle(backgroundModel.getItemStyle());
            // Only cartesian2d support borderRadius.
            if (coord.type === 'cartesian2d') {
                (bgEl as Rect).setShape('r', barBorderRadius);
            }
            else {
                (bgEl as Sector).setShape('cornerRadius', barBorderRadius);
            }
            bgEls[dataIndex] = bgEl;
            return bgEl;
        };
        data.diff(oldData)
            .add(function (dataIndex) {
                const itemModel = data.getItemModel<BarDataItemOption>(dataIndex);
                const layout = getLayout[coord.type](data, dataIndex, itemModel);

                if (drawBackground) {
                    createBackground(dataIndex);
                }

                // If dataZoom in filteMode: 'empty', the baseValue can be set as NaN in "axisProxy".
                if (!data.hasValue(dataIndex) || !isValidLayout[coord.type](layout)) {
                    return;
                }

                let isClipped = false;
                if (needsClip) {
                    // Clip will modify the layout params.
                    // And return a boolean to determine if the shape are fully clipped.
                    isClipped = clip[coord.type](coordSysClipArea, layout);
                }

                const el = elementCreator[coord.type](
                    seriesModel,
                    data,
                    dataIndex,
                    layout,
                    isHorizontalOrRadial,
                    animationModel,
                    baseAxis.model,
                    false,
                    roundCap
                );
                if (realtimeSortCfg) {
                    /**
                     * Force label animation because even if the element is
                     * ignored because it's clipped, it may not be clipped after
                     * changing order. Then, if not using forceLabelAnimation,
                     * the label animation was never started, in which case,
                     * the label will be the final value and doesn't have label
                     * animation.
                     */
                    (el as ECElement).forceLabelAnimation = true;
                }

                updateStyle(
                    el, data, dataIndex, itemModel, layout,
                    seriesModel, isHorizontalOrRadial, coord.type === 'polar'
                );
                if (isInitSort) {
                    (el as Rect).attr({ shape: layout });
                }
                else if (realtimeSortCfg) {
                    updateRealtimeAnimation(
                        realtimeSortCfg,
                        animationModel,
                        el as Rect,
                        layout as LayoutRect,
                        dataIndex,
                        isHorizontalOrRadial,
                        false,
                        false
                    );
                }
                else {
                    initProps(el, {shape: layout} as any, seriesModel, dataIndex);
                }

                data.setItemGraphicEl(dataIndex, el);

                group.add(el);
                el.ignore = isClipped;
            })
            .update(function (newIndex, oldIndex) {
                const itemModel = data.getItemModel<BarDataItemOption>(newIndex);
                const layout = getLayout[coord.type](data, newIndex, itemModel);

                if (drawBackground) {
                    let bgEl: Rect | Sector;
                    if (oldBgEls.length === 0) {
                        bgEl = createBackground(oldIndex);
                    }
                    else {
                        bgEl = oldBgEls[oldIndex];
                        bgEl.useStyle(backgroundModel.getItemStyle());
                        // Only cartesian2d support borderRadius.
                        if (coord.type === 'cartesian2d') {
                            (bgEl as Rect).setShape('r', barBorderRadius);
                        }
                        else {
                            (bgEl as Sector).setShape('cornerRadius', barBorderRadius);
                        }
                        bgEls[newIndex] = bgEl;
                    }
                    const bgLayout = getLayout[coord.type](data, newIndex);
                    const shape = createBackgroundShape(isHorizontalOrRadial, bgLayout, coord);
                    updateProps<RectProps | SectorProps>(bgEl, { shape }, animationModel, newIndex);
                }

                let el = oldData.getItemGraphicEl(oldIndex) as BarPossiblePath;
                if (!data.hasValue(newIndex) || !isValidLayout[coord.type](layout)) {
                    group.remove(el);
                    return;
                }

                let isClipped = false;
                if (needsClip) {
                    isClipped = clip[coord.type](coordSysClipArea, layout);
                    if (isClipped) {
                        group.remove(el);
                    }
                }

                if (!el) {
                    el = elementCreator[coord.type](
                        seriesModel,
                        data,
                        newIndex,
                        layout,
                        isHorizontalOrRadial,
                        animationModel,
                        baseAxis.model,
                        !!el,
                        roundCap
                    );
                }
                else {
                    saveOldStyle(el);
                }

                if (realtimeSortCfg) {
                    (el as ECElement).forceLabelAnimation = true;
                }

                if (isChangeOrder) {
                    const textEl = el.getTextContent();
                    if (textEl) {
                        const labelInnerStore = labelInner(textEl);
                        if (labelInnerStore.prevValue != null) {
                            /**
                             * Set preValue to be value so that no new label
                             * should be started, otherwise, it will take a full
                             * `animationDurationUpdate` time to finish the
                             * animation, which is not expected.
                             */
                            labelInnerStore.prevValue = labelInnerStore.value;
                        }
                    }
                }
                // Not change anything if only order changed.
                // Especially not change label.
                else {
                    updateStyle(
                        el, data, newIndex, itemModel, layout,
                        seriesModel, isHorizontalOrRadial, coord.type === 'polar'
                    );
                }

                if (isInitSort) {
                    (el as Rect).attr({ shape: layout });
                }
                else if (realtimeSortCfg) {
                    updateRealtimeAnimation(
                        realtimeSortCfg,
                        animationModel,
                        el as Rect,
                        layout as LayoutRect,
                        newIndex,
                        isHorizontalOrRadial,
                        true,
                        isChangeOrder
                    );
                }
                else {
                    updateProps(el, {
                        shape: layout
                    } as any, seriesModel, newIndex, null);
                }

                data.setItemGraphicEl(newIndex, el);
                el.ignore = isClipped;
                group.add(el);
            })
            .remove(function (dataIndex) {
                const el = oldData.getItemGraphicEl(dataIndex) as Path;
                el && removeElementWithFadeOut(el, seriesModel, dataIndex);
            })
            .execute();

        const bgGroup = this._backgroundGroup || (this._backgroundGroup = new Group());
        bgGroup.removeAll();

        for (let i = 0; i < bgEls.length; ++i) {
            bgGroup.add(bgEls[i]);
        }
        group.add(bgGroup);
        this._backgroundEls = bgEls;

        this._data = data;
    }

    private _renderLarge(seriesModel: BarSeriesModel, ecModel: GlobalModel, api: ExtensionAPI): void {
        this._clear();
        createLarge(seriesModel, this.group);
        this._updateLargeClip(seriesModel);
    }

    private _incrementalRenderLarge(params: StageHandlerProgressParams, seriesModel: BarSeriesModel): void {
        this._removeBackground();
        createLarge(seriesModel, this.group, this._progressiveEls, true);
    }

    private _updateLargeClip(seriesModel: BarSeriesModel): void {
        // Use clipPath in large mode.
        const clipPath = seriesModel.get('clip', true)
            && createClipPath(seriesModel.coordinateSystem, false, seriesModel);
        const group = this.group;
        if (clipPath) {
            group.setClipPath(clipPath);
        }
        else {
            group.removeClipPath();
        }
    }

    private _enableRealtimeSort(
        realtimeSortCfg: RealtimeSortConfig,
        data: ReturnType<BarSeriesModel['getData']>,
        api: ExtensionAPI
    ): void {
        // If no data in the first frame, wait for data to initSort
        if (!data.count()) {
            return;
        }

        const baseAxis = realtimeSortCfg.baseAxis;

        if (this._isFirstFrame) {
            this._dispatchInitSort(data, realtimeSortCfg, api);
            this._isFirstFrame = false;
        }
        else {
            const orderMapping = (idx: number) => {
                const el = (data.getItemGraphicEl(idx) as Rect);
                const shape = el && el.shape;
                return (shape && (
                    // The result should be consistent with the initial sort by data value.
                    // Do not support the case that both positive and negative exist.
                    Math.abs(
                        baseAxis.isHorizontal()
                            ? shape.height
                            : shape.width
                    )
                // If data is NaN, shape.xxx may be NaN, so use || 0 here in case
                )) || 0;
            };
            this._onRendered = () => {
                this._updateSortWithinSameData(data, orderMapping, baseAxis, api);
            };
            api.getZr().on('rendered', this._onRendered);
        }
    }

    private _dataSort(
        data: SeriesData<BarSeriesModel, DefaultDataVisual>,
        baseAxis: Axis2D,
        orderMapping: OrderMapping
    ): OrdinalSortInfo {
        type SortValueInfo = {
            dataIndex: number,
            mappedValue: number,
            ordinalNumber: OrdinalNumber
        };
        const info: SortValueInfo[] = [];
        data.each(data.mapDimension(baseAxis.dim), (ordinalNumber: OrdinalNumber, dataIdx: number) => {
            let mappedValue = orderMapping(dataIdx);
            mappedValue = mappedValue == null ? NaN : mappedValue;
            info.push({
                dataIndex: dataIdx,
                mappedValue,
                ordinalNumber
            });
        });

        info.sort((a, b) => {
            // If NaN, it will be treated as min val.
            return b.mappedValue - a.mappedValue;
        });

        return {
            ordinalNumbers: map(info, item => item.ordinalNumber)
        };
    }

    private _isOrderChangedWithinSameData(
        data: SeriesData<BarSeriesModel, DefaultDataVisual>,
        orderMapping: OrderMapping,
        baseAxis: Axis2D
    ): boolean {
        const scale = baseAxis.scale as OrdinalScale;
        const ordinalDataDim = data.mapDimension(baseAxis.dim);

        let lastValue = Number.MAX_VALUE;
        for (let tickNum = 0, len = scale.getOrdinalMeta().categories.length; tickNum < len; ++tickNum) {
            const rawIdx = data.rawIndexOf(ordinalDataDim, scale.getRawOrdinalNumber(tickNum));
            const value = rawIdx < 0
                // If some tick have no bar, the tick will be treated as min.
                ? Number.MIN_VALUE
                // PENDING: if dataZoom on baseAxis exits, is it a performance issue?
                : orderMapping(data.indexOfRawIndex(rawIdx));
            if (value > lastValue) {
                return true;
            }
            lastValue = value;
        }
        return false;
    }

    /*
     * Consider the case when A and B changed order, whose representing
     * bars are both out of sight, we don't wish to trigger reorder action
     * as long as the order in the view doesn't change.
     */
    private _isOrderDifferentInView(
        orderInfo: OrdinalSortInfo,
        baseAxis: Axis2D
    ): boolean {
        const scale = baseAxis.scale as OrdinalScale;
        const extent = scale.getExtent();

        let tickNum = Math.max(0, extent[0]);
        const tickMax = Math.min(extent[1], scale.getOrdinalMeta().categories.length - 1);
        for (;tickNum <= tickMax; ++tickNum) {
            if (orderInfo.ordinalNumbers[tickNum] !== scale.getRawOrdinalNumber(tickNum)) {
                return true;
            }
        }
    }

    private _updateSortWithinSameData(
        data: SeriesData<BarSeriesModel, DefaultDataVisual>,
        orderMapping: OrderMapping,
        baseAxis: Axis2D,
        api: ExtensionAPI
    ) {
        if (!this._isOrderChangedWithinSameData(data, orderMapping, baseAxis)) {
            return;
        }

        const sortInfo = this._dataSort(data, baseAxis, orderMapping);

        if (this._isOrderDifferentInView(sortInfo, baseAxis)) {
            this._removeOnRenderedListener(api);
            api.dispatchAction({
                type: 'changeAxisOrder',
                componentType: baseAxis.dim + 'Axis',
                axisId: baseAxis.index,
                sortInfo: sortInfo
            });
        }
    }

    private _dispatchInitSort(
        data: SeriesData<BarSeriesModel, DefaultDataVisual>,
        realtimeSortCfg: RealtimeSortConfig,
        api: ExtensionAPI
    ) {
        const baseAxis = realtimeSortCfg.baseAxis;
        const sortResult = this._dataSort(
            data,
            baseAxis,
            dataIdx => data.get(
                data.mapDimension(realtimeSortCfg.otherAxis.dim),
                dataIdx
            ) as number
        );
        api.dispatchAction({
            type: 'changeAxisOrder',
            componentType: baseAxis.dim + 'Axis',
            isInitSort: true,
            axisId: baseAxis.index,
            sortInfo: sortResult
        });
    }

    remove(ecModel: GlobalModel, api: ExtensionAPI) {
        this._clear(this._model);
        this._removeOnRenderedListener(api);
    }

    dispose(ecModel: GlobalModel, api: ExtensionAPI) {
        this._removeOnRenderedListener(api);
    }

    private _removeOnRenderedListener(api: ExtensionAPI) {
        if (this._onRendered) {
            api.getZr().off('rendered', this._onRendered);
            this._onRendered = null;
        }
    }

    private _clear(model?: SeriesModel): void {
        const group = this.group;
        const data = this._data;
        if (model && model.isAnimationEnabled() && data && !this._isLargeDraw) {
            this._removeBackground();
            this._backgroundEls = [];

            data.eachItemGraphicEl(function (el: Path) {
                removeElementWithFadeOut(el, model, getECData(el).dataIndex);
            });
        }
        else {
            group.removeAll();
        }
        this._data = null;
        this._isFirstFrame = true;
    }

    private _removeBackground(): void {
        this.group.remove(this._backgroundGroup);
        this._backgroundGroup = null;
    }
}

interface Clipper {
    (coordSysBoundingRect: PolarCoordArea | CartesianCoordArea, layout: RectLayout | SectorLayout): boolean
}
const clip: {
    [key in 'cartesian2d' | 'polar']: Clipper
} = {
    cartesian2d(coordSysBoundingRect: CartesianCoordArea, layout: Rect['shape']) {
        const signWidth = layout.width < 0 ? -1 : 1;
        const signHeight = layout.height < 0 ? -1 : 1;
        // Needs positive width and height
        if (signWidth < 0) {
            layout.x += layout.width;
            layout.width = -layout.width;
        }
        if (signHeight < 0) {
            layout.y += layout.height;
            layout.height = -layout.height;
        }

        const coordSysX2 = coordSysBoundingRect.x + coordSysBoundingRect.width;
        const coordSysY2 = coordSysBoundingRect.y + coordSysBoundingRect.height;
        const x = mathMax(layout.x, coordSysBoundingRect.x);
        const x2 = mathMin(layout.x + layout.width, coordSysX2);
        const y = mathMax(layout.y, coordSysBoundingRect.y);
        const y2 = mathMin(layout.y + layout.height, coordSysY2);

        const xClipped = x2 < x;
        const yClipped = y2 < y;

        // When xClipped or yClipped, the element will be marked as `ignore`.
        // But we should also place the element at the edge of the coord sys bounding rect.
        // Because if data changed and the bar shows again, its transition animation
        // will begin at this place.
        layout.x = (xClipped && x > coordSysX2) ? x2 : x;
        layout.y = (yClipped && y > coordSysY2) ? y2 : y;
        layout.width = xClipped ? 0 : x2 - x;
        layout.height = yClipped ? 0 : y2 - y;

        // Reverse back
        if (signWidth < 0) {
            layout.x += layout.width;
            layout.width = -layout.width;
        }
        if (signHeight < 0) {
            layout.y += layout.height;
            layout.height = -layout.height;
        }

        return xClipped || yClipped;
    },

    polar(coordSysClipArea: PolarCoordArea, layout: Sector['shape']) {
        const signR = layout.r0 <= layout.r ? 1 : -1;
        // Make sure r is larger than r0
        if (signR < 0) {
            const tmp = layout.r;
            layout.r = layout.r0;
            layout.r0 = tmp;
        }

        const r = mathMin(layout.r, coordSysClipArea.r);
        const r0 = mathMax(layout.r0, coordSysClipArea.r0);

        layout.r = r;
        layout.r0 = r0;

        const clipped = r - r0 < 0;

        // Reverse back
        if (signR < 0) {
            const tmp = layout.r;
            layout.r = layout.r0;
            layout.r0 = tmp;
        }

        return clipped;
    }
};

interface ElementCreator {
    (
        seriesModel: BarSeriesModel, data: SeriesData, newIndex: number,
        layout: RectLayout | SectorLayout, isHorizontalOrRadial: boolean,
        animationModel: BarSeriesModel,
        axisModel: CartesianAxisModel | AngleAxisModel | RadiusAxisModel,
        isUpdate: boolean,
        roundCap?: boolean
    ): BarPossiblePath
}

const elementCreator: {
    [key in 'polar' | 'cartesian2d']: ElementCreator
} = {

    cartesian2d(
        seriesModel, data, newIndex, layout: RectLayout, isHorizontal,
        animationModel, axisModel, isUpdate, roundCap
    ) {
        const rect = new Rect({
            shape: extend({}, layout),
            z2: 1
        });
        (rect as any).__dataIndex = newIndex;

        rect.name = 'item';

        if (animationModel) {
            const rectShape = rect.shape;
            const animateProperty = isHorizontal ? 'height' : 'width' as 'width' | 'height';
            rectShape[animateProperty] = 0;
        }
        return rect;
    },

    polar(
        seriesModel, data, newIndex, layout: SectorLayout, isRadial: boolean,
        animationModel, axisModel, isUpdate, roundCap
    ) {
        const ShapeClass = (!isRadial && roundCap) ? Sausage : Sector;
        const sector = new ShapeClass({
            shape: layout,
            z2: 1
        });

        sector.name = 'item';

        const positionMap = createPolarPositionMapping(isRadial);
        sector.calculateTextPosition = createSectorCalculateTextPosition(positionMap, {
            isRoundCap: ShapeClass === Sausage
        });

        // Animation
        if (animationModel) {
            const sectorShape = sector.shape;
            const animateProperty = isRadial ? 'r' : 'endAngle' as 'r' | 'endAngle';
            const animateTarget = {} as SectorShape;
            sectorShape[animateProperty] = isRadial ? layout.r0 : layout.startAngle;
            animateTarget[animateProperty] = layout[animateProperty];
            (isUpdate ? updateProps : initProps)(sector, {
                shape: animateTarget
                // __value: typeof dataValue === 'string' ? parseInt(dataValue, 10) : dataValue
            }, animationModel);
        }

        return sector;
    }
};

function shouldRealtimeSort(
    seriesModel: BarSeriesModel,
    coordSys: Cartesian2D | Polar
): RealtimeSortConfig {
    const realtimeSortOption = seriesModel.get('realtimeSort', true);
    const baseAxis = coordSys.getBaseAxis() as Axis2D;
    if (__DEV__) {
        if (realtimeSortOption) {
            if (baseAxis.type !== 'category') {
                warn('`realtimeSort` will not work because this bar series is not based on a category axis.');
            }
            if (coordSys.type !== 'cartesian2d') {
                warn('`realtimeSort` will not work because this bar series is not on cartesian2d.');
            }
        }
    }
    if (realtimeSortOption && baseAxis.type === 'category' && coordSys.type === 'cartesian2d') {
        return {
            baseAxis: baseAxis as Axis2D,
            otherAxis: coordSys.getOtherAxis(baseAxis)
        };
    }
}

function updateRealtimeAnimation(
    realtimeSortCfg: RealtimeSortConfig,
    seriesAnimationModel: BarSeriesModel,
    el: Rect,
    layout: LayoutRect,
    newIndex: number,
    isHorizontal: boolean,
    isUpdate: boolean,
    isChangeOrder: boolean
) {
    let seriesTarget;
    let axisTarget;
    if (isHorizontal) {
        axisTarget = {
            x: layout.x,
            width: layout.width
        };
        seriesTarget = {
            y: layout.y,
            height: layout.height
        };
    }
    else {
        axisTarget = {
            y: layout.y,
            height: layout.height
        };
        seriesTarget = {
            x: layout.x,
            width: layout.width
        };
    }

    if (!isChangeOrder) {
        // Keep the original growth animation if only axis order changed.
        // Not start a new animation.
        (isUpdate ? updateProps : initProps)(el, {
            shape: seriesTarget
        }, seriesAnimationModel, newIndex, null);
    }

    const axisAnimationModel = seriesAnimationModel ? realtimeSortCfg.baseAxis.model : null;
    (isUpdate ? updateProps : initProps)(el, {
        shape: axisTarget
    }, axisAnimationModel, newIndex);
}

function checkPropertiesNotValid<T extends Record<string, any>>(obj: T, props: readonly (keyof T)[]) {
    for (let i = 0; i < props.length; i++) {
        if (!isFinite(obj[props[i]])) {
            return true;
        }
    }
    return false;
}


const rectPropties = ['x', 'y', 'width', 'height'] as const;
const polarPropties = ['cx', 'cy', 'r', 'startAngle', 'endAngle'] as const;
const isValidLayout: Record<'cartesian2d' | 'polar', (layout: RectLayout | SectorLayout) => boolean> = {
    cartesian2d(layout: RectLayout) {
        return !checkPropertiesNotValid(layout, rectPropties);
    },

    polar(layout: SectorLayout) {
        return !checkPropertiesNotValid(layout, polarPropties);
    }
} as const;

interface GetLayout {
    (data: SeriesData, dataIndex: number, itemModel?: Model<BarDataItemOption>): RectLayout | SectorLayout
}
const getLayout: {
    [key in 'cartesian2d' | 'polar']: GetLayout
} = {
    // itemModel is only used to get borderWidth, which is not needed
    // when calculating bar background layout.
    cartesian2d(data, dataIndex, itemModel?): RectLayout {
        const layout = data.getItemLayout(dataIndex) as RectLayout;
        const fixedLineWidth = itemModel ? getLineWidth(itemModel, layout) : 0;

        // fix layout with lineWidth
        const signX = layout.width > 0 ? 1 : -1;
        const signY = layout.height > 0 ? 1 : -1;
        return {
            x: layout.x + signX * fixedLineWidth / 2,
            y: layout.y + signY * fixedLineWidth / 2,
            width: layout.width - signX * fixedLineWidth,
            height: layout.height - signY * fixedLineWidth
        };
    },

    polar(data, dataIndex, itemModel?): SectorLayout {
        const layout = data.getItemLayout(dataIndex);
        return {
            cx: layout.cx,
            cy: layout.cy,
            r0: layout.r0,
            r: layout.r,
            startAngle: layout.startAngle,
            endAngle: layout.endAngle,
            clockwise: layout.clockwise
        } as SectorLayout;
    }
};

function isZeroOnPolar(layout: SectorLayout) {
    return layout.startAngle != null
        && layout.endAngle != null
        && layout.startAngle === layout.endAngle;
}

function createPolarPositionMapping(isRadial: boolean)
    : (position: PolarBarLabelPosition) => SectorTextPosition {
    return ((isRadial: boolean) => {
        const arcOrAngle = isRadial ? 'Arc' : 'Angle';
        return (position: PolarBarLabelPosition) => {
            switch (position) {
                case 'start':
                case 'insideStart':
                case 'end':
                case 'insideEnd':
                    return position + arcOrAngle as SectorTextPosition;
                default:
                    return position;
            }
        };
    })(isRadial);
}

function updateStyle(
    el: BarPossiblePath,
    data: SeriesData,
    dataIndex: number,
    itemModel: Model<BarDataItemOption>,
    layout: RectLayout | SectorLayout,
    seriesModel: BarSeriesModel,
    isHorizontalOrRadial: boolean,
    isPolar: boolean
) {
    const style = data.getItemVisual(dataIndex, 'style');

    if (!isPolar) {
        const borderRadius = itemModel
            .get(['itemStyle', 'borderRadius']) as number | number[] || 0;
        (el as Rect).setShape('r', borderRadius);
    }
    else if (!seriesModel.get('roundCap')) {
        const sectorShape = (el as Sector).shape;
        const cornerRadius = getSectorCornerRadius(
            itemModel.getModel('itemStyle'),
            sectorShape,
            true
        );
        extend(sectorShape, cornerRadius);
        (el as Sector).setShape(sectorShape);
    }

    el.useStyle(style);

    const cursorStyle = itemModel.getShallow('cursor');
    cursorStyle && (el as Path).attr('cursor', cursorStyle);

    const labelPositionOutside = isPolar
        ? (isHorizontalOrRadial
            ? ((layout as SectorLayout).r >= (layout as SectorLayout).r0 ? 'endArc' : 'startArc')
            : ((layout as SectorLayout).endAngle >= (layout as SectorLayout).startAngle
                ? 'endAngle'
                : 'startAngle'
            )
        )
        : (isHorizontalOrRadial
            ? ((layout as RectLayout).height >= 0 ? 'bottom' : 'top')
            : ((layout as RectLayout).width >= 0 ? 'right' : 'left'));

    const labelStatesModels = getLabelStatesModels(itemModel);

    setLabelStyle(
        el, labelStatesModels,
        {
            labelFetcher: seriesModel,
            labelDataIndex: dataIndex,
            defaultText: getDefaultLabel(seriesModel.getData(), dataIndex),
            inheritColor: style.fill as ColorString,
            defaultOpacity: style.opacity,
            defaultOutsidePosition: labelPositionOutside as BuiltinTextPosition
        }
    );

    const label = el.getTextContent();
    if (isPolar && label) {
        const position = itemModel.get(['label', 'position']);
        el.textConfig.inside = position === 'middle' ? true : null;
        setSectorTextRotation(
            el as Sector,
            position === 'outside' ? labelPositionOutside : position,
            createPolarPositionMapping(isHorizontalOrRadial),
            itemModel.get(['label', 'rotate'])
        );
    }

    setLabelValueAnimation(
        label,
        labelStatesModels,
        seriesModel.getRawValue(dataIndex) as ParsedValue,
        (value: number) => getDefaultInterpolatedLabel(data, value)
    );

    const emphasisModel = itemModel.getModel(['emphasis']);
    toggleHoverEmphasis(el, emphasisModel.get('focus'), emphasisModel.get('blurScope'), emphasisModel.get('disabled'));
    setStatesStylesFromModel(el, itemModel);

    if (isZeroOnPolar(layout as SectorLayout)) {
        el.style.fill = 'none';
        el.style.stroke = 'none';
        each(el.states, (state) => {
            if (state.style) {
                state.style.fill = state.style.stroke = 'none';
            }
        });
    }
}

// In case width or height are too small.
function getLineWidth(
    itemModel: Model<BarDataItemOption>,
    rawLayout: RectLayout
) {
    // Has no border.
    const borderColor = itemModel.get(['itemStyle', 'borderColor']);
    if (!borderColor || borderColor === 'none') {
        return 0;
    }
    const lineWidth = itemModel.get(['itemStyle', 'borderWidth']) || 0;
    // width or height may be NaN for empty data
    const width = isNaN(rawLayout.width) ? Number.MAX_VALUE : Math.abs(rawLayout.width);
    const height = isNaN(rawLayout.height) ? Number.MAX_VALUE : Math.abs(rawLayout.height);
    return Math.min(lineWidth, width, height);
}

class LagePathShape {
    points: ArrayLike<number>;
}
interface LargePathProps extends PathProps {
    shape?: LagePathShape
}
class LargePath extends Path<LargePathProps> {
    type = 'largeBar';

    shape: LagePathShape;

    baseDimIdx: number;
    largeDataIndices: ArrayLike<number>;
    barWidth: number;

    constructor(opts?: LargePathProps) {
        super(opts);
    }

    getDefaultShape() {
        return new LagePathShape();
    }

    buildPath(ctx: CanvasRenderingContext2D, shape: LagePathShape) {
        // Drawing lines is more efficient than drawing
        // a whole line or drawing rects.
        const points = shape.points;
        const baseDimIdx = this.baseDimIdx;
        const valueDimIdx = 1 - this.baseDimIdx;
        const startPoint = [];
        const size = [];
        const barWidth = this.barWidth;

        for (let i = 0; i < points.length; i += 3) {
            size[baseDimIdx] = barWidth;
            size[valueDimIdx] = points[i + 2];
            startPoint[baseDimIdx] = points[i + baseDimIdx];
            startPoint[valueDimIdx] = points[i + valueDimIdx];
            ctx.rect(startPoint[0], startPoint[1], size[0], size[1]);
        }
    }
}

function createLarge(
    seriesModel: BarSeriesModel,
    group: Group,
    progressiveEls?: Element[],
    incremental?: boolean
) {
    // TODO support polar
    const data = seriesModel.getData();
    const baseDimIdx = data.getLayout('valueAxisHorizontal') ? 1 : 0;

    const largeDataIndices = data.getLayout('largeDataIndices');
    const barWidth = data.getLayout('size');

    const backgroundModel = seriesModel.getModel('backgroundStyle');
    const bgPoints = data.getLayout('largeBackgroundPoints');

    if (bgPoints) {
        const bgEl = new LargePath({
            shape: {
                points: bgPoints
            },
            incremental: !!incremental,
            silent: true,
            z2: 0
        });
        bgEl.baseDimIdx = baseDimIdx;
        bgEl.largeDataIndices = largeDataIndices;
        bgEl.barWidth = barWidth;
        bgEl.useStyle(backgroundModel.getItemStyle());
        group.add(bgEl);

        progressiveEls && progressiveEls.push(bgEl);
    }

    const el = new LargePath({
        shape: {points: data.getLayout('largePoints')},
        incremental: !!incremental,
        ignoreCoarsePointer: true,
        z2: 1
    });
    el.baseDimIdx = baseDimIdx;
    el.largeDataIndices = largeDataIndices;
    el.barWidth = barWidth;
    group.add(el);
    el.useStyle(data.getVisual('style'));

    // Enable tooltip and user mouse/touch event handlers.
    getECData(el).seriesIndex = seriesModel.seriesIndex;

    if (!seriesModel.get('silent')) {
        el.on('mousedown', largePathUpdateDataIndex);
        el.on('mousemove', largePathUpdateDataIndex);
    }
    progressiveEls && progressiveEls.push(el);
}

// Use throttle to avoid frequently traverse to find dataIndex.
const largePathUpdateDataIndex = throttle(function (this: LargePath, event: ZRElementEvent) {
    const largePath = this;
    const dataIndex = largePathFindDataIndex(largePath, event.offsetX, event.offsetY);
    getECData(largePath).dataIndex = dataIndex >= 0 ? dataIndex : null;
}, 30, false);

function largePathFindDataIndex(largePath: LargePath, x: number, y: number) {
    const baseDimIdx = largePath.baseDimIdx;
    const valueDimIdx = 1 - baseDimIdx;
    const points = largePath.shape.points;
    const largeDataIndices = largePath.largeDataIndices;
    const startPoint = [];
    const size = [];
    const barWidth = largePath.barWidth;

    for (let i = 0, len = points.length / 3; i < len; i++) {
        const ii = i * 3;
        size[baseDimIdx] = barWidth;
        size[valueDimIdx] = points[ii + 2];
        startPoint[baseDimIdx] = points[ii + baseDimIdx];
        startPoint[valueDimIdx] = points[ii + valueDimIdx];
        if (size[valueDimIdx] < 0) {
            startPoint[valueDimIdx] += size[valueDimIdx];
            size[valueDimIdx] = -size[valueDimIdx];
        }

        if (x >= startPoint[0] && x <= startPoint[0] + size[0]
            && y >= startPoint[1] && y <= startPoint[1] + size[1]
        ) {
            return largeDataIndices[i];
        }
    }

    return -1;
}

function createBackgroundShape(
    isHorizontalOrRadial: boolean,
    layout: SectorLayout | RectLayout,
    coord: CoordSysOfBar
): SectorShape | RectShape {
    if (isCoordinateSystemType<Cartesian2D>(coord, 'cartesian2d')) {
        const rectShape = layout as RectShape;
        const coordLayout = coord.getArea();
        return {
            x: isHorizontalOrRadial ? rectShape.x : coordLayout.x,
            y: isHorizontalOrRadial ? coordLayout.y : rectShape.y,
            width: isHorizontalOrRadial ? rectShape.width : coordLayout.width,
            height: isHorizontalOrRadial ? coordLayout.height : rectShape.height
        } as RectShape;
    }
    else {
        const coordLayout = coord.getArea();
        const sectorShape = layout as SectorShape;
        return {
            cx: coordLayout.cx,
            cy: coordLayout.cy,
            r0: isHorizontalOrRadial ? coordLayout.r0 : sectorShape.r0,
            r: isHorizontalOrRadial ? coordLayout.r : sectorShape.r,
            startAngle: isHorizontalOrRadial ? sectorShape.startAngle : 0,
            endAngle: isHorizontalOrRadial ? sectorShape.endAngle : Math.PI * 2
        } as SectorShape;
    }
}

function createBackgroundEl(
    coord: CoordSysOfBar,
    isHorizontalOrRadial: boolean,
    layout: SectorLayout | RectLayout
): Rect | Sector {
    const ElementClz = coord.type === 'polar' ? Sector : Rect;
    return new ElementClz({
        shape: createBackgroundShape(isHorizontalOrRadial, layout, coord) as any,
        silent: true,
        z2: 0
    });
}

export default BarView;
