/*
* 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, bind, each, extend, keys, noop } from 'zrender/src/core/util';
import { initExtentForUnion, isValidBoundsForExtent } from '../util/model';
import { NullUndefined } from '../util/types';
import { AxisBreakParsingResult, BreakScaleMapper, getScaleBreakHelper } from './break';
import { error } from '../util/log';
import type { ValueTransformLookupOpt } from './helper';
import { DataSanitizationFilter } from '../data/helper/dataValueHelper';


// ------ START: Scale Mapper Core ------

/**
 * Illustration:
 *  SCALE_EXTENT_KIND_EFFECTIVE:     |------------|     (always exist)
 *  SCALE_EXTENT_KIND_MAPPING:   |---|------------|--|  (present only when it is specified by `setExtent2`)
 *
 *  - [SCALE_EXTENT_KIND_EFFECTIVE]:
 *    It is a portion of a scale extent that is functional on most features, including:
 *      - All tick/label-related calculation.
 *      - `dataZoom` controlled ends.
 *      - Cartesian2D `clampData`.
 *      - line series start.
 *      - heatmap series range.
 *      - markerArea range.
 *      - etc.
 *    `SCALE_EXTENT_KIND_EFFECTIVE` always exists.
 *
 *  - [SCALE_EXTENT_KIND_MAPPING]:
 *    It is an expanded extent from ends of `SCALE_EXTENT_KIND_EFFECTIVE` to accommodate shapes at edges to
 *    avoid overflow. They can be typically used by bar/candlestick series on category axis with
 *    `boundaryGap: false`, or on other numeric axes. ec option `xxxAxis.containShape` is the switch.
 *    In this case, we need to:
 *      - Do not render ticks and labels in the portion "SCALE_EXTENT_KIND_MAPPING - SCALE_EXTENT_KIND_EFFECTIVE",
 *        since they are considered meaningless there.
 *      - Prevent "nice strategy" from triggering unexpectedly by the "contain shape expansion".
 *        Otherwise, for example, the original extent is `[0, 1000]`, then the expanded
 *        extent, say `[-5, 1000]`, can cause a considerable negative expansion by "nice",
 *        like `[-200, 10000]`, which is commonly unexpected. And it is exacerbated in LogScale.
 *      - Prevent the min/max tick label from displaying, since they are commonly meaningless
 *        and probably misleading.
 *    Therefore, `SCALE_EXTENT_KIND_MAPPING` is only used for:
 *      - mapping between data and pixel, such as,
 *        - `scaleMapper.normalize/scale`;
 *        - Cartesian2D `calcAffineTransform` (a quick path of `scaleMapper.normalize/scale`).
 *      - `grid` boundary related calculation in view rendering, such as, `barGrid` calculates
 *        `barWidth` for numeric scales based on the data extent.
 *      - Axis line position determination (such as `canOnZeroToAxis`);
 *      - `axisPointer` triggering (otherwise users may be confused if using `SCALE_EXTENT_KIND_EFFECTIVE`).
 *    `SCALE_EXTENT_KIND_MAPPING` can be absent, which can be used to determine whether it is used.
 *
 * @see SCALE_EXTENT_CONSTRUCTION for the full processing flow.
 */
export type ScaleExtentKind =
    typeof SCALE_EXTENT_KIND_EFFECTIVE
    | typeof SCALE_EXTENT_KIND_MAPPING;
export const SCALE_EXTENT_KIND_EFFECTIVE = 0;
export const SCALE_EXTENT_KIND_MAPPING = 1;


const SCALE_MAPPER_METHOD_NAMES_MAP: Record<keyof ScaleMapper, 1> = {
    needTransform: 1,
    normalize: 1,
    scale: 1,
    transformIn: 1,
    transformOut: 1,
    contain: 1,
    getExtent: 1,
    getExtentUnsafe: 1,
    setExtent: 1,
    setExtent2: 1,
    getFilter: 1,
    sanitize: 1,
    getDefaultStartValue: 1,
    freeze: 1,
};
const SCALE_MAPPER_METHOD_NAMES = keys(SCALE_MAPPER_METHOD_NAMES_MAP);

/**
 * - [SCALE_MAPPER_DEPTH_OUT_OF_BREAK]:
 *   In `transformIn`, it transforms a value from the outermost space to the space before break being applied.
 *   In `transformOut`, it transforms a value from the space before break being applied to the outermost space.
 *   Typically nice axis ticks are picked in that space due to the current design of nice ticks
 *   algorithm, while size related features may use `SCALE_MAPPER_DEPTH_INNERMOST`.
 * - [SCALE_MAPPER_DEPTH_INNERMOST]:
 *   Currently only linear space is used as the innermost space.
 */
export type ScaleMapperDepthOpt = {
    depth?: NullUndefined
        | typeof SCALE_MAPPER_DEPTH_OUT_OF_BREAK
        | typeof SCALE_MAPPER_DEPTH_INNERMOST;
};
export const SCALE_MAPPER_DEPTH_OUT_OF_BREAK = 2;
export const SCALE_MAPPER_DEPTH_INNERMOST = 3;

export type ScaleMapperTransformOutOpt = (
    // depth: NullUndefined means SCALE_MAPPER_DEPTH_INNERMOST.
    ScaleMapperDepthOpt
    & ValueTransformLookupOpt
);
export type ScaleMapperTransformInOpt =
    // depth: NullUndefined means SCALE_MAPPER_DEPTH_INNERMOST.
    ScaleMapperDepthOpt;

/**
 * @tutorial [SCALE_COMPOSITION_AND_TRANSFORMATION]:
 *  `ScaleMapper` is designed for multiple steps of numeric transformations from a certain space to a linear space,
 *  or vice versa. Each steps is implemented as a `ScaleMapper`, and composed like a decorator pattern. And some
 *  steps, such as "axis breaks transfromation", can be skipped when no breaks for performance consideration.
 *  Currently we support:
 *    - step#0: extent based linear scaling.
 *              This is implemented in `LinearScaleMapper`.
 *              It is mixed into `IntervalScale`, `TimeScale`;
 *              and it is also composited into `BreakScaleMapper`, `OrdinalScale`, `LogScale`.
 *    - step#1: axis breaks.
 *              This is implemented in `BreakScaleMapper`.
 *              This step may be absent if no breaks.
 *    - step#2: logarithmic (implemented in `LogScale`), or
 *              ordinal-related handling, or
 *              others to be supported, such as asinh ...
 *  Illustration of some currently supported cases:
 *    - linear_space(in an IntervalScale)
 *    - break_space(in an IntervalScale method bound by a BreakScaleMapper)
 *       └─break_transform─► linear_space(in a LinearScaleMapper owned by a BreakScaleMapper)
 *    - log_space(in a LogScale)
 *       └─log_transform─► linear_space(in an IntervalScale)
 *    - log_space(in a LogScale)
 *       └─log_transform─► break_space(in an IntervalScale method bound by a BreakScaleMapper)
 *                          └─break_transform─► linear_space(in a LinearScaleMapper owned by a BreakScaleMapper)
 *    - linear_space(in a TimeScale)
 *    - break_space(in a TimeScale method bound by a BreakScaleMapper)
 *       └─break_transform─► linear_space(in a LinearScaleMapper owned by a BreakScaleMapper)
 *    - category_values(in a OrdinalScale)
 *       └─category_to_numeric─► linear_space(in a LinearScaleMapper owned by a BreakScaleMapper)
 *
 *
 * @tutorial [SCALE_EXTENT_CONSTRUCTION]:
 *  The full construction processing of the scale extent in EC_FULL_UPDATE_CYCLE:
 *  - step#1. At `CoordinateSystem#create` stage, requirements of collecting series data extents are
 *            committed to `associateSeriesWithAxis`, and `Scale` instances are created.
 *  - step#2. Call `scaleRawExtentInfoCreate` to really collect series data extent and create
 *            `ScaleRawExtentInfo` instances to manage extent related configurations
 *              - at "data processing" stage for dataZoom controlled axes, if any, or
 *              - at "CoordinateSystem#update" stage for all other axes.
 *  - step#3. Perform "nice" (see `scaleCalcNice`) or "align" (see `scaleCalcAlign`) strategies to
 *            modify the original extent from `ScaleRawExtentInfo` instance, if needed, at
 *            "CoordinateSystem#update" stage.
 *  - step#4. `calcContainShape` is performed. Set `SCALE_EXTENT_KIND_MAPPING` if needed.
 *            See AXIS_CONTAIN_SHAPE_PROCESSING_ORDER for more details.
 *
 */
export interface ScaleMapper extends ScaleMapperGeneric<ScaleMapper> {}
export interface ScaleMapperGeneric<This> {

    /**
     * Enable a fast path in large data traversal - the call of `transformIn`/`transformOut`
     * can be omitted, and this is the most case.
     */
    needTransform(this: This): boolean;

    /**
     * Normalize a value to linear [0, 1], return 0.5 if extent span is 0.
     * The typical logic is:
     *  `transformIn_self` -> `transformIn_inner` -> ... to the innermost space,
     *  then do linear normalization based on innermost extent.
     */
    normalize: (this: This, val: number) => number;

    /**
     * Scale a normalized value to extent. It's the inverse of `normalize`.
     */
    scale: (this: This, val: number) => number;

    /**
     * [NOTICE]:
     *  - This method must be available since the instance is constructed.
     *  - This method has nothing to do with extent - transforming out of extent is supported.
     *
     * This method transforms a value forward into a inner space.
     * The typical logic is:
     *  `transformIn_self` -> `transformIn_inner` -> ... to the innermost space.
     * In most cases axis ticks are laid out in linear space, and some features
     * (such as LogScale, axis breaks) transform values from their own spaces into linear space.
     */
    transformIn: (
        this: This,
        val: number,
        opt: ScaleMapperTransformInOpt | NullUndefined
    ) => number;

    /**
     * [NOTICE]:
     *  - This method must be available since the instance is constructed.
     *  - This method has nothing to do with extent - transforming out of extent is supported.
     *
     * The inverse method of `transformIn`.
     */
    transformOut: (
        this: This,
        val: number,
        opt: ScaleMapperTransformOutOpt | NullUndefined
    ) => number;

    /**
     * Whether the extent contains the given value.
     */
    contain: (this: This, val: number) => boolean;

    /**
     * [NOTICE]:
     *  In EC_FULL_UPDATE_CYCLE, scale extent is finally determined at `coordSys#update` stage.
     *
     * Get a clone of the scale extent.
     * An extent is always in an increase order.
     * It always returns an array - never be a null/undefined.
     *
     * @see SCALE_EXTENT_CONSTRUCTION for the full processing flow.
     */
    getExtent(this: This): number[];

    /**
     * [NOTICE]:
     *  Callers must NOT modify the return.
     *
     * @see SCALE_EXTENT_CONSTRUCTION for the full processing flow.
     */
    getExtentUnsafe(
        this: This,
        kind: ScaleExtentKind,
        // NullUndefined means the outermost space.
        depth: ScaleMapperDepthOpt['depth'] | NullUndefined
    ): number[] | NullUndefined;

    /**
     * [NOTICE]:
     *  The caller must ensure `start <= end` and both are finite number!
     *
     * `setExtent` is identical to `setExtent2(SCALE_EXTENT_KIND_EFFECTIVE)`.
     *
     * @see SCALE_EXTENT_CONSTRUCTION for the full processing flow.
     */
    setExtent(this: This, start: number, end: number): void;
    setExtent2(this: This, kind: ScaleExtentKind, start: number, end: number): void;

    /**
     * Filter for sanitization.
     */
    getFilter?: () => DataSanitizationFilter;

    /**
     * NOTICE:
     *  - Should not sanitize invalid values (e.g., NaN, Infinity, null, undefined),
     *    since it probably has special meaning, and always properly handled in every Scale.
     *  - Should not depend on the current `extent`, since it can be used before `extent` determined.
     *
     * Sanitize a value if possible. For example, for LogScale, the negative part will be clampped.
     * This provides some permissiveness to ec option like `xxxAxis.min/max`.
     */
    sanitize?: (
        (this: This, value: number | NullUndefined, dataExtent: number[]) => number | NullUndefined
    ) | NullUndefined;

    /**
     * If not provided, use `0`.
     */
    getDefaultStartValue?: (() => number) | NullUndefined;

    /**
     * Restrict the modification behavior of a scale for robustness. e.g., avoid subsequently
     * modifying `SCALE_EXTENT_KIND_EFFECTIVE` but no sync to `SCALE_EXTENT_KIND_MAPPING`.
     */
    freeze(this: This): void;
}

export function initBreakOrLinearMapper(
    // If input `null/undefined`, a mapper will be created.
    mapper: ScaleMapper | NullUndefined,
    breakParsed: AxisBreakParsingResult | NullUndefined,
    initialExtent: number[] | NullUndefined,
): {
    // If breaks are not available, `brk` is `null/undefined`.
    brk: BreakScaleMapper | NullUndefined;
    // Never be `null/undefined`.
    mapper: ScaleMapper;
} {
    let brk: BreakScaleMapper | NullUndefined;
    mapper = mapper || {} as ScaleMapper;

    const scaleBreakHelper = getScaleBreakHelper();
    if (scaleBreakHelper) {

        const brkMapper = scaleBreakHelper.createBreakScaleMapper(breakParsed, initialExtent);

        if (brkMapper.hasBreaks()) {
            // Some `ScaleMapper` methods (such as `normalize`) needs to be fast for large data
            // when no breaks, so mount break methods only when breaks really exist.
            each(SCALE_MAPPER_METHOD_NAMES, function (methodName) {
                if (brkMapper[methodName]) {
                    (mapper as any)[methodName] = bind(brkMapper[methodName], brkMapper);
                }
            });
            brk = brkMapper;
        }
    }

    if (brk == null) {
        initLinearScaleMapper(mapper, initialExtent);
    }

    return {brk, mapper};
}

export type DecoratedScaleMapperMethods<THost extends ScaleMapper> = Omit<ScaleMapperGeneric<THost>, 'freeze'>;

export function decorateScaleMapper<THost extends ScaleMapper>(
    host: THost,
    decoratedMapperMethods: Omit<ScaleMapperGeneric<THost>, 'freeze'>
): void {
    each(SCALE_MAPPER_METHOD_NAMES, function (methodName) {
        (host as any)[methodName] = (decoratedMapperMethods as ScaleMapperGeneric<THost>)[methodName];
    });
}

export function enableScaleMapperFreeze(host: ScaleMapper, subMapper: ScaleMapper): void {
    host.freeze = noop;
    if (__DEV__) {
        host.freeze = function () {
            subMapper.freeze();
        };
    };
}

export function getScaleExtentForTickUnsafe(mapper: ScaleMapper): number[] {
    return mapper.getExtentUnsafe(SCALE_EXTENT_KIND_EFFECTIVE, SCALE_MAPPER_DEPTH_OUT_OF_BREAK);
}

export function getScaleExtentForMappingUnsafe(
    mapper: ScaleMapper,
    // NullUndefined means the outermost space.
    depth: ScaleMapperDepthOpt['depth'] | NullUndefined
): number[] {
    return mapper.getExtentUnsafe(SCALE_EXTENT_KIND_MAPPING, depth)
        || mapper.getExtentUnsafe(SCALE_EXTENT_KIND_EFFECTIVE, depth);
}

export function getScaleLinearSpanForMapping(mapper: ScaleMapper): number {
    const extent = getScaleExtentForMappingUnsafe(mapper, SCALE_MAPPER_DEPTH_INNERMOST);
    return extent[1] - extent[0];
}

export function getScaleLinearSpanEffective(mapper: ScaleMapper): number {
    const extent = mapper.getExtentUnsafe(SCALE_EXTENT_KIND_EFFECTIVE, SCALE_MAPPER_DEPTH_INNERMOST);
    return extent[1] - extent[0];
}

// ------ END: Scale Mapper Core ------


// ------ START: Linear Scale Mapper ------

/**
 * Generally, no need to export `LinearScaleMapper` and not recommended
 * to visit `_extent` directly outside, otherwise it may be incorrect
 * due to possible polymorphism - use `getExtentUnsafe()` instead.
 */
interface LinearScaleMapper extends ScaleMapper {
    /**
     * [CAVEAT]:
     *  - Should update only by `setExtent` or `setExtent2`!
     *  - The caller of `setExtent()` should ensure `extent[0] <= extent[1]`,
     *    but it is initialized as `[Infinity, -Infinity]`.
     *    With these restriction, `extent` can only be either:
     *      + `extent[0] < extent[1]` and both finite, or
     *      + `extent[0] === extent[1]` and both finite, or
     *      + `extent[0] === Infinity && extent[1] === -Infinity`
     *
     * Structure: `_extent[ScaleExtentKind][]`
     */
    readonly _extents: number[][];
    readonly _frozen: boolean;
}

export function initLinearScaleMapper(
    // If input `null/undefined`, a mapper will be created.
    mapper: ScaleMapper | NullUndefined,
    initialExtent: number[] | NullUndefined
): ScaleMapper {
    const linearMapper = (mapper || {}) as LinearScaleMapper;

    const extendList: number[][] = [];
    // @ts-ignore
    linearMapper._extents = extendList;

    extendList[SCALE_EXTENT_KIND_EFFECTIVE] = initialExtent ? initialExtent.slice() : initExtentForUnion();

    extend(linearMapper, linearScaleMapperMethods);

    return linearMapper;
}

const linearScaleMapperMethods: ScaleMapperGeneric<LinearScaleMapper> = {

    needTransform() {
        return false;
    },

    normalize(val) {
        const extent = this._extents[SCALE_EXTENT_KIND_MAPPING] || this._extents[SCALE_EXTENT_KIND_EFFECTIVE];
        if (extent[1] === extent[0]) {
            return 0.5;
        }
        return (val - extent[0]) / (extent[1] - extent[0]);
    },

    scale(val) {
        const extent = this._extents[SCALE_EXTENT_KIND_MAPPING] || this._extents[SCALE_EXTENT_KIND_EFFECTIVE];
        return val * (extent[1] - extent[0]) + extent[0];
    },

    transformIn(val) {
        return val;
    },

    transformOut(val) {
        return val;
    },

    contain(val) {
        // This method is typically used in axis trigger and markers.
        // Users may be confused if the extent is restricted to `SCALE_EXTENT_KIND_EFFECTIVE`.
        const extent = getScaleExtentForMappingUnsafe(this, null);
        return val >= extent[0] && val <= extent[1];
    },

    getExtent() {
        return this._extents[SCALE_EXTENT_KIND_EFFECTIVE].slice();
    },

    getExtentUnsafe(kind) {
        return this._extents[kind];
    },

    setExtent(start, end) {
        if (__DEV__) {
            assert(!this._frozen);
        }
        writeExtent(this._extents, SCALE_EXTENT_KIND_EFFECTIVE, start, end);
    },

    setExtent2(kind, start, end) {
        if (__DEV__) {
            assert(!this._frozen);
        }
        const extentList = this._extents;
        if (!extentList[kind]) {
            extentList[kind] = extentList[SCALE_EXTENT_KIND_EFFECTIVE].slice();
        }
        writeExtent(extentList, kind, start, end);
    },

    freeze() {
        if (__DEV__) {
            // @ts-ignore
            this._frozen = true;
        }
    }

};

function writeExtent(
    extentList: number[][], kind: ScaleExtentKind, start: number, end: number
): void {
    // NOTE: `NaN` should be excluded. e.g., `scaleRawExtentInfo.resultMinMax` may be `[NaN, NaN]`.
    if (isValidBoundsForExtent(start, end)) {
        extentList[kind][0] = start;
        extentList[kind][1] = end;
    }
    else {
        if (__DEV__) {
            // PENDING: should use `assert` after fixing all invalid calls.
            if (start != null && end != null && start <= end) {
                error(`Invalid setExtent call - start: ${start}, end: ${end}`);
            }
        }
    }
}

// ------ END: Linear Scale Mapper ------
