import * as React from 'react';
import * as Bloodhound from 'bloodhound-js';
import { addPopoverBehavior } from '../../mmui-widget';

export interface MmuiTokenTypeaheadProps {
    elmtId;
    placeHolder: string;
    data: any;
    url: string;
    hasReset: boolean;
    isSingleSelect: boolean;
    inputName: string;
}
export interface MmuiTokenTypeaheadState {
    elmtId;
    placeHolder: string;
    hasReset: boolean;
    tokens: any[];
    tokensInital: any[];
    typeaheadInput: string;
    suggestions: any[];
    keepSuggestions;
    isExpanded: boolean;
    hasChanged: boolean;
    inputName: string;
}

/**
 * Parse token object from given token HTMLElement
 * @param tokenElmt - html element representing the token.
 * @param tokenElmt.dataset.tokenId - unique identifier for the token
 * @param tokenElmt.dataset.tokenDisplay - what is displayed in the token
 */
function getToken(tokenElmt: HTMLElement): any {
    let token: any;
    if (tokenElmt) {
        token = {
            id: tokenElmt.dataset.tokenId,
            display: tokenElmt.dataset.tokenDisplay,
        };
    }

    return token;
}

/**
 * Parse Tokenfield html, with provided id attr, into Tokenfield data object
 * @param id
 */
export function getMmuiTokenfieldData(id: string): any {
    let data = { tokens: [] },
        elmt = document.getElementById(id),
        tokenElmt,
        tokenElmts = elmt.querySelectorAll('.mmui-token'),
        token;

    for (let i = 0; i < tokenElmts.length; i++) {
        tokenElmt = tokenElmts[i];
        token = getToken(tokenElmt);

        data.tokens.push(token);
    }

    return data;
}

/**
 * Token Typeahead Component
 * It is an input field that asynchronously queries retrieves results, from a provided url,
 * that matches the users input as they type. If the user selects a result, the result is displayed as a token.
 * If this Token Typeahead is part of a submitted form, the tokens will be submitted with that form as hidden inputs.
 *
 * token: a thing serving as a visible or tangible representation of a fact
 */
export class MmuiTokenTypeaheadComponent<
    P extends MmuiTokenTypeaheadProps,
    S extends MmuiTokenTypeaheadState
> extends React.Component<P, S> {
    typeaheadEngine;
    typeaheadPromise;

    typeaheadEngineRemotePrepare;

    constructor(props) {
        super(props);

        // init state
        const state: any = {
            elmtId: props.elmtId,
            placeHolder: props.placeHolder,
            hasReset: false,
            tokens: props.data.tokens,
            tokensInital: props.data.tokens,
            typeaheadInput: '',
            suggestions: [],
            keepSuggestions: false,
            isExpanded: false,
            hasChanged: false,
            inputName: props.elmtId,
        };

        if (props.inputName) {
            state.inputName = props.inputName;
        }

        if (props.hasReset) {
            state.hasReset = props.hasReset;
        }

        if (props.remotePrepare) {
            this.typeaheadEngineRemotePrepare = (query, settings) => {
                return props.remotePrepare(query, settings, this.state);
            };
        } else {
            this.typeaheadEngineRemotePrepare = (query, settings) => {
                let paramStr = '',
                    queryStr = encodeURIComponent(query),
                    tokenStr = this.state.tokens
                        .map((t) => encodeURIComponent(t.id))
                        .map((t) => `t=${t}`)
                        .join('&');

                if (tokenStr) {
                    paramStr = `${tokenStr}&q=${queryStr}`;
                } else {
                    paramStr = `q=${queryStr}`;
                }

                return `${settings.url}?${paramStr}`;
            };
        }

        // setup Bloodhound typeahead engine
        this.typeaheadEngine = new Bloodhound({
            // local: [{ id: 1, display: 'one' }, { id: 2, display: 'two' }, { id: 3, display: 'three' }],
            remote: {
                url: props.url,
                // A function to prepare the settings object passed to transport when a request is about to be made
                prepare: this.typeaheadEngineRemotePrepare,
            },
            queryTokenizer: Bloodhound.tokenizers.whitespace,
            // A function that transforms a datum into an array of string tokens
            datumTokenizer: function (d) {
                return Bloodhound.tokenizers.whitespace(d.display);
            },
        });
        this.typeaheadPromise = this.typeaheadEngine.initialize();

        this.state = state;
    }

    /**
     * Put the UI focus on the typeahead input field.
     */
    focusOnTypeaheadInput() {
        const typeaheadInputSelector = `#${this.state.elmtId} .mmui-typeahead-input`;
        (
            document.querySelector(typeaheadInputSelector) as HTMLAnchorElement
        ).focus();
    }

    /**
     * Clear typeahead suggestions and input field.
     */
    clearInputSuggestions() {
        this.setState({ suggestions: [], typeaheadInput: '' });
    }

    /**
     * Call typeaheadEngine.search with typeaheadInput value.
     * Then update state with suggestions returned by typeahead engine.
     * @param e: onChange event
     */
    onTypeaheadInput = (e: React.SyntheticEvent) => {
        const typeaheadInputElmt = e.target as HTMLInputElement,
            typeaheadInput = typeaheadInputElmt.value;
        this.setState({ typeaheadInput: typeaheadInput });

        this.typeaheadPromise.then(() => {
            this.typeaheadEngine.search(
                typeaheadInput,

                // synchronous response callback
                // eslint-disable-next-line @typescript-eslint/no-unused-vars
                (rArray) => {
                    // console.log("sync", rArray);
                },
                // asynchronous response callback
                (rArray) => {
                    if (rArray) {
                        this.setState({ suggestions: rArray });
                    }
                }
            );
        });
    };

    /**
     * Add the target suggested to state tokens.
     * @param e: onClick event
     */
    onSelectSuggestion = (e: React.SyntheticEvent) => {
        e.preventDefault();

        let s,
            tokenElmt = e.target as HTMLElement,
            newToken = getToken(tokenElmt),
            newTokenArray = [...this.state.tokens],
            newSuggestions = [];
        if (this.state.keepSuggestions) {
            for (s of this.state.suggestions) {
                if (s.id !== newToken.id) {
                    newSuggestions.push(s);
                }
            }
        }

        newTokenArray.push(newToken);
        this.setState({ tokens: newTokenArray, hasChanged: true });

        //wait to update the new suggestions so that the component doesn't re-render before click event.
        setTimeout(() => {
            if (!this.state.keepSuggestions) {
                this.clearInputSuggestions();
            } else {
                this.setState({ suggestions: newSuggestions });
            }

            if (this.props.isSingleSelect) {
                this.setState({ isExpanded: false });
            } else {
                this.focusOnTypeaheadInput();
            }
        }, 100);
    };

    /**
     * Reset the tokenfield back to its original state
     * @param e
     */
    onReset = (e: React.SyntheticEvent) => {
        e.preventDefault();
        this.setState({ tokens: this.state.tokensInital, hasChanged: false });
    };

    /**
     * Remove the target token from state
     * @param e: onClick event
     */
    onTokenRemove = (e: React.SyntheticEvent) => {
        e.preventDefault();
        e.stopPropagation();
        e.nativeEvent.stopImmediatePropagation();

        const tokenElmt = e.target as HTMLElement,
            tokenId = tokenElmt.dataset.tokenId;
        setTimeout(() => {
            this.removeToken(tokenId);
        }, 100);
    };

    removeToken(tokenId: string) {
        let newTokenArray = [],
            token;

        for (token of this.state.tokens) {
            if (token.id !== tokenId) {
                newTokenArray.push(token);
            }
        }

        this.setState({ tokens: newTokenArray, hasChanged: true });

        const popover = document.querySelector('.popover');
        if (popover) {
            popover.remove();
        }
    }

    /**
     * This function is used to detect when the user clicks outside of the visual area of the Component
     *  and then sets state to collapsed (isExpanded == false) and cleans up the event listeners.
     * @param e
     */
    onBlur = (e) => {
        this.blur(e);
    };

    /**
     * Check if this Component element the event target
     * @param e
     */
    getIsComponentElmtEventTarget(e) {
        let elmt = e.target,
            isComponentElmtTarget = false;
        while (elmt) {
            isComponentElmtTarget =
                isComponentElmtTarget || elmt.id === this.state.elmtId;
            elmt = elmt.parentElement;
        }
        return isComponentElmtTarget;
    }

    blur(e) {
        if (this.state.isExpanded) {
            if (!this.getIsComponentElmtEventTarget(e)) {
                this.setState({ isExpanded: false });
                document.removeEventListener('click', this.onBlur);
                document.removeEventListener('keyup', this.onNextSelection);
                this.clearInputSuggestions();
            }
        }
    }

    /**
     * This function is used to detect ArrowUp and ArrowDown keyboard
     *  and moves up the Selection item list on ArrowUp and down Selection item list on ArrowDown
     * @param e
     */
    onNextSelection = (e) => {
        if (this.state.isExpanded && this.state.suggestions.length > 0) {
            const suggestionsSelector = `#${this.state.elmtId} .mmui-token-suggestion`,
                isArrowUp = e.code === 'ArrowUp',
                isArrowDown = e.code === 'ArrowDown';
            if (isArrowUp || isArrowDown) {
                const focusedSuggestion = document.querySelector(
                    suggestionsSelector + ':focus'
                ) as HTMLElement;
                if (focusedSuggestion) {
                    if (isArrowUp && focusedSuggestion.previousSibling) {
                        (
                            focusedSuggestion.previousSibling as HTMLAnchorElement
                        ).focus();
                    } else if (isArrowDown && focusedSuggestion.nextSibling) {
                        (
                            focusedSuggestion.nextSibling as HTMLAnchorElement
                        ).focus();
                    }
                } else {
                    (
                        document.querySelector(
                            suggestionsSelector
                        ) as HTMLAnchorElement
                    ).focus();
                }
            }
        }
    };

    /**
     * This function is used to detect when the Component has been clicked into
     *  and setups up the event listeners
     * @param e
     */
    onExpandTokenfield = (e: React.SyntheticEvent) => {
        e.preventDefault();

        addPopoverBehavior();

        if (this.props.isSingleSelect && this.state.tokens.length > 0) {
            return;
        }

        if (!this.state.isExpanded) {
            document.addEventListener('click', this.onBlur);
            document.addEventListener('keyup', this.onNextSelection);

            this.setState({ isExpanded: true });

            setTimeout(() => {
                this.focusOnTypeaheadInput();
            }, 100);
        }
    };

    tokenDisplayRender(token) {
        const key = `token-${token.id}`;
        let popover;
        const dataContent = 'data-content';
        const dataToggle = 'data-toggle';
        const dataPlacement = 'data-placement';
        const variableAttribute = {
            [dataContent]: token.display,
            [dataToggle]: 'popover',
            [dataPlacement]: 'top',
        };

        if (token.display.length > 14) {
            popover = variableAttribute;
        }

        return (
            <div
                key={key}
                id={key}
                className="mmui-token badge mmui-badge"
                data-token-id={token.id}
                data-token-display={token.display}
                {...popover}
            >
                <span className="mmui-token-display">{token.display}</span>
                <a
                    href="#"
                    className="mmui-token-remove"
                    data-token-id={token.id}
                    onClick={this.onTokenRemove}
                >
                    <i className="fa fa-times-circle"></i>
                </a>
            </div>
        );
    }

    suggestionRender(suggestion) {
        const key = `mmui-suggestion-${suggestion.id}`;
        return (
            <a
                key={key}
                id={key}
                href="#"
                className="mmui-token-suggestion"
                data-token-id={suggestion.id}
                data-token-display={suggestion.display}
                onClick={this.onSelectSuggestion}
            >
                {suggestion.display}
            </a>
        );
    }

    getQueryInputName(){
        return `${this.state.elmtId}-input`;
    }

    renderTokenField(
        tokenElmtFirst,
        summaryDisplay,
        summaryInput,
        tokenElmts,
        suggestionElmts,
        tokenInputs){

        const tokenfieldClassArray = ['mmui-tokenfield'];
        if (this.state.isExpanded) {
            tokenfieldClassArray.push('mmui-tokenfield-expanded');
        } else {
            tokenfieldClassArray.push('mmui-tokenfield-collapsed');
        }

        const tokenfieldClassNames = tokenfieldClassArray.join(' ');

        let resetLink,
            resetLinkStyle = { display: 'none' },
            suggestionsStyle = { display: 'none' }
        ;
        if (this.state.hasChanged) {
            resetLinkStyle.display = 'block';
        }
        if (this.state.suggestions.length > 0) {
            suggestionsStyle.display = 'block';
        }

        if (this.state.hasReset) {
            resetLink = (
                <a
                    href="#"
                    className="mmui-tokenfield-reset"
                    style={resetLinkStyle}
                    onClick={this.onReset}
                >
                    Reset
                </a>
            );
        }

        return (
            <div
                className={tokenfieldClassNames}
                onClick={this.onExpandTokenfield}
                role={'button'}
                tabIndex={0}
                onFocus={this.onExpandTokenfield}
            >
                <div className="mmui-tokenfield-summary">
                    {tokenElmtFirst}
                    {summaryDisplay}
                    {summaryInput}
                    {resetLink}
                </div>
                <div className="mmui-tokenfield-details">
                    <div className="mmui-tokens-input clearfix">
                        <div className="mmui-tokens">{tokenElmts}</div>
                        <input
                            type="text"
                            className="form-control mmui-form-control mmui-typeahead-input"
                            name={this.getQueryInputName()}
                            value={this.state.typeaheadInput}
                            placeholder={this.state.placeHolder}
                            onChange={this.onTypeaheadInput}
                        />
                    </div>
                    <div
                        className="mmui-token-suggestions"
                        style={suggestionsStyle}
                    >
                        {suggestionElmts}
                    </div>
                </div>
                {tokenInputs}
            </div>
        );
    }

    /**
     * Render the Component
     */
    render() {
        let tokenInputs,
            tokenElmts,
            tokenElmtFirst,
            suggestionElmts,
            summaryDisplay,
            summaryInput,
            tokenElmtMapper = (token) => {
                return this.tokenDisplayRender(token);
            }
        ;

        tokenInputs = this.state.tokens.map((token) => {
            const key = `token-input-${token.id}`;

            return (
                <input
                    key={key}
                    type="hidden"
                    name={this.state.inputName}
                    value={token.id}
                />
            );
        });

        if (this.state.tokens.length > 0) {
            if (this.state.tokens.length > 1) {
                const moreCountDisplay = `+${
                    this.state.tokens.length - 1
                } more`;
                summaryDisplay = (
                    <span className="mmui-tokens-summary-display">
                        {moreCountDisplay}
                    </span>
                );
            }

            /**
             * Map state tokens to html render.
             */
            tokenElmtFirst = tokenElmtMapper(this.state.tokens[0]);
            tokenElmts = this.state.tokens.map(tokenElmtMapper);
        } else {
            summaryInput = (
                <input
                    type="text"
                    name={this.getQueryInputName()}
                    className="form-control mmui-form-control"
                    placeholder={this.state.placeHolder}
                />
            );
        }

        /**
         * Map state suggestions to html render.
         */
        if (this.state.suggestions.length > 0) {
            suggestionElmts = this.state.suggestions.map((suggestion) => {
                return this.suggestionRender(suggestion);
            });
        }

        return this.renderTokenField(
            tokenElmtFirst,
            summaryDisplay,
            summaryInput,
            tokenElmts,
            suggestionElmts,
            tokenInputs
        );
    }
}
