/**
 * The number input element
 *
 * @author Platon Fedorov
 * @date 2020-01-14
 */
import * as React from 'react';

import {Icon, Input, joinClassNames, safeInvoke, SIZE} from '../../index';
import {Props, SizeClassNames} from './Input.types';
import * as styles from './input.m.scss';
import {getSizeThemeKey} from '../../utils/getSizeThemeKey';

export type IInputNumber = Omit<Props, 'onChange' | 'value' | 'defaultValue'> & {
    isInteger?: boolean;
    isOnlyNegative?: boolean;
    isOnlyPositive?: boolean;

    min?: number;
    max?: number;
    step?: number;
    value?: number | string;
    defaultValue?: number | string;

    parser?: (displayValue: string | undefined) => string;
    formatter?: (value: number | string | undefined) => string;
    /**
     * Code works correct with a separator consisting of a single symbol
     */
    decimalSeparator?: DecimalSeparator;
    precision?: number;
    onChange?: (value: number | undefined) => void;

    maxErrorMessage?: string;
    minErrorMessage?: string;
}

type DefaultPropsKeys = 'min' | 'max' | 'step';

export type DecimalSeparator = ',' | '.';

export type DefaultProps = Required<Pick<IInputNumber, DefaultPropsKeys>>;

export type PropsWithDefault = IInputNumber & DefaultProps;

type IState = {
    value: number;
    displayValue: string;
    showValue: boolean;
}

export class InputNumber extends React.Component<IInputNumber, IState> {
    static defaultProps: DefaultProps = {
        min: Number.MIN_SAFE_INTEGER,
        max: Number.MAX_SAFE_INTEGER,
        step: 1
    }

    override state = this.initState();

    inputRef = React.createRef<HTMLInputElement>();

    initState (): IState {
        const {value, defaultValue} = this.props;

        return {
            value: defaultValue !== undefined ? Number(defaultValue) : Number(value),
            displayValue: defaultValue !== undefined ? defaultValue.toString() : (value?.toString() || ''),
            showValue: defaultValue === undefined
        };
    }

    createRef () {
        if (this.props.forwardRef) {
            this.inputRef = this.props.forwardRef;
        }

        return this.inputRef;
    }

    isValuePossible = (value: string): boolean => {
        value = this.disableSeparator(value);

        if (value === '' || value === '-') {
            return true;
        }

        const minusIndex = value.indexOf('-', 1),
            expCount = value.match(/e/g),
            dotCount = value.match(/\./g);

        if (minusIndex !== -1 && value[minusIndex - 1] !== 'e' ||
            expCount && expCount.length > 1 ||
            dotCount && dotCount.length > 1 ||
            value.indexOf('e.') !== -1
        ) {
            return false;
        }

        return true;
    };

    onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
        const possibleSymbols = /(e|-|\.|\d)/;

        let toForcePrevent = !possibleSymbols.test(e.key);

        if (this.props.decimalSeparator) {
            toForcePrevent = toForcePrevent && (e.key !== this.props.decimalSeparator);
        }

        if (!toForcePrevent && this.inputRef.current && this.inputRef.current.selectionStart !== null) {
            const oldValue = this.inputRef.current.value.split('');

            oldValue.splice(this.inputRef.current.selectionStart, 0, e.key);

            const newValue = this.parser(oldValue.join(''));

            toForcePrevent = toForcePrevent || !this.isValuePossible(newValue);

            if (e.key === '-' && newValue[0] === '-' && this.props.isOnlyPositive) {
                toForcePrevent = true;
            }
        }

        if (toForcePrevent || this.props.isInteger && (e.key === '.' || e.key === 'e')) {
            e.preventDefault();
        }

        safeInvoke(this.props.onKeyPress, e);
    }

    getMinMaxErrorMessage (hasMinError: boolean, hasMaxError: boolean) {
        const {minErrorMessage, maxErrorMessage} = this.props;

        if (hasMinError && minErrorMessage !== undefined) {
            return minErrorMessage;
        }

        if (hasMaxError && maxErrorMessage !== undefined) {
            return maxErrorMessage;
        }

        return undefined;
    }

    hasMinError = (value: number) => {
        let {min} = this.props as PropsWithDefault;

        if (this.props.isOnlyPositive) {
            min = Math.max(min, 0);
        }

        return value < min;
    }

    hasMaxError = (value: number) => {
        let {max} = this.props as PropsWithDefault;

        if (this.props.isOnlyNegative) {
            max = Math.min(max, 0);
        }

        return value > max;
    }

    escapeRegExp = (str: string) => {
        return str.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
    }

    replaceAll = (str: string, find: string, replace: string) => {
        return str.replace(new RegExp(this.escapeRegExp(find), 'g'), replace);
    }

    enableSeparator = (value: string):string => {
        if (this.props.decimalSeparator) {
            return this.replaceAll(value, '.', this.props.decimalSeparator);
        } else {
            return value;
        }
    }

    disableSeparator = (value: string):string => {
        if (this.props.decimalSeparator) {
            return this.replaceAll(value, this.props.decimalSeparator, '.');
        } else {
            return value;
        }
    }

    formatter = (value: string | number | undefined): string => {
        if (this.props.formatter) {
            return this.props.formatter(value);
        }

        return value ? value.toString() : '';
    }

    parser = (displayValue: string | undefined): string => {
        if (this.props.parser) {
            return this.props.parser(displayValue);
        }

        return displayValue ? displayValue : '';
    }

    getDisplayValueFromNumber = (value: number, toPrecision: boolean = true) => {
        let result = value.toString();

        if (this.props.precision && toPrecision) {
            result = value.toFixed(this.props.precision);
        }

        return this.formatter(this.enableSeparator(result));
    }

    getDisplayValueFromString = (value: string) => {
        return this.formatter(this.enableSeparator(value));
    }

    onIncrease = () => {
        const {step, value} = this.props as PropsWithDefault;
        const stateValue = this.state.value ? this.state.value : 0;
        const newValue = (value !== undefined ? Number(value) : stateValue) + step;

        if (this.props.isOnlyNegative && newValue > 0) {
            return;
        }

        this.setNewValue(newValue);
    };

    onDecrease = () => {
        const {step, value} = this.props as PropsWithDefault;
        const stateValue = this.state.value ? this.state.value : 0;
        const newValue = (value !== undefined ? Number(value) : stateValue) - step;

        if (this.props.isOnlyPositive && newValue < 0) {
            return;
        }

        this.setNewValue(newValue);
    };

    setNewValue (newValue: number, newDisplayValue?: string) {
        this.setState({
            value: newValue,
            showValue: true,
            displayValue: newDisplayValue !== undefined ?
                this.getDisplayValueFromString(newDisplayValue) :
                this.getDisplayValueFromNumber(newValue)
        });

        safeInvoke(this.props.onChange, newValue);
    }

    onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        if (e.currentTarget.value === '') {
            this.setState({
                showValue: true,
                value: NaN,
                displayValue: this.getDisplayValueFromString('')
            });

            safeInvoke(this.props.onChange, undefined);
        } else {
            let newDisplayValue = this.disableSeparator(this.parser(e.currentTarget.value));
            let newValue = Number(newDisplayValue);

            const oldValue = (this.props.value !== undefined && this.props.value !== '') ?
                Number(this.props.value) : this.state.value;

            if (!Number.isNaN(newValue) && oldValue !== newValue) {

                if (this.props.isOnlyNegative && newValue > 0) {
                    newValue = -newValue;
                    newDisplayValue = '-' + newDisplayValue;
                }

                this.setNewValue(newValue, newDisplayValue);
            } else {
                this.setState({
                    showValue: false,
                    displayValue: this.getDisplayValueFromString(newDisplayValue)
                });
            }
        }
    }

    onBlur = (e: React.FocusEvent<HTMLInputElement>) => {
        let value = this.state.value;

        if (this.props.value !== undefined && !Number.isNaN(this.props.value) && this.props.value !== '') {
            value = Number(this.props.value);
        }

        this.setState({
            displayValue: !Number.isNaN(value) ? this.getDisplayValueFromNumber(value) : '',
            showValue: false
        });

        safeInvoke(this.props.onBlur, e);
    }

    override render () {
        const {
            min,
            max,
            type,
            step,
            value,
            onBlur,
            parser,
            onChange,
            formatter,
            precision,
            isInteger,
            onKeyPress,
            forwardRef,
            defaultValue,
            isOnlyNegative,
            isOnlyPositive,
            decimalSeparator,
            size = SIZE.MIDDLE,
            hasError,
            errorMessage,
            ...props} = this.props;

        const sizeThemeKey = getSizeThemeKey<SizeClassNames>('', size);
        const isDisabled = this.props.readOnly || this.props.isDisabled;
        const onIncrease = isDisabled ? undefined : this.onIncrease;
        const onDecrease = isDisabled ? undefined : this.onDecrease;

        let displayValue = this.state.displayValue;

        if (value !== undefined && (this.state.showValue || defaultValue === undefined)) {
            if (typeof value === 'number') {
                if (value !== Number(displayValue)) {
                    displayValue = this.getDisplayValueFromNumber(value, false);
                }
            } else {
                displayValue = this.getDisplayValueFromString(value);
            }
        }

        const containerClassName = joinClassNames(
            styles.incrementArrowsContainer,
            styles[sizeThemeKey],
            [styles.disabled, Boolean(this.props.isDisabled)]
        );

        const numberedDisplayValue = Number(displayValue);
        const hasMinError = displayValue === '' ? false : this.hasMinError(numberedDisplayValue);
        const hasMaxError = displayValue === '' ? false : this.hasMaxError(numberedDisplayValue);

        return (
            <Input
                {...props}
                hasError={hasError || hasMinError || hasMaxError}
                errorMessage={(hasMinError || hasMaxError) ? this.getMinMaxErrorMessage(hasMinError, hasMaxError) : this.props.errorMessage}
                forwardRef={this.createRef()}
                onKeyPress={this.onKeyPress}
                onChange={this.onChange}
                onBlur={this.onBlur}
                value={displayValue}
                size={size}
                suffix={
                    this.props.readOnly !== true && (
                        <div className={containerClassName}>
                            <div
                                className={joinClassNames(styles.incrementArrowsButton)}
                                onClick={onIncrease}
                            >
                                <Icon name={'chevron-up'} size={size}/>
                            </div>
                            <div
                                className={joinClassNames(styles.incrementArrowsButton)}
                                onClick={onDecrease}
                            >
                                <Icon name={'chevron-down'} size={size}/>
                            </div>
                        </div>
                    )
                }
            />
        );
    }
}
