import * as React from 'react';
import * as _ from 'lodash';
import { InputDropDownDrawer } from '../InputDropDownDrawer';

export namespace TokenSuggest {
    export interface Props {
        /**
         * Custom input component
         */
        customInput?: any;

        customInputProps?: any;
        /**
         * Custom component to show when no options
         */
        customEmptyStateMessage?: any;
        /**
         * String to be added automatically after the token closer
         * By default it is a space
         */
        defaultAfterToken?: string;
        /**
         * Message to show when no options
         */
        emptyStateMessage?: string;
        /**
         * List of options to be displayed when the token opener is typed (the state will manage them)
         */
        initialSuggestOptions: { toString: () => string }[];
        /**
         * Value to display inside the input
         */
        inputValue: string;
        /**
         * Function called when the user hits Enter and the dropdown is closed
         * This function is not executed when the Enter is used to select an option
         */
        onEnterKeyPressed?: () => void;
        /**
         * Function called when the value of the input changes
         */
        onValueChange: (value: string, isValid: boolean) => void;
        /**
         * Function called when rendering the options, being responsible of saying whether the value matches the option or not
         * By default function includes is used (case insensitive)
         */
        optionMatches?: (option: { toString: () => string }, currentValue: string) => boolean;
        /**
         * Placeholder of the input
         */
        placeholder?: string;
        /**
         * Indicates if the input is read only
         */
        readOnly?: boolean;
        /**
         * Indicates if the dropdown is displayed when there are no options matching the value
         * It must be true to display emptyStateMessage or customEmptyStateMessage
         */
        showDropDownOnEmpty?: boolean;
        /**
         * Prop to customize styles
         */
        styles?: Styles;
        /**
         * Value of the token closer
         * By default it is >
         */
        tokenCloser?: string;
        /**
         * Value of the token opener
         * By default it is <
         */
        tokenOpener?: string;

        id?: string;

        name?: string;

    }

    export interface State {
        currentToken: string;
        suggestOptions: { toString: () => string }[];
        showDropDown: boolean;
    }

    export interface Styles {
        /**
         * Styles for the component that renders the input and the dropdown
         */
        InputDropDownDrawer?: InputDropDownDrawer.Styles;
    }
}

/**
 * Component that displays an input that drops down a list of tokens
 * The list matches with what the user is typing, and it is showed only after the user typed the token opener
 * 
 * @version 1.0.0
 * 
 */
export default class TokenSuggest extends React.Component<TokenSuggest.Props, TokenSuggest.State> {
    private reservedCharsInRegEx = '.!@#$%^&|()[]{}_+*=';
    private escapedTokenCloser;
    private escapedTokenOpener;
    private currentTokenOpenerIndex;

    static defaultProps = {
        defaultAfterToken: ' ',
        tokenCloser: '>',
        tokenOpener: '<'
    };

    constructor(props: TokenSuggest.Props) {
        super(props);

        this.state = {
            currentToken: '',
            showDropDown: false,
            suggestOptions: []
        };

        this.escapedTokenCloser = this.escapeCharacterForRegEx(props.tokenCloser);
        this.escapedTokenOpener = this.escapeCharacterForRegEx(props.tokenOpener);
    }

    componentWillReceiveProps(nextProps: TokenSuggest.Props) {
        this.escapedTokenCloser = this.escapeCharacterForRegEx(nextProps.tokenCloser);
        this.escapedTokenOpener = this.escapeCharacterForRegEx(nextProps.tokenOpener);
    }

    /**
     * Function called when rendering the options, being responsible of saying whether the value matches the option or not
     * It calls prop optionsMatches is passed
     * By default function startsWith is used (case insensitive)
     */
    filterOptions = (options: { toString: () => string }[], value: string) => (
        options.filter((option) => (
            this.props.optionMatches ?
                this.props.optionMatches(option, value)
                :
                _.startsWith(option.toString().toUpperCase(), value.toUpperCase())
        ))
    );

    handleDropDownEvent = (event, show: boolean): void => {
        this.setState((prevState: TokenSuggest.State, props: TokenSuggest.Props) => ({
            showDropDown: show && (prevState.suggestOptions.length > 0 || Boolean(props.showDropDownOnEmpty))
        }));
    };

    handleSelectOption = (event, option: string): void => {
        let replaceable;
        const { inputValue, tokenCloser } = this.props;
        let defaultAfterToken = this.props.defaultAfterToken;
        const tokenOpener = this.props.tokenOpener || '';
        const currentPosition = event.target.selectionStart
        let indexOfTokenBegin = inputValue.lastIndexOf(tokenOpener);

        if (currentPosition < indexOfTokenBegin) {
            const fromIndex = this.currentTokenOpenerIndex;
            const toIndex = currentPosition;
            const subs = inputValue.substring(fromIndex, toIndex)
            replaceable = subs;
            indexOfTokenBegin = fromIndex;
            defaultAfterToken = inputValue.substring(currentPosition)
        }
        else {
            replaceable = inputValue.substring(indexOfTokenBegin);
        }
        const tokenName = inputValue.substring(indexOfTokenBegin + 1, currentPosition);
        const replaceToken = replaceable.replace(tokenOpener + tokenName, tokenOpener + option + tokenCloser + defaultAfterToken);
        const replacedTokenPrefix = inputValue.substring(0, indexOfTokenBegin);
        const replacedToken = replacedTokenPrefix + replaceToken;

        this.setState({
            currentToken: '',
            suggestOptions: [],
            showDropDown: false
        });

        this.props.onValueChange(replacedToken, this.valueIsValid(replacedToken, this.props.initialSuggestOptions));
    };

    indexesOfCharInString = (char, string) => {
        let a: number[] = [];
        let i = -1;

        while ((i = string.indexOf(char, i + 1)) >= 0) {

            a.push(i);
        }
        return a;
    }

    findNextTokenOpenerIndex = (array, index) => {
        let i = 0
        while (array[i] < index) {
            i = i + 1;
        }
        return i;
    }

    handleChangeValue = (event): void => {
        const { tokenCloser, tokenOpener } = this.props;
        const newValue = event.target.value;
        const numberOfUnonpenedTokens = Math.abs(_.split(newValue, tokenCloser).length - _.split(newValue, tokenOpener).length);

        if (numberOfUnonpenedTokens !== 0) {
            const currentPosition = event.target.selectionStart;
            const indexOfTokenBegin = newValue.lastIndexOf(tokenOpener);
            let currentToken = newValue.substring(indexOfTokenBegin + 1, currentPosition);
            const tokenOpenerArray = this.indexesOfCharInString(tokenOpener, newValue);
            const indexOfCurrentPos = tokenOpenerArray.indexOf(currentPosition);

            if (currentPosition <= indexOfTokenBegin) {
                if (newValue[currentPosition - 1] === tokenOpener) {
                    this.currentTokenOpenerIndex = currentPosition - 1;
                }
                const indexOfNextTokenOpener = this.findNextTokenOpenerIndex(tokenOpenerArray, currentPosition)
                const fromIndex = this.currentTokenOpenerIndex;
                const toIndex = tokenOpenerArray[indexOfNextTokenOpener];
                const subs = newValue.substring(fromIndex, toIndex);
                currentToken = subs.split(tokenOpener).join('');
            }
            const newOptions = this.filterOptions(this.props.initialSuggestOptions, currentToken.trim());
            this.setState((prevState: TokenSuggest.State, props: TokenSuggest.Props) => ({
                currentToken: currentToken,
                showDropDown: newOptions.length > 0 || Boolean(props.showDropDownOnEmpty),
                suggestOptions: newOptions
            }));
        } else {
            this.setState({ suggestOptions: [], showDropDown: false });
        }

        this.props.onValueChange(newValue, this.valueIsValid(newValue, this.props.initialSuggestOptions));
    };


    /**
     * If the character is a reserved one for regular expressions, then it is added \\ as prefix, to escape it
     */
    escapeCharacterForRegEx = (character) => (
        this.reservedCharsInRegEx.indexOf(character) < 0 ? character : '\\' + character
    );

    /**
     * Indicates if there is a token closer that was not opened
     */
    closerWithoutOpener = (value: string, tokenOpener, tokenCloser) => (
        value.match(`(^|${tokenCloser})[^${tokenOpener}]*${tokenCloser}`)
    );

    /**
     * Indicates if there is a token opener that was not closed
     */
    openerWithoutCloser = (value: string, tokenOpener, tokenCloser) => (
        value.match(`${tokenOpener}[^${tokenCloser}]*(${tokenOpener}|$)`)
    );

    /**
     * Indicates if the value is valid
     * To be valid, the value has to contain tokens that were correctly opened and closed
     * To be valid, all the tokens must be in the options
     */
    valueIsValid = (value, options) => {
        if (this.closerWithoutOpener(value, this.escapedTokenOpener, this.escapedTokenCloser) || this.openerWithoutCloser(value, this.escapedTokenOpener, this.escapedTokenCloser)) {
            return false;
        }

        const tokensRegEx = new RegExp(`${this.escapedTokenOpener}([^${this.escapedTokenCloser}]*)${this.escapedTokenCloser}`, 'g');
        const stringOptions = options.map((option) => option.toString());
        let currentToken;
        while (currentToken = tokensRegEx.exec(value)) {
            if (stringOptions.indexOf(currentToken[1]) < 0) {
                return false;
            }
        }

        return true;
    };

    render(): JSX.Element {
        const Styles: TokenSuggest.Styles = _.merge({}, this.props.styles);
        return (
            <InputDropDownDrawer
                customInput={this.props.customInput}
                customInputProps={this.props.customInputProps}
                customEmptyStateMessage={this.props.customEmptyStateMessage}
                emptyStateMessage={this.props.emptyStateMessage}
                highlightedChunk={this.state.currentToken}
                inputValue={this.props.inputValue}
                onDropDownEvent={this.handleDropDownEvent}
                onEnterKeyPressed={this.props.onEnterKeyPressed}
                onSelectOption={this.handleSelectOption}
                onValueChange={this.handleChangeValue}
                placeholder={this.props.placeholder}
                readOnly={this.props.readOnly}
                showDropDown={this.state.showDropDown}
                suggestOptions={this.state.suggestOptions}
                styles={Styles.InputDropDownDrawer}
                id={this.props.id && this.props.id}
                name={this.props.name && this.props.name}
            />
        );
    }
}
