import _ from 'lodash';
import React from 'react';
import suggestApi from 'api/suggest';
import { i18n } from 'lib/i18n';
import { toRecord, serialize } from 'records/util';

import UserModalActions from 'actions/Modal.User';
import UserStore from 'stores/Users';

import ScrollArea from 'ui-externals/ScrollArea';
import Input from '../Input';

import './index.styl';

const LIST_GROUP_ORDER = ['groups', 'departments', 'users'];

const LIST_PROPS = [
    'before', 'after', 'empty', 'maxListHeight', 'hideGroupTitles',
    'type', 'types', 'limit',
];

const IGNORED_INPUT_PROPS = ['name', 'onValidityChange', 'onSelect'].concat(LIST_PROPS);

const OBJECT_TYPE_MAP = {
    users: 'user',
    departments: 'department',
    groups: 'group',
};

const StateUpdate = {
    ADD_ITEM: 'add_item',
    REMOVE_ITEM: 'remove_item',
    CLEAR_ITEMS: 'clear_items',
};

const Components = {
    ADD_USER: 'ADD_USER',
};

function isEmptyList(data) {
    return !data || _.every(LIST_GROUP_ORDER, type => _.isEmpty(data[type]));
}

function count(data) {
    let totalCount = 0;

    LIST_GROUP_ORDER.forEach(type => {
        totalCount += _.get(data, type, []).length;
    });

    return totalCount;
}

function getItem(data, index) {
    const totalCount = count(data);
    let selectedItem;

    LIST_GROUP_ORDER.forEach(type => {
        _.get(data, type, []).forEach((item, k) => {
            if (typeof index === 'number' && (index % totalCount === k)) {
                selectedItem = { object: item, type };
            }
        });
    });

    return selectedItem;
}

function toCustomListComponent(value, listComponentProps) {
    if (value === Components.ADD_USER) {
        const onSubmit = data => {
            const user = data ? UserStore.find({ nickname: data.nickname }) : null;

            if (user) {
                listComponentProps.onSelect({
                    object: { id: user.getId(), nickname: user.getNickname() },
                    type: 'user',
                });
            }
        };

        return <AddUser onSubmit={onSubmit} />;
    }

    if (typeof value === 'string') {
        return <div className={`${Search._name}__item`}>{value}</div>;
    }

    return value;
}

function toSerializedString(item) {
    if (item instanceof Array) {
        return item.map(toSerializedString).join(';');
    }

    if (!item || !item.type || !item.object) {
        return '';
    }

    return [
        item.type,
        item.object.id,
        item.object.nickname || item.object.login || item.object.label,
    ].join(':');
}

const Search = React.createClass({

    getInitialState() {
        return this._getNextState(this.props.value);
    },

    componentDidMount() {
        window.addEventListener('click', event => {
            if (!this._element || this._element.contains(event.target)) {
                return;
            }
            this.setState({
                data: null,
                focused: false,
                cursor: -1,
            });
        });
    },

    componentWillUpdate(nextProps, nextState) {
        const wasEmpty = isEmptyList(this.state.data);
        const isEmpty = isEmptyList(nextState.data);

        if (isEmpty !== wasEmpty) {
            const { onListOpen, onListClose } = this.props;

            if (wasEmpty && onListOpen) {
                onListOpen(nextState.data);
            }
            if (isEmpty && onListClose) {
                onListClose();
            }
        }
    },

    componentDidUpdate(prevProps, prevState) {
        if (this.props.onValidityChange && this.state.valid !== prevState.valid) {
            this.props.onValidityChange(this.state.valid);
        }
    },

    componentWillReceiveProps(nextProps) {
        if (nextProps.value !== undefined && nextProps.value !== this.props.value) {
            const nextState = this._getNextState(nextProps.value);

            if (nextState.serializedValue !== this.state.serializedValue) {
                this.setState(nextState);
            }
        }
    },

    _initialize(element) {
        this._element = element;
    },

    _onInput(event) {
        const { props } = this;

        clearTimeout(this._relaxationTimeout);

        this.setState({
            inputText: event.target.value,
            busy: true,
            focused: true,
        });

        suggestApi.read(event.target.value, props.limit)
            .then(data => {
                this.setState({
                    data: _.pick(data ? data.suggest : null, props.type || props.types),
                });
                this._relaxationTimeout = setTimeout(() => {
                    this.setState({ busy: false });
                }, 80);
            })
            .catch(() => {
                this.setState({ busy: false });
            });
    },

    _onKeyDown(event) {
        if (event.keyCode === 13) {
            event.preventDefault();

            const { data, cursor } = this.state;
            const selectedItem = getItem(data, cursor);

            if (selectedItem) {
                this._onSelect(selectedItem);
            }

            return;
        }

        let cursorChange = 0;

        if (event.keyCode === 38) {
            cursorChange = -1;
        }

        if (event.keyCode === 40) {
            cursorChange = 1;
        }

        if (cursorChange) {
            event.preventDefault();
            let cursor = (this.state.cursor || 0) + cursorChange;

            if (cursor < -1 || cursor >= Number.MAX_VALUE - 2) {
                cursor = -1;
            }

            this.setState({ cursor });
        }
    },

    _onFocus(event) {
        this.setState({ focused: true, cursor: -1 });
        this._onInput(event);
    },

    _onBlur() {
        this.setState({ focused: false });
        this._validate();
    },

    _validate() {
        setTimeout(() => {
            this.setState({
                valid: Boolean(!this.state.inputText || this.state.serializedValue),
            });
        }, 0);
    },

    _clear() {
        this._onSelect(null, StateUpdate.CLEAR_ITEMS);
    },

    _getNextState(item, updateType) {
        item = serialize(item);

        let items = (this.state ? this.state.selectedItems : null) || [];

        switch (updateType) {
            case StateUpdate.REMOVE_ITEM:
                items = items.filter(selectedItem =>
                    selectedItem.type !== item.type ||
                    selectedItem.object.id !== item.object.id
                );
                break;

            case StateUpdate.CLEAR_ITEMS:
                items = [];
                break;

            case StateUpdate.ADD_ITEM:
                if (item) {
                    items = items.filter(i => i.object.id !== item.object.id);
                    items.push(item);
                }
                break;

            default:
                items = item ? [item] : [];
        }

        return {
            inputText: '',
            data: null,
            cursor: -1,
            selectedItems: items || null,
            serializedValue: toSerializedString(items),
        };
    },

    _updateItem(item, updateType) {
        const { onSelect, multiple } = this.props;
        const nextState = this._getNextState(item, updateType);

        this.setState(nextState);
        this._validate();

        if (onSelect && nextState.selectedItems) {
            onSelect(multiple ? nextState.selectedItems : nextState.selectedItems[0]);
        }
    },

    _onSelect(payload) {
        this._updateItem(payload, StateUpdate.ADD_ITEM);
    },

    _onClose(item) {
        this._updateItem(item, StateUpdate.REMOVE_ITEM);

        setTimeout(() => {
            if (!this.state.selectedItems.length) {
                this._element.querySelector('input[type="text"]').focus();
            }
        }, 0);
    },

    _renderValues() {
        return this.state.selectedItems.map((item, index) => (
            <Unit
                key={index}
                item={item}
                closable
                onClose={this._onClose}
                onClick={null}
            />
        ));
    },

    render() {
        const base = Search._name;

        const { props, state } = this;
        const className = [
            base,
            props.width === 'available' ? `${base}_full-width` : null,
            props.multiple ? `${base}_multiple` : null,
            state.busy ? `${base}_busy` : null,
            state.focused ? `${base}_active` : null,
            state.serializedValue ? null : `${base}_editable`,
        ].filter(Boolean).join(' ');

        return (
            <div className={className} ref={this._initialize}>
                <input
                    type="hidden"
                    name={props.name}
                    value={state.serializedValue}
                />
                <List
                    {..._.pick(props, LIST_PROPS)}
                    data={state.data}
                    value={state.inputText}
                    busy={state.busy}
                    focused={state.focused}
                    cursor={state.cursor}
                    onSelect={this._onSelect}
                />
                <div className={`${base}__container`}>
                    <Input
                        {..._.omit(props, IGNORED_INPUT_PROPS)}
                        value={state.inputText}
                        valid={state.valid}
                        border="none"
                        onInput={this._onInput}
                        onKeyDown={this._onKeyDown}
                        onFocus={this._onFocus}
                        onBlur={this._onBlur}
                    />
                    <div className={`${base}__values`}>
                        {this._renderValues()}
                    </div>
                </div>
            </div>
        );
    },

});

Search.defaultProps = {
    limit: 5,
    maxListHeight: 'auto',
};

Search._name = 'lego-search';

const List = props => {
    const base = `${Search._name}__list`;

    if (!props.value) {
        return null;
    }

    // не показываем заголовки групп списка (Люди, Команды, Отделы), если указано
    // явно в props.hideGroupTitles или если указано, что в списке нужно показывать
    // объекты только одного типа - в этом случае заголовки избыточны

    const hideGroupTitles =
        typeof props.hideGroupTitles === 'boolean' ?
            props.hideGroupTitles :
            Boolean(props.type) || Boolean(props.types && props.types.length === 1);
    const totalCount = count(props.data);

    let listGroups = LIST_GROUP_ORDER.map(type => {
        const items = _.get(props.data, type, []).map((object, index) => {
            const item = { object, type: OBJECT_TYPE_MAP[type] || type };

            return (
                <Unit
                    item={item}
                    key={`${type}-${object.id}`}
                    active={typeof props.cursor === 'number' && (props.cursor % totalCount === index)}
                    onClick={props.onSelect ? () => props.onSelect(item) : null}
                />
            );
        });

        if (!items.length) {
            return null;
        }

        const groupTitle = hideGroupTitles ?
            null :
            (
                <div className={`${base}-group-title`}>
                    {i18n(`search.suggest.${type}`)}
                </div>
            );

        return (
            <div className={`${base}-group`} key={type}>
                {groupTitle}
                <div className={`${base}-group-items`}>
                    {items}
                </div>
            </div>
        );
    }).filter(Boolean);

    const className = [
        base,
        listGroups.length ? null : `${base}_empty`,
    ].filter(Boolean).join(' ');

    // отрисовываем список, если есть элементы списка, или если есть
    // кастомные элементы props.before или props.after;
    // кастомные элементы при пустом основном списке не отображаем,
    // пока саджест ждёт отработки запроса или если он не в фокусе

    const shouldRender =
        listGroups.length ||
        ((props.before || props.after || props.empty) && !props.busy && props.focused !== false);

    if (!shouldRender) {
        return null;
    }

    let listPlaceholder;

    if (!listGroups.length && props.empty) {
        listGroups = null;
        listPlaceholder = <ListPlaceholder {...props} />;
    }

    return (
        <div className={className}>
            <ScrollArea
                className={`${base}-scrollarea`}
                minScrollSize={18}
                style={{ maxHeight: props.maxListHeight }}
            >
                {toCustomListComponent(props.before, props)}
                {listGroups || listPlaceholder}
                {toCustomListComponent(props.after, props)}
            </ScrollArea>
        </div>
    );
};

const ListPlaceholder = props => {
    if (!props.empty) {
        return null;
    }

    if (typeof props.empty === 'string') {
        return <div className={`${Search._name}__list-placeholder`}>{props.empty}</div>;
    }

    return props.empty;
};

const Unit = props => {
    const base = `${Search._name}__unit`;

    if (!props.item || !props.item.object) {
        return null;
    }

    const record = toRecord(props.item);

    let closeButton;
    const className = [
        base,
        props.item ? `${base}_type-${props.item.type}` : null,
        props.closable ? `${base}_closable` : null,
        props.active ? `${base}_active` : null,
    ].filter(Boolean).join(' ');

    if (props.closable) {
        closeButton = (
            <div
                className={`${base}-close`}
                onClick={event => {
                    event.stopPropagation();
                    props.onClose(props.item);
                }}
            />
        );
    }

    if (!record || !record.getName || !record.getAvatar) {
        return null;
    }

    return (
        <div
            className={className}
            onClick={() => props.onClick && props.onClick(props.item)}
        >
            <div
                className={`${base}-avatar`}
                style={{ backgroundImage: `url(${record.getAvatar()})` }}
            />
            <div className={`${base}-name`}>
                {record.getName()}
            </div>
            {closeButton}
        </div>
    );
};

const AddUser = props => {
    const base = `${Search._name}__add-user`;

    return (
        <div
            className={base}
            onClick={() => UserModalActions.create({ onSubmit: props.onSubmit, redirect: false })}
        >
            {i18n('search.add_user')}
        </div>
    );
};

export default _.assign(Search, Components);
