/*
* 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 { assert, clone, each, filter, find, isString, map, trim } from 'zrender/src/core/util';
import {
    NullUndefined, ParsedAxisBreak, ParsedAxisBreakList, AxisBreakOption,
    AxisBreakOptionIdentifierInAxis, ScaleTick, VisualAxisBreak,
} from '../util/types';
import { error } from '../util/log';
import type Scale from './Scale';
import {
    AxisBreakParsingResult, registerScaleBreakHelperImpl, ParamPruneByBreak, BreakScaleMapper, ParseBreakOptionOpt
} from './break';
import { mathMax, mathMin, mathRound } from '../util/number';
import { AxisLabelFormatterExtraParams } from '../coord/axisCommonTypes';
import {
    DecoratedScaleMapperMethods,
    decorateScaleMapper, enableScaleMapperFreeze, initLinearScaleMapper,
    SCALE_EXTENT_KIND_EFFECTIVE,
    SCALE_MAPPER_DEPTH_OUT_OF_BREAK,
    ScaleMapper,
    ScaleMapperTransformOutOpt
} from './scaleMapper';
import { isValidBoundsForExtent } from '../util/model';
import { ValueTransformLookupOpt } from './helper';

/**
 * @caution
 *  Must not export anything except `installScaleBreakHelper`
 */

interface BreakScaleMapperImpl extends BreakScaleMapper {
    // Linear space, the elapsed result.
    _linear: ScaleMapper;
}
class BreakScaleMapperImpl {

    // [CAVEAT]: Should set only by the constructor!
    readonly breaks: ParsedAxisBreakList;

    // Only used to save the original extent to avoid rounding error.
    private _outOfBrk: ScaleMapper;

    constructor(
        breakParsed: AxisBreakParsingResult | NullUndefined,
        initialExtent: number[] | NullUndefined
    ) {
        decorateScaleMapper(this, BreakScaleMapperImpl.decoratedMethods);

        this._outOfBrk = initLinearScaleMapper(null, initialExtent);
        const mapper = this._linear = initLinearScaleMapper(null, initialExtent);
        enableScaleMapperFreeze(this, mapper);

        this.breaks = breakParsed && breakParsed.breaks || [];
    }

    hasBreaks(): boolean {
        return !!this.breaks.length;
    }

    /**
     * When iteratively generating ticks by nice interval, currently the `interval`, which is
     * calculated by break-elapsed extent span, is probably very small comparing to the original
     * extent, leading to a large number of iteration and tick generation, even over `safeLimit`.
     * Thus stepping over breaks is necessary in that loop.
     *
     * "Nice" should be ensured on ticks when step over the breaks. Thus this method returns
     * a integer multiple of the "nice tick interval".
     *
     * This method does little work; it is just for unifying and restricting the behavior.
     */
    calcNiceTickMultiple(
        tickVal: number,
        estimateNiceMultiple: (tickVal: number, brkEnd: number) => number
    ): number {
        for (let idx = 0; idx < this.breaks.length; idx++) {
            const brk = this.breaks[idx];
            if (brk.vmin < tickVal && tickVal < brk.vmax) {
                const multiple = estimateNiceMultiple(tickVal, brk.vmax);
                if (__DEV__) {
                    // If not, it may cause dead loop or not nice tick.
                    assert(multiple >= 0 && mathRound(multiple) === multiple);
                }
                return multiple;
            }
        }
        return 0;
    }

    static decoratedMethods: DecoratedScaleMapperMethods<BreakScaleMapperImpl> = {

        needTransform() {
            return !this.breaks.length;
        },

        getExtent() {
            return this._outOfBrk.getExtent();
        },

        getExtentUnsafe(kind, depth) {
            return (depth == null
                || depth === SCALE_MAPPER_DEPTH_OUT_OF_BREAK
            )
                ? this._outOfBrk.getExtentUnsafe(kind, null)
                : this._linear.getExtentUnsafe(kind, null);
        },

        setExtent(start, end) {
            this.setExtent2(SCALE_EXTENT_KIND_EFFECTIVE, start, end);
        },

        setExtent2(kind, start, end) {
            if (isValidBoundsForExtent(start, end)) {
                if (kind === SCALE_EXTENT_KIND_EFFECTIVE) {
                    updateAxisBreakGapReal(this, [start, end]);
                }
                this._outOfBrk.setExtent2(kind, start, end);
                this._linear.setExtent2(
                    kind,
                    this.transformIn(start, null),
                    this.transformIn(end, null)
                );
            }
        },

        normalize(val) {
            return this._linear.normalize(this.transformIn(val, null));
        },

        scale(val) {
            return this.transformOut(this._linear.scale(val), null);
        },

        contain(val) {
            return this._outOfBrk.contain(val);
        },

        /**
         * a.k.a., "elapse"
         * Suppose:
         *    AXIS_BREAK_LAST_BREAK_END_BASE: 0
         *    AXIS_BREAK_ELAPSED_BASE: 0
         *    breaks: [
         *        {start: -400, end: -300, gap: 27},
         *        {start: -100, end: 100, gap: 10},
         *        {start: 200, end: 400, gap: 300},
         *    ]
         * The mapping will be:
         *        |        |
         *    400 +   ->   +  237
         *     |  |        |   |  (gap: 300)
         *    200 +   ->   + -63
         *        |        |
         *    100 +   ->   + -163
         *     |  |        |   |  (gap: 10)
         *   -100 +   ->   + -173
         *        |        |
         *   -300 +   ->   + -373
         *     |  |        |   |  (gap: 27)
         *   -400 +   ->   + -400
         *        |        |
         *   origianl     elapsed
         *
         * Note:
         *   `transformIn` and `transformOut` has nothing to do with "scale extent" - out of extent is supported.
         */
        transformIn(val, opt) {
            if (opt && opt.depth === SCALE_MAPPER_DEPTH_OUT_OF_BREAK) {
                return val;
            }

            // If the value is in the break, return the normalized value in the break
            let elapsedVal = AXIS_BREAK_ELAPSED_BASE;
            let lastBreakEnd = AXIS_BREAK_LAST_BREAK_END_BASE;
            let stillOver = true;
            for (let i = 0; i < this.breaks.length; i++) {
                const brk = this.breaks[i];
                if (val <= brk.vmax) {
                    if (val > brk.vmin) {
                        elapsedVal += brk.vmin - lastBreakEnd
                            + (val - brk.vmin) / (brk.vmax - brk.vmin) * brk.gapReal;
                    }
                    else {
                        elapsedVal += val - lastBreakEnd;
                    }
                    lastBreakEnd = brk.vmax;
                    stillOver = false;
                    break;
                }
                elapsedVal += brk.vmin - lastBreakEnd + brk.gapReal;
                lastBreakEnd = brk.vmax;
            }
            if (stillOver) {
                elapsedVal += val - lastBreakEnd;
            }
            return elapsedVal;
        },

        /**
         * @see transformIn
         * a.k.a., "unelapse"
         */
        transformOut(elapsedVal, opt) {
            if (opt && opt.depth === SCALE_MAPPER_DEPTH_OUT_OF_BREAK) {
                return elapsedVal;
            }

            let lastElapsedEnd = AXIS_BREAK_ELAPSED_BASE;
            let lastBreakEnd = AXIS_BREAK_LAST_BREAK_END_BASE;
            let stillOver = true;
            let unelapsedVal = 0;
            for (let i = 0; i < this.breaks.length; i++) {
                const brk = this.breaks[i];
                const elapsedStart = lastElapsedEnd + brk.vmin - lastBreakEnd;
                const elapsedEnd = elapsedStart + brk.gapReal;
                if (elapsedVal <= elapsedEnd) {
                    if (elapsedVal > elapsedStart) {
                        unelapsedVal = brk.vmin
                            + (elapsedVal - elapsedStart) / (elapsedEnd - elapsedStart) * (brk.vmax - brk.vmin);
                    }
                    else {
                        unelapsedVal = lastBreakEnd + elapsedVal - lastElapsedEnd;
                    }
                    lastBreakEnd = brk.vmax;
                    stillOver = false;
                    break;
                }
                lastElapsedEnd = elapsedEnd;
                lastBreakEnd = brk.vmax;
            }
            if (stillOver) {
                unelapsedVal = lastBreakEnd + elapsedVal - lastElapsedEnd;
            }
            return unelapsedVal;
        },

    };

};

function createBreakScaleMapper(
    breakParsed: AxisBreakParsingResult | NullUndefined,
    initialExtent: number[] | NullUndefined
): BreakScaleMapper {
    return new BreakScaleMapperImpl(breakParsed, initialExtent);
}


// Both can start with any finite value, and are not necessarily equal. But they need to
// be the same in `axisBreakElapse` and `axisBreakUnelapse` respectively.
const AXIS_BREAK_ELAPSED_BASE = 0;
const AXIS_BREAK_LAST_BREAK_END_BASE = 0;


/**
 * `gapReal` in brkMapper.breaks will be calculated.
 */
function updateAxisBreakGapReal(
    brkMapper: BreakScaleMapper,
    scaleExtent: number[]
): void {
    // Considered the effect:
    //  - Use dataZoom to move some of the breaks outside the extent.
    //  - Some scenarios that `series.clip: false`.
    //
    // How to calculate `prctBrksGapRealSum`:
    //  Based on the formula:
    //      xxx.span = brk.vmax - brk.vmin
    //      xxx.tpPrct.val / xxx.tpAbs.val means ParsedAxisBreak['gapParsed']['val']
    //      .S/.E means a break that is semi in scaleExtent[0] or scaleExtent[1]
    //      valP = (
    //          + (fullyInExtBrksSum.tpAbs.gapReal - fullyInExtBrksSum.tpAbs.span)
    //          + (semiInExtBrk.S.tpAbs.gapReal - semiInExtBrk.S.tpAbs.span) * semiInExtBrk.S.tpAbs.inExtFrac
    //          + (semiInExtBrk.E.tpAbs.gapReal - semiInExtBrk.E.tpAbs.span) * semiInExtBrk.E.tpAbs.inExtFrac
    //      )
    //      valQ = (
    //          - fullyInExtBrksSum.tpPrct.span
    //          - semiInExtBrk.S.tpPrct.span * semiInExtBrk.S.tpPrct.inExtFrac
    //          - semiInExtBrk.E.tpPrct.span * semiInExtBrk.E.tpPrct.inExtFrac
    //      )
    //      gapPrctSum = sum(xxx.tpPrct.val)
    //      gapPrctSum = prctBrksGapRealSum / (
    //          + (scaleExtent[1] - scaleExtent[0]) + valP + valQ
    //          + fullyInExtBrksSum.tpPrct.gapReal
    //          + semiInExtBrk.S.tpPrct.gapReal * semiInExtBrk.S.tpPrct.inExtFrac
    //          + semiInExtBrk.E.tpPrct.gapReal * semiInExtBrk.E.tpPrct.inExtFrac
    //      )
    //  Assume:
    //      xxx.tpPrct.gapReal = xxx.tpPrct.val / gapPrctSum * prctBrksGapRealSum
    //         (NOTE: This is not accurate when semi-in-extent break exist because its
    //         proportion is not linear, but this assumption approximately works.)
    //  Derived as follows:
    //      prctBrksGapRealSum = gapPrctSum * ( (scaleExtent[1] - scaleExtent[0]) + valP + valQ )
    //          / (1
    //              - fullyInExtBrksSum.tpPrct.val
    //              - semiInExtBrk.S.tpPrct.val * semiInExtBrk.S.tpPrct.inExtFrac
    //              - semiInExtBrk.E.tpPrct.val * semiInExtBrk.E.tpPrct.inExtFrac
    //          )

    let gapPrctSum = 0;
    const fullyInExtBrksSum = {
        tpAbs: {span: 0, val: 0},
        tpPrct: {span: 0, val: 0},
    };
    const init = () => ({has: false, span: NaN, inExtFrac: NaN, val: NaN});
    const semiInExtBrk = {
        S: {tpAbs: init(), tpPrct: init()},
        E: {tpAbs: init(), tpPrct: init()},
    };

    each(brkMapper.breaks, brk => {
        const gapParsed = brk.gapParsed;

        if (gapParsed.type === 'tpPrct') {
            gapPrctSum += gapParsed.val;
        }

        const clampedBrk = clampBreakByExtent(brk, scaleExtent);
        if (clampedBrk) {
            const vminClamped = clampedBrk.vmin !== brk.vmin;
            const vmaxClamped = clampedBrk.vmax !== brk.vmax;
            const clampedSpan = clampedBrk.vmax - clampedBrk.vmin;

            if (vminClamped && vmaxClamped) {
                // Do nothing, which simply makes the result `gapReal` cover the entire scaleExtent.
                // This transform is not consistent with the other cases but practically works.
            }
            else if (vminClamped || vmaxClamped) {
                const sOrE = vminClamped ? 'S' : 'E';
                semiInExtBrk[sOrE][gapParsed.type].has = true;
                semiInExtBrk[sOrE][gapParsed.type].span = clampedSpan;
                semiInExtBrk[sOrE][gapParsed.type].inExtFrac = clampedSpan / (brk.vmax - brk.vmin);
                semiInExtBrk[sOrE][gapParsed.type].val = gapParsed.val;
            }
            else {
                fullyInExtBrksSum[gapParsed.type].span += clampedSpan;
                fullyInExtBrksSum[gapParsed.type].val += gapParsed.val;
            }
        }
    });

    const prctBrksGapRealSum = gapPrctSum
        * (0
            + (scaleExtent[1] - scaleExtent[0])
            + (fullyInExtBrksSum.tpAbs.val - fullyInExtBrksSum.tpAbs.span)
            + (semiInExtBrk.S.tpAbs.has
                ? (semiInExtBrk.S.tpAbs.val - semiInExtBrk.S.tpAbs.span) * semiInExtBrk.S.tpAbs.inExtFrac : 0
            )
            + (semiInExtBrk.E.tpAbs.has
                ? (semiInExtBrk.E.tpAbs.val - semiInExtBrk.E.tpAbs.span) * semiInExtBrk.E.tpAbs.inExtFrac : 0
            )
            - fullyInExtBrksSum.tpPrct.span
            - (semiInExtBrk.S.tpPrct.has ? semiInExtBrk.S.tpPrct.span * semiInExtBrk.S.tpPrct.inExtFrac : 0)
            - (semiInExtBrk.E.tpPrct.has ? semiInExtBrk.E.tpPrct.span * semiInExtBrk.E.tpPrct.inExtFrac : 0)
        ) / (1
            - fullyInExtBrksSum.tpPrct.val
            - (semiInExtBrk.S.tpPrct.has ? semiInExtBrk.S.tpPrct.val * semiInExtBrk.S.tpPrct.inExtFrac : 0)
            - (semiInExtBrk.E.tpPrct.has ? semiInExtBrk.E.tpPrct.val * semiInExtBrk.E.tpPrct.inExtFrac : 0)
        );

    each(brkMapper.breaks, brk => {
        const gapParsed = brk.gapParsed;
        if (gapParsed.type === 'tpPrct') {
            brk.gapReal = gapPrctSum !== 0
                // prctBrksGapRealSum is supposed to be non-negative but add a safe guard
                ? mathMax(prctBrksGapRealSum, 0) * gapParsed.val / gapPrctSum : 0;
        }
        if (gapParsed.type === 'tpAbs') {
            brk.gapReal = gapParsed.val;
        }
        if (brk.gapReal == null) {
            brk.gapReal = 0;
        }
    });
}

function pruneTicksByBreak<TItem extends ScaleTick | number>(
    pruneByBreak: ParamPruneByBreak,
    ticks: TItem[],
    breaks: ParsedAxisBreakList,
    getValue: (item: TItem) => number,
    interval: number,
    scaleExtent: number[],
): void {
    if (pruneByBreak === 'no') {
        return;
    }
    each(breaks, brk => {
        // break.vmin/vmax that out of extent must not impact the visible of
        // normal ticks and labels.
        const clampedBrk = clampBreakByExtent(brk, scaleExtent);
        if (!clampedBrk) {
            return;
        }
        // Remove some normal ticks to avoid zigzag shapes overlapping with split lines
        // and to avoid break labels overlapping with normal tick labels (thouth it can
        // also be avoided by `axisLabel.hideOverlap`).
        // It's OK to O(n^2) since the number of `ticks` are small.
        for (let j = ticks.length - 1; j >= 0; j--) {
            const tick = ticks[j];
            const val = getValue(tick);
            // 1. Ensure there is no ticks inside `break.vmin` and `break.vmax`.
            // 2. Use an empirically gap value here. Theoritically `zigzagAmplitude` is
            //  supposed to be involved to provide better precision but it will brings
            //  more complexity. The empirically gap value is conservative because break
            //  labels and normal tick lables are prone to overlapping.
            const gap = interval * 3 / 4;
            if (val > clampedBrk.vmin - gap
                && val < clampedBrk.vmax + gap
                && (
                    pruneByBreak !== 'preserve_extent_bound'
                    || (
                        val !== scaleExtent[0] && val !== scaleExtent[1]
                    )
                )
            ) {
                ticks.splice(j, 1);
            }
        }
    });
}

function addBreaksToTicks(
    // The input ticks should be in accending order.
    ticks: ScaleTick[],
    breaks: ParsedAxisBreakList,
    scaleExtent: number[],
    // Keep the break ends at the same level to avoid an awkward appearance.
    getTimeProps?: (clampedBrk: ParsedAxisBreak) => ScaleTick['time'],
): void {
    each(breaks, brk => {
        const clampedBrk = clampBreakByExtent(brk, scaleExtent);
        if (!clampedBrk) {
            return;
        }

        // - When neight `break.vmin` nor `break.vmax` is in scale extent,
        //  break label should not be displayed and we do not add them to the result.
        // - When only one of `break.vmin` and `break.vmax` is inside the extent and the
        //  other is outsite, we comply with the extent and display only part of the breaks area,
        //  because the extent might be determined by user settings (such as `axis.min/max`)
        ticks.push({
            value: clampedBrk.vmin,
            break: {
                type: 'vmin',
                parsedBreak: clampedBrk,
            },
            time: getTimeProps ? getTimeProps(clampedBrk) : undefined,
        });
        // When gap is 0, start tick overlap with end tick, but we still count both of them. Break
        // area shape can address that overlapping. `axisLabel` need draw both start and end separately,
        // otherwise it brings complexity to the logic of label overlapping resolving (e.g., when label
        // rotated), and introduces inconsistency to users in `axisLabel.formatter` between gap is 0 or not.
        ticks.push({
            value: clampedBrk.vmax,
            break: {
                type: 'vmax',
                parsedBreak: clampedBrk,
            },
            time: getTimeProps ? getTimeProps(clampedBrk) : undefined,
        });
    });
    if (breaks.length) {
        ticks.sort((a, b) => a.value - b.value);
    }
}

/**
 * If break and extent does not intersect, return null/undefined.
 * If the intersection is only a point at scaleExtent[0] or scaleExtent[1], return null/undefined.
 */
function clampBreakByExtent(
    brk: ParsedAxisBreak,
    scaleExtent: number[]
): NullUndefined | ParsedAxisBreak {
    const vmin = mathMax(brk.vmin, scaleExtent[0]);
    const vmax = mathMin(brk.vmax, scaleExtent[1]);
    return (
            vmin < vmax
            || (vmin === vmax && vmin > scaleExtent[0] && vmin < scaleExtent[1])
        )
        ? {
            vmin,
            vmax,
            breakOption: brk.breakOption,
            gapParsed: brk.gapParsed,
            gapReal: brk.gapReal,
        }
        : null;
}

function parseAxisBreakOption(
    // raw user input breaks, retrieved from axis model.
    breakOptionList: AxisBreakOption[] | NullUndefined,
    scale: {parse: Scale['parse']},
    opt?: ParseBreakOptionOpt
): AxisBreakParsingResult {
    const parsedBreaks: ParsedAxisBreakList = [];

    if (!breakOptionList) {
        return {breaks: parsedBreaks};
    }

    function validatePercent(normalizedPercent: number, msg: string): boolean {
        if (normalizedPercent >= 0 && normalizedPercent < 1 - 1e-5) { // Avoid division error.
            return true;
        }
        if (__DEV__) {
            error(`${msg} must be >= 0 and < 1, rather than ${normalizedPercent} .`);
        }
        return false;
    }

    each(breakOptionList, brkOption => {
        if (!brkOption || brkOption.start == null || brkOption.end == null) {
            if (__DEV__) {
                error('The input axis breaks start/end should not be empty.');
            }
            return;
        }
        if (brkOption.isExpanded) {
            return;
        }

        const parsedBrk: ParsedAxisBreak = {
            breakOption: clone(brkOption),
            vmin: scale.parse(brkOption.start),
            vmax: scale.parse(brkOption.end),
            gapParsed: {type: 'tpAbs', val: 0},
            gapReal: null
        };

        if (brkOption.gap != null) {
            let isPrct = false;
            if (isString(brkOption.gap)) {
                const trimmedGap = trim(brkOption.gap);
                if (trimmedGap.match(/%$/)) {
                    let normalizedPercent = parseFloat(trimmedGap) / 100;
                    if (!validatePercent(normalizedPercent, 'Percent gap')) {
                        normalizedPercent = 0;
                    }
                    parsedBrk.gapParsed.type = 'tpPrct';
                    parsedBrk.gapParsed.val = normalizedPercent;
                    isPrct = true;
                }
            }
            if (!isPrct) {
                let absolute = scale.parse(brkOption.gap);
                if (!isFinite(absolute) || absolute < 0) {
                    if (__DEV__) {
                        error(`Axis breaks gap must positive finite rather than (${brkOption.gap}).`);
                    }
                    absolute = 0;
                }
                parsedBrk.gapParsed.type = 'tpAbs';
                parsedBrk.gapParsed.val = absolute;
            }
        }
        if (parsedBrk.vmin === parsedBrk.vmax) {
            parsedBrk.gapParsed.type = 'tpAbs';
            parsedBrk.gapParsed.val = 0;
        }

        if (opt && opt.noNegative) {
            each(['vmin', 'vmax'] as const, se => {
                if (parsedBrk[se] < 0) {
                    if (__DEV__) {
                        error(`Axis break.${se} must not be negative.`);
                    }
                    parsedBrk[se] = 0;
                }
            });
        }

        // Ascending numerical order is the prerequisite of the calculation in Scale#normalize.
        // User are allowed to input desending vmin/vmax for simplifying the usage.
        if (parsedBrk.vmin > parsedBrk.vmax) {
            const tmp = parsedBrk.vmax;
            parsedBrk.vmax = parsedBrk.vmin;
            parsedBrk.vmin = tmp;
        }

        parsedBreaks.push(parsedBrk);
    });

    // Ascending numerical order is the prerequisite of the calculation in Scale#normalize.
    parsedBreaks.sort((item1, item2) => item1.vmin - item2.vmin);
    // Make sure that the intervals in breaks are not overlap.
    let lastEnd = -Infinity;
    each(parsedBreaks, (brk, idx) => {
        if (lastEnd > brk.vmin) {
            if (__DEV__) {
                error('Axis breaks must not overlap.');
            }
            parsedBreaks[idx] = null;
        }
        lastEnd = brk.vmax;
    });

    return {
        breaks: filter(parsedBreaks, brk => !!brk),
    };
}

function identifyAxisBreak(
    brk: AxisBreakOption,
    identifier: AxisBreakOptionIdentifierInAxis
): boolean {
    return serializeAxisBreakIdentifier(identifier) === serializeAxisBreakIdentifier(brk);
}

function serializeAxisBreakIdentifier(identifier: AxisBreakOptionIdentifierInAxis): string {
    // We use user input start/end to identify break. Considered cases like `start: new Date(xxx)`,
    // Theoretically `Scale#parse` should be used here, but not used currently to reduce dependencies,
    // since simply converting to string happens to be correct.
    return identifier.start + '_\0_' + identifier.end;
}

/**
 * - A break pair represents `[vmin, vmax]`,
 * - Only both vmin and vmax item exist, they are counted as a pair.
 */
function retrieveAxisBreakPairs<TItem, TReturnIdx extends boolean>(
    itemList: TItem[],
    getVisualAxisBreak: (item: TItem) => VisualAxisBreak | NullUndefined,
    returnIdx: TReturnIdx
): (
    TReturnIdx extends false ? TItem[][] : number[][]
) {
    const idxPairList: number[][] = [];
    each(itemList, (el, idx) => {
        const vBreak = getVisualAxisBreak(el);
        if (vBreak && vBreak.type === 'vmin') {
            idxPairList.push([idx]);
        }
    });
    each(itemList, (el, idx) => {
        const vBreak = getVisualAxisBreak(el);
        if (vBreak && vBreak.type === 'vmax') {
            const idxPair = find(
                idxPairList,
                // parsedBreak may be changed, can only use breakOption to match them.
                pr => identifyAxisBreak(
                    getVisualAxisBreak(itemList[pr[0]]).parsedBreak.breakOption,
                    vBreak.parsedBreak.breakOption
                )
            );
            idxPair && idxPair.push(idx);
        }
    });
    const result = [] as (TReturnIdx extends false ? TItem[][] : number[][]);
    each(idxPairList, idxPair => {
        if (idxPair.length === 2) {
            result.push(returnIdx
                ? (idxPair as any)
                : ([itemList[idxPair[0]] as any, itemList[idxPair[1]] as any])
            );
        }
    });
    return result;
}

function getTicksBreakOutwardTransform(
    scale: ScaleMapper,
    tick: ScaleTick,
    outermostBreaks: ParsedAxisBreakList,
    lookup: ValueTransformLookupOpt['lookup']
): {
    tickVal: number;
    vBreak: VisualAxisBreak | NullUndefined;
    // Return: If no break, return null/undefined.
} | NullUndefined {

    if (!tick.break) {
        return;
    }

    const brk = tick.break.parsedBreak;
    const originalBrkItem = find(outermostBreaks, brk => identifyAxisBreak(
        brk.breakOption, tick.break.parsedBreak.breakOption
    ));
    // NOTE: `tick.break` may have been clamped by scale extent.
    const opt: ScaleMapperTransformOutOpt = {lookup, depth: SCALE_MAPPER_DEPTH_OUT_OF_BREAK};
    const vmin = scale.transformOut(brk.vmin, opt);
    const vmax = scale.transformOut(brk.vmax, opt);
    const parsedBreak = {
        vmin,
        vmax,
        breakOption: brk.breakOption, // It is not changed by extent clamping.
        gapParsed: clone(originalBrkItem.gapParsed),
        gapReal: brk.gapReal,
    };
    return {
        tickVal: parsedBreak[tick.break.type],
        vBreak: {type: tick.break.type, parsedBreak},
    };
}

function parseAxisBreakOptionInwardTransform(
    breakOptionList: AxisBreakOption[] | NullUndefined,
    scale: Scale,
    parseOpt: ParseBreakOptionOpt,
    lookupStartIdx: number,
    out: {
        lookup: ValueTransformLookupOpt['lookup'];
        original?: AxisBreakParsingResult;
        transformed?: AxisBreakParsingResult;
    }
): void {
    out.original = parseAxisBreakOption(breakOptionList, scale, parseOpt);
    const transformed = out.transformed = parseAxisBreakOption(breakOptionList, scale, parseOpt);
    const lookup = out.lookup;

    transformed.breaks = map(transformed.breaks, (brk, idx) => {
        const transOpt = {depth: SCALE_MAPPER_DEPTH_OUT_OF_BREAK} as const;
        const vmin = scale.transformIn(brk.vmin, transOpt);
        const vmax = scale.transformIn(brk.vmax, transOpt);
        const gapParsed = {
            type: brk.gapParsed.type,
            val: brk.gapParsed.type === 'tpAbs'
                ? scale.transformIn(brk.vmin + brk.gapParsed.val, transOpt) - vmin
                : brk.gapParsed.val,
        };

        lookup.from[lookupStartIdx + idx] = vmin;
        lookup.to[lookupStartIdx + idx] = brk.vmin;
        lookup.from[lookupStartIdx + idx + 1] = vmax;
        lookup.to[lookupStartIdx + idx + 1] = brk.vmax;

        return {
            vmin,
            vmax,
            gapParsed,
            gapReal: brk.gapReal,
            breakOption: brk.breakOption
        };
    });
}

const BREAK_MIN_MAX_TO_PARAM = {vmin: 'start', vmax: 'end'} as const;
function makeAxisLabelFormatterParamBreak(
    extraParam: AxisLabelFormatterExtraParams | NullUndefined,
    vBreak: VisualAxisBreak | NullUndefined
): AxisLabelFormatterExtraParams | NullUndefined {
    if (vBreak) {
        extraParam = extraParam || ({} as AxisLabelFormatterExtraParams);
        extraParam.break = {
            type: BREAK_MIN_MAX_TO_PARAM[vBreak.type],
            start: vBreak.parsedBreak.vmin,
            end: vBreak.parsedBreak.vmax,
        };
    }
    return extraParam;
}

export function installScaleBreakHelper(): void {
    registerScaleBreakHelperImpl({
        createBreakScaleMapper,
        pruneTicksByBreak,
        addBreaksToTicks,
        parseAxisBreakOption,
        identifyAxisBreak,
        serializeAxisBreakIdentifier,
        retrieveAxisBreakPairs,
        getTicksBreakOutwardTransform,
        parseAxisBreakOptionInwardTransform,
        makeAxisLabelFormatterParamBreak,
    });
}
