/*
* 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 {
    getPrecision, round, nice, quantityExponent,
    mathPow, mathMax, mathRound,
    mathLog, mathAbs, mathFloor, mathCeil
} from '../util/number';
import type IntervalScale from './Interval';
import type LogScale from './Log';
import type Scale from './Scale';
import type TimeScale from './Time';
import { NullUndefined, ScaleTick } from '../util/types';
import type OrdinalScale from './Ordinal';
import {
    ScaleExtentFixMinMax, ScaleRawExtentResultFinal
} from '../coord/scaleRawExtentInfo';
import { isValidNumberForExtent } from '../util/model';
import { getScaleExtentForTickUnsafe } from './scaleMapper';


type intervalScaleNiceTicksResult = {
    interval: number,
    intervalPrecision: number,
    niceTickExtent: [number, number]
};

export type IntervalScaleGetLabelOpt = {
    // If 'auto', use nice precision.
    precision?: 'auto' | number,
    // `true`: returns 1.50 but not 1.5 if precision is 2.
    pad?: boolean
};

/**
 * See also method `nice` in `src/util/number.ts`.
 */
// export function isValueNice(val: number) {
//     const exp10 = Math.pow(10, quantityExponent(Math.abs(val)));
//     const f = Math.abs(round(val / exp10, 0));
//     return f === 0
//         || f === 1
//         || f === 2
//         || f === 3
//         || f === 5;
// }

export function isIntervalOrLogScale(scale: Scale): scale is (LogScale | IntervalScale) {
    return isIntervalScale(scale) || isLogScale(scale);
}

export function isIntervalOrTimeScale(scale: Scale): scale is (IntervalScale | TimeScale) {
    return isIntervalScale(scale) || isTimeScale(scale);
}

export function isIntervalScale(scale: Scale): scale is IntervalScale {
    return scale.type === 'interval';
}

export function isTimeScale(scale: Scale): scale is TimeScale {
    return scale.type === 'time';
}

export function isLogScale(scale: Scale): scale is LogScale {
    return scale.type === 'log';
}

export function isOrdinalScale(scale: Scale): scale is OrdinalScale {
    return scale.type === 'ordinal';
}

/**
 * @param extent Both extent[0] and extent[1] should be valid number.
 *               Should be extent[0] < extent[1].
 * @param splitNumber splitNumber should be >= 1.
 */
export function intervalScaleNiceTicks(
    extent: number[],
    spanWithBreaks: number,
    splitNumber: number,
    minInterval?: number,
    maxInterval?: number
): intervalScaleNiceTicksResult {

    const result = {} as intervalScaleNiceTicksResult;

    let interval = result.interval = nice(spanWithBreaks / splitNumber, true);
    if (minInterval != null && interval < minInterval) {
        interval = result.interval = minInterval;
    }
    if (maxInterval != null && interval > maxInterval) {
        interval = result.interval = maxInterval;
    }
    const precision = result.intervalPrecision = getIntervalPrecision(interval);
    // Niced extent inside original extent
    result.niceTickExtent = [
        round(mathCeil(extent[0] / interval) * interval, precision),
        round(mathFloor(extent[1] / interval) * interval, precision)
    ];

    return result;
}

/**
 * The input `niceInterval` should be generated
 * from `nice` method in `src/util/number.ts`, or
 * from `increaseInterval` itself.
 */
export function increaseInterval(niceInterval: number) {
    const exponent = quantityExponent(niceInterval);
    // No rounding error in Math.pow(10, integer).
    const exp10 = mathPow(10, exponent);
    // Fix IEEE 754 float rounding error
    let f = mathRound(niceInterval / exp10);
    if (!f) {
        f = 1;
    }
    else if (f === 2) {
        f = 3;
    }
    else if (f === 3) {
        f = 5;
    }
    else { // f is 1 or 5
        f *= 2;
    }
    // Fix IEEE 754 float rounding error
    return round(f * exp10, -exponent);
}

export function getIntervalPrecision(niceInterval: number): number {
    // Tow more digital for tick.
    // NOTE: `2` was introduced in commit `af2a2a9f6303081d7c3b52f0a38add07b4c6e0c7`;
    // it works on "nice" interval, but seems not necessarily mathematically required.
    return getPrecision(niceInterval) + 2;
}

/**
 * Lookup table to avoid rounding error - if the value before transformed is in `lookup.from[i]`,
 * return `lookup.to[i]` directly without transform.
 * Rounding errors typically arise in logarithm transform, which can cause the tick to be displayed
 * like `5.999999999999999` when it is expected to be `6`.
 */
export type ValueTransformLookupOpt = {
    lookup?: {
        from: number[];
        to: number[];
    } | NullUndefined;
};

/**
 * NOTE:
 *  - If `val` is `NaN`, return `NaN`.
 *  - If `val` is `0`, return `-Infinity`.
 *  - If `val` is negative, return `NaN`.
 *
 * @see {DataStore#getDataExtent} It handles non-positive values for logarithm scale.
 */
export function logScaleLogTick(
    val: number,
    base: number,
): number {
    // NOTE:
    //  - rounding error may happen above, typically expecting `log10(1000)` but actually
    //    getting `2.9999999999999996`, but generally it does not matter since they are not
    //    used to display.
    //  - Consider backward compatibility and other log bases, do not use `Math.log10`.
    return mathLog(val) / mathLog(base);
}

/**
 * Cumulative rounding errors cause the logarithm operation to become non-invertible by simply exponentiation.
 *  - `Math.pow(10, integer)` itself has no rounding error. But,
 *  - If `linearTickVal` is generated internally by `calcNiceTicks`, it may be still "not nice" (not an integer)
 *    when it is `extent[i]`.
 *  - If `linearTickVal` is generated outside (e.g., by `scaleCalcAlign`) and set by `setExtent`,
 *    `logScaleLogTick` may already have introduced rounding errors even for "nice" values.
 * But invertible is required when the original `extent[i]` need to be respected, or "nice" ticks need to be
 * displayed instead of something like `5.999999999999999`, which is addressed in this function.
 * See also `#4158`.
 *
 * [CAUTION]:
 *  Monotonicity may be broken on extent ends - callers must make sure it does not matter.
 */
export function logScalePowTick(
    // `tickVal` should be in the linear space.
    linearTickVal: number,
    base: number,
    opt: ValueTransformLookupOpt | NullUndefined
): number {
    const lookup = opt && opt.lookup;
    if (lookup) {
        for (let i = 0; i < lookup.from.length; i++) {
            if (linearTickVal === lookup.from[i]) {
                return lookup.to[i];
            }
        }
    }
    return mathPow(base, linearTickVal);
}

/**
 * For `IntervalScale`, convert `rawExtent` to:
 *  - Be no non-finite number.
 *  - Be `extent[0] < extent[1]`- no equal; otherwise, additional handling is required
 *    in "nice" and "align" ticks.
 */
export function intervalScaleEnsureValidExtent(
    rawExtent: number[],
    fixMinMax: ScaleExtentFixMinMax,
    rawExtentResult?: ScaleRawExtentResultFinal | NullUndefined
): number[] {
    const extent = rawExtent.slice();

    // PENDING:
    //  This implementation is not rigorous, but has long been in use.

    // If extent start and end are same, expand them
    if (extent[0] === extent[1]) {
        // If `containShape`, the extent must be evenly distributed to both sides;
        // otherwise, shape (e.g., bars) may be overflow and clipped.
        const containShapeRequired = rawExtentResult && rawExtentResult.ctnShp;

        if (extent[0] !== 0) {
            // Expand extent
            // Note that extents can be both negative. See #13154
            const expandSize = mathAbs(extent[0]);
            // In the fowllowing case
            //      Axis has been fixed max 100
            //      Plus data are all 100 and axis extent are [100, 100].
            // Extend to the both side will cause expanded max is larger than fixed max.
            // So only expand to the smaller side.
            if (!fixMinMax[1]) {
                extent[1] += expandSize / 2;
                extent[0] -= expandSize / 2;
            }
            else {
                extent[0] -= expandSize / 2;
            }
        }
        else {
            if (containShapeRequired) {
                extent[0] = -1;
                extent[1] = 1;
            }
            else {
                extent[1] = 1;
            }
        }
    }
    // For example, if there are no series data, extent may be `[Infinity, -Infinity]` here.
    if (!isValidNumberForExtent(extent[0]) || !isValidNumberForExtent(extent[1])) {
        extent[0] = 0;
        extent[1] = 1;
    }
    if (extent[1] < extent[0]) {
        extent.reverse();
    }

    return extent;
}

export function extentDiffers(extent1: number[], extent2: number[]): boolean[] {
    return [extent1[0] !== extent2[0], extent1[1] !== extent2[1]];
}

export function ensureValidSplitNumber(
    rawSplitNumber: number | NullUndefined, defaultSplitNumber: number
): number {
    rawSplitNumber = rawSplitNumber || defaultSplitNumber;
    return mathRound(mathMax(rawSplitNumber, 1));
}

/**
 * NOTE: The result can have only one item, e.g., when `extent[0] === extent[1]`
 * and `categoryInterval === 0`.
 */
export function ordinalScaleCreateTicks(
    ordinalScale: OrdinalScale,
    // `categoryInterval` is the number part of `CategoryTickLabelSplitIntervalOption`.
    categoryInterval: number,
    addItem: (
        tick: ScaleTick,
        isExtentBoundary: boolean
    ) => void,
) {
    const extent = getScaleExtentForTickUnsafe(ordinalScale);
    let startTick = extent[0];
    const tickCount = ordinalScale.count();
    const step = Math.max((categoryInterval || 0) + 1, 1);

    // Calculate start tick based on zero if possible to keep label consistent
    // while zooming and moving while interval > 0. Otherwise the selection
    // of displayable ticks and symbols probably keep changing.
    if (startTick !== 0 && step > 1 && tickCount / step > 2) {
        startTick = Math.round(Math.ceil(startTick / step) * step);
    }

    // min max labels may be excluded if `startTick > 0`, but they should be always
    // included and the label display strategy is adopted uniformly later in `AxisBuilder`.
    if (startTick !== extent[0]) {
        addItemInternally(extent[0], true, true);
    }

    let tickValue = startTick;
    for (; tickValue <= extent[1]; tickValue += step) {
        addItemInternally(tickValue, false, tickValue === extent[0] || tickValue === extent[1]);
    }

    if (tickValue - step !== extent[1]) {
        addItemInternally(extent[1], true, true);
    }

    function addItemInternally(tickValue: number, offInterval: boolean, isExtentBoundary: boolean): void {
        addItem(
            {
                value: tickValue,
                offInterval,
            },
            isExtentBoundary
        );
    }
}
