/*
* 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 { ParsedValue, DimensionType } from '../../util/types';
import { parseDate, numericToNumber } from '../../util/number';
import { createHashMap, trim, hasOwn, isString, isNumber } from 'zrender/src/core/util';
import { throwError } from '../../util/log';


/**
 * Convert raw the value in to inner value in List.
 *
 * [Performance sensitive]
 *
 * [Caution]: this is the key logic of user value parser.
 * For backward compatibility, do not modify it until you have to!
 */
export function parseDataValue(
    value: any,
    // For high performance, do not omit the second param.
    opt: {
        // Default type: 'number'. There is no 'unknown' type. That is, a string
        // will be parsed to NaN if do not set `type` as 'ordinal'. It has been
        // the logic in `List.ts` for long time. Follow the same way if you need
        // to get same result as List did from a raw value.
        type?: DimensionType
    }
): ParsedValue {
    // Performance sensitive.
    const dimType = opt && opt.type;
    if (dimType === 'ordinal') {
        // If given value is a category string
        return value;
    }

    if (dimType === 'time'
        // spead up when using timestamp
        && !isNumber(value)
        && value != null
        && value !== '-'
    ) {
        value = +parseDate(value);
    }

    // dimType defaults 'number'.
    // If dimType is not ordinal and value is null or undefined or NaN or '-',
    // parse to NaN.
    // number-like string (like ' 123 ') can be converted to a number.
    // where null/undefined or other string will be converted to NaN.
    return (value == null || value === '')
        ? NaN
        // If string (like '-'), using '+' parse to NaN
        // If object, also parse to NaN
        : Number(value);
};




export type RawValueParserType = 'number' | 'time' | 'trim';
type RawValueParser = (val: unknown) => unknown;
const valueParserMap = createHashMap<RawValueParser, RawValueParserType>({
    'number': function (val): number {
        // Do not use `numericToNumber` here. We have `numericToNumber` by default.
        // Here the number parser can have loose rule:
        // enable to cut suffix: "120px" => 120, "14%" => 14.
        return parseFloat(val as string);
    },
    'time': function (val): number {
        // return timestamp.
        return +parseDate(val);
    },
    'trim': function (val) {
        return isString(val) ? trim(val) : val;
    }
});

export function getRawValueParser(type: RawValueParserType): RawValueParser {
    return valueParserMap.get(type);
}




export interface FilterComparator {
    evaluate(val: unknown): boolean;
}

const ORDER_COMPARISON_OP_MAP: {
    [key in OrderRelationOperator]: ((lval: unknown, rval: unknown) => boolean)
} = {
    lt: (lval, rval) => lval < rval,
    lte: (lval, rval) => lval <= rval,
    gt: (lval, rval) => lval > rval,
    gte: (lval, rval) => lval >= rval
};

class FilterOrderComparator implements FilterComparator {
    private _rvalFloat: number;
    private _opFn: (lval: unknown, rval: unknown) => boolean;
    constructor(op: OrderRelationOperator, rval: unknown) {
        if (!isNumber(rval)) {
            let errMsg = '';
            if (__DEV__) {
                errMsg = 'rvalue of "<", ">", "<=", ">=" can only be number in filter.';
            }
            throwError(errMsg);
        }
        this._opFn = ORDER_COMPARISON_OP_MAP[op];
        this._rvalFloat = numericToNumber(rval);
    }
    // Performance sensitive.
    evaluate(lval: unknown): boolean {
        // Most cases is 'number', and typeof maybe 10 times faseter than parseFloat.
        return isNumber(lval)
            ? this._opFn(lval, this._rvalFloat)
            : this._opFn(numericToNumber(lval), this._rvalFloat);
    }
}

export class SortOrderComparator {
    private _incomparable: number;
    private _resultLT: -1 | 1;
    /**
     * @param order by default: 'asc'
     * @param incomparable by default: Always on the tail.
     *        That is, if 'asc' => 'max', if 'desc' => 'min'
     *        See the definition of "incomparable" in [SORT_COMPARISON_RULE].
     */
    constructor(order: 'asc' | 'desc', incomparable: 'min' | 'max') {
        const isDesc = order === 'desc';
        this._resultLT = isDesc ? 1 : -1;
        if (incomparable == null) {
            incomparable = isDesc ? 'min' : 'max';
        }
        this._incomparable = incomparable === 'min' ? -Infinity : Infinity;
    }
    // See [SORT_COMPARISON_RULE].
    // Performance sensitive.
    evaluate(lval: unknown, rval: unknown): -1 | 0 | 1 {
        // Most cases is 'number', and typeof maybe 10 times faseter than parseFloat.
        let lvalFloat = isNumber(lval) ? lval : numericToNumber(lval);
        let rvalFloat = isNumber(rval) ? rval : numericToNumber(rval);
        const lvalNotNumeric = isNaN(lvalFloat as number);
        const rvalNotNumeric = isNaN(rvalFloat as number);

        if (lvalNotNumeric) {
            lvalFloat = this._incomparable;
        }
        if (rvalNotNumeric) {
            rvalFloat = this._incomparable;
        }
        if (lvalNotNumeric && rvalNotNumeric) {
            const lvalIsStr = isString(lval);
            const rvalIsStr = isString(rval);
            if (lvalIsStr) {
                lvalFloat = rvalIsStr ? lval as unknown as number : 0;
            }
            if (rvalIsStr) {
                rvalFloat = lvalIsStr ? rval as unknown as number : 0;
            }
        }

        return lvalFloat < rvalFloat ? this._resultLT
            : lvalFloat > rvalFloat ? (-this._resultLT as -1 | 1)
            : 0;
    }
}

class FilterEqualityComparator implements FilterComparator {
    private _isEQ: boolean;
    private _rval: unknown;
    private _rvalTypeof: string;
    private _rvalFloat: number;
    constructor(isEq: boolean, rval: unknown) {
        this._rval = rval;
        this._isEQ = isEq;
        this._rvalTypeof = typeof rval;
        this._rvalFloat = numericToNumber(rval);
    }
    // Performance sensitive.
    evaluate(lval: unknown): boolean {
        let eqResult = lval === this._rval;
        if (!eqResult) {
            const lvalTypeof = typeof lval;
            if (lvalTypeof !== this._rvalTypeof && (lvalTypeof === 'number' || this._rvalTypeof === 'number')) {
                eqResult = numericToNumber(lval) === this._rvalFloat;
            }
        }
        return this._isEQ ? eqResult : !eqResult;
    }
}

type OrderRelationOperator = 'lt' | 'lte' | 'gt' | 'gte';
export type RelationalOperator = OrderRelationOperator | 'eq' | 'ne';

/**
 * [FILTER_COMPARISON_RULE]
 * `lt`|`lte`|`gt`|`gte`:
 * + rval must be a number. And lval will be converted to number (`numericToNumber`) to compare.
 * `eq`:
 * + If same type, compare with `===`.
 * + If there is one number, convert to number (`numericToNumber`) to compare.
 * + Else return `false`.
 * `ne`:
 * + Not `eq`.
 *
 *
 * [SORT_COMPARISON_RULE]
 * All the values are grouped into three categories:
 * + "numeric" (number and numeric string)
 * + "non-numeric-string" (string that excluding numeric string)
 * + "others"
 * "numeric" vs "numeric": values are ordered by number order.
 * "non-numeric-string" vs "non-numeric-string": values are ordered by ES spec (#sec-abstract-relational-comparison).
 * "others" vs "others": do not change order (always return 0).
 * "numeric" vs "non-numeric-string": "non-numeric-string" is treated as "incomparable".
 * "number" vs "others": "others" is treated as "incomparable".
 * "non-numeric-string" vs "others": "others" is treated as "incomparable".
 * "incomparable" will be seen as -Infinity or Infinity (depends on the settings).
 * MEMO:
 *   Non-numeric string sort makes sense when we need to put the items with the same tag together.
 *   But if we support string sort, we still need to avoid the misleading like `'2' > '12'`,
 *   So we treat "numeric-string" sorted by number order rather than string comparison.
 *
 *
 * [CHECK_LIST_OF_THE_RULE_DESIGN]
 * + Do not support string comparison until required. And also need to
 *   avoid the misleading of "2" > "12".
 * + Should avoid the misleading case:
 *   `" 22 " gte "22"` is `true` but `" 22 " eq "22"` is `false`.
 * + JS bad case should be avoided: null <= 0, [] <= 0, ' ' <= 0, ...
 * + Only "numeric" can be converted to comparable number, otherwise converted to NaN.
 *   See `util/number.ts#numericToNumber`.
 *
 * @return If `op` is not `RelationalOperator`, return null;
 */
export function createFilterComparator(
    op: string,
    rval?: unknown
): FilterComparator {
    return (op === 'eq' || op === 'ne')
        ? new FilterEqualityComparator(op === 'eq', rval)
        : hasOwn(ORDER_COMPARISON_OP_MAP, op)
        ? new FilterOrderComparator(op as OrderRelationOperator, rval)
        : null;
}
