/*
 * Copyright 2017 Palantir Technologies, Inc. All rights reserved.
 * Licensed 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 * as React from "react";

import { Keys, NumericInput } from "@blueprintjs/core";
import { Example, ExampleProps } from "@blueprintjs/docs-theme";

export interface INumericInputExtendedExampleState {
    value?: string;
}

const NumberAbbreviation = {
    BILLION: "b",
    MILLION: "m",
    THOUSAND: "k",
};

const NUMBER_ABBREVIATION_REGEX = /((\.\d+)|(\d+(\.\d+)?))(k|m|b)\b/gi;
const SCIENTIFIC_NOTATION_REGEX = /((\.\d+)|(\d+(\.\d+)?))(e\d+)\b/gi;

export class NumericInputExtendedExample extends React.PureComponent<ExampleProps, INumericInputExtendedExampleState> {
    public state: INumericInputExtendedExampleState = {
        value: "",
    };

    public render() {
        const { value } = this.state;

        return (
            <Example options={false} {...this.props}>
                <NumericInput
                    allowNumericCharactersOnly={false}
                    onBlur={this.handleBlur}
                    onKeyDown={this.handleKeyDown}
                    onValueChange={this.handleValueChange}
                    placeholder="Enter a number or expression..."
                    value={value}
                />
            </Example>
        );
    }

    private handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
        this.handleConfirm((e.target as HTMLInputElement).value);
    };

    private handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
        // eslint-disable-next-line deprecation/deprecation
        if (e.keyCode === Keys.ENTER) {
            this.handleConfirm((e.target as HTMLInputElement).value);
        }
    };

    private handleValueChange = (_valueAsNumber: number, valueAsString: string) => {
        this.setState({ value: valueAsString });
    };

    private handleConfirm = (value: string) => {
        let result = value;
        result = this.expandScientificNotationTerms(result);
        result = this.expandNumberAbbreviationTerms(result);
        result = this.evaluateSimpleMathExpression(result);
        result = this.nanStringToEmptyString(result);
        this.setState({ value: result });

        // the user could have typed a different expression that evaluates to
        // the same value. force the update to ensure a render triggers even if
        // this is the case.
        this.forceUpdate();
    };

    private expandScientificNotationTerms = (value: string) => {
        // leave empty strings empty
        if (!value) {
            return value;
        }
        return value.replace(SCIENTIFIC_NOTATION_REGEX, this.expandScientificNotationNumber);
    };

    private expandNumberAbbreviationTerms = (value: string) => {
        // leave empty strings empty
        if (!value) {
            return value;
        }
        return value.replace(NUMBER_ABBREVIATION_REGEX, this.expandAbbreviatedNumber);
    };

    // Adapted from http://stackoverflow.com/questions/2276021/evaluating-a-string-as-a-mathematical-expression-in-javascript
    private evaluateSimpleMathExpression = (value: string) => {
        // leave empty strings empty
        if (!value) {
            return value;
        }

        // parse all terms from the expression. we allow simple addition and
        // subtraction only, so we'll split on the + and - characters and then
        // validate that each term is a number.
        const terms = value.split(/[+\-]/);

        // ex. "1 + 2 - 3 * 4" will parse on the + and - signs into
        // ["1 ", " 2 ", " 3 * 4"]. after trimming whitespace from each term
        // and coercing them to numbers, the third term will become NaN,
        // indicating that there was some illegal character present in it.
        const trimmedTerms = terms.map((term: string) => term.trim());
        const numericTerms = trimmedTerms.map((term: string) => +term);
        const illegalTerms = numericTerms.filter(isNaN);

        if (illegalTerms.length > 0) {
            return "";
        }

        // evaluate the expression now that we know it's valid
        let total = 0;

        // the regex below will match decimal numbers--optionally preceded by
        // +/- followed by any number of spaces—-including each of the
        // following:
        // ".1"
        // "  1"
        // "1.1"
        // "+ 1"
        // "-   1.1"
        const matches = value.match(/[+\-]*\s*(\.\d+|\d+(\.\d+)?)/gi) || [];
        for (const match of matches) {
            const compactedMatch = match.replace(/\s/g, "");
            total += parseFloat(compactedMatch);
        }
        const roundedTotal = this.roundValue(total);
        return roundedTotal.toString();
    };

    private nanStringToEmptyString = (value: string) => {
        // our evaluation logic isn't perfect, so use this as a final
        // sanitization step if the result was not a number.
        return value === "NaN" ? "" : value;
    };

    private expandAbbreviatedNumber = (value: string) => {
        if (!value) {
            return value;
        }

        const num = +value.substring(0, value.length - 1);
        const lastChar = value.charAt(value.length - 1).toLowerCase();

        let result: number;

        if (lastChar === NumberAbbreviation.THOUSAND) {
            result = num * 1e3;
        } else if (lastChar === NumberAbbreviation.MILLION) {
            result = num * 1e6;
        } else if (lastChar === NumberAbbreviation.BILLION) {
            result = num * 1e9;
        }

        const isValid = result != null && !isNaN(result);

        if (isValid) {
            result = this.roundValue(result);
        }

        return isValid ? result.toString() : "";
    };

    private expandScientificNotationNumber = (value: string) => {
        if (!value) {
            return value;
        }
        return (+value).toString();
    };

    private roundValue = (value: number, precision: number = 1) => {
        // round to at most two decimal places
        return Math.round(value * 10 ** precision) / 10 ** precision;
    };
}
