/*
* 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.
*/

/**
 * Grid is a region which contains at most 4 cartesian systems
 *
 * TODO Default cartesian
 */

import {isObject, each, indexOf, retrieve3, keys, assert, eqNaN, find, retrieve2, hasOwn} from 'zrender/src/core/util';
import {BoxLayoutReferenceResult, createBoxLayoutReference, getLayoutRect, LayoutRect} from '../../util/layout';
import {
    createScaleByModel,
    getScaleValuePositionKind,
    isNameLocationCenter,
    shouldAxisShow,
    retrieveAxisBreaksOption,
    determineAxisType,
    isOnAxisZeroDiscouraged,
    SCALE_VALUE_POSITION_KIND_OUTSIDE,
    getTickValueOutermost,
    isAxisOnBand,
} from '../../coord/axisHelper';
import Cartesian2D, {cartesian2DDimensions} from './Cartesian2D';
import Axis2D from './Axis2D';
import {ParsedModelFinder, ParsedModelFinderKnown, SINGLE_REFERRING} from '../../util/model';

// Depends on GridModel, AxisModel, which performs preprocess.
import GridModel, {
    COORD_SYS_TYPE_CARTESIAN_2D, GridOption, OUTER_BOUNDS_CLAMP_DEFAULT, OUTER_BOUNDS_DEFAULT
} from './GridModel';
import CartesianAxisModel from './AxisModel';
import GlobalModel from '../../model/Global';
import ExtensionAPI from '../../core/ExtensionAPI';
import { Dictionary } from 'zrender/src/core/types';
import {CoordinateSystemMaster} from '../CoordinateSystem';
import { NullUndefined, ScaleDataValue } from '../../util/types';
import {
    findAxisModels,
    createCartesianAxisViewCommonPartBuilder,
    updateCartesianAxisViewCommonPartBuilder,
} from './cartesianAxisHelper';
import { AxisBaseOptionCommon, NumericAxisBaseOptionCommon } from '../axisCommonTypes';
import { AxisBaseModel } from '../AxisBaseModel';
import { isIntervalOrLogScale, isOrdinalScale } from '../../scale/helper';
import { scaleCalcAlign } from '../axisAlignTicks';
import IntervalScale from '../../scale/Interval';
import LogScale from '../../scale/Log';
import { BoundingRect, expandOrShrinkRect, WH, XY } from '../../util/graphic';
import {
    AxisBuilderSharedContext,
    resolveAxisNameOverlapDefault,
    moveIfOverlapByLinearLabels,
    getLabelInner,
} from '../../component/axis/AxisBuilder';
import { error, log } from '../../util/log';
import { AxisTickLabelComputingKind } from '../axisTickLabelBuilder';
import { injectCoordSysByOption } from '../../core/CoordinateSystem';
import { mathMax, parsePositionSizeOption } from '../../util/number';
import { scaleCalcNice } from '../axisNiceTicks';
import { createDimNameMap } from '../../data/helper/SeriesDataSchema';
import type Axis from '../Axis';
import {
    AXIS_EXTENT_INFO_BUILD_FROM_COORD_SYS_UPDATE,
    scaleRawExtentInfoEnableBoxCoordSysUsage, scaleRawExtentInfoCreate
} from '../scaleRawExtentInfo';
import { hasBreaks } from '../../scale/break';
import { associateSeriesWithAxis } from '../axisStatistics';


type Cartesian2DDimensionName = 'x' | 'y';

type FinderAxisIndex = {xAxisIndex?: number, yAxisIndex?: number};
type AxesMap = {
    x: Axis2D[],
    y: Axis2D[]
};

type ParsedOuterBoundsContain = 'all' | 'axisLabel';

// margin is [top, right, bottom, left]
const XY_TO_MARGIN_IDX = [
    [3, 1], // xyIdx 0 => 'x'
    [0, 2]  // xyIdx 1 => 'y'
] as const;

class Grid implements CoordinateSystemMaster {

    // FIXME:TS where used (different from registered type 'cartesian2d')?
    readonly type: string = 'grid';

    private _coordsMap: Dictionary<Cartesian2D> = {};
    private _coordsList: Cartesian2D[] = [];
    private _axesMap: AxesMap = {} as AxesMap;
    private _axesList: Axis2D[] = [];
    private _rect: LayoutRect;

    readonly model: GridModel;
    readonly axisPointerEnabled = true;

    // Injected:
    name: string;

    // For deciding which dimensions to use when creating list data
    static dimensions = cartesian2DDimensions;
    readonly dimensions = cartesian2DDimensions;
    static dimIdxMap = createDimNameMap(cartesian2DDimensions);

    constructor(gridModel: GridModel, ecModel: GlobalModel, api: ExtensionAPI) {
        this._initCartesian(gridModel, ecModel, api);
        this.model = gridModel;
    }

    getRect(): LayoutRect {
        return this._rect;
    }

    update(ecModel: GlobalModel, api: ExtensionAPI): void {

        const axesMap = this._axesMap;

        each(this._axesList, function (axis) {
            scaleRawExtentInfoCreate(axis, AXIS_EXTENT_INFO_BUILD_FROM_COORD_SYS_UPDATE);
            const scale = axis.scale;
            if (isOrdinalScale(scale)) {
                scale.setSortInfo(axis.model.get('categorySortInfo'));
            }
        });

        function updateAxisTicks(axes: Record<number, Axis2D>) {
            // Axis is added in order of axisIndex.
            const axesIndices = keys(axes);
            const axisNeedsAlign: Axis2D[] = [];

            for (let i = axesIndices.length - 1; i >= 0; i--) { // Reverse order
                const axis = axes[+axesIndices[i]];
                if (axis.__alignTo) {
                    axisNeedsAlign.push(axis);
                }
                else {
                    scaleCalcNice(axis);
                }
            };
            each(axisNeedsAlign, axis => {
                if (incapableOfAlignNeedFallback(axis, axis.__alignTo as Axis2D)) {
                    scaleCalcNice(axis);
                }
                else {
                    scaleCalcAlign(
                        axis,
                        axis.__alignTo.scale as IntervalScale | LogScale
                    );
                }
            });
        }

        updateAxisTicks(axesMap.x);
        updateAxisTicks(axesMap.y);

        // Key: axisDim_axisIndex, value: boolean, whether onZero target.
        const onZeroRecords = {} as Dictionary<boolean>;

        each(axesMap.x, function (xAxis) {
            fixAxisOnZero(axesMap, 'y', xAxis, onZeroRecords);
        });
        each(axesMap.y, function (yAxis) {
            fixAxisOnZero(axesMap, 'x', yAxis, onZeroRecords);
        });

        // Resize again if containLabel is enabled
        // FIXME It may cause getting wrong grid size in data processing stage
        this.resize(this.model, api);
    }

    /**
     * Resize the grid.
     *
     * [NOTE]
     * If both "grid.containLabel/grid.contain" and pixel-required-data-processing (such as, "dataSampling")
     * exist, circular dependency occurs in logic.
     * The final compromised sequence is:
     *  1. Calculate "axis.extent" (pixel extent) and AffineTransform based on only "grid layout options".
     *      Not accurate if "grid.containLabel/grid.contain" is required, but it is a compromise to avoid
     *      circular dependency.
     *  2. Perform "series data processing" (where "dataSampling" requires "axis.extent").
     *  3. Calculate "scale.extent" (data extent) based on "processed series data".
     *  4. Modify "axis.extent" for "grid.containLabel/grid.contain":
     *      4.1. Calculate "axis labels" based on "scale.extent".
     *      4.2. Modify "axis.extent" by the bounding rects of "axis labels and names".
     */
    resize(gridModel: GridModel, api: ExtensionAPI, beforeDataProcessing?: boolean): void {

        const layoutRef = createBoxLayoutReference(gridModel, api);
        const gridRect = this._rect = getLayoutRect(gridModel.getBoxLayoutParams(), layoutRef.refContainer);
        // PENDING: whether to support that if the input `coord` is out of the base coord sys,
        //  do not render anything. At present, the behavior is undefined.

        const axesMap = this._axesMap;
        const coordsList = this._coordsList;

        const optionContainLabel = gridModel.get('containLabel'); // No `.get(, true)` for backward compat.

        // NOTE: The axis pixel extent is also required by some estimation, e.g., in coord sys update stage,
        // bars on 'time'/'value' axis need it to calculate the supplementary scale extent to avoid edge bars
        // overflowing the axis (see `barGrid.ts`). Therefore, axis pixel extent need to be set early, even
        // may not be accurate.
        updateAllAxisExtentTransByGridRect(axesMap, gridRect);

        if (!beforeDataProcessing) {
            const axisBuilderSharedCtx = createAxisBiulders(gridRect, coordsList, axesMap, optionContainLabel, api);

            let noPxChange: boolean;
            if (optionContainLabel) {
                if (legacyLayOutGridByContainLabel) {
                    // console.time('legacyLayOutGridByContainLabel');
                    legacyLayOutGridByContainLabel(this._axesList, gridRect);
                    updateAllAxisExtentTransByGridRect(axesMap, gridRect);
                    // console.timeEnd('legacyLayOutGridByContainLabel');
                }
                else {
                    if (__DEV__) {
                        log('Specified `grid.containLabel` but no `use(LegacyGridContainLabel)`;'
                            + 'use `grid.outerBounds` instead.',
                            true
                        );
                    }
                    noPxChange = layOutGridByOuterBounds(
                        gridRect.clone(), 'axisLabel', null, gridRect, axesMap, axisBuilderSharedCtx, layoutRef
                    );
                }
            }
            else {
                const {outerBoundsRect, parsedOuterBoundsContain, outerBoundsClamp} = prepareOuterBounds(
                    gridModel, gridRect, layoutRef
                );
                if (outerBoundsRect) {
                    // console.time('layOutGridByOuterBounds');
                    noPxChange = layOutGridByOuterBounds(
                        outerBoundsRect, parsedOuterBoundsContain, outerBoundsClamp,
                        gridRect, axesMap, axisBuilderSharedCtx, layoutRef
                    );
                    // console.timeEnd('layOutGridByOuterBounds');
                }
            }

            // console.time('buildAxesView_determine');
            createOrUpdateAxesView(
                gridRect,
                axesMap,
                AxisTickLabelComputingKind.determine,
                null,
                noPxChange,
                layoutRef
            );
            // console.timeEnd('buildAxesView_determine');

            each(this._coordsList, function (coord) {
                // Calculate affine matrix to accelerate the data to point transform.
                // If all the axes scales are time or value.
                coord.calcAffineTransform();
            });
        } // End of beforeDataProcessing
    }

    getAxis(dim: Cartesian2DDimensionName, axisIndex?: number): Axis2D {
        const axesMapOnDim = this._axesMap[dim];
        if (axesMapOnDim != null) {
            return axesMapOnDim[axisIndex || 0];
        }
    }

    getAxes(): Axis2D[] {
        return this._axesList.slice();
    }

    /**
     * Usage:
     *      grid.getCartesian(xAxisIndex, yAxisIndex);
     *      grid.getCartesian(xAxisIndex);
     *      grid.getCartesian(null, yAxisIndex);
     *      grid.getCartesian({xAxisIndex: ..., yAxisIndex: ...});
     *
     * When only xAxisIndex or yAxisIndex given, find its first cartesian.
     */
    getCartesian(finder: FinderAxisIndex): Cartesian2D;
    getCartesian(xAxisIndex?: number, yAxisIndex?: number): Cartesian2D;
    getCartesian(xAxisIndex?: number | FinderAxisIndex, yAxisIndex?: number) {
        if (xAxisIndex != null && yAxisIndex != null) {
            const key = 'x' + xAxisIndex + 'y' + yAxisIndex;
            return this._coordsMap[key];
        }

        if (isObject(xAxisIndex)) {
            yAxisIndex = (xAxisIndex as FinderAxisIndex).yAxisIndex;
            xAxisIndex = (xAxisIndex as FinderAxisIndex).xAxisIndex;
        }
        for (let i = 0, coordList = this._coordsList; i < coordList.length; i++) {
            if (coordList[i].getAxis('x').index === xAxisIndex
                || coordList[i].getAxis('y').index === yAxisIndex
            ) {
                return coordList[i];
            }
        }
    }

    getCartesians(): Cartesian2D[] {
        return this._coordsList.slice();
    }

    /**
     * @implements
     */
    convertToPixel(
        ecModel: GlobalModel, finder: ParsedModelFinder, value: ScaleDataValue | ScaleDataValue[]
    ): number | number[] {
        const target = this._findConvertTarget(finder);

        return target.cartesian
            ? target.cartesian.dataToPoint(value as ScaleDataValue[])
            : target.axis
            ? target.axis.toGlobalCoord(target.axis.dataToCoord(value as ScaleDataValue))
            : null;
    }

    /**
     * @implements
     */
    convertFromPixel(
        ecModel: GlobalModel, finder: ParsedModelFinder, value: number | number[]
    ): number | number[] {
        const target = this._findConvertTarget(finder);

        return target.cartesian
            ? target.cartesian.pointToData(value as number[])
            : target.axis
            ? target.axis.coordToData(target.axis.toLocalCoord(value as number))
            : null;
    }

    private _findConvertTarget(finder: ParsedModelFinderKnown): {
        cartesian: Cartesian2D,
        axis: Axis2D
    } {
        const seriesModel = finder.seriesModel;
        const xAxisModel = finder.xAxisModel
            || (seriesModel && seriesModel.getReferringComponents('xAxis', SINGLE_REFERRING).models[0]);
        const yAxisModel = finder.yAxisModel
            || (seriesModel && seriesModel.getReferringComponents('yAxis', SINGLE_REFERRING).models[0]);
        const gridModel = finder.gridModel;
        const coordsList = this._coordsList;
        let cartesian: Cartesian2D;
        let axis;

        if (seriesModel) {
            cartesian = seriesModel.coordinateSystem as Cartesian2D;
            indexOf(coordsList, cartesian) < 0 && (cartesian = null);
        }
        else if (xAxisModel && yAxisModel) {
            cartesian = this.getCartesian(xAxisModel.componentIndex, yAxisModel.componentIndex);
        }
        else if (xAxisModel) {
            axis = this.getAxis('x', xAxisModel.componentIndex);
        }
        else if (yAxisModel) {
            axis = this.getAxis('y', yAxisModel.componentIndex);
        }
        // Lowest priority.
        else if (gridModel) {
            const grid = gridModel.coordinateSystem;
            if (grid === this) {
                cartesian = this._coordsList[0];
            }
        }

        return {cartesian: cartesian, axis: axis};
    }

    /**
     * @implements
     */
    containPoint(point: number[]): boolean {
        const coord = this._coordsList[0];
        if (coord) {
            return coord.containPoint(point);
        }
    }

    /**
     * Initialize cartesian coordinate systems
     */
    private _initCartesian(
        gridModel: GridModel, ecModel: GlobalModel, api: ExtensionAPI
    ): void {
        const grid = this;
        const axisPositionUsed = {
            left: false,
            right: false,
            top: false,
            bottom: false
        };

        const axesMap = {
            x: {},
            y: {}
        } as AxesMap;
        const axesCount = {
            x: 0,
            y: 0
        };

        // Create axis
        ecModel.eachComponent('xAxis', createAxisCreator('x'), this);
        ecModel.eachComponent('yAxis', createAxisCreator('y'), this);

        if (!axesCount.x || !axesCount.y) {
            // Roll back when there no either x or y axis
            this._axesMap = {} as AxesMap;
            this._axesList = [];
            return;
        }

        this._axesMap = axesMap;

        // Create cartesian2d
        each(axesMap.x, (xAxis, xAxisIndex) => {
            each(axesMap.y, (yAxis, yAxisIndex) => {
                const key = 'x' + xAxisIndex + 'y' + yAxisIndex;
                const cartesian = new Cartesian2D(key);

                cartesian.master = this;
                cartesian.model = gridModel;

                this._coordsMap[key] = cartesian;
                this._coordsList.push(cartesian);

                cartesian.addAxis(xAxis);
                cartesian.addAxis(yAxis);
            });
        });

        prepareAlignToInCoordSysCreate(axesMap.x);
        prepareAlignToInCoordSysCreate(axesMap.y);

        function createAxisCreator(dimName: Cartesian2DDimensionName) {
            return function (axisModel: CartesianAxisModel, idx: number): void {
                if (!isAxisUsedInTheGrid(axisModel, gridModel)) {
                    return;
                }

                let axisPosition = axisModel.get('position');
                if (dimName === 'x') {
                    // Fix position
                    if (axisPosition !== 'top' && axisPosition !== 'bottom') {
                        // Default bottom of X
                        axisPosition = axisPositionUsed.bottom ? 'top' : 'bottom';
                    }
                }
                else {
                    // Fix position
                    if (axisPosition !== 'left' && axisPosition !== 'right') {
                        // Default left of Y
                        axisPosition = axisPositionUsed.left ? 'right' : 'left';
                    }
                }
                axisPositionUsed[axisPosition] = true;

                const axisType = determineAxisType(axisModel);
                const axis = new Axis2D(
                    dimName,
                    createScaleByModel(axisModel, axisType, true),
                    [0, 0],
                    axisType,
                    axisPosition
                );

                axis.onBand = isAxisOnBand(axis.scale, axisModel);
                axis.inverse = axisModel.get('inverse');

                // Inject axis into axisModel
                axisModel.axis = axis;

                // Inject axisModel into axis
                axis.model = axisModel;

                // Inject grid info axis
                axis.grid = grid;

                // Index of axis, can be used as key
                axis.index = idx;

                grid._axesList.push(axis);

                axesMap[dimName][idx] = axis;
                axesCount[dimName]++;
            };
        }
    }

    /**
     * @param dim 'x' or 'y' or 'auto' or null/undefined
     */
    getTooltipAxes(dim: Cartesian2DDimensionName | 'auto'): {
        baseAxes: Axis2D[], otherAxes: Axis2D[]
    } {
        const baseAxes = [] as Axis2D[];
        const otherAxes = [] as Axis2D[];

        each(this.getCartesians(), function (cartesian) {
            const baseAxis = (dim != null && dim !== 'auto')
                ? cartesian.getAxis(dim) : cartesian.getBaseAxis();
            const otherAxis = cartesian.getOtherAxis(baseAxis);
            indexOf(baseAxes, baseAxis) < 0 && baseAxes.push(baseAxis);
            indexOf(otherAxes, otherAxis) < 0 && otherAxes.push(otherAxis);
        });

        return {baseAxes: baseAxes, otherAxes: otherAxes};
    }


    static create(ecModel: GlobalModel, api: ExtensionAPI): Grid[] {
        const grids = [] as Grid[];
        ecModel.eachComponent('grid', function (gridModel: GridModel, idx) {
            const grid = new Grid(gridModel, ecModel, api);
            grid.name = 'grid_' + idx;
            // dataSampling requires axis extent, so resize
            // should be performed in create stage.
            grid.resize(gridModel, api, true);

            gridModel.coordinateSystem = grid;

            grids.push(grid);

            each(grid._axesList, function (axis) {
                scaleRawExtentInfoEnableBoxCoordSysUsage(axis, Grid.dimIdxMap);
            });
        });

        // Inject the coordinateSystems into seriesModel
        ecModel.eachSeries(function (seriesModel) {
            let xAxis: Axis;
            let yAxis: Axis;

            injectCoordSysByOption({
                targetModel: seriesModel,
                coordSysType: COORD_SYS_TYPE_CARTESIAN_2D,
                coordSysProvider: coordSysProvider
            });

            function coordSysProvider() {
                const axesModelMap = findAxisModels(seriesModel);
                const xAxisModel = axesModelMap.xAxisModel;
                const yAxisModel = axesModelMap.yAxisModel;
                xAxis = xAxisModel.axis;
                yAxis = yAxisModel.axis;

                const gridModel = xAxisModel.getCoordSysModel();

                if (__DEV__) {
                    if (!gridModel) {
                        throw new Error(
                            'Grid "' + retrieve3(
                                xAxisModel.get('gridIndex'),
                                xAxisModel.get('gridId'),
                                0
                            ) + '" not found'
                        );
                    }
                    if (xAxisModel.getCoordSysModel() !== yAxisModel.getCoordSysModel()) {
                        throw new Error('xAxis and yAxis must use the same grid');
                    }
                }

                const grid = gridModel.coordinateSystem as Grid;

                return grid.getCartesian(
                    xAxisModel.componentIndex, yAxisModel.componentIndex
                );
            }
            if (xAxis && yAxis) {
                associateSeriesWithAxis(xAxis, seriesModel, COORD_SYS_TYPE_CARTESIAN_2D);
                associateSeriesWithAxis(yAxis, seriesModel, COORD_SYS_TYPE_CARTESIAN_2D);
            }

        }, this);

        return grids;
    }

}

/**
 * Check if the axis is used in the specified grid.
 */
function isAxisUsedInTheGrid(axisModel: CartesianAxisModel, gridModel: GridModel): boolean {
    return axisModel.getCoordSysModel() === gridModel;
}

function fixAxisOnZero(
    axesMap: AxesMap,
    otherAxisDim: Cartesian2DDimensionName,
    axis: Axis2D,
    // Key: see `getOnZeroRecordKey`
    onZeroRecords: Dictionary<boolean>
): void {

    axis.getAxesOnZeroOf = function () {
        // TODO: onZero of multiple axes.
        return otherAxisOnZeroOf ? [otherAxisOnZeroOf] : [];
    };

    // onZero can not be enabled in these two situations:
    // 1. When any other axis is a category axis.
    // 2. When no axis is cross 0 point.
    const otherAxes = axesMap[otherAxisDim];

    let otherAxisOnZeroOf: Axis2D;
    const axisModel = axis.model;
    const onZero = axisModel.get(['axisLine', 'onZero']);
    const onZeroAxisIndex = axisModel.get(['axisLine', 'onZeroAxisIndex']);

    // For historical reason, ec option `axisLine.onZero: undefined` leads to "not on zero"
    // while leaving `axisLine.onZero` unspecified causes "on zero". This inconsistency goes
    // against common sense, but is preserved for backward compatibility.
    if (!onZero) {
        return;
    }

    // If target axis is specified.
    if (onZeroAxisIndex != null) {
        if (canOnZeroToAxis(onZero, otherAxes[onZeroAxisIndex])) {
            otherAxisOnZeroOf = otherAxes[onZeroAxisIndex];
        }
    }
    else {
        // Find the first available other axis.
        for (const idx in otherAxes) {
            if (hasOwn(otherAxes, idx)
                && canOnZeroToAxis(onZero, otherAxes[idx])
                // Consider that two Y axes on one value axis,
                // if both onZero, the two Y axes overlap.
                && !onZeroRecords[getOnZeroRecordKey(otherAxes[idx])]
            ) {
                otherAxisOnZeroOf = otherAxes[idx];
                break;
            }
        }
    }

    if (otherAxisOnZeroOf) {
        onZeroRecords[getOnZeroRecordKey(otherAxisOnZeroOf)] = true;
    }

    function getOnZeroRecordKey(axis: Axis2D) {
        return axis.dim + '_' + axis.index;
    }
}

/**
 * CAVEAT: Must not be called before `CoordinateSystem#update` due to `__dontOnMyZero`.
 */
function canOnZeroToAxis(
    onZeroOption: AxisBaseOptionCommon['axisLine']['onZero'],
    axis: Axis2D | NullUndefined
): boolean {
    if (!axis) {
        return false;
    }
    const scale = axis.scale;
    const kindEffective = getScaleValuePositionKind(scale, 0, false);

    let can = axis
        // PENDING: Historical behavior: `onZero` on 'category' and 'time' axis are always disabled
        // even if ec option gives `onZero: true`.
        && axis.type !== 'category' && axis.type !== 'time'
        // NOTE: Although the portion out of "effective" portion may also cross zero
        // (see `SCALE_EXTENT_KIND_MAPPING`), that is commonly meaningless, so we use
        // `SCALE_EXTENT_KIND_EFFECTIVE`
        && kindEffective !== SCALE_VALUE_POSITION_KIND_OUTSIDE;

    if (can && onZeroOption === 'auto'
        // Historically, "value" axis and "log" axis has been using `onZero: true` as the default.
        // It suitable for mathematic cases, even when dataZoom exists (e.g., `clip.html`), or cases
        // need to distinguish positive and negative data. However, it probably causes odd effect if
        // a "value axis" is laid on zero of a "base axis" in bar/candlestick, where the axis line
        // would likely cross shapes when `SCALE_EXTENT_KIND_MAPPING` is applied.
        // Therefore, we preserve backward compatibility of the default `onZero: true`, but exclude
        // cases that `containShape` is applied.
        && (
            isOnAxisZeroDiscouraged(axis)
            // || (
            //     // Avoid axis line cross series shape (typically, bar series on "value"/"time" axis) unexpectedly.
            //     kindEffective === SCALE_VALUE_POSITION_KIND_EDGE
            //     && getScaleValuePositionKind(scale, 0, true) === SCALE_VALUE_POSITION_KIND_INSIDE
            // )
        )
    ) {
        can = false;
    }
    // falsy value of `onZeroOption` has been handled in the previous logic.
    return can;
}

/**
 * [CAVEAT] This method is called before data processing stage.
 *  Do not rely on any info that is determined afterward.
 */
function prepareAlignToInCoordSysCreate(axes: Record<number, Axis2D>): void {
    // Axis is added in order of axisIndex.
    const axesIndices = keys(axes);

    let alignTo: Axis2D;
    const axisNeedsAlign: Axis2D[] = [];

    for (let i = axesIndices.length - 1; i >= 0; i--) { // Reverse order
        const axis = axes[+axesIndices[i]];
        if (
            isIntervalOrLogScale(axis.scale)
            // NOTE: `scale.hasBreaks()` is not available at this moment. Check it later.
            && retrieveAxisBreaksOption(axis.model, axis.type, true) == null
            // NOTE: `scale.getTicks()` is not available at this moment. Check it later.
        ) {
            // Request `alignTicks`.
            if ((axis.model as AxisBaseModel<NumericAxisBaseOptionCommon>).get('alignTicks')
                && (axis.model as AxisBaseModel<NumericAxisBaseOptionCommon>).get('interval') == null
            ) {
                axisNeedsAlign.push(axis);
            }
            else {
                // `alignTo` the last one that does not request `alignTicks`
                // (This rule is retained for backward compat).
                alignTo = axis;
            }
        }
    };
    // If all axes has set alignTicks, pick the first one as alignTo.
    // PENDING. Should we find the axis that both set interval, min, max and align to this one?
    // PENDING. Should we allow specifying alignTo via ec option?
    if (!alignTo) {
        alignTo = axisNeedsAlign.pop();
    }
    if (alignTo) {
        each(axisNeedsAlign, function (axis) {
            axis.__alignTo = alignTo;
        });
    }
}

/**
 * This is just a defence code. They are unlikely to be actually `true`,
 * since these cases have been addressed in `prepareAlignToInCoordSysCreate`.
 *
 * Can not be called BEFORE "nice" performed.
 */
function incapableOfAlignNeedFallback(targetAxis: Axis2D, alignTo: Axis2D): boolean {
    return hasBreaks(targetAxis.scale)
        || hasBreaks(alignTo.scale)
        // Normally ticks length are more than 2 even when axis is blank.
        // But still guard for corner cases and possible changes.
        || alignTo.scale.getTicks().length < 2;
}


function updateAxisTransform(axis: Axis2D, coordBase: number) {
    const axisExtent = axis.getExtent();
    const axisExtentSum = axisExtent[0] + axisExtent[1];

    // Fast transform
    axis.toGlobalCoord = axis.dim === 'x'
        ? function (coord) {
            return coord + coordBase;
        }
        : function (coord) {
            return axisExtentSum - coord + coordBase;
        };
    axis.toLocalCoord = axis.dim === 'x'
        ? function (coord) {
            return coord - coordBase;
        }
        : function (coord) {
            return axisExtentSum - coord + coordBase;
        };
}

function updateAllAxisExtentTransByGridRect(axesMap: AxesMap, gridRect: LayoutRect) {
    each(axesMap.x, axis => updateAxisExtentTransByGridRect(axis, gridRect.x, gridRect.width));
    each(axesMap.y, axis => updateAxisExtentTransByGridRect(axis, gridRect.y, gridRect.height));
}

function updateAxisExtentTransByGridRect(axis: Axis2D, gridXY: number, gridWH: number): void {
    const extent = [0, gridWH];
    const idx = axis.inverse ? 1 : 0;
    axis.setExtent(extent[idx], extent[1 - idx]);
    updateAxisTransform(axis, gridXY);
}

export type LegacyLayOutGridByContainLabel = (axesList: Axis2D[], gridRect: LayoutRect) => void;
let legacyLayOutGridByContainLabel: LegacyLayOutGridByContainLabel | NullUndefined;
export function registerLegacyGridContainLabelImpl(impl: LegacyLayOutGridByContainLabel): void {
    legacyLayOutGridByContainLabel = impl;
}

// Return noPxChange.
function layOutGridByOuterBounds(
    outerBoundsRect: BoundingRect,
    outerBoundsContain: ParsedOuterBoundsContain,
    outerBoundsClamp: number[] | NullUndefined,
    gridRect: LayoutRect,
    axesMap: AxesMap,
    axisBuilderSharedCtx: AxisBuilderSharedContext,
    layoutRef: BoxLayoutReferenceResult
): boolean {
    if (__DEV__) {
        assert(outerBoundsContain === 'all' || outerBoundsContain === 'axisLabel');
    }
    // Assume `updateAllAxisExtentTransByGridRect` has been performed once before this call.

    // [NOTE]:
    // - The bounding rect of the axis elements might be sensitve to variations in `axis.extent` due to strategies
    //  like hideOverlap/moveOverlap. @see the comment in `LabelLayoutBase['suggestIgnore']`.
    // - The final `gridRect` might be slightly smaller than the ideally expected result if labels are giant and
    //  get hidden due to overlapping. More iterations could improve precision, but not performant. We consider
    //  the current result acceptable, since no alignment among charts can be guaranteed when using this feature.
    createOrUpdateAxesView(
        gridRect,
        axesMap,
        AxisTickLabelComputingKind.estimate,
        outerBoundsContain,
        false,
        layoutRef
    );

    const margin = [0, 0, 0, 0];

    fillLabelNameOverflowOnOneDimension(0);
    fillLabelNameOverflowOnOneDimension(1);
    // If axis is blank, no label can be used to detect overflow.
    // gridRect itself should not overflow.
    fillMarginOnOneDimension(gridRect, 0, NaN);
    fillMarginOnOneDimension(gridRect, 1, NaN);

    const noPxChange = find(margin, item => item > 0) == null;
    expandOrShrinkRect(gridRect, margin, true, true, outerBoundsClamp);

    updateAllAxisExtentTransByGridRect(axesMap, gridRect);

    return noPxChange;

    function fillLabelNameOverflowOnOneDimension(xyIdx: number): void {
        each(axesMap[XY[xyIdx]], axis => {
            if (!shouldAxisShow(axis.model)) {
                return;
            }
            // FIXME: zr Group.union may wrongly union (0, 0, 0, 0) and not performant.
            // unionRect.union(axis.axisBuilder.group.getBoundingRect());

            // If ussing Group.getBoundingRect to calculate shrink space, it is not strictly accurate when
            // the outermost label is ignored and the secondary label is very long and contribute to the
            // union extension:
            //      -|---|---|---|
            //         1,000,000,000
            // Therefore we calculate them one by one.
            // Also considered axis may be blank or no labels.
            const sharedRecord = axisBuilderSharedCtx.ensureRecord(axis.model);
            const labelInfoList = sharedRecord.labelInfoList;
            if (labelInfoList) {
                for (let idx = 0; idx < labelInfoList.length; idx++) {
                    const labelInfo = labelInfoList[idx];
                    let proportion = axis.scale.normalize(
                        getTickValueOutermost(axis.scale, getLabelInner(labelInfo.label).labelInfo.tick)
                    );
                    proportion = xyIdx === 1 ? 1 - proportion : proportion;
                    // xAxis use proportion on x, yAxis use proprotion on y, otherwise not.
                    fillMarginOnOneDimension(labelInfo.rect, xyIdx, proportion);
                    fillMarginOnOneDimension(labelInfo.rect, 1 - xyIdx, NaN);
                }
            }
            const nameLayout = sharedRecord.nameLayout;
            if (nameLayout) {
                const proportion = isNameLocationCenter(sharedRecord.nameLocation) ? 0.5 : NaN;
                fillMarginOnOneDimension(nameLayout.rect, xyIdx, proportion);
                fillMarginOnOneDimension(nameLayout.rect, 1 - xyIdx, NaN);
            }
        });
    }

    function fillMarginOnOneDimension(
        itemRect: BoundingRect,
        xyIdx: number,
        proportion: number // NaN mean no use proportion
    ): void {
        let overflow1 = outerBoundsRect[XY[xyIdx]] - itemRect[XY[xyIdx]];
        let overflow2 = (itemRect[WH[xyIdx]] + itemRect[XY[xyIdx]])
            - (outerBoundsRect[WH[xyIdx]] + outerBoundsRect[XY[xyIdx]]);
        overflow1 = applyProportion(overflow1, 1 - proportion);
        overflow2 = applyProportion(overflow2, proportion);
        const minIdx = XY_TO_MARGIN_IDX[xyIdx][0];
        const maxIdx = XY_TO_MARGIN_IDX[xyIdx][1];
        margin[minIdx] = mathMax(margin[minIdx], overflow1);
        margin[maxIdx] = mathMax(margin[maxIdx], overflow2);
    }

    function applyProportion(overflow: number, proportion: number): number {
        // proportion is not likely to near zero. If so, give up shrink
        if (overflow > 0 && !eqNaN(proportion) && proportion > 1e-4) {
            overflow /= proportion;
        }
        return overflow;
    }
}

function createAxisBiulders(
    gridRect: LayoutRect,
    cartesians: Cartesian2D[],
    axesMap: AxesMap,
    optionContainLabel: GridOption['containLabel'],
    api: ExtensionAPI,
): AxisBuilderSharedContext {
    const axisBuilderSharedCtx = new AxisBuilderSharedContext(resolveAxisNameOverlapForGrid);
    each(axesMap, axisList => each(axisList, axis => {
        if (shouldAxisShow(axis.model)) {
            // See `AxisBaseOptionCommon['nameMoveOverlap']`.
            const defaultNameMoveOverlap = !optionContainLabel;
            axis.axisBuilder = createCartesianAxisViewCommonPartBuilder(
                gridRect, cartesians, axis.model, api, axisBuilderSharedCtx, defaultNameMoveOverlap
            );
        }
    }));
    return axisBuilderSharedCtx;
}

/**
 * Promote the axis-elements-building from "view render" stage to "coordinate system resize" stage.
 * This is aimed to resovle overlap across multiple axes, since currently it's hard to reconcile
 * multiple axes in "view render" stage.
 *
 * [CAUTION] But this promotion assumes that the subsequent "visual mapping" stage does not affect
 * this axis-elements-building; otherwise we have to refactor it again.
 */
function createOrUpdateAxesView(
    gridRect: LayoutRect,
    axesMap: AxesMap,
    kind: AxisTickLabelComputingKind,
    outerBoundsContain: ParsedOuterBoundsContain | NullUndefined,
    noPxChange: boolean,
    layoutRef: BoxLayoutReferenceResult
): void {
    const isDetermine = kind === AxisTickLabelComputingKind.determine;
    each(axesMap, axisList => each(axisList, axis => {
        if (shouldAxisShow(axis.model)) {
            updateCartesianAxisViewCommonPartBuilder(axis.axisBuilder, gridRect, axis.model);
            axis.axisBuilder.build(
                isDetermine
                    ? {axisTickLabelDetermine: true}
                    : {axisTickLabelEstimate: true},
                {noPxChange}
            );
        }
    }));

    const nameMarginLevelMap = {x: 0, y: 0};
    calcNameMarginLevel(0);
    calcNameMarginLevel(1);
    function calcNameMarginLevel(xyIdx: number): void {
        nameMarginLevelMap[XY[1 - xyIdx]] = gridRect[WH[xyIdx]] <= layoutRef.refContainer[WH[xyIdx]] * 0.5
            ? 0 : ((1 - xyIdx) === 1 ? 2 : 1);
    }

    each(axesMap, (axisList, xy) => each(axisList, axis => {
        if (shouldAxisShow(axis.model)) {
            if (outerBoundsContain === 'all' || isDetermine) {
                // To resolve overlap, `axisName` layout depends on `axisTickLabel` layout result
                // (all of the axes of the same `grid`; consider multiple x or y axes).
                axis.axisBuilder.build({axisName: true}, {nameMarginLevel: nameMarginLevelMap[xy]});
            }
            if (isDetermine) {
                axis.axisBuilder.build({axisLine: true});
            }
        }
    }));
}

function prepareOuterBounds(
    gridModel: GridModel,
    rawGridRect: BoundingRect,
    layoutRef: BoxLayoutReferenceResult,
): {
    outerBoundsRect: BoundingRect | NullUndefined
    parsedOuterBoundsContain: ParsedOuterBoundsContain
    outerBoundsClamp: number[]
} {
    let outerBoundsRect: BoundingRect | NullUndefined;
    const optionOuterBoundsMode = gridModel.get('outerBoundsMode', true);
    if (optionOuterBoundsMode === 'same') {
        outerBoundsRect = rawGridRect.clone();
    }
    else if (optionOuterBoundsMode == null || optionOuterBoundsMode === 'auto') {
        outerBoundsRect = getLayoutRect(
            gridModel.get('outerBounds', true) || OUTER_BOUNDS_DEFAULT, layoutRef.refContainer
        );
    }
    else if (optionOuterBoundsMode !== 'none') {
        if (__DEV__) {
            error(`Invalid grid[${gridModel.componentIndex}].outerBoundsMode.`);
        }
    }

    const optionOuterBoundsContain = gridModel.get('outerBoundsContain', true);
    let parsedOuterBoundsContain: ParsedOuterBoundsContain;
    if (optionOuterBoundsContain == null || optionOuterBoundsContain === 'auto') {
        parsedOuterBoundsContain = 'all';
    }
    else if (indexOf(['all', 'axisLabel'], optionOuterBoundsContain) < 0) {
        if (__DEV__) {
            error(`Invalid grid[${gridModel.componentIndex}].outerBoundsContain.`);
        }
        parsedOuterBoundsContain = 'all';
    }
    else {
        parsedOuterBoundsContain = optionOuterBoundsContain;
    }

    const outerBoundsClamp = [
        parsePositionSizeOption(
            retrieve2(gridModel.get('outerBoundsClampWidth', true), OUTER_BOUNDS_CLAMP_DEFAULT[0]), rawGridRect.width
        ),
        parsePositionSizeOption(
            retrieve2(gridModel.get('outerBoundsClampHeight', true), OUTER_BOUNDS_CLAMP_DEFAULT[1]), rawGridRect.height
        )
    ];

    return {outerBoundsRect, parsedOuterBoundsContain, outerBoundsClamp};
}

const resolveAxisNameOverlapForGrid: AxisBuilderSharedContext['resolveAxisNameOverlap'] = (
    cfg, ctx, axisModel, nameLayoutInfo, nameMoveDirVec, thisRecord
) => {
    const perpendicularDim = axisModel.axis.dim === 'x' ? 'y' : 'x';

    resolveAxisNameOverlapDefault(cfg, ctx, axisModel, nameLayoutInfo, nameMoveDirVec, thisRecord);

    // If nameLocation 'center', and there are multiple axes parallel to this axis, do not adjust by
    //  other axes, because the axis name should be close to its axis line as much as possible even
    //  if overlapping; otherwise it might cause misleading.
    // If nameLocation 'center', do not adjust by perpendicular axes, since they are not likely to overlap.
    // If nameLocation 'start'/'end', move name within the same direction to escape overlap with the
    //  perpendicular axes.
    if (!isNameLocationCenter(cfg.nameLocation)) {
        each(ctx.recordMap[perpendicularDim], perpenRecord => {
            // perpendicular axis may be no name.
            if (perpenRecord && perpenRecord.labelInfoList && perpenRecord.dirVec) {
                moveIfOverlapByLinearLabels(
                    perpenRecord.labelInfoList, perpenRecord.dirVec, nameLayoutInfo, nameMoveDirVec
                );
            }
        });
    }
};

export default Grid;
