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

/**
 * Linear continuous scale
 * http://en.wikipedia.org/wiki/Level_of_measurement
 */

import Scale from './Scale';
import OrdinalMeta from '../data/OrdinalMeta';
import {
    OrdinalRawValue,
    OrdinalNumber,
    OrdinalSortInfo,
    ScaleTick,
} from '../util/types';
import { CategoryAxisBaseOption } from '../coord/axisCommonTypes';
import { isArray, map, isObject, isString } from 'zrender/src/core/util';
import { mathMin, mathRound } from '../util/number';
import {
    DecoratedScaleMapperMethods,
    decorateScaleMapper, enableScaleMapperFreeze, getScaleExtentForTickUnsafe, initBreakOrLinearMapper,
    ScaleMapper, ScaleMapperGeneric
} from './scaleMapper';
import { ordinalScaleCreateTicks } from './helper';


type OrdinalScaleSetting = {
    ordinalMeta?: OrdinalMeta | CategoryAxisBaseOption['data'];
    extent?: number[];
};

/**
 * @final NEVER inherit me!
 */
interface OrdinalScale extends ScaleMapperGeneric<OrdinalScale> {
    _mapper: ScaleMapper;
}
class OrdinalScale extends Scale<OrdinalScale> {

    static type = 'ordinal';
    readonly type = 'ordinal' as const;

    private _ordinalMeta: OrdinalMeta;

    /**
     * For example:
     * Given original ordinal data:
     * ```js
     * option = {
     *     xAxis: {
     *         // Their raw ordinal numbers are:
     *         //      0    1    2    3    4    5
     *         data: ['a', 'b', 'c', 'd', 'e', 'f']
     *     },
     *     yAxis: {}
     *     series: {
     *         type: 'bar',
     *         data: [
     *             ['d', 110], // ordinalNumber: 3
     *             ['c', 660], // ordinalNumber: 2
     *             ['f', 220], // ordinalNumber: 5
     *             ['e', 550]  // ordinalNumber: 4
     *         ],
     *         realtimeSort: true
     *     }
     * };
     * ```
     * After realtime sorted (order by yValue desc):
     * ```js
     * _ordinalNumbersByTick: [
     *     2, // tick: 0, yValue: 660
     *     5, // tick: 1, yValue: 220
     *     3, // tick: 2, yValue: 110
     *     4, // tick: 3, yValue: 550
     *     0, // tick: 4, yValue: -
     *     1, // tick: 5, yValue: -
     * ],
     * _ticksByOrdinalNumber: [
     *     4, // ordinalNumber: 0, yValue: -
     *     5, // ordinalNumber: 1, yValue: -
     *     0, // ordinalNumber: 2, yValue: 660
     *     2, // ordinalNumber: 3, yValue: 110
     *     3, // ordinalNumber: 4, yValue: 550
     *     1, // ordinalNumber: 5, yValue: 220
     * ]
     * ```
     * NOTICE:
     *  - The index of `_ordinalNumbersByTick` is "tick number", i.e., `tick.value`,
     *    rather than the index of `scale.getTicks()`. They are not the same when
     *    `_extent[0]` is delibrately set to be not zero, or `axisTick/axisLabel.interval` > 0.
     *  - Currently we only support that the index of `_ordinalNumbersByTick` is
     *    from `0` to `ordinalMeta.categories.length - 1`.
     *  - `OrdinalNumber` is always from `0` to `ordinalMeta.categories.length - 1`.
     *
     * @see `Ordinal['getRawOrdinalNumber']`
     * @see `OrdinalSortInfo`
     */
    private _ordinalNumbersByTick: OrdinalNumber[];

    /**
     * This is the inverted map of `_ordinalNumbersByTick`.
     * The index is `OrdinalNumber`, which is from `0` to `ordinalMeta.categories.length - 1`.
     * after `_ticksByOrdinalNumber` is initialized.
     *
     * @see `Ordinal['_ordinalNumbersByTick']`
     * @see `Ordinal['_getTickNumber']`
     * @see `OrdinalSortInfo`
     */
    private _ticksByOrdinalNumber: number[];


    constructor(setting: OrdinalScaleSetting) {
        super();

        this.parse = OrdinalScale.parse;

        decorateScaleMapper(this, OrdinalScale.decoratedMethods);

        let ordinalMeta = setting.ordinalMeta;
        // Caution: Should not use instanceof, consider ec-extensions using
        // import approach to get OrdinalMeta class.
        if (!ordinalMeta) {
            ordinalMeta = new OrdinalMeta({});
        }
        if (isArray(ordinalMeta)) {
            ordinalMeta = new OrdinalMeta({
                categories: map(ordinalMeta, item => (isObject(item) ? item.value : item))
            });
        }
        this._ordinalMeta = ordinalMeta as OrdinalMeta;

        // Create an interval LinearScaleMapper, and decorate it.
        const res = initBreakOrLinearMapper(
            null,
            null, // Do not support break in OrdinalScale yet.
            setting.extent || [0, ordinalMeta.categories.length - 1]
        );
        this._mapper = res.mapper;

        enableScaleMapperFreeze(this, res.mapper);
    }

    private static parse(this: OrdinalScale, val: OrdinalRawValue | OrdinalNumber): OrdinalNumber {
        // Caution: Math.round(null) will return `0` rather than `NaN`
        if (val == null) {
            val = NaN;
        }
        else if (isString(val)) {
            val = this._ordinalMeta.getOrdinal(val);
            if (val == null) {
                val = NaN;
            }
        }
        else {
            // The val from user input might be float.
            val = mathRound(val);
        }
        return val;
    }

    static decoratedMethods: DecoratedScaleMapperMethods<OrdinalScale> = {

        needTransform() {
            return this._mapper.needTransform();
        },

        contain(this: OrdinalScale, val: OrdinalNumber): boolean {
            return this._mapper.contain(this._getTickNumber(val))
                && val >= 0 && val < this._ordinalMeta.categories.length;
        },

        normalize(this: OrdinalScale, val: OrdinalNumber): number {
            return this._mapper.normalize(this._getTickNumber(val));
        },

        scale(this: OrdinalScale, val: number): OrdinalNumber {
            return this.getRawOrdinalNumber(mathRound(this._mapper.scale(val)));
        },

        transformIn(val, opt) {
            return this._mapper.transformIn(this._getTickNumber(val), opt);
        },

        transformOut(val, opt) {
            return this.getRawOrdinalNumber(this._mapper.transformOut(val, opt));
        },

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

        getExtentUnsafe(kind, depth) {
            return this._mapper.getExtentUnsafe(kind, depth);
        },

        /**
         * NOTICE: OrdinalScale extent should always originates from
         * `[0, ordinalMeta.categories.length - 1]`, regardless of min/max of `series.data`.
         * But settings like `xxxAxis.min/max` can still modify the extent.
         * It is handled by constructor of `ScaleRawExtentInfo`.
         */
        setExtent(start, end) {
            return this._mapper.setExtent(start, end);
        },

        setExtent2(kind, start, end) {
            return this._mapper.setExtent2(kind, start, end);
        },

    };

    /**
     * PENDING: currently this method is not used.
     * `makeCategoryTicks` is effectively used.
     */
    getTicks(): ScaleTick[] {
        const ticks: ScaleTick[] = [];
        ordinalScaleCreateTicks(this, 0, function (tick) {
            ticks.push(tick);
        });
        return ticks;
    }

    getMinorTicks(splitNumber: number): number[][] {
        // Not support.
        return;
    }

    /**
     * @see `Ordinal['_ordinalNumbersByTick']`
     */
    setSortInfo(info: OrdinalSortInfo): void {
        if (info == null) {
            this._ordinalNumbersByTick = this._ticksByOrdinalNumber = null;
            return;
        }

        const infoOrdinalNumbers = info.ordinalNumbers;
        const ordinalsByTick = this._ordinalNumbersByTick = [] as OrdinalNumber[];
        const ticksByOrdinal = this._ticksByOrdinalNumber = [] as number[];

        // Unnecessary support negative tick in `realtimeSort`.
        let tickNum = 0;
        const allCategoryLen = this._ordinalMeta.categories.length;
        for (const len = mathMin(allCategoryLen, infoOrdinalNumbers.length); tickNum < len; ++tickNum) {
            const ordinalNumber = ordinalsByTick[tickNum] = infoOrdinalNumbers[tickNum];
            ticksByOrdinal[ordinalNumber] = tickNum;
        }
        // Handle that `series.data` only covers part of the `axis.category.data`.
        let unusedOrdinal = 0;
        for (; tickNum < allCategoryLen; ++tickNum) {
            while (ticksByOrdinal[unusedOrdinal] != null) {
                unusedOrdinal++;
            };
            ordinalsByTick[tickNum] = unusedOrdinal;
            ticksByOrdinal[unusedOrdinal] = tickNum;
        }
    }

    private _getTickNumber(ordinal: OrdinalNumber): number {
        const ticksByOrdinalNumber = this._ticksByOrdinalNumber;
        // also support ordinal out of range of `ordinalMeta.categories.length`,
        // where ordinal numbers are used as tick value directly.
        return (ticksByOrdinalNumber && ordinal >= 0 && ordinal < ticksByOrdinalNumber.length)
            ? ticksByOrdinalNumber[ordinal]
            : ordinal;
    }

    /**
     * @usage
     * ```js
     * const ordinalNumber = ordinalScale.getRawOrdinalNumber(tick.value);
     * // case0
     * const rawOrdinalValue = axisModel.getCategories()[ordinalNumber];
     * // case1
     * const rawOrdinalValue = this._ordinalMeta.categories[ordinalNumber];
     * // case2
     * const coord = axis.dataToCoord(ordinalNumber);
     * ```
     *
     * value may be out of range, e.g., when axis max is larger than `ordinalMeta.categories.length`,
     * where ordinal numbers are used as tick value directly.
     */
    getRawOrdinalNumber(tickValue: ScaleTick['value']): OrdinalNumber {
        const ordinalNumbersByTick = this._ordinalNumbersByTick;
        return (ordinalNumbersByTick && tickValue >= 0 && tickValue < ordinalNumbersByTick.length)
            ? ordinalNumbersByTick[tickValue]
            : tickValue;
    }

    /**
     * Get item on tick
     */
    getLabel(tick: ScaleTick): string {
        if (!this.isBlank()) {
            const ordinalNumber = this.getRawOrdinalNumber(tick.value);
            const category = this._ordinalMeta.categories[ordinalNumber];
            // Note that if no data, ordinalMeta.categories is an empty array.
            // Return empty if it's not exist.
            return category == null ? '' : category + '';
        }
    }

    /**
     * NOTICE: This is different from `.getOrdinalMeta().length` when extent
     * is specified by `xxxAxis.min/max` or by `dataZoom`.
     */
    count(): number {
        const extent = getScaleExtentForTickUnsafe(this._mapper);
        return extent[1] - extent[0] + 1;
    }

    getOrdinalMeta(): OrdinalMeta {
        return this._ordinalMeta;
    }

}

Scale.registerClass(OrdinalScale);

export default OrdinalScale;
