"use strict";

/**
 * Nice select for react.
 *
 *
 *
 * <i>Copyright (c) 2016 ItsAsbreuk - http://itsasbreuk.nl</i><br>
 * New BSD License - http://choosealicense.com/licenses/bsd-3-clause/
 *
 *
 * @module itsa-react-select
 * @class Select
 * @since 0.0.1
*/

require("itsa-dom");

const React = require("react"),
    PropTypes = require("prop-types"),
    ReactDOM = require("react-dom"),
    cloneProps = require("itsa-react-clone-props"),
    FocusContainer = require("itsa-react-focuscontainer"),
    Button = require("itsa-react-button"),
    utils = require("itsa-utils"),
    later = utils.later,
    async = utils.async,
    MAIN_CLASS = "itsa-select",
    MAIN_CLASS_PREFIX = MAIN_CLASS+"-",
    SPACED_MAIN_CLASS_PREFIX = " "+MAIN_CLASS_PREFIX,
    FORM_ELEMENT_CLASS_SPACES = " itsa-formelement",
    CONTAINER = "container",
    _SUB = "-sub",
    EMPTY_HTML = "Choose...",
    REQUIRED_MSG = "Selection is required",
    CLICK = "click",
    BTN_REFOCES_TRANS_TIME = 500,
    DEF_BUTTON_PRESS_TIME = 300;

class Select extends React.Component {
    constructor(props) {
        super(props);
        const instance = this;
        this.state = {
            expanded: false,
            btnPressed: false
        };
        instance.focus = instance.focus.bind(instance);
        instance.handleContainerFocus = instance.handleContainerFocus.bind(instance);
        instance.handleClick = instance.handleClick.bind(instance);
        instance.handleItemClick = instance.handleItemClick.bind(instance);
        instance.handleItemScroll = instance.handleItemScroll.bind(instance);
        instance.handleKeyDown = instance.handleKeyDown.bind(instance);
        instance.handleKeyUp = instance.handleKeyUp.bind(instance);
        instance.handleMouseDown = instance.handleMouseDown.bind(instance);
        instance.handleMouseUp = instance.handleMouseUp.bind(instance);
        instance._defaultItemRenderer = instance._defaultItemRenderer.bind(instance);
        instance._getUlContainerNode = instance._getUlContainerNode.bind(instance);
        instance._handleDocumentClick = instance._handleDocumentClick.bind(instance);
        instance._saveHTML = instance._saveHTML.bind(instance);
    }

    /**
     * componentDidMount does some initialization.
     *
     * @method componentDidMount
     * @since 0.0.1
     */
    componentDidMount() {
        const instance = this;
        // set outside clickHandler which watches for outside clicks that will collapse the component:
        instance.IE8_EVENTS = !instance._componentNode.addEventListener;
        if (instance.IE8_EVENTS) {
            document.attachEvent("on"+CLICK, instance._handleDocumentClick);
        }
        else {
            document.addEventListener(CLICK, instance._handleDocumentClick, true);
        }
        if (instance.props.autoFocus) {
            instance._focusLater = later(() => instance.focus(), 50);
        }
    }

    /**
     * componentWilUnmount does some cleanup.
     *
     * @method componentWillUnmount
     * @since 0.0.1
     */
    componentWillUnmount() {
        const instance = this;
        instance._removeTimer && instance._removeTimer.cancel();
        this._focusLater && this._focusLater.cancel();
        if (instance.IE8_EVENTS) {
            document.detachEvent("on"+CLICK, instance._handleDocumentClick);
        }
        else {
            document.removeEventListener(CLICK, instance._handleDocumentClick, true);
        }
    }

    /**
     * Sets the focus on the Component.
     *
     * @method focus
     * @param [transitionTime] {Number} transition-time to focus the element into the view
     * @chainable
     * @since 0.0.1
     */
    focus(transitionTime) {
        return this._button.focus(transitionTime);
    }

    /**
     * Callback that sets the focus to the descendent element by calling `focus()`
     *
     * @method handleContainerFocus
     * @param e {Object} event-payload
     * @since 0.1.0
     */
    handleContainerFocus(e) {
        (e.target===e.currentTarget) && this.focus();
    }

    /**
     * Callback-fn for the onClick-event.
     * Will invoke `this.props.onChange`
     *
     * @method handleClick
     * @since 0.0.1
     */
    handleClick(simulated) {
        const instance = this,
            props = instance.props,
            simulatedClick = (simulated===true),
            newExpanded = !instance.state.expanded;
        if (!props.disabled && !props.readOnly) {
            instance.setState({
                expanded: newExpanded,
                btnPressed: true
            }, () => {
                instance._removeTimer && instance._removeTimer.cancel();
                if (newExpanded) {
                    instance._focusContainer.focusActiveElement();
                }
                else {
                    instance.focus(true, BTN_REFOCES_TRANS_TIME);
                }
                // need to go async --> handleClick goes so quick, that is is before handleMouseDown
                async(() => {
                    if (!instance._mouseDown && !simulatedClick) {
                        instance._removeTimer = later(() => {
                            instance.setState({btnPressed: false});
                        }, DEF_BUTTON_PRESS_TIME);
                    }
                });
            });
        }
    }

    /**
     * Callback-fn when an item is clicked
     *
     * @method handleItemClick
     * @since 0.0.1
     */
    handleItemClick(e) {
        let newValue, itemInt, itemPosition;
        const instance = this,
            node = e.target,
            props = instance.props,
            item = node && node.getAttribute("data-id");
        if (!props.disabled && !props.readOnly && !this._buttonDown) {
            if (props.closeOnClick) {
                instance.setState({expanded: false});
                instance.focus(true, BTN_REFOCES_TRANS_TIME);
            }
            else {
                instance._focusContainer.focusElement(Array.prototype.indexOf.call(node.parentNode.querySelectorAll("li"), node));
            }
            if (item) {
                itemInt = parseInt(item, 10);
                if (props.multiSelect) {
                    newValue = props.selected;
                    if (typeof newValue==="number") {
                        newValue = [newValue];
                    }
                    else if (!newValue) {
                        newValue = [];
                    }
                    itemPosition = newValue.indexOf(itemInt);
                    if (itemPosition===-1) {
                        newValue.push(itemInt);
                    }
                    else {
                        newValue.splice(itemPosition, 1);
                    }
                }
                else {
                    newValue = itemInt;
                }
                instance.props.onChange(newValue);
            }
        }
    }

    /**
     * Callback-fn when an item is scrolled by keys
     *
     * @method handleItemScroll
     * @since 0.0.1
     */
    handleItemScroll(e) {
        const instance = this,
            keyCode = e.keyCode,
            props = instance.props;
        if (!props.disabled && !props.readOnly && (keyCode===13)) {
            instance.handleItemClick({target: document.activeElement});
        }
    }

    /**
     * Callback-fn for the onKeyDown-event.
     *
     * @method handleKeyDown
     * @since 0.0.1
     */
    handleKeyDown(e) {
        const instance = this,
            props = instance.props,
            state = instance.state,
            keyCode = e.keyCode;

        if (!props.disabled && !props.readOnly && !instance._buttonDown) {
            // if ((keyCode===27) && (state.expanded)) {
            if (((keyCode===27) || (keyCode===8) || (keyCode===9)) && (state.expanded)) {
                instance.setState({
                    expanded: false
                });
                instance.focus(true, BTN_REFOCES_TRANS_TIME); // in case the focus was on a listitem
            }
            else if (((keyCode===40) || (keyCode===32)) && !(state.expanded)) {
                // prevent minus windowscroll:
                e.preventDefault();
                instance.handleClick(true);
            }
        }
        instance._buttonDown = true;
    }

    /**
     * Callback-fn for the onKeyUp-event.
     *
     * @method handleKeyUp
     * @since 0.0.1
     */
    handleKeyUp() {
        this._buttonDown = false;
    }

    /**
     * Callback-fn for the onMouseDown-event.
     *
     * @method handleMouseDown
     * @since 0.0.1
     */
    handleMouseDown() {
        const props = this.props;
        if (!props.disabled && !props.readOnly) {
            this._mouseDown = true;
        }
    }

    /**
     * Callback-fn for the onMouseUp-event.
     *
     * @method handleMouseUp
     * @since 0.0.1
     */
    handleMouseUp() {
        if (this._mouseDown) {
            this._mouseDown = false;
            this.setState({
                btnPressed: false
            });
        }
    }

    /**
     * React render-method --> renderes the Component.
     *
     * @method render
     * @return ReactComponent
     * @since 0.0.1
     */
    render() {
        const instance = this,
            props = instance.props,
            state = instance.state,
            disabled = props.disabled,
            readOnly = props.readOnly,
            expanded = state.expanded && !disabled && !readOnly,
            itemRenderer = props.itemRenderer || instance._defaultItemRenderer,
            containerStyles = {maxHeight: props.listHeight},
            selected = props.selected,
            items = props.items,
            hasSelection = (typeof selected==="number") ?
                           (selected>=0) && (selected<items.length) :
                           (Array.isArray(selected) && (selected.length>0)),
            required = props.required,
            errored = !expanded && props.formValidated && required && !hasSelection,
            btnRenderer = props.btnRenderer;
        let className = props.className,
            buttonClass = props.btnClassName,
            elementClass = MAIN_CLASS+FORM_ELEMENT_CLASS_SPACES,
            containerClass = MAIN_CLASS_PREFIX+CONTAINER,
            containerSubClass = containerClass+_SUB,
            buttonProps = cloneProps.clone(props),
            listItems, buttonHTML, itemScroll, handleItemClick, errorMsg, handleKeyUp,
            ariaRequired, requiredMsg, handleKeyDown, handleMouseDown, handleContainerFocus;
        delete buttonProps.tabIndex;
        delete buttonProps.style;
        delete buttonProps.className;
        delete buttonProps.disabled;
        delete buttonProps.onClick;
        delete buttonProps.readOnly;
        delete buttonProps.ref;
        if (expanded) {
            containerClass += " "+MAIN_CLASS_PREFIX+"show";
            itemScroll = instance.handleItemScroll;
            handleItemClick = instance.handleItemClick;
        }
        // About the dropdown:
        // first: outerdiv which will be relative positioned
        // next: innerdiv which will be absolute positioned
        // also: hide the container by default --> updateUI could make it shown
        listItems = items.map((item, i) => {
            let classname, ariaLabel, dangerouslySetInnerHTML, match;
            const renderedItem = itemRenderer(item, i);
            if (Array.isArray(selected)) {
                match = (selected.indexOf(i)!==-1);
            }
            else {
                match = (i===selected);
            }
            if (match) {
                classname = "selected";
                btnRenderer || (buttonHTML=renderedItem);
            }
            ariaLabel = instance._saveHTML(renderedItem);
            dangerouslySetInnerHTML = {__html: renderedItem};
            return (<li aria-label={ariaLabel} className={classname} dangerouslySetInnerHTML={dangerouslySetInnerHTML}
                data-id={i} key={i} role="listitem"/>);
        });
        btnRenderer && (buttonHTML=btnRenderer.call(instance));
        buttonHTML || (buttonHTML=props.emptyHTML);
        if (!disabled && !readOnly && (state.btnPressed || expanded)) {
            buttonClass = buttonClass ? buttonClass+" "+"itsa-button-active" : "itsa-button-active";
        }
        if (errored) {
            buttonClass = (buttonClass ? buttonClass+SPACED_MAIN_CLASS_PREFIX : MAIN_CLASS_PREFIX)+"error";
            errorMsg = (<div className={MAIN_CLASS_PREFIX+"error-text"}>{props.errorMsg}</div>);
        }
        if (required && props.markRequired && !hasSelection) {
            requiredMsg = (<div className={MAIN_CLASS_PREFIX+"required"} />);
            ariaRequired = true;
        }
        if (!disabled && !readOnly) {
            handleKeyDown = instance.handleKeyDown;
            handleKeyUp = instance.handleKeyUp;
            handleMouseDown = instance.handleMouseDown;
            handleContainerFocus = instance.handleContainerFocus;
        }
        className && (elementClass+=" "+className);
        disabled && (elementClass+=" disabled");
        readOnly && (elementClass+=" readonly");
        return (
            <div
                aria-required={ariaRequired}
                className={elementClass}
                onFocus={handleContainerFocus}
                onKeyDown={handleKeyDown}
                onKeyUp={handleKeyUp}
                onMouseDown={handleMouseDown}
                onMouseUp={instance.handleMouseUp}
                ref={node => instance._componentNode = node}
                style={props.style}
                tabIndex={props.tabIndex} >
                <Button
                    {...props}
                    buttonHTML={buttonHTML}
                    className={buttonClass}
                    disabled={disabled}
                    onClick={instance.handleClick}
                    readOnly={props.readOnly}
                    ref={inst => instance._button = inst} />
                <div className={containerClass} onKeyDown={itemScroll}>
                    <FocusContainer
                        className={containerSubClass}
                        enterKey={13}
                        keyDown={40}
                        keyUp={38}
                        loop={props.loop}
                        ref={inst => instance._focusContainer = inst}
                        scrollIntoView={true}
                        selector="li[role='listitem']"
                        style={containerStyles} >
                        <ul
                            onClick={handleItemClick}
                            role="list">
                            {listItems}
                        </ul>
                    </FocusContainer>
                </div>
                {requiredMsg}
                {errorMsg}
            </div>
        );
    }

    /**
     * Returns a html that represent the item as how it should be rendered.
     *
     * @method _defaultItemRenderer
     * @private
     * @param String|Object list-item
     * @return String html that represent the item
     * @since 0.0.1
     */
    _defaultItemRenderer(item) {
        return item;
    }

    /**
     * Returns the ul-element that contains all li-elements.
     *
     * @method _getUlContainerNode
     * @private
     * @return Element
     * @since 0.0.1
     */
    _getUlContainerNode() {
        const instance = this;
        instance._ulNode || (instance._ulNode=instance._componentNode.getElementsByTagName("ul")[0]);
        return instance._ulNode;
    }

    /**
     * Callback for a click on the document. Is needed to close the Component when clicked outside.
     *
     * @method _handleDocumentClick
     * @private
     * @param Object e
     * @since 0.0.1
     */
    _handleDocumentClick(e) {
        const instance = this,
            targetNode = e.target;
        if ((instance._componentNode!==targetNode) && !instance._componentNode.contains(targetNode)) {
            instance.setState({
                expanded: false
            });
        }
    }

    /**
     * Returns a save string
     *
     * @method _saveHTML
     * @private
     * @param String html the text that should be removed from any html-entities
     * @return String
     * @since 0.0.1
     */
    _saveHTML(html) {
        return html && html.replace(/<[^>]*>/g, "");
    }
}

Select.propTypes = {
    /**
     * Whether to autofocus the Component.
     *
     * @property autoFocus
     * @type Boolean
     * @since 0.0.1
    */
    autoFocus: PropTypes.bool,

    /**
     * ClassName that should be set to the select-button
     *
     * @property btnClassName
     * @type String
     * @since 0.0.1
    */
    btnClassName: PropTypes.string,

    /**
     * A render-function for the buttonText. When not supplied (default),
     * The selected item will be rendered as the buttonText.
     *
     * @property btnRenderer
     * @type Function
     * @since 0.0.1
    */
    btnRenderer: PropTypes.func,

    /**
     * ClassName that should be set to the Element
     *
     * @property className
     * @type String
     * @since 0.0.1
    */
    className: PropTypes.string,

    /**
     * Whether the dropdown-list should be collapsed when an item is clicked.
     *
     * @property closeOnClick
     * @type Boolean
     * @default true
     * @since 15.3.32
    */
    closeOnClick: PropTypes.bool,

    /**
     * Whether the component is disabled
     *
     * @property disabled
     * @type Boolean
     * @since 0.0.1
    */
    disabled: PropTypes.bool,

    /**
     * The HTML that the select should show when no item is selected.
     * Defaults "Choose..."
     *
     * @property emptyHTML
     * @type String
     * @since 0.0.1
    */
    emptyHTML: PropTypes.string,

    /**
     * The errormessage to show when the Select is not validated.
     *
     * @property errorMsg
     * @type String
     * @since 0.0.1
    */
    errorMsg: PropTypes.string,

    /**
     * Whether the parent-form has been validated.
     * This value is needed to determine if the validate-status should be set.
     *
     * @property formValidated
     * @type Boolean
     * @since 0.0.1
    */
    formValidated: PropTypes.bool,

    /**
     * List with all the items, either as Strings or Objects.
     *
     * @property items
     * @type Array
     * @since 0.0.1
    */
    items: PropTypes.array.isRequired,

    /**
     * The height of the select-list. If not set, then all items are shown without a scroller.
     *
     * @property listHeight
     * @type Number
     * @since 0.0.1
    */
    listHeight: PropTypes.string,

    /**
     * Whether the loop the items when the last/first item is reached.
     *
     * @property loop
     * @default false
     * @type Boolean
     * @since 15.0.0
    */
    loop: PropTypes.bool,

    /**
     * Whether the Component should show an validate-reclamation (star)
     * when it requires a selected item yet when there is no item selected.
     *
     * @property markValidated
     * @type Boolean
     * @since 0.0.1
    */
    markRequired: PropTypes.bool,

    /**
     * Whether the dropdown-list accepts multiple selected items.
     *
     * @property multiSelect
     * @type Boolean
     * @default false
     * @since 15.3.32
    */
    multiSelect: PropTypes.bool,

    /**
     * The `onChange` function, which should update the `state`.
     *
     * @property onChange
     * @type Function
     * @since 0.0.1
    */
    onChange: PropTypes.func.isRequired,

    /**
     * Whether the checkbox is readonly
     *
     * @property readOnly
     * @type Boolean
     * @default false
     * @since 15.2.0
    */
    readOnly: PropTypes.bool,

    /**
     * Whether to Component requires a valid selection
     *
     * @property required
     * @type Boolean
     * @since 0.0.1
    */
    required: PropTypes.bool,

    /**
     * The index that is selected
     *
     * @property readOnly
     * @type number
     * @default null
     * @since 15.2.0
    */
    selected: PropTypes.oneOfType([PropTypes.number, PropTypes.array]),

    /**
     * Inline style
     *
     * @property style
     * @type object
     * @since 0.0.1
    */
    style: PropTypes.object,

    /**
     * The tabindex of the Component.
     *
     * @property type
     * @type Number
     * @since 0.1.2
    */
    tabIndex: PropTypes.number
};

Select.defaultProps = {
    autoFocus: false,
    closeOnClick: true,
    disabled: false,
    emptyHTML: EMPTY_HTML,
    errorMsg: REQUIRED_MSG,
    formValidated: false,
    loop: false,
    markRequired: false,
    multiSelect: false,
    readOnly: false,
    required: false
};

module.exports = Select;
